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:
@@ -1,108 +1,149 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Ghost.NativeWrapperGen.Transform;
|
||||
|
||||
namespace Ghost.NativeWrapperGen.Config;
|
||||
|
||||
public sealed class WrapperConfig
|
||||
{
|
||||
public required string LibraryName { get; init; }
|
||||
public required string NativeNamespace { get; init; }
|
||||
public required string WrapperNamespace { get; init; }
|
||||
public required string OutputNamespace { get; init; }
|
||||
public required string NativeTypePrefix { get; init; }
|
||||
public string? StaticApiClassName { get; init; }
|
||||
public List<string> SkipTypes { get; init; } = [];
|
||||
public Dictionary<string, string> TypeNameOverrides { get; init; } = new(StringComparer.Ordinal);
|
||||
public Dictionary<string, string> PublicTypeOverrides { get; init; } = new(StringComparer.Ordinal);
|
||||
public WrapperKindsConfig Wrappers { get; init; } = new();
|
||||
public SpecialTypesConfig SpecialTypes { get; init; } = new();
|
||||
public List<OwnedTypeConfig> OwnedTypes { get; init; } = [];
|
||||
public List<string> SkipFunctions { get; init; } = [];
|
||||
public List<RemapConfig> Remaps { get; init; } = [];
|
||||
public List<ActionConfig> Actions { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes how to remap a native type to a C# type at a call site.
|
||||
/// </summary>
|
||||
public sealed class RemapConfig
|
||||
{
|
||||
/// <summary>Native C# type to match (e.g. "sbyte*", "ufbx_string").</summary>
|
||||
public required string Src { get; init; }
|
||||
/// <summary>C# type to expose in the generated method signature (e.g. "ReadOnlySpan<byte>").</summary>
|
||||
public required string Dst { get; init; }
|
||||
/// <summary>Which scopes this remap applies to: "parameter" and/or "return".</summary>
|
||||
public List<string> Scope { get; init; } = [];
|
||||
/// <summary>Optional regex patterns applied to parameter names. If specified, only matching params are remapped.</summary>
|
||||
public List<string>? Filter { get; init; }
|
||||
/// <summary>If set, a sibling parameter with this suffix is consumed and replaced by the given expression.</summary>
|
||||
public DerivesFromConfig? DerivesFrom { get; init; }
|
||||
/// <summary>How to convert between src and dst.</summary>
|
||||
public AdapterConfig? Adapter { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes a parameter that is derived from another (e.g. name_len derived from name.Length).
|
||||
/// </summary>
|
||||
public sealed class DerivesFromConfig
|
||||
{
|
||||
/// <summary>The suffix of the sibling parameter name to consume (e.g. "_len").</summary>
|
||||
public required string ParamSuffix { get; init; }
|
||||
/// <summary>Expression to pass in place of the consumed parameter. $arg is replaced with the source param name.</summary>
|
||||
public required string Expr { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adapter: how to convert between src (native) and dst (C#) types.
|
||||
/// </summary>
|
||||
public sealed class AdapterConfig
|
||||
{
|
||||
/// <summary>dst → src conversion: wraps the call site and substitutes the argument.</summary>
|
||||
public ConvertBackConfig? ConvertBack { get; init; }
|
||||
/// <summary>src → dst conversion expression (for return values). $result is replaced with the src expression.</summary>
|
||||
public string? ConvertTo { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies how to wrap the generated call and what expression to pass for the remapped parameter.
|
||||
/// Magic variables: $arg = C# parameter name, $CALL = the full Api.xxx(...) call expression.
|
||||
/// </summary>
|
||||
public sealed class ConvertBackConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Manual overrides for specific functions (adapters, special parameters, etc.).
|
||||
/// Functions not listed here are auto-routed by the 3-rule dispatch logic.
|
||||
/// Template that wraps the entire call. Use $arg for the parameter name, $CALL for the call.
|
||||
/// Example: "fixed (byte* p$arg = $arg) { $CALL }"
|
||||
/// </summary>
|
||||
public List<StaticMethodConfig> StaticMethods { get; init; } = [];
|
||||
public List<MarshalledTypeConfig> MarshalledTypes { get; init; } = [];
|
||||
public List<string> PartialTypes { get; init; } = [];
|
||||
public required string WrapCall { get; init; }
|
||||
/// <summary>
|
||||
/// Native types that are treated as plain value types in wrapper land (not wrapped in a pointer struct).
|
||||
/// Functions returning or taking these as non-pointer params pass them by value.
|
||||
/// Expression passed as the native argument. Use $arg for the parameter name.
|
||||
/// Example: "(sbyte*)p$arg"
|
||||
/// </summary>
|
||||
public List<string> SkipFunctionPrefixes { get; init; } = [];
|
||||
public required string PassAs { get; init; }
|
||||
}
|
||||
|
||||
public sealed class WrapperKindsConfig
|
||||
/// <summary>
|
||||
/// Routing rule: determines where a function gets emitted and as what kind of method.
|
||||
/// </summary>
|
||||
public sealed class ActionConfig
|
||||
{
|
||||
public string DefaultKind { get; init; } = "struct";
|
||||
public string DefaultOwnedKind { get; init; } = "class";
|
||||
public Dictionary<string, string> Kinds { get; init; } = new(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public sealed class SpecialTypesConfig
|
||||
{
|
||||
public List<StringTypeConfig> Strings { get; init; } = [];
|
||||
public List<BlobTypeConfig> Blobs { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class StringTypeConfig
|
||||
{
|
||||
public required string Type { get; init; }
|
||||
public string DataField { get; init; } = "data";
|
||||
public string LengthField { get; init; } = "length";
|
||||
public int CharSize { get; init; } = 8;
|
||||
public string Encoding { get; init; } = "utf8";
|
||||
public bool EmitRawSpanProperty { get; init; } = true;
|
||||
public bool EmitStringProperty { get; init; } = true;
|
||||
}
|
||||
|
||||
public sealed class BlobTypeConfig
|
||||
{
|
||||
public required string Type { get; init; }
|
||||
public string DataField { get; init; } = "data";
|
||||
public string LengthField { get; init; } = "size";
|
||||
public string ElementType { get; init; } = "byte";
|
||||
}
|
||||
|
||||
public sealed class OwnedTypeConfig
|
||||
{
|
||||
public required string NativeType { get; init; }
|
||||
public string? FreeFunction { get; init; }
|
||||
public string? RetainFunction { get; init; }
|
||||
public string? WrapperKind { get; init; }
|
||||
public string? StaticType { get; init; }
|
||||
}
|
||||
|
||||
public sealed class StaticMethodConfig
|
||||
{
|
||||
public required string NativeFunction { get; init; }
|
||||
public string? MethodName { get; init; }
|
||||
public string? StaticType { get; init; }
|
||||
public bool ThrowOnNullReturn { get; init; }
|
||||
public string? FailureMessageMember { get; init; }
|
||||
public List<StaticParameterConfig> Parameters { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class StaticParameterConfig
|
||||
{
|
||||
public required string Native { get; init; }
|
||||
public required string Adapter { get; init; }
|
||||
public string? PublicName { get; init; }
|
||||
public string? Type { get; init; }
|
||||
public string? Source { get; init; }
|
||||
public bool OptionalDefault { get; init; }
|
||||
}
|
||||
|
||||
public sealed class MarshalledTypeConfig
|
||||
{
|
||||
public required string NativeType { get; init; }
|
||||
/// <summary>"EXTERN_API" = all DllImport methods on the Api class.</summary>
|
||||
public required string Filter { get; init; }
|
||||
/// <summary>
|
||||
/// Fields that need special (user-implemented partial) marshalling logic.
|
||||
/// The generator emits a backing field + partial property stub for these;
|
||||
/// the Dispose() partial stub is always emitted so the user can free them.
|
||||
/// Conditions that must all be true:
|
||||
/// "SELF_PTR" — first param type is T* where T is a known binding struct
|
||||
/// "FIRST_PARAM_OTHER_TYPE" — first param is NOT a known struct pointer
|
||||
/// "RETURN_BINDING_TYPE" — return type is T* where T is a known binding struct
|
||||
/// "VOID_RETURN" — return type is void
|
||||
/// "NAME_CONDITION(regex)" — native function name matches the regex ($TSelf/$TBare substituted)
|
||||
/// </summary>
|
||||
public List<MarshalledPropertyConfig> MarshalledProperties { get; init; } = [];
|
||||
public List<string> Conditions { get; init; } = [];
|
||||
/// <summary>"FIRST_PARAM_TYPE" or "RETURN_TYPE" — which type the method is placed on.</summary>
|
||||
public required string TargetType { get; init; }
|
||||
/// <summary>
|
||||
/// One or more apply steps. In JSON can be a single object or an array.
|
||||
/// Supported types: "INSTANCE_METHOD", "STATIC_METHOD", "INHERITANCE".
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(ActionApplyListConverter))]
|
||||
public required List<ActionApplyConfig> Apply { get; init; }
|
||||
public string? Comment { get; init; }
|
||||
}
|
||||
|
||||
public sealed class MarshalledPropertyConfig
|
||||
/// <summary>
|
||||
/// Describes a single apply step within an action.
|
||||
/// </summary>
|
||||
public sealed class ActionApplyConfig
|
||||
{
|
||||
/// <summary>Native field name (snake_case).</summary>
|
||||
public required string Native { get; init; }
|
||||
/// <summary>The C# wrapper type for this field (e.g. "cstring").</summary>
|
||||
/// <summary>"INSTANCE_METHOD", "STATIC_METHOD", or "INHERITANCE".</summary>
|
||||
public required string Type { get; init; }
|
||||
/// <summary>
|
||||
/// Optional per-apply-step options. Accessed dynamically:
|
||||
/// opts.removeFirstParam — bool [INSTANCE_METHOD] skip first param from public signature
|
||||
/// opts.passAs — string [INSTANCE_METHOD] self-pointer expression ($TSelf substituted)
|
||||
/// opts.name.set — string [INSTANCE/STATIC] fixed method name
|
||||
/// opts.name.remove — array [INSTANCE/STATIC] removal token list
|
||||
/// opts.baseType — array [INHERITANCE] list of base type strings
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(DynamicJsonConverter))]
|
||||
public dynamic? Opts { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the "apply" JSON field as either a single object or an array of objects,
|
||||
/// always producing a List<ActionApplyConfig>.
|
||||
/// </summary>
|
||||
internal sealed class ActionApplyListConverter : JsonConverter<List<ActionApplyConfig>>
|
||||
{
|
||||
public override List<ActionApplyConfig> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.StartArray)
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<ActionApplyConfig>>(ref reader, options)
|
||||
?? [];
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonTokenType.StartObject)
|
||||
{
|
||||
var single = JsonSerializer.Deserialize<ActionApplyConfig>(ref reader, options);
|
||||
return single is not null ? [single] : [];
|
||||
}
|
||||
|
||||
throw new JsonException($"Expected object or array for 'apply', got {reader.TokenType}.");
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, List<ActionApplyConfig> value, JsonSerializerOptions options)
|
||||
=> JsonSerializer.Serialize(writer, value, options);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ internal sealed class CodeWriter
|
||||
private readonly StringBuilder _builder = new();
|
||||
private int _indent;
|
||||
|
||||
public string CurrentIndentString => new(' ', _indent * 4);
|
||||
|
||||
public void WriteLine(string line = "")
|
||||
{
|
||||
if (line.Length == 0)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -82,7 +82,7 @@ public sealed class BindingParser
|
||||
ReturnType = NormalizeType(method.ReturnType.ToString()),
|
||||
Parameters = method.ParameterList.Parameters.Select(static p => new NativeParameter
|
||||
{
|
||||
Name = p.Identifier.ValueText,
|
||||
Name = p.Identifier.Text, // .Text preserves @ prefix for reserved keywords (e.g. @params, @base)
|
||||
TypeName = NormalizeType(p.Type?.ToString() ?? "void"),
|
||||
}).ToArray(),
|
||||
IsDllImport = method.AttributeLists.SelectMany(static a => a.Attributes).Any(static a => a.Name.ToString().Contains("DllImport", StringComparison.Ordinal)),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
96
src/Tools/Ghost.NativeWrapperGen/configs/nvtt.json
Normal file
96
src/Tools/Ghost.NativeWrapperGen/configs/nvtt.json
Normal file
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"libraryName": "nvtt",
|
||||
"nativeNamespace": "Ghost.Nvtt",
|
||||
"outputNamespace": "Ghost.Nvtt",
|
||||
"nativeTypePrefix": "Nvtt",
|
||||
"skipTypes": [
|
||||
"NativeAnnotationAttribute",
|
||||
"NativeTypeNameAttribute"
|
||||
],
|
||||
"skipFunctions": [],
|
||||
|
||||
"remaps": [
|
||||
{
|
||||
// sbyte* parameters whose names match name/filename/path/prop patterns
|
||||
// get remapped to ReadOnlySpan<byte>, with a fixed() wrapper and _len sibling consumed.
|
||||
"src": "sbyte*",
|
||||
"dst": "ReadOnlySpan<byte>",
|
||||
"scope": [ "parameter" ],
|
||||
"filter": [ ".*name", ".*filename", ".*path", ".*prop$" ],
|
||||
"adapter": {
|
||||
"convertBack": {
|
||||
"wrapCall": "fixed (byte* p$arg = $arg) { $CALL }",
|
||||
"passAs": "(sbyte*)p$arg"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"actions": [
|
||||
{
|
||||
// Dispose pattern: void return + T* param + name matches .*Destroy<Struct> → IDisposable.Dispose on T
|
||||
// Must come BEFORE the general INSTANCE_METHOD rule so it matches first.
|
||||
"comment": "Dispose pattern: void return + T* param → Dispose method on T",
|
||||
"filter": "EXTERN_API",
|
||||
"conditions": [ "VOID_RETURN", "SELF_PTR", "NAME_CONDITION(.*Destroy$TBare)" ],
|
||||
"targetType": "FIRST_PARAM_TYPE",
|
||||
"apply": [
|
||||
{
|
||||
"type": "INSTANCE_METHOD",
|
||||
"opts": {
|
||||
"removeFirstParam": true,
|
||||
"passAs": "($TSelf*)System.Runtime.CompilerServices.Unsafe.AsPointer(ref this)",
|
||||
"name": {
|
||||
"set": "Dispose"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "INHERITANCE",
|
||||
"opts": {
|
||||
"baseType": [ "System.IDisposable" ]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
// Instance method: first param is T* of a known binding struct → method on T
|
||||
"comment": "First param is T* of a known binding struct → instance method on T",
|
||||
"filter": "EXTERN_API",
|
||||
"conditions": [ "SELF_PTR" ],
|
||||
"targetType": "FIRST_PARAM_TYPE",
|
||||
"apply": {
|
||||
"type": "INSTANCE_METHOD",
|
||||
"opts": {
|
||||
"removeFirstParam": true,
|
||||
"passAs": "($TSelf*)System.Runtime.CompilerServices.Unsafe.AsPointer(ref this)",
|
||||
// Change "nvttCreateSurface" to "Create"
|
||||
"name": {
|
||||
"remove": [
|
||||
"PREFIX",
|
||||
"NO_PREFIX($TSelf)" // NO_PREFIX(NvttSurface) will change "NvttSurface" to "Surface", the prefix is determined by the "nativeTypePrefix" field at the top level of this config
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
// Static method: first param is NOT a known struct pointer, but return type is T* → static on T
|
||||
"comment": "Return type is T* of a known binding struct → static method on T",
|
||||
"filter": "EXTERN_API",
|
||||
"conditions": [ "FIRST_PARAM_OTHER_TYPE", "RETURN_BINDING_TYPE" ],
|
||||
"targetType": "RETURN_TYPE",
|
||||
"apply": {
|
||||
"type": "STATIC_METHOD",
|
||||
"opts": {
|
||||
"name": {
|
||||
"remove": [
|
||||
"PREFIX",
|
||||
"NO_PREFIX($TSelf)"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,221 +1,71 @@
|
||||
{
|
||||
"libraryName": "ufbx",
|
||||
"nativeNamespace": "Ghost.Ufbx",
|
||||
"wrapperNamespace": "Ghost.Ufbx",
|
||||
"outputNamespace": "Ghost.Ufbx",
|
||||
"nativeTypePrefix": "ufbx_",
|
||||
"staticApiClassName": "Ufbx",
|
||||
"skipTypes": [
|
||||
"NativeAnnotationAttribute",
|
||||
"NativeTypeNameAttribute"
|
||||
],
|
||||
"wrappers": {
|
||||
"defaultKind": "struct",
|
||||
"defaultOwnedKind": "class",
|
||||
"kinds": {
|
||||
"ufbx_node": "ref struct",
|
||||
"ufbx_mesh": "ref struct",
|
||||
"ufbx_element": "ref struct",
|
||||
"ufbx_anim": "ref struct",
|
||||
"ufbx_material": "ref struct",
|
||||
"ufbx_texture": "ref struct",
|
||||
"ufbx_props": "ref struct",
|
||||
"ufbx_prop": "ref struct",
|
||||
"ufbx_load_opts": "class"
|
||||
}
|
||||
},
|
||||
"specialTypes": {
|
||||
"strings": [
|
||||
{
|
||||
"type": "ufbx_string",
|
||||
"dataField": "data",
|
||||
"lengthField": "length",
|
||||
"charSize": 8,
|
||||
"encoding": "utf8",
|
||||
"emitRawSpanProperty": true,
|
||||
"emitStringProperty": true
|
||||
"skipFunctions": [],
|
||||
|
||||
"remaps": [
|
||||
{
|
||||
// sbyte* parameters whose names match name/filename/path/prop patterns
|
||||
// get remapped to ReadOnlySpan<byte>, with a fixed() wrapper and _len sibling consumed.
|
||||
"src": "sbyte*",
|
||||
"dst": "ReadOnlySpan<byte>",
|
||||
"scope": [ "parameter" ],
|
||||
"filter": [ ".*name", ".*filename", ".*path", ".*prop$" ],
|
||||
"derivesFrom": { "paramSuffix": "_len", "expr": "(nuint)$arg.Length" },
|
||||
"adapter": {
|
||||
"convertBack": {
|
||||
"wrapCall": "fixed (byte* p$arg = $arg) { $CALL }",
|
||||
"passAs": "(sbyte*)p$arg"
|
||||
}
|
||||
}
|
||||
],
|
||||
"blobs": [
|
||||
{
|
||||
"type": "ufbx_blob",
|
||||
"dataField": "data",
|
||||
"lengthField": "size",
|
||||
"elementType": "byte"
|
||||
}
|
||||
],
|
||||
|
||||
"actions": [
|
||||
{
|
||||
// Instance method: first param is T* of a known binding struct → method on T
|
||||
"comment": "First param is T* of a known binding struct → instance method on T",
|
||||
"filter": "EXTERN_API",
|
||||
"conditions": [ "SELF_PTR" ],
|
||||
"targetType": "FIRST_PARAM_TYPE",
|
||||
"apply": {
|
||||
"type": "INSTANCE_METHOD",
|
||||
"opts": {
|
||||
"removeFirstParam": true,
|
||||
"passAs": "($TSelf*)System.Runtime.CompilerServices.Unsafe.AsPointer(ref this)",
|
||||
// Change "ufbx_free_scene" to "free"
|
||||
"name": {
|
||||
"remove": [
|
||||
"PREFIX",
|
||||
"NO_PREFIX($TSelf)" // NO_PREFIX(ufbx_scene) will output "scene" change "free_scene" to "free", the prefix is determined by the "nativeTypePrefix" field at the top level of this config
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ownedTypes": [
|
||||
},
|
||||
{
|
||||
"nativeType": "ufbx_scene",
|
||||
"freeFunction": "ufbx_free_scene",
|
||||
"retainFunction": "ufbx_retain_scene",
|
||||
"wrapperKind": "class"
|
||||
// Static method: first param is NOT a known struct pointer, but return type is T* → static on T
|
||||
"comment": "Return type is T* of a known binding struct → static method on T",
|
||||
"filter": "EXTERN_API",
|
||||
"conditions": [ "FIRST_PARAM_OTHER_TYPE", "RETURN_BINDING_TYPE" ],
|
||||
"targetType": "RETURN_TYPE",
|
||||
"apply": {
|
||||
"type": "STATIC_METHOD",
|
||||
"opts": {
|
||||
"name": {
|
||||
"remove": [
|
||||
"PREFIX",
|
||||
"NO_PREFIX($TSelf)"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"staticMethods": [
|
||||
{
|
||||
"nativeFunction": "ufbx_load_file_len",
|
||||
"methodName": "LoadFile",
|
||||
"throwOnNullReturn": true,
|
||||
"failureMessageMember": "description",
|
||||
"parameters": [
|
||||
{
|
||||
"native": "filename",
|
||||
"adapter": "utf8Path",
|
||||
"publicName": "pathUtf8"
|
||||
},
|
||||
{
|
||||
"native": "filename_len",
|
||||
"adapter": "utf8Length",
|
||||
"source": "pathUtf8"
|
||||
},
|
||||
{
|
||||
"native": "opts",
|
||||
"adapter": "getPtr",
|
||||
"type": "LoadOpts",
|
||||
"publicName": "options"
|
||||
},
|
||||
{
|
||||
"native": "error",
|
||||
"adapter": "errorOut",
|
||||
"type": "ufbx_error",
|
||||
"publicName": "error"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nativeFunction": "ufbx_load_memory",
|
||||
"methodName": "LoadMemory",
|
||||
"throwOnNullReturn": true,
|
||||
"failureMessageMember": "description",
|
||||
"parameters": [
|
||||
{
|
||||
"native": "data",
|
||||
"adapter": "byteSpan",
|
||||
"publicName": "data"
|
||||
},
|
||||
{
|
||||
"native": "data_size",
|
||||
"adapter": "byteSpanLength",
|
||||
"source": "data"
|
||||
},
|
||||
{
|
||||
"native": "opts",
|
||||
"adapter": "inValue",
|
||||
"type": "ufbx_load_opts",
|
||||
"publicName": "options",
|
||||
"optionalDefault": true
|
||||
},
|
||||
{
|
||||
"native": "error",
|
||||
"adapter": "errorOut",
|
||||
"type": "ufbx_error",
|
||||
"publicName": "error"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nativeFunction": "ufbx_find_node_len",
|
||||
"methodName": "FindNode",
|
||||
"parameters": [
|
||||
{
|
||||
"native": "name",
|
||||
"adapter": "utf8Path",
|
||||
"publicName": "name"
|
||||
},
|
||||
{
|
||||
"native": "name_len",
|
||||
"adapter": "utf8Length",
|
||||
"source": "name"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nativeFunction": "ufbx_find_element_len",
|
||||
"methodName": "FindElement",
|
||||
"parameters": [
|
||||
{
|
||||
"native": "name",
|
||||
"adapter": "utf8Path",
|
||||
"publicName": "name"
|
||||
},
|
||||
{
|
||||
"native": "name_len",
|
||||
"adapter": "utf8Length",
|
||||
"source": "name"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nativeFunction": "ufbx_find_material_len",
|
||||
"methodName": "FindMaterial",
|
||||
"parameters": [
|
||||
{
|
||||
"native": "name",
|
||||
"adapter": "utf8Path",
|
||||
"publicName": "name"
|
||||
},
|
||||
{
|
||||
"native": "name_len",
|
||||
"adapter": "utf8Length",
|
||||
"source": "name"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nativeFunction": "ufbx_find_anim_stack_len",
|
||||
"methodName": "FindAnimStack",
|
||||
"parameters": [
|
||||
{
|
||||
"native": "name",
|
||||
"adapter": "utf8Path",
|
||||
"publicName": "name"
|
||||
},
|
||||
{
|
||||
"native": "name_len",
|
||||
"adapter": "utf8Length",
|
||||
"source": "name"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nativeFunction": "ufbx_find_prop_len",
|
||||
"methodName": "FindProp",
|
||||
"parameters": [
|
||||
{
|
||||
"native": "name",
|
||||
"adapter": "utf8Path",
|
||||
"publicName": "name"
|
||||
},
|
||||
{
|
||||
"native": "name_len",
|
||||
"adapter": "utf8Length",
|
||||
"source": "name"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"marshalledTypes": [
|
||||
{
|
||||
"nativeType": "ufbx_load_opts",
|
||||
"marshalledProperties": [
|
||||
{
|
||||
"native": "obj_mtl_path",
|
||||
"type": "cstring"
|
||||
},
|
||||
{
|
||||
"native": "filename",
|
||||
"type": "cstring"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"typeNameOverrides": {
|
||||
"ufbx_string": "UfbxString",
|
||||
"ufbx_blob": "UfbxBlob"
|
||||
},
|
||||
"publicTypeOverrides": {
|
||||
"ufbx_string": "string",
|
||||
"ufbx_blob": "ReadOnlySpan<byte>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user