Native (C)

Usage guide of libgfkits from C: build and link, a first program, every function with its signature (create from { project, lock }, step, inputs, the zone and its entity rows, events, hooks by C callback, services, registries, saves), the statuses, limits, versions.

Source: docs/usage/c-api.mdView Markdown source

Also read offline — CLI: grip-forge docs c-apiMCP tool: gripforge_docs { "topic": "c-api" }

The C API of the kit host (gfkits.h) #

Run the logic of a GripForge game — its Game Kits, their rules, hooks, registries and saves — from C, C++ or any language that binds a C header. Readable offline: grip-forge docs c-api, or the MCP tool gripforge_docs (topic c-api).

What it is for #

libgfkits is the kit host and the logic of every Game Kit as one native library. It is the same Rust logic the web player runs: a game created from the same two documents plays the same rules. You give it a game, you step it, you send it inputs, and you read what to draw from a block of memory it shares with you. It draws nothing and plays no sound: that is your engine's job. The Godot guide is this API wrapped in a node.

Prerequisites #

  • The GripForge repository and its Rust toolchain (rust-toolchain.toml); a C11 compiler.
  • A game file: one JSON object { "project": …, "lock": … } — the project (gripforge.gameproject/1) and its lockfile (gripforge.gamekits.lock/1), the two documents the web player is given.
  • Built and tested on Linux x86-64 only (see Known limits).

Install #

# in the folder of the native libraries of the GripForge sources
./build.sh --out dist
# dist/<platform>/libgfkits.so   the library
# dist/include/gfkits.h          the header
cc -std=c11 -I dist/include my_host.c -L dist/linux-x86_64 -lgfkits -lm -o my_host

The header is generated from the source of the library: do not edit it.

First example #

Create a game, press a key, step, and read where every entity is. Shortened from host.c, the C test of the library that runs the demonstration game through this API: the same calls, without its checks.

#include "gfkits.h"
#include <stdio.h>
#include <string.h>
#define S(text) (text), strlen(text)

int run(const char *game_json, size_t len) {
    if (gfk_abi_version() >> 16 != GFK_ABI >> 16) return 1;          /* another major: refuse */

    GfkGame *game = NULL;
    if (gfk_game_create(game_json, len, &game) != GFK_OK) {
        fprintf(stderr, "create: %s\n", gfk_last_error());
        return 1;
    }
    printf("%s\n", gfk_out(NULL));                                   /* { kits, skipped, fixedDt, lockHash } */

    gfk_events_watch(game, S("player.moved"));
    gfk_input_key(game, S("KeyW"), 1);                               /* the bindings are the game's */
    for (int i = 0; i < 45; i++) gfk_step(game, 1.0 / 60.0, NULL);   /* wall-clock seconds, fixed ticks inside */
    gfk_input_key(game, S("KeyW"), 0);

    uint32_t moved = 0;
    gfk_events_take(game, &moved);                                   /* JSON array in gfk_out */

    GfkZone zone;
    gfk_zone(game, &zone);
    for (uint32_t i = 0; i < zone.capacity; i++) {
        const uint8_t *row = zone.base + zone.rows + i * zone.row_bytes;
        uint32_t id; memcpy(&id, row, 4);
        if (id == 0 || id == 0xffffffffu) continue;                  /* empty row, destroyed entity */
        double x, y, z, yaw;
        memcpy(&x, row + 8, 8); memcpy(&y, row + 16, 8); memcpy(&z, row + 24, 8); memcpy(&yaw, row + 32, 8);
        printf("e%u at %f %f %f (yaw %f)\n", id, x, y, z, yaw);
    }
    gfk_game_destroy(game);
    return 0;
}

Rules of the interface #

  • Every function answers a status (int32_t): GFK_OK is 0, anything else is a named error. gfk_status_name(status) gives its name, gfk_last_error() the message of the last failed call on this thread.
  • A function that answers data writes it in the output of the calling thread: read it with gfk_out(&len) — UTF-8 JSON, NUL-terminated, valid until the next gfk_* call on that thread. Copy it if you keep it.
  • Texts go in as pointer + length (no NUL needed).
  • A game belongs to the thread that created it. Another thread is answered GFK_WRONG_THREAD.
  • A callback must not call the game it was called by: it is answered GFK_REENTRANT.
  • A panic of the logic is caught: the call answers GFK_PANICKED and the game is disabled.

