Overview
The [VirtualFunction] attribute is used to define wrappers for virtual member functions in native FFXIV classes. The source generator creates a virtual table struct and method wrappers that resolve function pointers through the instance’s vtable.
Attribute Definition
The zero-based index of the function in the class’s virtual table.
Basic Usage
Define a partial method with the [VirtualFunction] attribute and the vtable index:
[VirtualFunction(78)]
public partial StatusManager* GetStatusManager();
Generated Code
The source generator creates:
- A virtual table struct with function pointers at correct offsets
- A
VirtualTable field at offset 0 in your struct
- A method wrapper that calls through the vtable
Virtual Table Struct
[StructLayout(LayoutKind.Explicit)]
public unsafe struct CharacterVirtualTable
{
// Index 78 * 8 bytes per pointer = offset 624 (0x270)
[FieldOffset(624)]
public delegate* unmanaged<Character*, StatusManager*> GetStatusManager;
}
Instance Field
[FieldOffset(0)]
public CharacterVirtualTable* VirtualTable;
Method Wrapper
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public partial StatusManager* GetStatusManager()
=> VirtualTable->GetStatusManager(
(Character*)Unsafe.AsPointer(ref this));
Calling Virtual Functions
Call virtual functions just like regular methods:
// Get a character pointer
var character = GetLocalPlayerCharacter();
// Call virtual function
var statusManager = character->GetStatusManager();
The correct virtual function for the actual object type is called automatically, just like in C++.
Virtual Table Address
You can optionally specify the static vtable address for a class using [VTableAddress]:
[VTableAddress("48 8d 05 ?? ?? ?? ?? 48 89 03 48 8d 83 50 02 00 00", 3)]
public unsafe partial struct AddonRetainerTaskAsk
{
[VirtualFunction(48)]
public partial void OnSetup(uint a2, AtkValue* atkValues);
}
This generates a StaticVirtualTablePointer property that can be used for static hooking:
public static AddonRetainerTaskAskVirtualTable* StaticVirtualTablePointer
=> (AddonRetainerTaskAskVirtualTable*)Addresses.StaticVirtualTable.Value;
// Usage for hooking
var hook = Hook<OnSetupDelegate>.FromAddress(
(nint)AddonRetainerTaskAsk.StaticVirtualTablePointer->OnSetup,
OnSetupDetour);
Examples from the Codebase
UI Addon Virtual Functions
From AtkUnitBase.cs:268-419:
[VirtualFunction(3)]
public partial void Ctor();
[VirtualFunction(4)]
public partial void Dtor();
[VirtualFunction(5)]
public partial void SetPosition(short x, short y);
[VirtualFunction(6)]
public partial void SetAlpha(byte alpha);
[VirtualFunction(7)]
public partial void SetScale(float scale, bool scalePosition);
[VirtualFunction(8)]
public partial void SetScaleToHudLayoutScale();
[VirtualFunction(37)]
public partial void Focus();
[VirtualFunction(41)]
public partial void Initialize();
[VirtualFunction(43)]
public partial void OnUpdate(float delta);
Module Interfaces
From AtkModuleInterface.cs:9-78:
[VirtualFunction(0)]
public partial void Dtor(bool free);
[VirtualFunction(9)]
public partial bool IsReadyToDraw();
[VirtualFunction(66)]
public partial byte* GetStringForEventItemId(uint eventItemId, bool a3);
Event Listener Interface
From AtkModuleInterface.cs:71-78:
public interface IEventListener
{
[VirtualFunction(0)]
public partial bool ReceiveEvent(
AtkEventType eventType,
int eventParam,
AtkEvent* atkEvent,
nint atkEventData);
[VirtualFunction(1)]
public partial bool ReceiveEventWithResult(
AtkEventType eventType,
int eventParam,
AtkEvent* atkEvent,
ushort* outResult);
}
Component Virtual Functions
From AtkTexture.cs:50-54:
[VirtualFunction(0)]
public partial void Dtor();
[VirtualFunction(1), GenerateStringOverloads]
public partial bool LoadIconTexture(uint iconId, byte version);
Virtual Table Index Calculation
Virtual table indices are zero-based and measured in function pointer slots:
- Each function pointer is 8 bytes on 64-bit systems
- Index 0 is at offset 0x00
- Index 1 is at offset 0x08 (8 bytes)
- Index 78 is at offset 0x270 (78 * 8 = 624 bytes)
// Offset = Index * sizeof(void*)
// Index 78 => Offset 0x270 (624 decimal)
[FieldOffset(624)]
public delegate* unmanaged<Character*, StatusManager*> GetStatusManager;
Inheritance and Virtual Tables
Derived classes inherit their parent’s virtual table and may override or extend it:
// Base class
public partial struct AtkUnitBase
{
[VirtualFunction(3)]
public partial void Ctor();
[VirtualFunction(4)]
public partial void Dtor();
}
// Derived class adds more virtual functions
public partial struct AddonRetainerTaskAsk
{
// Inherits indices 3, 4 from AtkUnitBase
// Adds new virtual functions at higher indices
[VirtualFunction(48)]
public partial void OnSetup(uint a2, AtkValue* atkValues);
}
Best Practices
Virtual function indices must be accurate. An incorrect index will call the wrong function and likely crash.
Use IDA or Ghidra to verify virtual table layouts and function indices.
Finding Virtual Function Indices
- Open the game binary in IDA/Ghidra
- Find the class’s virtual table
- Count function pointers from the start (index 0)
- Verify the function signature matches your declaration
Virtual Functions vs Member Functions
// Use VirtualFunction for polymorphic behavior
[VirtualFunction(43)]
public partial void OnUpdate(float delta);
// Use MemberFunction for direct function calls
[MemberFunction("E8 ?? ?? ?? ?? C1 E7 0C")]
public partial void AddEvent(AtkEventType eventType, uint eventParam);
Return Types and Parameters
Virtual functions support the same parameter and return types as member functions:
// Pointers
[VirtualFunction(78)]
public partial StatusManager* GetStatusManager();
// Primitives
[VirtualFunction(8)]
public partial sbyte SetScaleToHudLayoutScale();
// Multiple parameters
[VirtualFunction(48)]
public partial void OnSetup(uint a2, AtkValue* atkValues);
Static Hooking with VTableAddress
When you need to hook a virtual function statically (before any instance exists):
// 1. Define the struct with VTableAddress
[VTableAddress("48 8d 05 ?? ?? ?? ?? 48 89 03", 3)]
public unsafe partial struct AddonRetainerTaskAsk
{
[VirtualFunction(48)]
public partial void OnSetup(uint a2, AtkValue* atkValues);
}
// 2. Create a hook using the static vtable pointer
private delegate void OnSetupDelegate(
AddonRetainerTaskAsk* addon,
uint a2,
AtkValue* atkValues);
private Hook<OnSetupDelegate> onSetupHook;
public void Initialize()
{
this.onSetupHook = Hook<OnSetupDelegate>.FromAddress(
(nint)AddonRetainerTaskAsk.StaticVirtualTablePointer->OnSetup,
this.OnSetupDetour);
this.onSetupHook.Enable();
}
private void OnSetupDetour(
AddonRetainerTaskAsk* addon,
uint a2,
AtkValue* atkValues)
{
// Your code here
this.onSetupHook.Original(addon, a2, atkValues);
}
See Also