Animist C ABI

Animist exposes a small, stable C API for driving NPC conversations from any language or engine that can load a shared library and call C functions. This page describes contracts, JSON payloads, and ownership rules. The authoritative declarations live in the repository’s include/animist.h file.

Overview

The library is implemented in Rust and compiled to a native artifact (.dylib, .so, or .dll). Exported symbols use the animist_ prefix. Opaque pointer types (AnimistRuntimeHandle, AnimistResponseHandle) are incomplete structs in C — treat them as handles you only pass back into the library.

Every operation returns an AnimistStatus integer. When the status is not ANIMIST_OK, call animist_last_error_message() for a UTF-8 C string describing what went wrong (see errors).

Library artifacts

After a full build (for example ./scripts/build.sh at the repo root), the FFI crate produces a library named libanimist_ffi with the platform’s conventional suffix. The reference site and Node adapter load libanimist_ffi.dylib / .so / .dll from the dist/ directory next to your binary or via an absolute path.

Header and linking

Ship animist.h alongside your project and link against the Animist shared or static library using your toolchain’s normal rules. A minimal C program is included under examples/c_smoke/smoke.c in the repository.

Typical lifecycle

  1. animist_version — optional; returns a heap-allocated version string.
  2. animist_runtime_create — creates a runtime; config_json must include license_token and license_public_key_pem (developer license). Add api_key for a live OpenAI-compatible LLM; omit api_key for the offline stub provider.
  3. animist_character_register — once per NPC id you will use in conversations.
  4. animist_conversation_begin — allocates a new conversation id.
  5. animist_conversation_send_message — returns an AnimistResponseHandle on success.
  6. Use animist_response_get_* to read fields, then animist_response_destroy.
  7. animist_runtime_destroy — when the game or server shuts down.

Status codes and errors

ValueNameMeaning
0ANIMIST_OKSuccess.
1ANIMIST_NULL_ARGUMENTA required pointer argument was NULL.
2ANIMIST_PANICThe Rust side caught a panic at the FFI boundary.
3ANIMIST_INTERNAL_ERRORValidation, I/O, or LLM errors; check the last-error string.

After a non-OK status, animist_last_error_message() returns a pointer to a static-ish thread-local message valid until the next Animist call on the same OS thread. Do not free it. If there is no message, the pointer may be NULL.

Memory ownership

  • Any char* (or char** output) produced by the library for you to read — including animist_version, animist_conversation_export, and every animist_response_get_* out string — is heap-allocated in Rust. You must release it with animist_string_free when you are done.
  • Do not call animist_string_free on pointers returned by animist_last_error_message; those are not owned by the caller.
  • Each successful animist_conversation_send_message returns a fresh AnimistResponseHandle*. Destroy it with animist_response_destroy after you have read the fields you need (you may call multiple get functions on the same handle; each get that returns a string allocates a new C string you must free).
  • animist_runtime_destroy invalidates the runtime pointer; do not use it again.

Threading

A single AnimistRuntimeHandle is designed to be used as you would a typical game subsystem: from the owning thread or behind your own synchronization. The last-error buffer is thread-local, so always handle failures on the same thread that made the call when reading animist_last_error_message.

Runtime configuration

animist_runtime_create(const char* config_json, ...): config_json must not be NULL. It must be JSON that includes a valid signed license_token and matching license_public_key_pem (from your developer license email). Add an api_key for the default OpenAI-compatible chat endpoint; omit api_key (or use "") for the offline stub provider.

Recognized fields (invalid JSON or license fails runtime creation):

{
  "license_token": "<signed token>",
  "license_public_key_pem": "-----BEGIN PUBLIC KEY-----\\n...\\n-----END PUBLIC KEY-----\\n",
  "api_key": "<omit or empty for stub LLM>",
  "model": "gpt-4o",
  "endpoint_url": "https://api.openai.com/v1/chat/completions",
  "temperature": 0.7,
  "max_tokens": 256,
  "system_prompt": null,
  "max_history_messages": 20
}

max_history_messages caps how much dialogue is kept verbatim before older lines are summarized into memory (omit or use null for no summarization cap). Omit fields you are happy to leave at their defaults.

Character profiles

animist_character_register takes a numeric character_id and a JSON object describing the NPC:

{
  "name": "Merchant",
  "attributes": [
    { "name": "role", "value": "Shopkeeper in a fantasy town." },
    { "name": "tone", "value": "Friendly, concise." }
  ]
}

name is required; attributes is an array of name / value strings (empty array is allowed). The runtime turns this into the LLM system message. The display name passed to animist_conversation_send_message is ignored when a profile is registered (the profile’s name is used).