API reference #

Signatures are those of the header gfkits.h (dist/include/gfkits.h).

Library #

uint32_t    gfk_abi_version(void);                 /* major << 16 | minor */
const char* gfk_status_name(int32_t status);       /* "ok", "bad_json", "refused"… "unknown" otherwise */
const char* gfk_last_error(void);
const char* gfk_out(size_t* len);
int32_t     gfk_info(void);                        /* out: { abi, contracts, physics, kits, testKits } */
int32_t     gfk_compat(const char* project, size_t len);   /* out: [{ path, message }], [] when the project runs */
int32_t     gfk_json_at(const char* json, size_t len, const char* pointer, size_t pointer_len);

gfk_json_at reads the value at a JSON pointer (/fixture/ops/3) of a JSON text, for a caller without a JSON reader; GFK_NOT_FOUND when nothing is there.

A game #

int32_t gfk_game_create(const char* json, size_t len, GfkGame** game);
void    gfk_game_destroy(GfkGame* game);

json is { "project", "lock", "seed"?: 1, "fixedDt"?: 1/60, "physics"?: "rapier" | "none", "schema"?, "registries"?: { collection: [entries] }, "saves"?: { "local": { key: text } }, "assets"?: { slot: { "url" } }, "locale"?, "now"?, "tzOffset"? }. The kits of the lock are mounted and the game starts. Output: { kits: [mounted], skipped: [kits without logic in this library], fixedDt, lockHash }.

  • registries adds documents to the registries of the game before the kits mount (an item, a status, an archetype); mods and inheritance (extends) then apply to them as to any entry.
  • saves: with it (even { "local": {} }) the game keeps one local store for the whole play; read it with gfk_store and give it back at the next create.
  • schema is the game schema (gripforge.game-schema/1) when an export computed one; without it an entity row has the fixed base only.

Time #

int32_t gfk_step(GfkGame* game, double dt, uint32_t* ticks);        /* dt seconds of wall clock */
int32_t gfk_step_ticks(GfkGame* game, uint32_t n);                  /* exactly n fixed ticks */
int32_t gfk_time(GfkGame* game, uint64_t* tick, double* elapsed);
int32_t gfk_clock(GfkGame* game, double ms);                        /* the date, ms since the Unix epoch */

gfk_clock is only for a game created with now or tzOffset.

Inputs #

int32_t gfk_input_key(GfkGame* game, const char* code, size_t len, int32_t down);
int32_t gfk_input_gamepad(GfkGame* game, const double* buttons, size_t button_count, const double* axes, size_t axis_count);
int32_t gfk_input_axis(GfkGame* game, const char* action, size_t len, double value);
int32_t gfk_input_button(GfkGame* game, const char* action, size_t len, int32_t down);
int32_t gfk_input_specs(GfkGame* game);                             /* out: the actions the kits declared */

Key codes are the web's (KeyW, Space, Mouse0); pad buttons (0..1) and axes (-1..1) are in the standard order. gfk_input_axis / gfk_input_button drive an action of the game directly (an on-screen stick, your engine's own input map).

The zone and its entity rows #

int32_t gfk_zone(GfkGame* game, GfkZone* out);
int32_t gfk_column(GfkGame* game, const char* name, size_t len, GfkColumn* out);
typedef struct GfkZone {
    uint32_t struct_bytes;      /* bytes of this struct as the library wrote it */
    uint32_t size;              /* bytes of the zone */
    const uint8_t* base;        /* the first byte of the zone */
    uint32_t input, state, events, world, regions;   /* where those buffers start */
    uint32_t capacity;          /* rows of the table */
    uint32_t row_bytes;         /* bytes of a row */
    uint32_t rows;              /* where the first row starts */
    uint32_t used;              /* rows in use */
    uint32_t owner;             /* the world's tag: differs after a reload */
    uint64_t epoch;             /* unchanged: no row changed */
} GfkZone;

typedef struct GfkColumn { uint32_t offset; uint32_t bytes; uint32_t kind; } GfkColumn;

