Skip to main content
FFXIVClientStructs provides C# wrappers for C++ STD string types. These strings use Small String Optimization (SSO) to avoid heap allocations for short strings.

StdString

A byte string wrapper for std::string using system default encoding (UTF-8).

Memory Layout

[StructLayout(LayoutKind.Explicit, Size = 0x20)]
public unsafe struct StdString
{
    [FieldOffset(0x0)] public byte* BufferPtr;  // Pointer when large
    [FieldOffset(0x0)] public fixed byte Buffer[16]; // Inline buffer when small
    [FieldOffset(0x10)] public ulong Length;    // String length in bytes
    [FieldOffset(0x18)] public ulong Capacity;  // Allocated capacity
}
Size: 32 bytes (0x20)

Small String Optimization (SSO)

The first 16 bytes serve dual purpose:

Small Mode (Length ≤ 15 bytes)

  • String stored inline in Buffer[16]
  • No heap allocation
  • Capacity ≤ 15

Large Mode (Length > 15 bytes)

  • BufferPtr points to heap-allocated buffer
  • Capacity > 15
  • First 8 bytes contain pointer
// Check mode
bool isLarge = str.Capacity > 15;

// Access data (handles both modes)
byte* data = str.BufferPtr; // Works for both modes
ulong length = str.Length;

Reading Strings

// As Span<byte>
Span<byte> bytes = stdString.AsSpan();

// Convert to C# string (UTF-8)
string text = Encoding.UTF8.GetString(stdString.AsSpan());

// ToString() uses default encoding
string text2 = stdString.ToString();

// Null-terminated pointer
byte* ptr = stdString.BufferPtr;
string text3 = Marshal.PtrToStringUTF8((nint)ptr);

Writing Strings

// From C# string
stdString.AddString("Hello, world!");

// From bytes
ReadOnlySpan<byte> utf8Bytes = Encoding.UTF8.GetBytes("Text");
stdString.AddSpanCopy(utf8Bytes);

// Clear
stdString.Clear();

// Insert
stdString.InsertString(5, " inserted");

Common Operations

// Search
int index = stdString.IndexOfString("search");
if (index >= 0)
{
    // Found at index
}

bool contains = stdString.ContainsString("substring");

// Substring operations
long lastIndex = stdString.LongLastIndexOfString("pattern");

// Length and capacity
ulong length = stdString.Length;
ulong capacity = stdString.Capacity;
stdString.Resize(newLength);
stdString.EnsureCapacity(requiredCapacity);

StdWString

A wide string wrapper for std::wstring using UTF-16 encoding.

Memory Layout

[StructLayout(LayoutKind.Explicit, Size = 0x20)]
public unsafe struct StdWString
{
    [FieldOffset(0x0)] public StdBasicString<char, IStaticEncoding.Unicode, IStaticMemorySpace.Default> BasicString;
}
Size: 32 bytes (0x20)

Small String Optimization

UTF-16 strings have smaller SSO threshold:

Small Mode

  • Length ≤ 7 characters (7 × 2 = 14 bytes + null terminator)
  • Stored inline

Large Mode

  • Length > 7 characters
  • Heap allocated

Reading Wide Strings

// As Span<char>
Span<char> chars = wstring.AsSpan();

// Direct to C# string
string text = wstring.ToString();

// Manual conversion
string text2 = new string(wstring.AsSpan());

// Access individual characters
char firstChar = wstring[0];

Writing Wide Strings

// From C# string (most common)
wstring.AddString("Unicode text: \u65E5\u672C\u8A9E");

// From char span
ReadOnlySpan<char> chars = "Text".AsSpan();
wstring.AddSpanCopy(chars);

// Character by character
wstring.AddCopy('A');
wstring.AddCopy('\u3042'); // Japanese hiragana

StdBasicString<T, TEncoding, TMemorySpace>

The generic base for string types.

Memory Layout

[StructLayout(LayoutKind.Sequential, Size = 0x20)]
public unsafe struct StdBasicString<T, TEncoding, TMemorySpace>
    where T : unmanaged, IBinaryNumber<T>
    where TEncoding : IStaticEncoding
    where TMemorySpace : IStaticMemorySpace
{
    public fixed byte BufferBytes[16]; // SSO buffer or pointer
    public ulong ULongLength;          // Length in elements
    public ulong ULongCapacity;        // Capacity in elements
}
Size: 32 bytes

SSO Thresholds

Small string capacity varies by element size:
const int BufByteSize = 16;
int ssoCapacity = (BufByteSize / sizeof(T)) - 1;

// Examples:
// byte:  15 elements (StdString)
// char:  7 elements (StdWString)
// int:   3 elements
// long:  1 element

Properties

// Pointers (automatically handle SSO mode)
T* First = basicString.First;  // Points to first element
T* Last = basicString.Last;    // Points past last element
T* End = basicString.End;      // Points to capacity end

// Size
long count = basicString.LongCount;
long capacity = basicString.LongCapacity;

// Mode detection
bool isLarge = basicString.ULongCapacity > (BufByteSize / sizeof(T) - 1);

