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
// 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);
}
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