Row i is at base + rows + i * row_bytes. In a row: u32 id number at 0 (e12 → 12; 0 an empty row, 0xffffffff a row left by a destroyed entity), u32 flags at 4 (bit 0: the entity has a transform), f64 x, y, z at 8, f64 yaw at 32, then the columns of the game schema at the offsets gfk_column answers by name (core.position@1, combat.health@1…). Column kinds: 1 u8, 2 bool, 3 u16, 4 u32, 5 i32, 6 f32, 7 atom, 8 u64, 9 f64, 10 f64x3, 11 entity, 12 bits128.

Read in place, nothing is copied. The address is valid until the next gfk_* call of this game (the zone may grow): copy what you need before calling again. Offsets never change during a play.

Reading the game as JSON #

int32_t gfk_entity(GfkGame* game, const char* id, size_t len);          /* "e12", or "player" */
int32_t gfk_snapshot(GfkGame* game);                                    /* the whole game: tools and tests */
int32_t gfk_state(GfkGame* game, const char* path, size_t len);         /* a state var; "" for all of them */
int32_t gfk_config(GfkGame* game, const char* kit, size_t len);         /* a kit's config, after the mods */
int32_t gfk_data(GfkGame* game, const char* collection, size_t len);    /* a registry, as the kits use it */
int32_t gfk_logs(GfkGame* game);                                        /* [[level, line]] */

A frame reads the rows; these are for decisions, UI and tools.

Operations and services #

int32_t gfk_apply(GfkGame* game, const char* op, size_t len);
int32_t gfk_call(GfkGame* game, const char* service, size_t service_len, const char* method, size_t method_len, const char* args, size_t args_len);

gfk_apply takes one operation of the kit host as JSON, the ones a web page sends: {"op":"step","ticks":n}, {"op":"idle"}, {"op":"key","code":"KeyW","down":true}, {"op":"gamepad","buttons":[],"axes":[]}, {"op":"virtual","action":"move_x","axis":1}, {"op":"emit","topic","payload"}, {"op":"set","path","value"}, {"op":"call","service","method","args":[]}, {"op":"spawn","spec"}, {"op":"create","kind","tags","components"}, {"op":"move","entity","to"}, {"op":"start_mission","id"}, {"op":"save"}, {"op":"reload"}, {"op":"hud"}, {"op":"present","needs"?}, {"op":"command","command"}. Its result is in the output (null when it has none).

gfk_call calls a service of a kit; args is a JSON array (or empty). From host.c:

gfk_apply(game, S("{\"op\":\"create\",\"kind\":\"npc\",\"tags\":[\"enemy\"],\"components\":{\"transform\":{\"x\":4,\"y\":0,\"z\":2,\"yaw\":0}}}"));
/* gfk_out: the id of the new entity, a JSON text such as "e12" */
gfk_call(game, S("inventory.items"), S("give"), S("[\"coin\",3]"));     /* gfk_out: 3 */

The services, their methods and arguments are in each kit's API reference (grip-forge kit api <id>).

Events #

int32_t gfk_events_watch(GfkGame* game, const char* pattern, size_t len);   /* "*", "combat.*", "inventory.item.added" */
int32_t gfk_events_take(GfkGame* game, uint32_t* count);

gfk_events_take answers the events dispatched since the last take that match a watched pattern, in order: a JSON array of { topic, payload, seq, tick, time, source, entity?, actor? }.

Hooks #

typedef int32_t (*GfkHookFn)(void* user, const char* hook, size_t hook_len, const char* payload, size_t payload_len, GfkReply* reply);

int32_t gfk_hooks(GfkGame* game);
int32_t gfk_hook_tap(GfkGame* game, const char* hook, size_t hook_len, const char* opts, size_t opts_len, GfkHookFn f, void* user);
int32_t gfk_hook_untap(GfkGame* game, const char* id, size_t len);
int32_t gfk_reply_json(GfkReply* reply, const char* json, size_t len);
int32_t gfk_hooks_trace(uint32_t keep);

A kit asks a hook each time it decides something another party may change (combat.damage, inventory.give…). gfk_hooks lists the hooks of the game: per hook its kit, the fields a tap may change (mutable) and whether a tap may cancel it (cancellable). gfk_hook_tap registers your callback; opts is { "priority"?: -1000..1000, "id"?: text, "owner"?: text } or empty; the output is the tap id, for gfk_hook_untap.