Players, characters, conversations

  • Player id — an arbitrary uint64_t you assign per human or session. It scopes analytics and history logic inside the runtime; use a stable id if you want consistent behavior across saves.
  • Character id — must have been registered with animist_character_register before starting or continuing a conversation.
  • Conversation id — returned by animist_conversation_begin or animist_conversation_import. Pass the same id to send_message and export until you destroy the runtime or retire that conversation.

Sending messages

animist_conversation_send_message requires a valid runtime, an existing conversation id, the character id for that NPC, a NUL-terminated character_name, the same player id you used when beginning the conversation, and the user’s message text. On success it writes a non-null AnimistResponseHandle* to out_response.

If the conversation id is unknown or the character is not registered, you get ANIMIST_INTERNAL_ERROR with a descriptive last-error string.

Reading responses

From a response handle you can query:

  • animist_response_get_text — NPC reply body.
  • animist_response_get_character_name — display name echoed from the call.
  • animist_response_get_character_id — numeric id.
  • animist_response_get_summary — when summarization ran, a JSON object such as {"messages_summarized":8,"summary":"..."}; if there was no summary event on this turn, the API still succeeds and returns an empty string (not NULL) which you must free like any other owned string.

Save and restore

animist_conversation_export serializes one conversation’s message history to a JSON string you free with animist_string_free. The payload looks like:

{
  "character_id": 100,
  "messages": [
    { "role": "Player", "content": "Hello!" },
    { "role": "Npc", "content": "Well met, traveler." }
  ]
}

animist_conversation_import accepts that JSON and returns a new conversation id plus the character id from the blob. Always use the new id for subsequent send_message calls. The character must still be registered on this runtime before you continue the thread.

FFI in other languages

The same rules apply everywhere:

  • C# (P/Invoke): map AnimistStatus to int, use IntPtr for opaque handles, out IntPtr for out-pointer indirection, and Marshal.PtrToStringUTF8 plus your binding’s free function for owned strings (or expose animist_string_free and call it after copying).
  • C++: include the C header inside extern "C" and wrap handles in RAII types that call destroy/free in destructors.
  • Python / Node / Lua: use ctypes, cffi, Koffi, or similar — declare the exact signatures from animist.h, pass a JSON license config string (never NULL), and mirror the ownership rules above (the bundled site server uses Koffi to load libanimist_ffi).
  • Go: use syscall or purego/cgo with C string helpers; ensure Goroutine/thread consistency if you read last-error messages.

See also the live demo source: its sample Express routes are a thin wrapper around this exact ABI from Node — not a separate hosted product API.

Engine integration

Animist exposes one C ABI for every stack. Ship animist.h, the shared library (.dylib / .so / .dll), and optionally the static .a / .lib.

  • Unity (C#)DllImport; place the native library under Assets/Plugins/; free strings with animist_string_free.
  • Unreal (C++) — link from your module Build.cs; ship the shared library beside your game or on the loader path.
  • Godot — GDExtension/C# wrappers around the same C calls.
  • Other — any language that can call C (ctypes, Swift, etc.).

Function reference

Names and parameter types match include/animist.h.

Function Notes
animist_version(char** out_version) Owned UTF-8 string; free with animist_string_free.
animist_runtime_create(const char* config_json, AnimistRuntimeHandle** out) Non-NULL JSON with license_token + PEM; optional api_key.
animist_runtime_destroy(AnimistRuntimeHandle* rt) Idempotent on valid pointer only; do not double-free.
animist_character_register(rt, uint64_t id, const char* profile_json) Profile JSON: name and attributes (array of name/value).
animist_conversation_begin(rt, player_id, character_id, uint64_t* out_conv) Creates empty history.
animist_conversation_export(rt, conv_id, char_id, char** out_json) Owned JSON string; free with animist_string_free.
animist_conversation_import(rt, const char* json, uint64_t* out_conv, uint64_t* out_char) New conversation id; register character before messaging.
animist_conversation_send_message(rt, conv, char_id, name, player, msg, AnimistResponseHandle** out) Allocates response handle; destroy when finished reading outputs.
animist_response_get_text / _character_name / _summary (…, char** out) Each non-null out string is owned; free each with animist_string_free.
animist_response_get_character_id(…, uint64_t* out) No allocation.
animist_response_destroy(AnimistResponseHandle* r) Frees the handle; not the strings you already extracted (those are separate).
animist_string_free(char* s) Required for every owned string from the library (except last-error pointer).
animist_last_error_message(void) Const pointer; thread-local; do not free.