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