Skip to main content
The InteropGenerator is a C# source generator that automatically generates interop code for native memory access and function calling based on attributes applied to struct declarations.

Overview

The InteropGenerator is an incremental source generator that:
  • Processes structs marked with [GenerateInterop]
  • Generates P/Invoke-style method implementations
  • Creates address resolution code for signature scanning
  • Generates convenient accessors for fixed-size arrays and bitfields
  • Handles struct inheritance and virtual tables
  • Produces string overloads for C-string parameters

How It Works

1. Struct Discovery

The generator uses Roslyn’s ForAttributeWithMetadataName to find all structs decorated with [GenerateInterop]:
[GenerateInterop]
[StructLayout(LayoutKind.Explicit, Size = 0x800)]
public unsafe partial struct ActionManager {
    [StaticAddress("48 8D 0D ?? ?? ?? ?? F3 0F 10 13", 3)]
    public static partial ActionManager* Instance();
    
    [MemberFunction("E8 ?? ?? ?? ?? EB 41 83 FF 1C")]
    public partial BattleChara* LookupPetByOwnerObject(BattleChara* owner);
}

2. Code Generation

For each struct, the generator creates a partial class file: {StructName}.InteropGenerator.g.cs Input:
[GenerateInterop]
[StructLayout(LayoutKind.Explicit, Size = 0x100)]
public unsafe partial struct Example {
    [MemberFunction("E8 ?? ?? ?? ?? 48 8B C8")]
    public partial void DoSomething(int param);
}
Generated Output:
// <auto-generated/>
namespace YourNamespace;

unsafe partial struct Example
{
    public const int StructSize = 256;
    
    public static class Addresses
    {
        public static readonly global::InteropGenerator.Runtime.Address DoSomething = 
            new global::InteropGenerator.Runtime.Address(
                "YourNamespace.Example.DoSomething",
                "E8 ?? ?? ?? ?? 48 8B C8",
                new ushort[] {},
                new ulong[] { 0xC88B48000000E8 },
                new ulong[] { 0xFFFFFF0000FFFF },
                0
            );
    }
    
    private delegate void DoSomethingDelegate(Example* self, int param);
    private static DoSomethingDelegate? _doSomethingFunc;
    
    public partial void DoSomething(int param) {
        if (_doSomethingFunc == null)
            _doSomethingFunc = Marshal.GetDelegateForFunctionPointer<DoSomethingDelegate>(Addresses.DoSomething.Value);
        fixed (Example* self = &this)
            _doSomethingFunc(self, param);
    }
}

Generated Components

Struct Size Constant

When StructLayout specifies a Size, a constant is generated:
public const int StructSize = {size};

Address Objects

For each method with signature-based attributes (MemberFunction, StaticAddress, VirtualTable), an Address object is generated in the nested Addresses class:
public static class Addresses
{
    public static readonly global::InteropGenerator.Runtime.Address {MethodName} = 
        new global::InteropGenerator.Runtime.Address(
            "{FullTypeName}.{MethodName}",
            "{signature}",
            new ushort[] { {relativeOffsets} },
            new ulong[] { {signatureBytes} },
            new ulong[] { {maskBytes} },
            0
        );
}
Components:
  • Name: Fully qualified identifier for debugging
  • Signature: Original pattern with wildcards (??)
  • Relative Offsets: Array of offsets for following relative jumps/calls
  • Signature Bytes: Pattern converted to ulong[] for fast comparison
  • Mask Bytes: Mask indicating which bytes to compare (0xFF for known, 0x00 for wildcards)
  • Value: Resolved address (initialized to 0, filled by resolver)

Delegate Types

Function pointer delegates are generated for MemberFunction and VirtualFunction methods:
private delegate {ReturnType} {MethodName}Delegate({StructName}* self, {parameters});
private static {MethodName}Delegate? _{methodName}Func;

Method Implementations

MemberFunction Implementation

