Memory Layout & FFI Bridge

To interface a high-level managed runtime (.NET 8.0) with a safe native systems language (Rust) without incurring garbage collection or marshalling overhead, CrawlCipher (cc) uses a zero-copy Foreign Function Interface (FFI) based on standard C-ABI specifications.


1. Managed Lifetime Control (GCHandle)

The C# Game Engine is instantiated on the managed heap. Normally, the .NET Garbage Collector (GC) dynamically moves objects in memory to defragment the heap. If the GC moves the engine while Rust holds a raw pointer to it, subsequent Rust invocations would access corrupt memory or segfault.

To prevent this:

  1. When CreateGame is called, the C# engine allocates the GameEngine instance.
  2. It wraps the instance in a pinned GCHandle:
    var engine = new GameEngine(config);
    var handle = GCHandle.Alloc(engine, GCHandleType.Normal);
    return GCHandle.ToIntPtr(handle);
  3. This integer pointer (IntPtr) is returned to Rust. The GC is instructed to treat this object as a root, preventing it from being collected or moved.
  4. When invoking engine updates, Rust passes this pointer back. C# resolves the object instance:
    var handle = GCHandle.FromIntPtr(gamePtr);
    var engine = (GameEngine)handle.Target!;
  5. When the TUI is closed, Rust calls DestroyGame, which frees the handle, letting the GC reclaim the memory:
    var handle = GCHandle.FromIntPtr(gamePtr);
    handle.Free();

2. ABI Struct Alignments & Padding

For zero-copy reading, Rust and C# must agree exactly on the byte representation of structs. The engine enforces this using sequential layouts:

C# Definition

[StructLayout(LayoutKind.Sequential)]
public struct PlayerStateFFI
{
    public int PlayerId;
    public int PositionX;
    public int PositionY;
    public int Energy;
    public int Score;
    public int Kills;
    public int Length;
    public int Direction;
    public int IsIdle;
    public int IsAutopilot;
}

Rust Definition

#[repr(C)]
pub struct PlayerState {
    pub player_id: i32,
    pub position_x: i32,
    pub position_y: i32,
    pub energy: i32,
    pub score: i32,
    pub kills: i32,
    pub length: i32,
    pub direction: i32,
    pub is_idle: i32,
    pub is_autopilot: i32,
}

Key Alignment Rules:

  • Integer Types: int in C# is mapped strictly to i32 in Rust (4 bytes).
  • Boolean Logic: C# managed bool sizes can vary by compiler target. To guarantee cross-platform memory alignment, booleans are marshalled as i32 integers (0 = false, 1 = true).
  • Structure Padding: Because all fields are 4-byte integers, the structure aligns perfectly to 4-byte boundaries with zero implicit padding bytes generated by the compiler.

3. String & Array Handling across the FFI

Managed C# string heap references cannot be passed directly to Rust. To pass strings (e.g. item asset codes or UUIDs), the engine uses fixed-size unmanaged byte arrays:

[StructLayout(LayoutKind.Sequential)]
public struct InventoryItemFFI
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 36)]
    public byte[] Id;          // 36-byte UUID ASCII string
    
    public int Type;           // ItemType enum (4 bytes)
    
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 12)]
    public byte[] AssetCode;   // 12-byte zero-padded item code (e.g., "LASER\0\0...")
    
    public int Durability;     // 4 bytes
}

In Rust:

#[repr(C)]
pub struct InventoryItem {
    pub id: [u8; 36],
    pub item_type: i32,
    pub asset_code: [u8; 12],
    pub durability: i32,
}

This design avoids allocating managed string objects during the render loop, maintaining zero GC overhead and low memory latency.