Skip to main content

Overview

FFXIVClientStructs makes extensive use of C# Source Generators to eliminate boilerplate and create seamless native interop. Instead of writing marshalled delegates manually, the library uses function pointers with generated wrappers.
Source generators are compile-time code generation tools that run during the build process. They analyze your code and generate additional C# files automatically.

The InteropGenerator System

The InteropGenerator is a custom source generator that powers FFXIVClientStructs. It generates:
  • Function pointer wrappers for native calls
  • Virtual table structs
  • String conversion overloads
  • Fixed-size array accessors
  • BitField property accessors
  • Null safety checks

GenerateInterop Attribute

Structs that need generated code must use the [GenerateInterop] attribute:
[GenerateInterop]
[StructLayout(LayoutKind.Explicit, Size = 0x35D8)]
public unsafe partial struct Framework {
    // Generator will create wrappers for attributed methods
}
For inherited structs:
[GenerateInterop(isInherited: true)]
[Inherits<AtkEventListener>]
[StructLayout(LayoutKind.Explicit, Size = 0x238)]
public unsafe partial struct AtkUnitBase : ICreatable {
    // Generator includes base class members
}
Structs using the generator must be marked partial. Without partial, the generator cannot add its generated code.

Function Pointer Generation

MemberFunction Attribute

The [MemberFunction] attribute generates wrappers for non-virtual member functions:
[MemberFunction("E8 ?? ?? ?? ?? C1 E7 0C")]
public partial void AddEvent(
    AtkEventType eventType, 
    uint eventParam, 
    AtkEventListener* listener,
    AtkResNode* nodeParam, 
    bool isGlobalEvent
);
Generated code:
// 1. Function pointer holder
public static class MemberFunctionPointers
{
    public static delegate* unmanaged<
        AtkResNode*, 
        AtkEventType, 
        uint, 
        AtkEventListener*, 
        AtkResNode*, 
        bool, 
        void
    > AddEvent;
}

// 2. Wrapper implementation
public partial void AddEvent(
    AtkEventType eventType, 
    uint eventParam, 
    AtkEventListener* listener,
    AtkResNode* nodeParam, 
    bool isGlobalEvent
)
{
    // Null check
    if (MemberFunctionPointers.AddEvent is null)
    {
        InteropGenerator.Runtime.ThrowHelper.ThrowNullAddress(
            "MemberFunctionPointers.AddEvent", 
            "E8 ?? ?? ?? ?? C1 E7 0C"
        );
    }
    
    // Call with 'this' pointer automatically
    MemberFunctionPointers.AddEvent(
        (AtkResNode*)Unsafe.AsPointer(ref this),  // Auto-added!
        eventType, 
        eventParam, 
        listener, 
        nodeParam, 
        isGlobalEvent
    );
}
Key features:
  • Automatic this pointer passing (for non-static functions)
  • Null safety check before calling
  • Function pointer resolved from signature at runtime
  • Zero marshalling overhead

VirtualFunction Attribute

The [VirtualFunction] attribute generates wrappers for virtual member functions:
[VirtualFunction(78)]
public partial StatusManager* GetStatusManager();
Generated code:
// 1. Virtual table struct
[StructLayout(LayoutKind.Explicit)]
public unsafe struct CharacterVirtualTable
{
    // 78 * 8 = offset 624 (0x270)
    [FieldOffset(624)] 
    public delegate* unmanaged<Character*, StatusManager*> GetStatusManager;
}

// 2. VTable pointer field
[FieldOffset(0)] 
public CharacterVirtualTable* VirtualTable;

// 3. Wrapper implementation  
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public partial StatusManager* GetStatusManager() 
    => VirtualTable->GetStatusManager(
        (Character*)Unsafe.AsPointer(ref this)
    );
Benefits:
  • Calls through virtual table (respects C++ polymorphism)
  • Aggressive inlining for performance
  • No signature required (uses vtable index)
  • Type-safe function pointer

StaticAddress Attribute

The [StaticAddress] attribute resolves singleton instances:
[StaticAddress("48 8B 1D ?? ?? ?? ?? 8B 7C 24", 3, isPointer: true)]
public static partial Framework* Instance();
Generated code:
// 1. Address holder
public unsafe static class StaticAddressPointers
{
    public static Framework** ppInstance 
        => (Framework**)Framework.Addresses.Instance.Value;
}