The callback receives the payload as JSON. It answers 0, after gfk_reply_json(reply, …) when it changes something: { "set": { field: value }, "cancel"?: true, "reason"?: text }. From host.c — hits on enemies count double, and one item cannot be given:

static int32_t double_enemy_hits(void *user, const char *hook, size_t hook_len, const char *payload, size_t payload_len, GfkReply *reply) {
    char team[64] = "";
    if (gfk_json_at(payload, payload_len, S("/targetTeam")) == GFK_OK) snprintf(team, sizeof team, "%s", gfk_out(NULL));
    if (strcmp(team, "\"enemy\"") != 0) return 0;
    return gfk_reply_json(reply, S("{\"set\":{\"multiply\":2},\"reason\":\"c_double\"}"));
}
static int32_t no_herb(void *user, const char *hook, size_t hook_len, const char *payload, size_t payload_len, GfkReply *reply) {
    if (gfk_json_at(payload, payload_len, S("/item")) != GFK_OK || strcmp(gfk_out(NULL), "\"herb\"") != 0) return 0;
    return gfk_reply_json(reply, S("{\"cancel\":true,\"reason\":\"allergic\"}"));
}

gfk_hook_tap(game, S("combat.damage"), S("{\"id\":\"c_double\"}"), double_enemy_hits, NULL);
gfk_hook_tap(game, S("inventory.give"), NULL, 0, no_herb, NULL);

The rules of a hook stay those of its declaration: a field that is not mutable, or a cancel on a hook that is not cancellable, is refused and told by the event engine.hook.refused (reason: not_mutable, invalid, not_cancellable) — watch it while testing. gfk_hooks_trace(keep) keeps the last keep decisions (who changed or cancelled what) and answers them; 0 stops.

Presentation: what the kits draw #

On the web, what a kit shows is drawn by its presentation module: a .wasm without any engine in it (gripforge:present@1, described in present.wit: gripforge docs abi) that reads the snapshots of the game and answers commands — meshes, materials, nodes, lights, effects, UI. The library runs those same modules and hands you their commands; your engine applies them with its own objects.

FunctionRole
gfk_present_native(path, len)Loads the library of the native presentation modules (libgfpresent, built with the addon: the modules of the kits compiled for the machine from the sources of their .wasm). Once per process, before the games register their modules; the environment variable GFK_PRESENT_NATIVE names it too. GFK_REFUSED (reason in gfk_last_error) when it is not loaded.
gfk_present_kit(game, kit, len)Registers the NATIVE module of a kit — no .wasm, no interpreter. In the output: { kit, version, needs, runner: "native" }. GFK_REFUSED when no library is loaded or it has no module of that kit: register its .wasm then (gfk_present_module, run by the embedded interpreter — the case of a third-party module).
gfk_present_module(game, wasm, len)Registers a module of the game from the bytes of its .wasm (as present-modules.json of a web export names them). In the output: { kit, version, needs }.
gfk_present_start(game, options, len)Starts. options: { "engine", "backend", "caps", "assets": { slot: { "clips": [] } }, "modelSlots": [], "textureSlots": [], "generic": {}, "settings": {}, "shared": { name: text }, "needs": { "components", "views", "events", "commands" } }. In the output: { modules, needs }.
gfk_present_frame(game, dt, frame, len, &commands)One drawn frame, after the steps: the snapshot is made (transforms interpolated between the two last ticks, the components and views the modules declared, the events since the previous frame) and every module is called. frame: { "camera": { "position", "rotation", "fovY", "viewport", "viewProj", "mode" }, "stage": { "exposure", "composed" } }.
gfk_present_snapshot(game)The snapshot of that frame, in the output: what your own layer draws (entities, their model slot and clip state, the camera.* vars).
gfk_present_cmds(game, json)The commands of that frame, in the output. json 0: postcard(Vec<(u32 module, Vec<u8> command)>), the encoding of the present ABI — read its length from gfk_out. json 1: JSON text, [[module, { "<Command>": { fields } }]].
gfk_present_result(game, module, result, len)An answer to a module, as JSON: { "Refused": { "index": 0, "reason": "MaterialWgsl" } } for a command you do not draw, { "Loaded": { "id", "ok" } }, { "Bound": … }, { "Bounds": … }, { "Slot": … }, { "Model": … }.
gfk_present_command(game, command, len)A write of your own layer on the kit host ({ "service", "method", "args" } or { "emit", "payload" }), allowed by needs.commands.
gfk_present_info(game){ started, frames, msPerFrame, stage, modules: [{ index, kit, version, dead, calls, slept, cmds, msPerFrame, runner: "native" | "wasm" | "verify" }], needs, undrawn, logs, shared }.

