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:
@@ -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);
|
||||
}
|
||||
59
src/Tools/Ghost.NativeWrapperGen/Transform/JsonConverter.cs
Normal file
59
src/Tools/Ghost.NativeWrapperGen/Transform/JsonConverter.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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('_');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user