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
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);
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
| Scenario | Approach |
|---|
| Constant strings | UTF-8 string literals ("text"u8) |
| Dynamic strings | String overload with auto-conversion |
| Performance-critical | Cache byte* conversion |
| Returned strings | Keep as byte*, convert on demand |
See Also