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

@@ -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&lt;byte&gt;").</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&lt;ActionApplyConfig&gt;.
/// </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);
}

View File

@@ -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)

View File

@@ -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)),

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);
}
}

View 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)"
]
}
}
}
}
]
}

View File

@@ -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>"
}
]
}