feat(nativegen)!: refactor to struct-based native wrappers

Major overhaul of native wrapper generation for ufbx and nvtt.
Replaces all hand-written and class-based wrappers with auto-generated partial struct wrappers that directly expose native API methods via pointers. Introduces a new JSON-driven configuration system using "remaps" and "actions" for flexible parameter/return mapping and method routing. Removes legacy config sections and helper classes, focusing solely on method wrappers. Updates all usages and tests to use the new pointer-based API. Cleans up obsolete code and ensures resource management is handled via struct Dispose methods. The result is a thinner, more direct, and maintainable interop layer.

BREAKING CHANGE: All managed wrapper classes and helpers are removed in favor of struct-based pointer wrappers. API usage and resource management patterns have changed.
This commit is contained in:
2026-03-15 20:48:54 +09:00
parent 3e4084c42a
commit 6cadd8edeb
278 changed files with 5387 additions and 12057 deletions

View File

@@ -0,0 +1,38 @@
using Ghost.NativeWrapperGen.Model;
using Ghost.NativeWrapperGen.Parsing;
namespace Ghost.NativeWrapperGen.Transform;
/// <summary>
/// Resolves whether a given C# type is a pointer to a known binding struct.
/// Used by the emitter to apply the SELF_PTR / RETURN_BINDING_TYPE action conditions.
/// </summary>
public sealed class BindingTypeResolver
{
private readonly NativeLibrary _library;
public BindingTypeResolver(NativeLibrary library)
{
_library = library;
}
/// <summary>
/// Returns the base struct name if <paramref name="typeName"/> is a single-pointer to a known binding struct,
/// otherwise returns null.
/// Example: "ufbx_scene*" → "ufbx_scene", "ufbx_scene**" → null, "sbyte*" → null.
/// </summary>
public string? TryGetBindingStructName(string typeName)
{
if (BindingParser.GetPointerDepth(typeName) != 1)
{
return null;
}
var baseName = BindingParser.TrimPointers(typeName);
return _library.StructsByName.ContainsKey(baseName) ? baseName : null;
}
/// <summary>Returns true if the type is a known binding struct (without pointer).</summary>
public bool IsBindingStruct(string typeName) =>
_library.StructsByName.ContainsKey(typeName);
}

View File

@@ -0,0 +1,59 @@
using System.Dynamic;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Ghost.NativeWrapperGen.Transform;
internal class DynamicJsonConverter : JsonConverter<dynamic>
{
public override dynamic Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// Parse the JSON into a strict JsonElement, then wrap it in our dynamic class
using var document = JsonDocument.ParseValue(ref reader);
return DynamicJsonWrapper.Wrap(document.RootElement.Clone())!;
}
public override void Write(Utf8JsonWriter writer, dynamic value, JsonSerializerOptions options)
{
// (Skipped for brevity, but you would serialize the object back here if needed)
throw new NotImplementedException();
}
}
internal class DynamicJsonWrapper : DynamicObject
{
private readonly JsonElement _element;
public DynamicJsonWrapper(JsonElement element)
{
_element = element;
}
// This method intercepts dynamic property access (e.g., .NestedValue)
public override bool TryGetMember(GetMemberBinder binder, out object? result)
{
if (_element.ValueKind == JsonValueKind.Object && _element.TryGetProperty(binder.Name, out var property))
{
result = Wrap(property);
return true; // Property found!
}
result = null;
return true; // Property not found — return null instead of throwing RuntimeBinderException.
}
// Converts JsonElements into primitives or wraps nested objects
public static object? Wrap(JsonElement element)
{
return element.ValueKind switch
{
JsonValueKind.Object => new DynamicJsonWrapper(element),
JsonValueKind.String => element.GetString(),
JsonValueKind.Number => element.TryGetInt32(out int i) ? i : element.GetDouble(),
JsonValueKind.Array => element.EnumerateArray().Select(Wrap).ToArray(),
JsonValueKind.True => true,
JsonValueKind.False => false,
_ => null
};
}
}

View File

