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
animist_version— optional; returns a heap-allocated version string.animist_runtime_create— creates a runtime;config_jsonmust includelicense_tokenandlicense_public_key_pem(developer license). Addapi_keyfor a live OpenAI-compatible LLM; omitapi_keyfor the offline stub provider.animist_character_register— once per NPC id you will use in conversations.animist_conversation_begin— allocates a new conversation id.-
animist_conversation_send_message— returns anAnimistResponseHandleon success. -
Use
animist_response_get_*to read fields, thenanimist_response_destroy. animist_runtime_destroy— when the game or server shuts down.
Status codes and errors
| Value | Name | Meaning |
|---|---|---|
0 | ANIMIST_OK | Success. |
1 | ANIMIST_NULL_ARGUMENT | A required pointer argument was NULL. |
2 | ANIMIST_PANIC | The Rust side caught a panic at the FFI boundary. |
3 | ANIMIST_INTERNAL_ERROR | Validation, 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*(orchar**output) produced by the library for you to read — includinganimist_version,animist_conversation_export, and everyanimist_response_get_*outstring — is heap-allocated in Rust. You must release it withanimist_string_freewhen you are done. -
Do not call
animist_string_freeon pointers returned byanimist_last_error_message; those are not owned by the caller. -
Each successful
animist_conversation_send_messagereturns a freshAnimistResponseHandle*. Destroy it withanimist_response_destroyafter you have read the fields you need (you may call multiplegetfunctions on the same handle; eachgetthat returns a string allocates a new C string you must free). -
animist_runtime_destroyinvalidates 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_tyou 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_registerbefore starting or continuing a conversation. -
Conversation id — returned by
animist_conversation_beginoranimist_conversation_import. Pass the same id tosend_messageandexportuntil 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 (notNULL) 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
AnimistStatustoint, useIntPtrfor opaque handles,out IntPtrfor out-pointer indirection, andMarshal.PtrToStringUTF8plus your binding’s free function for owned strings (or exposeanimist_string_freeand 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 (neverNULL), and mirror the ownership rules above (the bundled site server uses Koffi to loadlibanimist_ffi). -
Go: use
syscallorpurego/cgowith 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 underAssets/Plugins/; free strings withanimist_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. |