Encoding Types

IStaticEncoding.System

System default encoding (UTF-8 on modern systems):
StdBasicString<byte, IStaticEncoding.System, IStaticMemorySpace.Default>
// Equivalent to StdString

IStaticEncoding.Unicode

UTF-16 encoding:
StdBasicString<char, IStaticEncoding.Unicode, IStaticMemorySpace.Default>
// Equivalent to StdWString

Common Patterns

Safe String Reading

// Always check for valid data
if (stdString.Length > 0)
{
    string text = stdString.ToString();
}

// Defensive copy for long operations
string safeCopy = stdString.ToString();
// Use safeCopy, stdString may be modified by game

String Comparison

// Compare strings
int cmp = StdString.Compare(str1, str2);
if (cmp == 0)
{
    // Equal
}

// Equality
if (StdString.ContentEquals(str1, str2))
{
    // Equal
}

// Case-insensitive (convert to C# first)
string s1 = str1.ToString();
string s2 = str2.ToString();
if (s1.Equals(s2, StringComparison.OrdinalIgnoreCase))
{
    // Equal (case-insensitive)
}

Building Strings Efficiently

// Pre-allocate for known size
StdString result = default;
result.EnsureCapacity(estimatedLength);

// Append multiple times
result.AddString("Part 1");
result.AddString(" Part 2");
result.AddString(" Part 3");

string final = result.ToString();
result.Dispose(); // Don't forget cleanup if you created it

Substring Extraction

// Extract substring as Span
Span<byte> substring = stdString.AsSpan(startIndex, length);
string extracted = Encoding.UTF8.GetString(substring);

// Or use LINQ-style
byte[] subArray = stdString.ToArray(startIndex, length);
string extracted2 = Encoding.UTF8.GetString(subArray);

Interoperability

Passing to Native Functions

// StdString as byte* parameter
byte* ptr = stdString.BufferPtr;
SomeNativeFunction(ptr);

// Ensure null-terminated
if (stdString.Last != null && *stdString.Last != 0)
{
    // String is null-terminated (standard for C++ strings)
}

Creating Temporary Strings

// For output parameters
StdString tempString = default;
try
{
    SomeGameFunction(&tempString);
    string result = tempString.ToString();
}
finally
{
    tempString.Dispose();
}

Fixed Strings

// Pin string during native call
fixed (byte* bufPtr = stdString.Buffer)
{
    // Buffer is pinned, safe to pass to native
    NativeFunction(bufPtr);
}

Performance Considerations

SSO Awareness

// Avoid unnecessary allocations
StdString shortString = default;
shortString.AddString("Short");  // Stays in SSO, no allocation

StdString longString = default;
longString.AddString("This is a very long string that exceeds SSO");
// Triggers heap allocation

Span-based Access

// Prefer Span for byte-level operations
Span<byte> span = stdString.AsSpan();
for (int i = 0; i < span.Length; i++)
{
    // Direct memory access, no copies
    byte b = span[i];
}

// Avoid repeated ToString()
string cached = stdString.ToString();
for (int i = 0; i < 100; i++)
{
    ProcessString(cached); // Use cached version
}

Capacity Management

// Pre-allocate when building
StdString builder = default;
builder.EnsureCapacity(1000); // Avoid reallocations

for (int i = 0; i < 100; i++)
{
    builder.AddString("Item ");
    // No reallocations if total < 1000 bytes
}

// Trim excess memory
builder.TrimExcess();

Memory Management

Disposal

// Always dispose created strings
StdString str = default;
str.AddString("Data");
try
{
    // Use string
}
finally
{
    str.Dispose(); // Frees heap allocation if large
}
Never dispose strings that are part of game structures. Only dispose strings you explicitly create.

Copy vs Move

// Copy (creates new allocation if large)
StdString copy = default;
StdString.ConstructCopyInPlace(original, out copy);

// Move (transfers ownership, no allocation)
StdString moved = default;
StdString.ConstructMoveInPlace(ref original, out moved);
// original is now empty

Common Pitfalls

String Lifetime

// BAD: Returned string may be invalidated
public string GetPlayerName()
{
    var playerName = gameStruct->Name; // StdString
    return playerName.ToString(); // May be freed by game!
}

// GOOD: Immediate copy
public string GetPlayerName()
{
    return gameStruct->Name.ToString(); // Immediate conversion
}

Encoding Issues

// BAD: Wrong encoding
StdString utf8String = /* from game */;
string text = Encoding.Unicode.GetString(utf8String.AsSpan()); // Corrupted!

// GOOD: Use correct encoding
string text = Encoding.UTF8.GetString(utf8String.AsSpan());
// Or use ToString() which handles it
string text2 = utf8String.ToString();

Null Termination

// C++ strings are null-terminated, but Length excludes null terminator
StdString str = /* ... */;
long length = str.Length; // Does NOT include null terminator

// Total buffer includes null terminator
long totalBytes = str.Length + 1; // +1 for null terminator

See Also

Build docs developers (and LLMs) love