Getting Started Welcome Setting Up Game Tree GuiRoot Method
Writing a Memory Library Reading and Writing Page Iterator Pattern Scanning Buffer Strategies
Writing a Roblox Library C++ String

C++ String

In this guide, we will learn how std::string is laid out in memory and how to read it externally.

When reading std::string from an external process on MSVC x64, we need to understand its exact memory layout. The MSVC implementation of std::string is exactly 32 bytes and includes an optimization called Short String Optimization (SSO).

Memory Layout (x64 MSVC)

The 32-byte structure is divided into three parts:

  • Offset 0 (16 bytes): union _Bxty — either char _Buf[16] (short string data stored inline) or char* _Ptr (pointer to heap-allocated buffer for long strings)
  • Offset 16 (8 bytes): size_t _Mysize — the current length of the string
  • Offset 24 (8 bytes): size_t _Myres — the capacity of the string. If > 15 (_BUF_SIZE), it is a long string using _Ptr. If <= 15, it is a short string using _Buf directly.
flowchart TD subgraph "std::string (32 bytes)" BX["Offset 0: _Bxty union (16 bytes)"] SZ["Offset 16: _Mysize (8 bytes)"] CAP["Offset 24: _Myres (8 bytes)"] end CAP -->|_Myres > 15| LONG["Long: _Bx._Ptr → heap buffer"] CAP -->|_Myres <= 15| SHORT["Short: _Bx._Buf[0..15] inline"]
packet-beta 0-15: "_Bx union" 16-23: "_Mysize" 24-31: "_Myres"

When _Myres > 15, the string is stored on the heap and the first 8 bytes of the union contain a pointer to the heap buffer. When _Myres <= 15, the string data fits entirely within the 16-byte _Buf array and no heap allocation occurs.

Reading std::string in Zig

The following Zig code reads a std::string from external process memory, handling both the short and long string cases.

pub fn readString(memory_accessor: MemoryAccessor, str_address: usize, buffer: []u8) ![]const u8 {
    // Read the entire data structure at once:
    const StringData = extern struct {
        sso: extern union {
            short: [16]u8,
            ptr: usize,
        },               // _Bxty union (short data or heap pointer)
        len: usize,      // _Mysize
        capacity: usize, // _Myres
    };
    
    const string_data = try memory_accessor.read(str_address, StringData);
    if(buffer.len < string_data.len)
        return error.InsufficientBufferSize;

    const result = buffer[0..string_data.len];
    if(string_data.capacity < 16) {
        // Short string: string contains the data directly
        @memcpy(result, string_data.sso.short[0..string_data.len]);
        return result;
    }
    // Long string: first 8 bytes are the heap pointer
    try memory_accessor.readSlice(string_data.sso.ptr, u8, result);
    return result;
}

The StringData extern struct mirrors the 32-byte MSVC std::string layout exactly, so reading it requires just one memory_accessor.read call. readString then inspects capacity (aka _Myres) to decide whether to dereference a heap pointer (long string) or copy inline bytes (short string), returning a directly usable []u8 slice.

Example: Reading a Roblox Instance Name

Roblox Instance objects store their name as a std::string pointer at the Name offset from the config. Here is how you would read it:

pub fn main() !void {
    const process_handle = win32.GetCurrentProcess(); // Replace with OpenProcess
    const memory_accessor: MemoryAccessor = .init(process_handle);

    const instance_ptr: usize = 0x12345678; // Replace with actual address

    var buffer: [4096]u8 = undefined;
    const name_address = try memory_accessor.read(instance_ptr + Config.get("Offset/Instance/Name"), usize);
    const name = try readString(memory_accessor, name_address, &buffer);

    std.debug.print("Instance name: {s}\n", .{name});
}