@@ -11,39 +11,91 @@ public sealed class NamingConventions
_config = config;
}
public string GetWrapperTypeName(string nativeTypeName)
/// <summary>
/// Converts a native function name to a method name using the action's name.remove chain.
/// Each entry in the remove list is applied in order, then leading/trailing underscores are trimmed.
///
/// Supported remove tokens:
/// "PREFIX" — strip the config's NativeTypePrefix from the start (e.g. "nvtt", "ufbx_")
/// "NO_PREFIX($TSelf)" — strip the target struct name minus its type prefix from the start,
/// case-insensitively (e.g. NvttSurface → "Surface" stripped from "SurfaceWidth")
///
/// nameOpts is the dynamic opts.name object from JSON (may be null).
/// If no nameOpts are provided, the name is returned with only the library prefix stripped.
/// </summary>
public string GetMethodName(string nativeFunctionName, dynamic? nameOpts, string targetStructName)
{
if (_config.TypeNameOverrides.TryGetValue(nativeTypeName, out var overrideName))
var name = nativeFunctionName;
if (nameOpts is null)
{
return overrideName;
// Fallback: just strip the library prefix.
return TrimUnderscores(StripPrefixIgnoreCase(name, _config.NativeTypePrefix));
}
return ToPascalCase(StripKnownPrefix(nativeTypeName));
}
public string GetPropertyName(string nativeName)
{
return ToPascalCase(nativeName);
}
private string StripKnownPrefix(string nativeTypeName)
{
if (nativeTypeName.StartsWith(_config.NativeTypePrefix, StringComparison.Ordinal))
string? set = nameOpts.set as string;
if (!string.IsNullOrEmpty(set))
{
return nativeTypeName[_config.NativeTypePrefix.Length..];
return set;
}
return nativeTypeName;
}
public static string ToPascalCase(string value)
{
var parts = value.Split('_', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (parts.Length == 0)
var removeTokens = nameOpts.remove as object?[] ?? [];
foreach (var tokenObj in removeTokens)
{
return value;
var token = tokenObj as string ?? string.Empty;
if (string.Equals(token, "PREFIX", StringComparison.Ordinal))
{
name = StripPrefixIgnoreCase(name, _config.NativeTypePrefix);
}
else if (token.StartsWith("NO_PREFIX(", StringComparison.Ordinal) && token.EndsWith(')'))
{
// Extract $TSelf — it's the literal token "NO_PREFIX($TSelf)", so the struct name
// is resolved from the targetStructName argument passed in.
// Strip the config prefix from the struct name to get the "bare" part.
// Try prefix first, then suffix (handles both nvtt "SurfaceWidth"→"Width"
// and ufbx "free_scene"→"free_" styles).
var bareStructName = StripPrefixIgnoreCase(targetStructName, _config.NativeTypePrefix);
// Remove directly, the name maybe nvttSetOutputOptionsOutputHeader, if we only remove prefix and suffix, OutputOptions in the middle will be ignored, so we remove the bare struct name directly, case-insensitively.
name = name.Replace(bareStructName, string.Empty, StringComparison.OrdinalIgnoreCase);
}
name = TrimUnderscores(name);
}
return string.Concat(parts.Select(static part => char.ToUpperInvariant(part[0]) + part[1..]));
return name;
}
/// <summary>Strips the native type prefix (e.g. "ufbx_") from a type name.</summary>
public string StripKnownPrefix(string nativeTypeName)
{
return StripPrefixIgnoreCase(nativeTypeName, _config.NativeTypePrefix);
}
// ─── Helpers ──────────────────────────────────────────────────────────────
private static string StripPrefixIgnoreCase(string name, string prefix)
{
if (name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
return name[prefix.Length..];
}
return name;
}
private static string StripSuffixIgnoreCase(string name, string suffix)
{
if (name.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
{
return name[..^suffix.Length];
}
return name;
}
private static string TrimUnderscores(string name)
{
return name.Trim('_');
}
}

View File

@@ -1,56 +0,0 @@
using Ghost.NativeWrapperGen.Config;
using Ghost.NativeWrapperGen.Model;
using Ghost.NativeWrapperGen.Parsing;
namespace Ghost.NativeWrapperGen.Transform;
public sealed class PublicTypeResolver
{
private readonly NativeLibrary _library;
private readonly WrapperConfig _config;
private readonly NamingConventions _naming;
public PublicTypeResolver(NativeLibrary library, WrapperConfig config, NamingConventions naming)
{
_library = library;
_config = config;
_naming = naming;
}
public string GetPublicType(string nativeTypeName)
{
if (string.Equals(nativeTypeName, "void", StringComparison.Ordinal))
{
return "void*";
}
if (_config.PublicTypeOverrides.TryGetValue(nativeTypeName, out var overrideType))
{
return overrideType;
}
var pointerDepth = BindingParser.GetPointerDepth(nativeTypeName);
var baseType = BindingParser.TrimPointers(nativeTypeName);
if (pointerDepth == 0)
{
return baseType;
}
if (_library.StructsByName.ContainsKey(baseType))
{
return pointerDepth switch
{
1 => _naming.GetWrapperTypeName(baseType),
_ => nativeTypeName,
};
}
return nativeTypeName;
}
public bool HasWrapper(string nativeTypeName)
{
return _library.StructsByName.ContainsKey(nativeTypeName);
}
}