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.
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.