public partial {ReturnType} {MethodName}({parameters}) {
    if (_{methodName}Func == null)
        _{methodName}Func = Marshal.GetDelegateForFunctionPointer<{MethodName}Delegate>(Addresses.{MethodName}.Value);
    fixed ({StructName}* self = &this)
        return _{methodName}Func(self, {parameterNames});
}
Features:
  • Lazy initialization of function pointer
  • Automatic marshaling of this pointer for non-static methods
  • Type-safe parameter passing

VirtualFunction Implementation

public partial {ReturnType} {MethodName}({parameters}) {
    var vf = VirtualTable->{MethodName};
    fixed ({StructName}* self = &this)
        return vf(self, {parameterNames});
}
Features:
  • Direct virtual table access
  • No delegate caching needed (vtable is already a function pointer)
  • Minimal overhead

StaticAddress Implementation

public static partial {ReturnType}* {MethodName}() {
    return ({ReturnType}*)Addresses.{MethodName}.Value;
}
When isPointer: true:
public static partial {ReturnType}* {MethodName}() {
    return *({ReturnType}**)Addresses.{MethodName}.Value;
}

Virtual Table Struct

For structs with [VirtualTable] and [VirtualFunction] attributes:
[StructLayout(LayoutKind.Explicit, Size = {functionCount * 8})]
public unsafe partial struct {StructName}VirtualTable
{
    [FieldOffset({index * 8})] 
    public delegate* unmanaged<{StructName}*, {parameters}, {returnType}> {MethodName};
    
    // ... more function pointers
}

[FieldOffset(0)] public {StructName}VirtualTable* VirtualTable;
Features:
  • Explicit layout matching native vtable structure
  • Function pointers at correct offsets
  • Unmanaged calling convention

Fixed-Size Array Accessors

For fields marked with [FixedSizeArray]:
// Internal field
[FieldOffset(0x50), FixedSizeArray] 
internal FixedSizeArray24<uint> _blueMageActions;

// Generated accessor
public Span<uint> BlueMageActions => _blueMageActions.AsSpan();
String accessors (when isString: true):
[FieldOffset(0x00), FixedSizeArray(isString: true)]
internal FixedSizeArray64<byte> _name;

// Generated
public string Name {
    get => Encoding.UTF8.GetString(_name.AsSpan().TrimEnd((byte)0));
    set {
        var bytes = Encoding.UTF8.GetBytes(value);
        bytes.AsSpan().CopyTo(_name.AsSpan());
        _name[Math.Min(bytes.Length, 63)] = 0;
    }
}
BitArray accessors (when isBitArray: true):
[FieldOffset(0x100), FixedSizeArray(isBitArray: true, bitCount: 128)]
internal FixedSizeArray16<byte> _flags;

// Generated
public BitArray Flags => new BitArray(_flags.AsSpan(), 128);

BitField Properties

For fields with [BitField<T>] attributes:
[BitField<bool>(nameof(IsCompleted), 0)]
[FieldOffset(0x0A)] public byte Flags;

// Generated
public bool IsCompleted {
    get => (Flags & (1 << 0)) != 0;
    set => Flags = value ? (byte)(Flags | (1 << 0)) : (byte)(Flags & ~(1 << 0));
}
Multi-bit fields:
[BitField<byte>(nameof(Level), 0, 7)]
[FieldOffset(0x10)] public byte LevelData;

// Generated
public byte Level {
    get => (byte)((LevelData >> 0) & 0x7F);
    set => LevelData = (byte)((LevelData & ~(0x7F << 0)) | ((value & 0x7F) << 0));
}

String Overloads

For methods marked with [GenerateStringOverloads]:
// Original
[MemberFunction("E8 ?? ?? ?? ??")]
[GenerateStringOverloads]
public partial BattleChara* LookupByName(CStringPointer name, bool onlyPlayers);

// Generated: string overload
public BattleChara* LookupByName(string name, bool onlyPlayers) {
    var bytes = Encoding.UTF8.GetBytes(name + "\0");
    fixed (byte* ptr = bytes)
        return LookupByName(new CStringPointer(ptr), onlyPlayers);
}