// 2. Address registry
public static class Addresses
{
    public static readonly Address Instance = new Address(
        "FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance",
        "48 8B 1D ?? ?? ?? ?? 8B 7C 24",
        /* ... */
    );
}

// 3. Wrapper implementation
public static partial Framework* Instance()
{
    if (StaticAddressPointers.ppInstance is null)
    {
        ThrowHelper.ThrowNullAddress(
            "Framework.Instance", 
            "48 8B 1D ?? ?? ?? ?? 8B 7C 24"
        );
    }
    return *StaticAddressPointers.ppInstance;
}

String Overload Generation

GenerateStringOverloads Attribute

Since DisableRuntimeMarshalling is enabled, C string arguments must be byte*. The generator creates convenient overloads:
[MemberFunction("E8 ?? ?? ?? ?? 48 8B F8 41 B0 01")]
[GenerateStringOverloads]
public partial AtkUnitBase* GetAddonByName(byte* name, int index = 1);
Generated overloads:
// 1. String overload (UTF-16 to UTF-8 conversion)
public AtkUnitBase* GetAddonByName(string name, int index = 1)
{
    int nameUTF8StrLen = Encoding.UTF8.GetByteCount(name);
    
    // Stack allocate for small strings, heap for large
    Span<byte> nameBytes = nameUTF8StrLen <= 512 
        ? stackalloc byte[nameUTF8StrLen + 1] 
        : new byte[nameUTF8StrLen + 1];
    
    Encoding.UTF8.GetBytes(name, nameBytes);
    nameBytes[nameUTF8StrLen] = 0;  // Null terminator
    
    fixed (byte* namePtr = nameBytes)
    {
        return GetAddonByName(namePtr, index);
    }
}

// 2. ReadOnlySpan<byte> overload (for UTF-8 literals)
public AtkUnitBase* GetAddonByName(ReadOnlySpan<byte> name, int index = 1)
{
    fixed (byte* namePtr = name)
    {
        return GetAddonByName(namePtr, index);
    }
}
Usage examples:
// Using string (automatic conversion)
var addon = manager->GetAddonByName("CharacterStatus");

// Using UTF-8 literal (C# 11+, zero allocation)
var addon = manager->GetAddonByName("CharacterStatus"u8);

// Using byte* directly (manual control)
fixed (byte* namePtr = "CharacterStatus"u8)
{
    var addon = manager->GetAddonByName(namePtr);
}
The ReadOnlySpan<byte> overload exists primarily to support UTF-8 string literals, which can eliminate allocations entirely.

Ignoring Specific Arguments

Some arguments shouldn’t get string overloads:
[GenerateStringOverloads(ignoreArgument: "buffer")]
public partial void ReadIntoBuffer(byte* buffer, byte* name);
// Only generates overloads for 'name', not 'buffer'

FixedSizeArray Generation

The generator creates Span<T> accessors for inline arrays:
[FieldOffset(0x2300), FixedSizeArray(isString: true)] 
internal FixedSizeArray7<byte> _freeCompanyTag;
Generated for regular arrays:
public Span<byte> FreeCompanyTag => _freeCompanyTag;
Generated for string arrays:
public string FreeCompanyTagString
{
    get => Encoding.UTF8.GetString(
        MemoryMarshal.CreateReadOnlySpanFromNullTerminated(
            (byte*)Unsafe.AsPointer(ref _freeCompanyTag[0])
        )
    );
    set
    {
        if (Encoding.UTF8.GetByteCount(value) > 7 - 1)
        {
            ThrowHelper.ThrowStringSizeTooLarge("FreeCompanyTagString", 7);
        }
        Encoding.UTF8.GetBytes(value.AsSpan(), _freeCompanyTag);
        _freeCompanyTag[6] = 0;  // Ensure null termination
    }
}

BitField Generation

The generator creates properties for bit-packed fields:
[BitField<bool>(nameof(IsPartyMember), 0)]
[BitField<bool>(nameof(IsAllianceMember), 1)]
[BitField<bool>(nameof(IsFriend), 2)]
[FieldOffset(0x1CE2)] public byte RelationFlags;
Generated properties:
public partial bool IsPartyMember
{
    get => (RelationFlags & (1 << 0)) != 0;
    set => RelationFlags = value 
        ? (byte)(RelationFlags | (1 << 0)) 
        : (byte)(RelationFlags & ~(1 << 0));
}

