Skip to main content

Overview

FFXIV uses UTF-8 encoded C-style strings internally, but C# uses UTF-16 strings. The library provides automatic conversion through the [GenerateStringOverloads] attribute and handles the Utf8String class used throughout the game.

The String Problem

C# and FFXIV use different string representations:
  • C# strings: UTF-16 encoded, managed objects
  • FFXIV strings: UTF-8 encoded, null-terminated byte arrays (char* in C++)
  • C# char: 16-bit type, incompatible with 8-bit UTF-8 characters
This means you cannot pass C# strings directly to native functions.

UTF-8 String Parameters

Native functions that take C strings must declare parameters as byte*:
// Correct - uses byte* for C string
[MemberFunction("E8 ?? ?? ?? ?? 48 8B F8 41 B0 01")]
public partial AtkUnitBase* GetAddonByName(byte* name, int index = 1);

// Wrong - cannot use string directly
[MemberFunction("E8 ?? ?? ?? ?? 48 8B F8 41 B0 01")]
public partial AtkUnitBase* GetAddonByName(string name, int index = 1);

GenerateStringOverloads Attribute

The [GenerateStringOverloads] attribute generates convenient wrapper methods:

Attribute Definition

ignoreArgument
string
Optional parameter name to exclude from overload generation.

Basic Usage

[MemberFunction("E8 ?? ?? ?? ?? 48 8B F8 41 B0 01")]
[GenerateStringOverloads]
public partial AtkUnitBase* GetAddonByName(byte* name, int index = 1);

Generated Overloads

The generator creates two additional overloads:

String Overload

Converts C# string to UTF-8 bytes:
public AtkUnitBase* GetAddonByName(string name, int index = 1)
{
    int nameUTF8StrLen = Encoding.UTF8.GetByteCount(name);
    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);
    }
}

ReadOnlySpan<byte> Overload

For UTF-8 string literals and byte spans:
public AtkUnitBase* GetAddonByName(ReadOnlySpan<byte> name, int index = 1)
{
    fixed (byte* namePtr = name)
    {
        return GetAddonByName(namePtr, index);
    }
}

Using String Overloads

With C# Strings

// Original byte* version
var nameBytes = "CharacterInspect"u8;
fixed (byte* ptr = nameBytes)
{
    var addon = atkModule->GetAddonByName(ptr);
}

// With string overload - much cleaner!
var addon = atkModule->GetAddonByName("CharacterInspect");

With UTF-8 String Literals (C# 11+)

// UTF-8 string literal (note the u8 suffix)
var addon = atkModule->GetAddonByName("CharacterInspect"u8);

With Byte Spans

ReadOnlySpan<byte> addonName = stackalloc byte[] 
    { (byte)'T', (byte)'e', (byte)'s', (byte)'t', 0 };
var addon = atkModule->GetAddonByName(addonName);

Examples from the Codebase

UI Module

From AtkModule.cs:70-71:
[VirtualFunction(65), GenerateStringOverloads]
public partial bool OpenMapWithMapLink(CStringPointer mapLink);

Havok Resources

From hkRootLevelContainer.cs:20-23:
[MemberFunction("48 89 5C 24 ?? 48 89 6C 24 ?? 57 48 83 EC 20 33 DB 48 8B EA"), 
 GenerateStringOverloads]
public partial hkRefVariant* FindObjectByName(byte* name, void* typeInfo);

[MemberFunction("48 89 5C 24 ?? 48 89 6C 24 ?? 57 48 83 EC 20 33 DB 48 8B EA"), 
 GenerateStringOverloads]
public partial hkRefVariant* FindObjectByType(void* typeInfo, byte* name);

Character Manager

From CharacterManager.cs:23:
[GenerateStringOverloads]
public partial BattleChara* LookupBattleCharaByName(byte* name, bool a3 = false);

Agent Context

From AgentContext.cs:61-79:
[GenerateStringOverloads]
public partial void OpenSubContextMenu(
    uint ownerAddon, 
    byte* addonName, 
    AtkEventListener* listener, 
    float x, 
    float y, 
    bool alternatePosition, 
    byte orientation);

[GenerateStringOverloads]
public partial AtkComponentList* GetSubContextMenuList(
    uint ownerAddon, 
    byte* addonName, 
    int a4);

[GenerateStringOverloads]
public partial void AddMenuItem(
    MenuTargetType* targetType, 
    uint targetId, 
    byte* text, 
    nint userParam, 
    bool disabled, 
    bool submenu, 
    bool isReturn, 
    uint displayOrder, 
    byte indicatorIcon);

Performance Considerations

Stack vs Heap Allocation

The generated string overload uses stack allocation for small strings (≤512 bytes):
// Small string - uses stack (fast)
Span<byte> nameBytes = nameUTF8StrLen <= 512 
    ? stackalloc byte[nameUTF8StrLen + 1]  // Stack
    : new byte[nameUTF8StrLen + 1];        // Heap

Caching Conversions

If you call a function repeatedly with the same string, consider caching the conversion:
// Bad - converts every frame
for (int i = 0; i < 1000; i++)
{
    var addon = atkModule->GetAddonByName("CharacterInspect");
}

// Good - convert once
var nameBytes = "CharacterInspect"u8;
fixed (byte* namePtr = nameBytes)
{
    for (int i = 0; i < 1000; i++)
    {
        var addon = atkModule->GetAddonByName(namePtr);
    }
}

String Returns

Functions never return C# string types to avoid making assumptions about memory lifetime:
// Correct - returns byte*
[VirtualFunction(66)]
public partial byte* GetStringForEventItemId(uint eventItemId, bool a3);

// Usage - convert to C# string when needed
var stringPtr = module->GetStringForEventItemId(eventItemId, false);
if (stringPtr != null)
{
    var text = Marshal.PtrToStringUTF8((nint)stringPtr);
}

Utf8String Class

FFXIV has its own string class similar to std::string. Access it directly:
// Reading from Utf8String
var utf8String = someObject->Name;
var bytes = utf8String.AsSpan();
var text = Encoding.UTF8.GetString(bytes);

// Or use helper methods if available
var text = utf8String.ToString();

Inline String Arrays

For inline string buffers in structs, use FixedSizeArray with isString: true:
[FieldOffset(0x2300), FixedSizeArray(isString: true)] 
internal FixedSizeArray7<byte> _freeCompanyTag;
This generates a String property with get/set:
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; // null terminator
    }
}

Best Practices

Never use C# string type in native function signatures. Always use byte*.
Use UTF-8 string literals ("text"u8) for better performance when available.

Guidelines

// Good - byte* with string overload
[MemberFunction("..."), GenerateStringOverloads]
public partial AtkUnitBase* GetAddonByName(byte* name, int index = 1);

// Good - explicit UTF-8 conversion
var nameBytes = Encoding.UTF8.GetBytes("AddonName\0");
fixed (byte* ptr = nameBytes)
{
    var addon = GetAddonByName(ptr);
}

// Good - UTF-8 string literal
var addon = GetAddonByName("AddonName"u8);

// Avoid - string type in signature
public partial AtkUnitBase* GetAddonByName(string name);

When to Use Each Approach

ScenarioApproach
Constant stringsUTF-8 string literals ("text"u8)
Dynamic stringsString overload with auto-conversion
Performance-criticalCache byte* conversion
Returned stringsKeep as byte*, convert on demand

See Also

Build docs developers (and LLMs) love