Skip to content

Memory in the sandbox

Lesson 11.1

A C mod lives in a WebAssembly sandbox. It is still C: malloc, free, arrays, structs and the standard library all work. But the sandbox has its own rules about memory and files, and knowing them saves you from strange bugs.

Your own memory

Your mod gets its own block of memory, separate from the game and from every other mod. Pointers in your mod point into that block and nowhere else. That is why the API uses handles: your code can’t hold a pointer to the game’s objects, and the game never trusts a pointer from your code without checking it.

If your code passes the game a pointer that points outside your memory (a broken pointer, or a length that is too big), the game stops the call with a trap instead of reading somewhere it shouldn’t. The log says:

[my_mod] callback trapped: clight: invalid guest pointer passed to the API

Inside your own memory, the usual C rules apply: writing past the end of an array doesn’t crash the game, but it can quietly destroy your mod’s other data, exactly as in a normal C program. Use snprintf instead of sprintf, keep array sizes in mind, and turn on -Wall -Wextra (see The manifest).

The memory limit

Each mod may use up to 256 MB by default. That includes everything: your globals, the heap from malloc, and the stack. When the limit is reached, malloc returns NULL, like on a full computer. Check it:

int* scores = malloc(1000 * sizeof *scores);
if (!scores) {
    CL_LOGF(CL_LOG_ERROR, "out of memory");
    return;
}

Most mods need a few MB. If yours really needs more, raise the limit in the manifest, up to 4096:

"limits": { "memory_mb": 512 }

The stack is small

Local variables live on the stack, and a WebAssembly mod’s stack is small: 64 KiB. A few char name[64] arrays are fine. A char big[100000] inside a function is not: it is bigger than the whole stack, and it destroys data or traps.

Put big things in static storage or on the heap:

static char g_page[100000];          // fine: static, not on the stack

static void build_report(void) {
    char* buf = malloc(100000);      // fine: on the heap
    if (!buf) return;
    // ...
    free(buf);
}

Deep recursion uses the stack too, so prefer loops for things like flood fills.

Who owns what

You already know the rules from Handles and strings. Here they are in one place:

  • Your memory (malloc, arrays, globals) is yours: free what you allocate.
  • Owned handles (texts, stacks, buffers you create or receive as owned) are yours too: release them.
  • Borrowed things (players, levels, event payloads, the data buffer in a payload handler, ctx->stack in an item behaviour) belong to the game: use them during the call, don’t free or release them.
  • Strings from the game (cl_str fields in payloads) are valid during the callback only. Copy them if you need them later.
  • Strings you give the game (a cl_str in a descriptor, a message) are copied by the game during the call, so a local array is fine, except for changed event fields like a chat message, which must point at memory that is still there after your handler returns: use a static buffer.
  • Pointers you register (user pointers, the strings in a screen’s state) must stay valid as long as the registration exists: use static data or malloc.

The SDK’s scratch space for API calls, about 256 KB of your memory, is set aside when the mod loads. You don’t need to do anything about it; it is why a tiny mod already uses a little memory.

Files

A C mod can use fopen, but it only sees two folders:

Path in your code Is Access
/ Your mod’s own folder (assets/, datapack/, the manifest, …) read only
/data Your mod’s data folder read and write
// Save a value.
static void save_visits(int visits) {
    FILE* f = fopen("/data/visits.txt", "w");
    if (!f) return;
    fprintf(f, "%d\n", visits);
    fclose(f);
}

// Load it again.
static int load_visits(void) {
    int visits = 0;
    FILE* f = fopen("/data/visits.txt", "r");
    if (!f) return 0;   // first start: no file yet
    if (fscanf(f, "%d", &visits) != 1) visits = 0;
    fclose(f);
    return visits;
}

/data is mods/<id>/data/ in the game folder, or build/run/<id>/data/ while you test with clight run. It isn’t part of your published package, and it survives updates of your mod. Everything else (C:\, other mods, saves) doesn’t exist for your mod: fopen on it simply fails. W3Schools covers files in Write to files and Read files.

Writing a file takes time. Save in CL_EV_LIFECYCLE_SERVER_STOPPING or now and then from a timer, not every tick.

No threads, no network, no programs

The sandbox has no threads (pthread isn’t available), no sockets, and no way to start programs or load native libraries. getenv finds nothing, and there are no command-line arguments. This is what makes C mods safe to install, and the API gives you game-friendly ways to do the things mods need: timers instead of threads, payloads instead of sockets.

Try it

This function sometimes destroys other data in the mod and sometimes traps. Find the problem and fix it.

static void log_all_names(void) {
    char names[200][64];
    cl_entity players[200];
    size_t count = 0;
    cl_server_players(players, 200, &count);
    for (size_t i = 0; i < count; ++i) {
        cl_easy_player_name(players[i], names[i], 64);
        CL_LOG("%s", names[i]);
    }
}
Show a solution

Two problems. names is 200 x 64 = 12,800 bytes and players another 1,600, on a 64 KiB stack: that fits, but it is a big part of it, and called from deep inside other code it may not. The real bug is count: cl_server_players reports the total number of players, which can be more than 200. Then the loop reads past the end of both arrays.

static void log_all_names(void) {
    static cl_entity players[200];
    size_t count = 0;
    if (cl_server_players(players, 200, &count) != CL_OK) return;
    if (count > 200) count = 200;
    for (size_t i = 0; i < count; ++i) {
        char name[64];
        cl_easy_player_name(players[i], name, sizeof name);
        CL_LOG("%s", name);
    }
}

The player list is static now, the names don’t need to be kept at all, and count is capped at the array size.

Recap: your mod has its own memory (256 MB by default, a 64 KiB stack), the game traps bad pointers, borrowed things stay the game’s, registered pointers must stay valid, and files are limited to your own folder (read only) and /data (read and write).