Files
GhostEngine/src/Tools/Ghost.NativeWrapperGen
Misaki a00cb27529 feat(wrapper): span-based interop, resource API refactor
Refactored native wrappers to use ReadOnlySpan<T> for pointer parameters, improving .NET safety and interop. Enhanced wrapper generator with $TYPE and prefix/suffix-based parameter remapping. Added platform-specific native library loading for meshoptimizer, nvtt, and ufbx. Updated D3D12GraphicsEngineFactory for native DLL resolution and removed redundant logic from UnitTestApp. Changed RenderGraphBuilder's resource extraction API to use QueryTextureExtraction/QueryBufferExtraction with explicit handles and flags. Removed IRenderer and D3D12Renderer, moving RenderContext to RenderPipeline. Improved mesh loading, resource management, and updated test shader conventions. Updated project references, build settings, and added launchSettings.json for tooling.

BREAKING CHANGE: Native wrapper APIs now use ReadOnlySpan<T> instead of pointers. RenderGraphBuilder resource extraction API has changed. IRenderer and D3D12Renderer have been removed.
2026-04-01 14:50:20 +09:00
..
2026-03-15 02:19:40 +09:00

Ghost.NativeWrapperGen

Ghost.NativeWrapperGen is a CLI tool that reads low-level generated C# native bindings (e.g. ClangSharp output) and emits a thin, config-driven wrapper layer that routes DllImport functions from the Api class onto the native struct types they belong to.

It is built for bindings that look like ClangSharp output:

  • many unsafe partial struct native types
  • raw pointer parameters like NvttSurface*
  • native methods inside a static Api class decorated with [DllImport]

What it generates

For each routed function the generator emits a method on the appropriate partial struct in a *.nativegen.cs file. No wrapper classes, no properties, no owned/marshalled types — just methods.

Example: nvttDestroySurface(NvttSurface*)public void Dispose() on NvttSurface

// NvttSurface.nativegen.cs  (auto-generated)
public unsafe partial struct NvttSurface : System.IDisposable
{
    // From: nvttCreateSurface()
    public static NvttSurface* Create() => Api.nvttCreateSurface();

    // From: nvttDestroySurface(NvttSurface*)
    public void Dispose()
    {
        Api.nvttDestroySurface((NvttSurface*)System.Runtime.CompilerServices.Unsafe.AsPointer(ref this));
    }

    // From: nvttSurfaceWidth(NvttSurface*)
    public int Width()
    {
        return Api.nvttSurfaceWidth((NvttSurface*)System.Runtime.CompilerServices.Unsafe.AsPointer(ref this));
    }
}

CLI usage

dotnet run --project src/Tools/Ghost.NativeWrapperGen/Ghost.NativeWrapperGen.csproj -- \
  --config <config-path> \
  --input  <generated-binding-folder> \
  --output <wrapper-output-folder>

Arguments:

  • --config — JSON config for one library
  • --input — folder containing generated .cs binding files (ClangSharp output)
  • --output — folder where *.nativegen.cs files are written

The generator deletes all existing *.nativegen.cs files in the output folder before writing new ones.

Validated commands

dotnet run --project src/Tools/Ghost.NativeWrapperGen/Ghost.NativeWrapperGen.csproj -- --config src/Tools/Ghost.NativeWrapperGen/configs/nvtt.json --input src/ThridParty/Ghost.Nvtt/Generated --output src/ThridParty/Ghost.Nvtt/WrapperGen
dotnet run --project src/Tools/Ghost.NativeWrapperGen/Ghost.NativeWrapperGen.csproj -- --config src/Tools/Ghost.NativeWrapperGen/configs/ufbx.json --input src/ThridParty/Ghost.Ufbx/Generated --output src/ThridParty/Ghost.Ufbx/Wrapper

Important files

  • src/Tools/Ghost.NativeWrapperGen/Program.cs
  • src/Tools/Ghost.NativeWrapperGen/Config/WrapperConfig.cs
  • src/Tools/Ghost.NativeWrapperGen/Parsing/BindingParser.cs
  • src/Tools/Ghost.NativeWrapperGen/Transform/NamingConventions.cs
  • src/Tools/Ghost.NativeWrapperGen/Transform/JsonConverter.cs
  • src/Tools/Ghost.NativeWrapperGen/Emit/WrapperGeneratorEmitter.cs
  • src/Tools/Ghost.NativeWrapperGen/configs/nvtt.json
  • src/Tools/Ghost.NativeWrapperGen/configs/ufbx.json

Config schema

Top-level fields:

Field Type Description
libraryName string Human-readable library name
nativeNamespace string Namespace of the low-level generated bindings
outputNamespace string Namespace used in emitted files
nativeTypePrefix string Prefix stripped when deriving method names (e.g. "Nvtt", "ufbx_")
skipTypes string[] Types ignored by parsing/emission
skipFunctions string[] Native function names excluded from routing
remaps RemapConfig[] Parameter type remappings (e.g. sbyte*ReadOnlySpan<byte>)
actions ActionConfig[] Routing rules, evaluated in order (first match wins)

remaps

Each remap entry replaces a native parameter type with a C# type at the call site.

