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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user