What a module asks of the kit host — a service call, an event, the config of a kit, a state shared between modules, a log line — is done by the library; it never reaches you. A module that traps is left out (dead) and the game goes on. gfk_events_take still answers every event: the presentation reads them too, without taking them.

The modules are run by an embedded interpreter (wasmi): no compiled runner yet.

The window, and HUD rows without a game (1.3) #

int32_t gfk_host_requests(GfkGame* game);            /* out: { kits: { display, pointer }, requests: [{ port, call }] } */
int32_t gfk_hud_rows(const char* doc, size_t len);   /* doc: { vars: [declared vars], values: { path: value } } */

gfk_host_requests takes what the kits display.fullscreen and input.pointer_lock asked of the window since the last call (port display: request | exit; port pointer: lock | unlock). Your engine executes on its window and tells the kits what really happened through their services (gfk_call: attach, hostChanged, hostRefused, gesture, hostMove). gfk_hud_rows answers the HUD rows of declared state vars (the hudVars of a snapshot) for given values, without a game: what {"op":"hud"} answers for a running one.

Saves #

int32_t gfk_save(GfkGame* game);                                /* out: gripforge.save/1 */
int32_t gfk_load(GfkGame* game, const char* save, size_t len);
int32_t gfk_store(GfkGame* game);                               /* out: { key: text }, the kits' local store */

A save is in the format of the web player: it moves between the two. gfk_load answers GFK_REFUSED with the reason when a save does not fit.

Errors #

StatusNameMeaning
GFK_OK 0okDone.
GFK_BAD_ARGUMENT 1bad_argumentA null pointer, a text that is not UTF-8, a value of the wrong kind.
GFK_BAD_JSON 2bad_jsonA text that is not JSON.
GFK_BAD_PROJECT 3bad_projectNo project or no lock, or a registry entry that cannot be added.
GFK_NO_GAME 4no_gameA null game.
GFK_WRONG_THREAD 5wrong_threadThe game belongs to the thread that created it.
GFK_REENTRANT 6reentrantThe game is running: a callback called it back.
GFK_PANICKED 7panickedThe logic panicked; the game is disabled.
GFK_REFUSED 8refusedThe kit host or a kit refused (unknown operation or service, a save that does not fit): gfk_last_error says why.
GFK_NOT_FOUND 9not_foundNo such entity, column, kit, tap or JSON path.
GFK_BAD_SCHEMA 10bad_schemaThe game schema does not hold: gfk_last_error says what.
GFK_PARITY_DIFFERS 11parity_differsA trace differs from its reference (the test functions gfk_trace_frame, gfk_parity_check).

host.c checks these: gfk_game_create("{not json") → GFK_BAD_JSON; {"project":{}} → GFK_BAD_PROJECT; gfk_step(NULL, …) → GFK_NO_GAME; {"op":"nonsense"} and an unknown service → GFK_REFUSED; the config of a kit that is not in the game, an unknown column, an unknown tap → GFK_NOT_FOUND.

Known limits #

  • Logic only. Nothing is drawn: the views the kits declare are answered as data ({"op":"present"}) for your engine to draw. No HUD.
  • Linux x86-64 is the only platform built and tested. No prebuilt binaries are published.
  • One library holds every kit; gfk_info lists them. A game whose kit has no logic in the library starts without it and names it in skipped: check gfk_compat first.
  • Network kits (net.*) need host services this interface does not take yet.
  • Asset bytes cannot be handed to the game: assets binds slots to URLs only.
  • One game per thread; a game is single-threaded.
  • JSON crosses the interface for everything that is not a row: keep per-frame reads on the rows.

Versions #

GFK_ABI is 0x00010003 (1.3). gfk_abi_version() answers major << 16 | minor: refuse another major. A function, or a struct field added at the end (GfkZone.struct_bytes says how many bytes the library wrote), raises the minor. Saves are gripforge.save/1, game files gripforge.gameproject/1 + gripforge.gamekits.lock/1.