public partial bool IsAllianceMember
{
    get => (RelationFlags & (1 << 1)) != 0;
    set => RelationFlags = value 
        ? (byte)(RelationFlags | (1 << 1)) 
        : (byte)(RelationFlags & ~(1 << 1));
}

public partial bool IsFriend
{
    get => (RelationFlags & (1 << 2)) != 0;
    set => RelationFlags = value 
        ? (byte)(RelationFlags | (1 << 2)) 
        : (byte)(RelationFlags & ~(1 << 2));
}
Multi-bit fields:
[BitField<byte>(nameof(ClipCount), 9, 8)]  // 8 bits starting at bit 9
[FieldOffset(0xB0)] public uint DrawFlags;
public partial byte ClipCount
{
    get => (byte)((DrawFlags >> 9) & 0xFF);
    set => DrawFlags = (DrawFlags & ~(0xFFu << 9)) | ((uint)value << 9);
}

Inheritance Generation

The [Inherits<T>] attribute generates base class integration:
[GenerateInterop(isInherited: true)]
[Inherits<GameObject>, Inherits<CharacterData>]
[StructLayout(LayoutKind.Explicit, Size = 0x2370)]
public unsafe partial struct Character {
    // Generator creates casting helpers and includes base members
}
Generated helpers:
// Implicit casts to base types
public static implicit operator GameObject*(Character* ptr) 
    => (GameObject*)ptr;

public static implicit operator CharacterData*(Character* ptr) 
    => (CharacterData*)((byte*)ptr + GameObjectSize);

Viewing Generated Code

In Your IDE

Most IDEs show generated files in the solution explorer:
  • Visual Studio: Expand the project node → Dependencies → Analyzers → InteropGenerator
  • Rider: Look in the obj/ directory for generated files

Build Output

Generated files are written to:
obj/Debug/net8.0/generated/InteropGenerator/InteropGenerator.Generator/
You can examine the generated code to understand exactly what the generators are creating. This is invaluable for debugging and learning.

Performance Impact

Compile-Time

  • Adds a few seconds to initial builds
  • Incremental builds are fast (only regenerates changed files)
  • No runtime performance cost

Runtime

  • Zero overhead - all code is generated at compile time
  • Function pointers have the same performance as direct calls
  • No reflection, no dynamic code generation
  • Aggressive inlining for hot paths

Common Patterns

Null-Safe Function Calls

Every generated function includes a null check:
public partial void SomeFunction()
{
    if (MemberFunctionPointers.SomeFunction is null)
    {
        ThrowHelper.ThrowNullAddress(/* ... */);
    }
    MemberFunctionPointers.SomeFunction(/* ... */);
}
This ensures you get a helpful error message instead of a null reference exception.

AggressiveInlining

Virtual function wrappers use aggressive inlining:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public partial StatusManager* GetStatusManager() 
    => VirtualTable->GetStatusManager(
        (Character*)Unsafe.AsPointer(ref this)
    );
This eliminates the wrapper overhead entirely in optimized builds.

Stack Allocation Optimization

String overloads use stack allocation for small strings:
// Small strings (<= 512 bytes): stack allocated, zero heap pressure
Span<byte> nameBytes = nameUTF8StrLen <= 512 
    ? stackalloc byte[nameUTF8StrLen + 1] 
    : new byte[nameUTF8StrLen + 1];
This optimization means most string conversions never allocate on the heap, reducing GC pressure.

Best Practices

Always Use Partial

// ✅ Required for generators
public unsafe partial struct Character { }

// ❌ Won't compile with generators
public unsafe struct Character { }

Prefer UTF-8 Literals

// ✅ Best - zero allocation
var addon = manager->GetAddonByName("CharacterStatus"u8);

// ⚠️ OK but allocates
var addon = manager->GetAddonByName("CharacterStatus");

Cache Function Pointers When Appropriate

For very hot loops, consider caching:
// If the function is called millions of times
var getStatusMgr = Character.VirtualTable->GetStatusManager;
for (int i = 0; i < veryLargeCount; i++)
{
    var statusMgr = getStatusMgr(character);
    // Use statusMgr...
}
However, for most use cases, the generated wrappers are already optimal.

Build docs developers (and LLMs) love