Skip to main content
InteropGenerator is a Roslyn source generator that automatically generates interop code for structs in FFXIVClientStructs. It provides function pointers, fixed-size array accessors, inheritance helpers, and signature resolution at compile time.

Overview

InteropGenerator is a compile-time tool that runs as a C# source generator during the build process. It scans structs marked with [GenerateInterop] and automatically generates:
  • Function pointer wrappers with type-safe accessors
  • Fixed-size array properties with bounds checking
  • Inheritance chain helpers for struct composition
  • Signature resolution and address initialization code
  • String and BitArray accessors for byte arrays

Project Structure

  • InteropGenerator - The source generator (.csproj with IsRoslynComponent=true)
  • InteropGenerator.Runtime - Runtime attributes and support types
  • InteropGenerator.Tests - Unit tests for the generator

Installation

The generator is automatically included when you reference FFXIVClientStructs. It runs during compilation and requires no manual invocation.
<ItemGroup>
  <ProjectReference Include="..\FFXIVClientStructs\FFXIVClientStructs.csproj" />
</ItemGroup>

Configuration

You can configure the interop namespace in your .csproj file:
<PropertyGroup>
  <InteropGenerator_InteropNamespace>YourProject.Generated</InteropGenerator_InteropNamespace>
</PropertyGroup>
Default: InteropGenerator.Runtime.Generated

Attributes

GenerateInterop

Marks a struct for interop code generation. This is the primary attribute that enables all generator features.
[GenerateInterop]
[StructLayout(LayoutKind.Explicit, Size = 0x10)]
public unsafe partial struct MyStruct {
    // Fields here
}
Parameters:
  • isInherited (bool) - Set to true if this struct is inherited by another struct using the Inherits attribute
Requirements:
  • Struct must be partial
  • Struct must have unsafe modifier if using pointers

FixedSizeArray

Generates type-safe accessors for fixed-size arrays. Use with private FixedSizeArray#<T> fields.
[FieldOffset(0x00)] 
[FixedSizeArray<byte>(isString: true)] 
private FixedSizeArray64<byte> _name;

// Generator creates:
// public Span<byte> Name => ...
// public string NameString { get; set; }
Parameters:
  • isString (bool) - Generate string accessors for byte/char arrays
  • isBitArray (bool) - Generate BitArray accessor for byte arrays
  • bitCount (int) - Required when isBitArray is true

MemberFunction

Marks a method as having a game function signature for resolution.
[MemberFunction("E8 ?? ?? ?? ?? 48 8B 5C 24 ?? 48 83 C4 20")]
public partial bool Update();

StaticAddress

Marks a static field/property that should be resolved to a game address.
[StaticAddress("48 8D 0D ?? ?? ?? ??", 3)]
public static partial MyStruct* Instance();
Parameters:
  • signature (string) - The signature pattern
  • offset (params ushort[]) - Relative follow offsets
  • isPointer (bool) - Whether the address points to a pointer

Inherits

Specifies that this struct inherits fields from another struct.
[GenerateInterop]
[Inherits<BaseStruct>]
public unsafe partial struct DerivedStruct {
    // Automatically includes BaseStruct fields
}

VirtualFunction

Marks a method as a virtual function that should be accessed via vtable.
[VirtualFunction(5)]
public partial void DoSomething();

BitField

Defines a bit field within a larger integral type.
[FieldOffset(0x00)]
[BitField<byte>("Flag1", offset: 0, length: 1)]
[BitField<byte>("Flag2", offset: 1, length: 1)]
[BitField<byte>("Counter", offset: 2, length: 6)]
private byte _flags;

// Generator creates properties:
// public byte Flag1 { get; set; }
// public byte Flag2 { get; set; }
// public byte Counter { get; set; }

Generated Code

The generator creates a .InteropGenerator.g.cs file for each struct with:

Function Pointer Properties

// From: [MemberFunction("E8 ?? ?? ?? ??")]
//       public partial bool Update();

private static delegate* unmanaged<MyStruct*, bool> _updatePtr;
public bool Update() => _updatePtr(this);

Fixed Array Accessors

// From: [FixedSizeArray<byte>(isString: true)]
//       private FixedSizeArray64<byte> _name;

public Span<byte> Name => _name;
public string NameString {
    get => Encoding.UTF8.GetString(Name);
    set => Encoding.UTF8.GetBytes(value, Name);
}

Address Resolution