// Generated: ReadOnlySpan<byte> overload
public BattleChara* LookupByName(ReadOnlySpan<byte> name, bool onlyPlayers) {
    Span<byte> buffer = stackalloc byte[name.Length + 1];
    name.CopyTo(buffer);
    buffer[name.Length] = 0;
    fixed (byte* ptr = buffer)
        return LookupByName(new CStringPointer(ptr), onlyPlayers);
}

Inheritance Code

For structs with [Inherits<T>], a separate file is generated: {StructName}.Inheritance.InteropGenerator.g.cs
[GenerateInterop]
[Inherits<AgentInterface>]
public partial struct AgentAchievement { }

// Generated inheritance file:
unsafe partial struct AgentAchievement
{
    // All public fields from AgentInterface
    public uint AgentId => ((AgentInterface*)Unsafe.AsPointer(ref this))->AgentId;
    
    // All public methods from AgentInterface
    public void Show() => ((AgentInterface*)Unsafe.AsPointer(ref this))->Show();
    
    // ... etc
}

Resolver Initialization

A global resolver initialization file is generated: {InteropNamespace}.Addresses.g.cs
namespace InteropGenerator.Runtime.Generated;

public static class AddressResolver
{
    public static void Register() {
        var resolver = global::InteropGenerator.Runtime.Resolver.GetInstance;
        
        // Register all addresses
        resolver.RegisterAddress(YourNamespace.Example.Addresses.DoSomething);
        resolver.RegisterAddress(YourNamespace.ActionManager.Addresses.Instance);
        // ... all other addresses
    }
}
Usage:
var resolver = Resolver.GetInstance;
resolver.Setup(modulePointer, moduleSize, version: gameVersion, cacheFile: cacheFileInfo);
AddressResolver.Register();
resolver.Resolve();

Fixed Array Type Generation

A single file is generated for all fixed array types: {InteropNamespace}.FixedArrays.g.cs
namespace InteropGenerator.Runtime.Generated;

[StructLayout(LayoutKind.Sequential, Size = 24)]
public unsafe struct FixedSizeArray24<T> where T : unmanaged {
    private fixed byte _data[24 * sizeof(T)];
    
    public Span<T> AsSpan() {
        fixed (byte* ptr = _data)
            return new Span<T>(ptr, 24);
    }
    
    public ref T this[int index] {
        get {
            if (index < 0 || index >= 24)
                throw new IndexOutOfRangeException();
            fixed (byte* ptr = _data)
                return ref ((T*)ptr)[index];
        }
    }
}

// ... FixedSizeArray10, FixedSizeArray64, etc.

Generator Configuration

Configure the generator via MSBuild properties:
<PropertyGroup>
  <InteropGenerator_InteropNamespace>InteropGenerator.Runtime.Generated</InteropGenerator_InteropNamespace>
</PropertyGroup>
Default namespace: InteropGenerator.Runtime.Generated

Performance Considerations

Incremental Generation

The generator is fully incremental:
  • Only regenerates files when their source structs change
  • Inheritance trees are cached and reused
  • Minimal impact on build times

Runtime Performance

  • Delegate caching: Function pointers cached after first use
  • Virtual functions: Direct vtable access, no caching overhead
  • Fixed arrays: Zero-cost abstraction over raw memory
  • Bitfields: Inline bit manipulation, no allocations

Code Size

  • Generated code is minimal and focused
  • String overloads use stackalloc for small strings
  • No reflection or dynamic code generation at runtime

Diagnostics

The generator includes analyzers that emit warnings/errors for:
  • Invalid attribute usage
  • Missing partial keyword
  • Incorrect method signatures
  • Invalid signature patterns
  • Mismatched inheritance hierarchies
  • Invalid bitfield configurations
Example diagnostics:
// Error: Method must be partial
[MemberFunction("E8 ?? ?? ?? ??")]
public void DoSomething(); // ❌ Missing 'partial'

// Error: Signature must not be empty
[MemberFunction("")]
public partial void Invalid(); // ❌ Empty signature

// Error: Invalid BitField type
[BitField<string>(nameof(Name), 0)] // ❌ string not supported
public byte Flags;

See Also

Build docs developers (and LLMs) love