{
  "src": "sbyte*",
  "dst": "ReadOnlySpan<byte>",
  "scope": ["parameter"],
  "filter": [".*name", ".*path"],
  "adapter": {
    "convertBack": {
      "wrapCall": "fixed (byte* p$arg = $arg) { $CALL }",
      "passAs":   "(sbyte*)p$arg"
    }
  }
}

Fields:

  • src — native C# type to match
  • dst — public C# type to expose in the generated signature
  • scope"parameter" and/or "return"
  • filter — optional regex list applied to parameter names; only matching parameters are remapped
  • adapter.convertBack.wrapCall — wraps the whole call site; $arg = param name, $CALL = the Api call expression
  • adapter.convertBack.passAs — expression passed as the native argument; $arg = param name
  • derivesFrom — if set, a sibling parameter (matched by name suffix) is consumed and replaced by an expression

actions

Actions are evaluated in order; the first one whose conditions all match is used.

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

filter

Only "EXTERN_API" is supported — matches all [DllImport] methods on the Api class.

conditions

All conditions must be true for the action to match. Evaluated left-to-right.

Condition Description
SELF_PTR First parameter is T* where T is a known binding struct
FIRST_PARAM_OTHER_TYPE First parameter 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 (case-insensitive). Supports $TSelf (full struct name, e.g. NvttSurface) and $TBare (struct name with nativeTypePrefix stripped, e.g. Surface)

targetType

Determines which struct the method is emitted on.

Value Description
FIRST_PARAM_TYPE The binding struct type of the first parameter
RETURN_TYPE The binding struct type of the return value

apply

Can be a single object or an array of objects. Each apply step has a type and optional opts.

opts is dynamic — fields are read directly from the JSON object by name. Missing fields return null.

Apply type: INSTANCE_METHOD

Emits a public instance method on the target struct.

opts field Type Description
removeFirstParam bool If true, skip the first native parameter from the public signature (it becomes the self pointer)
passAs string Expression used as the first native argument. $TSelf is replaced with the struct name
name.set string Fixed method name (overrides name.remove)
name.remove array Ordered list of removal tokens applied to derive the method name (see Naming)
Apply type: STATIC_METHOD

Same as INSTANCE_METHOD but emits a public static method. No self pointer is injected.

opts field Type Description
name.set string Fixed method name
name.remove array Removal token list
Apply type: INHERITANCE

Adds one or more base types to the target struct's partial declaration header. Does not emit a method.

opts field Type Description
baseType string[] Fully-qualified interface/type names to add (e.g. ["System.IDisposable"])

Naming

When name.set is given, it is used verbatim.

Otherwise name.remove is a list of tokens applied in sequence, each followed by underscore-trimming:

Token Effect
PREFIX Strip nativeTypePrefix from the start of the name (case-insensitive)
NO_PREFIX($TSelf) Strip the struct's bare name (struct name minus nativeTypePrefix) from the start or end of the name (case-insensitive)

Examples with nativeTypePrefix = "Nvtt", struct = NvttSurface (bare = Surface):

Native name Tokens Result
nvttSurfaceWidth ["PREFIX", "NO_PREFIX($TSelf)"] Width
nvttCreateSurface ["PREFIX", "NO_PREFIX($TSelf)"] Create

Full config example

{
  "libraryName": "nvtt",
  "nativeNamespace": "Ghost.Nvtt",
  "outputNamespace": "Ghost.Nvtt",
  "nativeTypePrefix": "Nvtt",
  "skipTypes": ["NativeAnnotationAttribute", "NativeTypeNameAttribute"],
  "skipFunctions": [],

  "remaps": [
    {
      "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": [
    {
      "comment": "Dispose pattern: void return + T* param + name matches .*Destroy<Bare>",
      "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"] }
        }
      ]
    },
    {
      "comment": "First param is T* → 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)",
          "name": { "remove": ["PREFIX", "NO_PREFIX($TSelf)"] }
        }
      }
    },
    {
      "comment": "Return type is T* → 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. Build the tool
dotnet build src/Tools/Ghost.NativeWrapperGen/Ghost.NativeWrapperGen.csproj

# 2. Regenerate wrappers
dotnet run --project src/Tools/Ghost.NativeWrapperGen/Ghost.NativeWrapperGen.csproj -- --config src/Tools/Ghost.NativeWrapperGen/configs/nvtt.json --input src/ThridParty/Ghost.Nvtt/Generated --output src/ThridParty/Ghost.Nvtt/WrapperGen
dotnet run --project src/Tools/Ghost.NativeWrapperGen/Ghost.NativeWrapperGen.csproj -- --config src/Tools/Ghost.NativeWrapperGen/configs/ufbx.json --input src/ThridParty/Ghost.Ufbx/Generated --output src/ThridParty/Ghost.Ufbx/Wrapper

# 3. Build the libraries
dotnet build src/ThridParty/Ghost.Nvtt/Ghost.Nvtt.csproj
dotnet build src/ThridParty/Ghost.Ufbx/Ghost.Ufbx.csproj

Adding a new library

  1. Create src/Tools/Ghost.NativeWrapperGen/configs/mylib.json following the schema above.
  2. Run the generator against your ClangSharp output folder.
  3. Add the output folder to your .csproj as a glob: <Compile Include="WrapperGen\*.nativegen.cs" />.
  4. Build and iterate.

The key design principle: keep policy in config, keep the emitter generic.