For all structs with signatures, a resolver initializer is generated:
namespace InteropGenerator.Runtime.Generated {
    public static class Addresses {
        public static void Initialize(ResolverContext context) {
            // Resolve all signatures and populate function pointers
        }
    }
}

Build Process

  1. Analysis Phase: Generator scans all [GenerateInterop] structs
  2. Parsing Phase: Extracts fields, methods, attributes, and inheritance info
  3. Grouping Phase: Groups structs with their inherited types
  4. Rendering Phase: Generates .g.cs files with interop code
  5. Integration: Generated files are automatically included in compilation

Usage Example

using InteropGenerator.Runtime.Attributes;

namespace FFXIVClientStructs.FFXIV.Client.Game {
    [GenerateInterop]
    [StructLayout(LayoutKind.Explicit, Size = 0x18)]
    public unsafe partial struct InventoryItem {
        [FieldOffset(0x00)] public uint ItemId;
        [FieldOffset(0x04)] public uint Quantity;
        
        [FieldOffset(0x08)]
        [FixedSizeArray<byte>(isString: true)]
        private FixedSizeArray16<byte> _crafterId;
        
        [MemberFunction("E8 ?? ?? ?? ?? 84 C0 74 4C")]
        public partial bool IsHQ();
    }
}
Generated output provides:
  • CrafterId property (Span<byte>)
  • CrafterIdString property (string)
  • IsHQ() implementation with function pointer

Troubleshooting

Generator Not Running

Problem: No .g.cs files are generated. Solutions:
  • Ensure struct is marked partial
  • Verify [GenerateInterop] attribute is present
  • Check that InteropGenerator is referenced in the project
  • Clean and rebuild the solution

”Struct must be partial” Error

Problem: Compiler error about struct not being partial. Solution: Add partial keyword to struct declaration:
public unsafe partial struct MyStruct { } // Correct
public unsafe struct MyStruct { }         // Wrong

Inheritance Not Working

Problem: Inherited struct fields not accessible. Solutions:
  • Mark the base struct with [GenerateInterop(isInherited: true)]
  • Ensure the derived struct uses [Inherits<BaseStruct>]
  • Verify both structs are in the same assembly or the base is accessible

Fixed Array Not Generating

Problem: Fixed array properties not created. Solutions:
  • Field must be private
  • Field type must be FixedSizeArray#<T> where # is the size
  • Ensure [FixedSizeArray] attribute is present

Signature Resolution Failing

Problem: Function pointers are null at runtime. Solutions:
  • Verify signature is correct and exists in the game binary
  • Check that Addresses.Initialize() is called during initialization
  • Enable signature scanning debug output
  • Ensure the game version matches the expected patterns

Build Performance Issues

Problem: Slow builds with many generated structs. Solutions:
  • Use incremental builds (default in modern MSBuild)
  • Avoid unnecessary rebuilds by not changing generator attributes
  • The generator is incremental - it only regenerates changed structs

Technical Details

Generator Type

InteropGenerator uses IIncrementalGenerator for optimal performance:
  • Only processes changed files
  • Caches parsed results
  • Parallelizes independent work
  • Minimal memory overhead

Target Framework

  • Generator: netstandard2.0 (required for Roslyn)
  • Runtime: net10.0 (matches FFXIVClientStructs)

Dependencies

  • Microsoft.CodeAnalysis.CSharp 5.0.0
  • PolySharp 1.15.0 (for polyfills)

Advanced Topics

Custom Resolver Integration

You can integrate with custom signature resolvers:
var context = new ResolverContext(scannerInstance);
InteropGenerator.Runtime.Generated.Addresses.Initialize(context);

Multi-level Inheritance

The generator supports deep inheritance chains:
[GenerateInterop(isInherited: true)]
public partial struct Base { }

[GenerateInterop(isInherited: true)]
[Inherits<Base>]
public partial struct Middle { }

[GenerateInterop]
[Inherits<Middle>]
public partial struct Derived { }
// Derived gets fields from both Base and Middle

Mixing Generated and Manual Code

You can combine generated code with manual implementations:
[GenerateInterop]
public unsafe partial struct MyStruct {
    // Generated
    [MemberFunction("E8 ?? ?? ?? ??")]
    public partial bool Update();
    
    // Manual
    public void CustomMethod() {
        if (Update()) {
            // Custom logic
        }
    }
}

See Also

Build docs developers (and LLMs) love