Language Bindings
ai-core-ffi is the C ABI layer for non-Rust SDKs. Treat it as the stable boundary for Python, Go, PHP, Swift, Kotlin, Flutter, and service-side integrations.
What This Section Covers
This page explains:
- which C ABI functions to wrap;
- what each function accepts and returns;
- how to pass config and request JSON;
- how to free returned strings;
- how to shape Go, Python, and PHP bindings.
Result Wrapper
Every string-returning function returns an allocated UTF-8 C string containing one of these JSON envelopes:
{"ok":true,"data":{}}{"ok":false,"error":{"code":"config_error","message":"..."}}The host must parse the envelope, copy data into host-owned objects, and free the returned string with ai_core_string_free.
Memory Ownership
ai_core_handle_newreturns an opaque runtime handle.ai_core_handle_freereleases that handle.- Every returned
char *must be released withai_core_string_free. - Do not keep raw C pointers in long-lived host objects.
- Copy returned JSON into host-managed memory before freeing the SDK string.
Run FFI calls off the UI/main async executor thread. For parallel work, keep one long-lived handle per worker or tenant.
Multiple handles in one host process share the native local-model managers below the FFI layer. A builtin-local-llm build keeps one resident llama_model per process/model/backend key after first use. A builtin-speech-whisper build keeps a resident Whisper context pool per process/model path. Separate OS processes do not share these managers.
Use models[].local.max_concurrent_requests and speech.whisper.max_concurrent_requests to bound active native work inside one process. Use max_pending_requests only as a small in-process admission queue; durable queues, retries, and backpressure stay in the host.
Methods
ai_core_ffi_abi_version
- Inputs: none.
- Returns:
uint32_tABI version. Current value:1. - Use when: the binding wants to reject incompatible shared libraries before making JSON calls.
ai_core_version_json
- Inputs: none.
- Returns: JSON with the AI Core version and compiled native-provider flags:
builtin_local_llmandbuiltin_speech_whisper. - Free with:
ai_core_string_free.
ai_core_handle_new
- Inputs: none.
- Returns:
AiCoreFfiHandle *. - Use when: creating a runtime instance for a worker, tenant, or process.
- Notes: the handle registers built-in core tools and built-in HTTP model providers. Builds with
builtin-local-llmalso register the native local provider.
FFI native-provider feature variants:
# HTTP providers only.
cargo build -p ai-core-ffi --release
# Local GGUF chat only.
cargo build -p ai-core-ffi --release --features builtin-local-llm
# Local Whisper transcription only.
cargo build -p ai-core-ffi --release --features builtin-speech-whisper
# Local GGUF chat plus local Whisper transcription.
cargo build -p ai-core-ffi --release --features builtin-local-allai_core_handle_free
- Inputs:
AiCoreFfiHandle *. - Returns: nothing.
- Use when: shutting down a worker or tenant runtime.
ai_core_apply_config_json
- Inputs: handle and config JSON string.
- Returns: apply report JSON with
model_count,has_model_selection, andplugin_config_count. - Use when: the host has loaded or changed model, plugin, secret-reference, or speech settings.
- Local LLM: config JSON may include
local_llm.backendwithauto,cpu, ormetal.models[].local.backendcan override it for onebuilt_in_localmodel. - Local LLM concurrency:
models[].local.max_concurrent_requestsdefaults to1and must be greater than zero.models[].local.max_pending_requestsdefaults to0. The resident GGUF weights are shared inside the process, but each active request still creates its ownllama_contextand KV cache. JSON hosts can pass4294967295for an effectively unbounded active limit, but that is a deliberate memory-risk choice. - Whisper concurrency:
speech.whisper.max_concurrent_requestsdefaults to1and must be greater than zero.speech.whisper.max_pending_requestsdefaults to0. With defaults, concurrent transcription calls are rejected while the resident context is busy. Builds withoutbuiltin-speech-whisperstill accept this config but cannot execute native transcription. JSON hosts can pass4294967295for an effectively unbounded active limit only when they provide external backpressure.
ai_core_settings_catalog_json
- Inputs: handle.
- Returns: JSON catalog of current core settings, supported model kinds, configured models, selected model slots, plugin config schema metadata, and Wasm limits.
- Use when: a UI needs to render available runtime settings.
ai_core_add_core_license_document
- Inputs: handle, license document bytes, byte length.
- Returns: current license document count.
- Availability: normal marketplace builds only.
- Use when: the host stores a license document separately from KEM key material.
ai_core_add_core_license_key_material
- Inputs: handle, license document bytes, document length, matching private KEM key bytes, key length.
- Returns: current license key-material count.
- Availability: normal marketplace builds only.
- Use when: installing encrypted marketplace packages.
ai_core_clear_core_license_documents
- Inputs: handle.
- Returns: zeroed license document and key-material counts.
- Availability: normal marketplace builds only.
- Use when: signing out, changing tenants, or clearing runtime license state.
ai_core_install_package_bytes
- Inputs: handle, package bytes, package byte length,
valid_atUnix timestamp. - Returns: package install report with plugin id, replacement flag, verification status, and issuer.
- Use when: loading a
.aipplugin or pipeline package.
ai_core_pipeline_start_json
- Inputs: handle and start request JSON.
- Returns: pipeline run update JSON.
- Use when: starting a package pipeline from fresh user input.
Start request:
{
"run_id": "run-1",
"task_id": "task-1",
"plugin_id": "unpack_systems_pipeline",
"user_text": "build an audit for a new product"
}ai_core_pipeline_resume_json
- Inputs: handle and resume request JSON.
- Returns: pipeline run update JSON.
- Use when: continuing or retrying from a checkpoint.
Resume request:
{
"state": {
"schema": "ai_core.pipeline_state.v1",
"run_id": "run-1",
"task_id": "task-1",
"plugin_id": "unpack_systems_pipeline",
"status": "waiting_for_approval",
"next_step_index": 1
},
"user_text": "continue",
"previous_outputs": [],
"control": "continue"
}control can be "continue" or "retry". For "retry", the host supplies the replacement user_text; the rerun still receives the step skill, output schema, and earlier previous_outputs.
ai_core_wasm_run_action_json
- Inputs: handle, plugin id, fully qualified action id, action input JSON.
- Returns: action output JSON plus emitted events and queued host calls.
- Use when: the host already knows the exact installed Wasm action to run.
Example:
char *out = ai_core_wasm_run_action_json(
core,
"plugin_id",
"plugin_id.action_id",
"{\"some\":\"input\"}"
);ai_core_model_chat_json
- Inputs: handle and model chat request JSON.
- Returns: chat response JSON.
- Use when: calling a configured model directly by
model_id.
Request:
{
"model_id": "qwen/qwen3.5-9b",
"messages": [
{"role": "user", "content": "hello"}
],
"response_format": {"kind": "text"},
"max_output_tokens": 512
}Supported roles are system, user, and assistant. Supported response formats are text, json_object, and json_schema with a required schema string. Returned content is a nested JSON value when the model produced valid JSON; otherwise it is the model's raw string.
ai_core_transcribe_audio_json
- Inputs: handle, audio byte pointer, audio byte length, and request JSON.
- Returns: transcription response JSON.
- Use when: calling the built-in Whisper provider through FFI.
- Availability: exported in every FFI build. Builds without
builtin-speech-whisperreturntranscription_provider_unavailable.
Request:
{
"format": "pcm16",
"sample_rate_hz": 16000,
"locale": "ru-RU"
}Supported format values are wav, pcm16, m4a, mp3, flac, ogg_opus, webm_opus, and unknown. For predictable native behavior, hosts should decode compressed uploads to WAV PCM16 or raw PCM16 before calling FFI.
Successful response data:
{
"text": "transcribed text",
"locale": "ru"
}Common error codes:
transcription_provider_unavailable: shared library was built withoutbuiltin-speech-whisper.transcription_request_error: request JSON or audio format is invalid.transcription_provider_error: Whisper provider failed, for example because the configured model file is missing.
Config JSON
ai_core_apply_config_json accepts this host-facing shape:
The host-owned config snapshot and model-selection fields are documented in Runtime Contracts.
Worker Pattern
The examples below show thin binding layers, not full worker orchestration. Application code should keep one long-lived handle per worker or tenant and run blocking FFI calls off the UI thread or async executor.
Example: Python Binding
import ctypes
import json
lib = ctypes.CDLL("./libai_core_ffi.dylib")
lib.ai_core_handle_new.restype = ctypes.c_void_p
lib.ai_core_handle_free.argtypes = [ctypes.c_void_p]
lib.ai_core_string_free.argtypes = [ctypes.c_void_p]
lib.ai_core_apply_config_json.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
lib.ai_core_apply_config_json.restype = ctypes.c_void_p
class AiCore:
def __init__(self):
self.handle = lib.ai_core_handle_new()
def _call_json(self, fn, *args):
ptr = fn(*args)
try:
raw = ctypes.cast(ptr, ctypes.c_char_p).value.decode("utf-8")
envelope = json.loads(raw)
if not envelope["ok"]:
raise RuntimeError(envelope["error"]["message"])
return envelope["data"]
finally:
lib.ai_core_string_free(ptr)
def apply_config(self, config):
payload = json.dumps(config).encode("utf-8")
return self._call_json(
lib.ai_core_apply_config_json,
self.handle,
ctypes.c_char_p(payload),
)
def close(self):
lib.ai_core_handle_free(self.handle)Call the binding from a background thread or process worker. Do not run FFI calls directly on an event-loop thread.
Example: Go Binding
package aicore
/*
#include "ai_core_ffi.h"
#include <stdlib.h>
*/
import "C"
import (
"encoding/json"
"errors"
"unsafe"
)
type Core struct {
handle *C.AiCoreFfiHandle
}
func New() *Core {
return &Core{handle: C.ai_core_handle_new()}
}
func (c *Core) Close() {
C.ai_core_handle_free(c.handle)
}
func decode(ptr *C.char, out any) error {
defer C.ai_core_string_free(ptr)
var env struct {
Ok bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
if err := json.Unmarshal([]byte(C.GoString(ptr)), &env); err != nil {
return err
}
if !env.Ok {
return errors.New(env.Error.Message)
}
return json.Unmarshal(env.Data, out)
}
func (c *Core) ApplyConfig(config []byte, out any) error {
input := C.CString(string(config))
defer C.free(unsafe.Pointer(input))
return decode(C.ai_core_apply_config_json(c.handle, input), out)
}Use the binding from a worker goroutine or service worker pool and keep the handle alive across requests instead of creating one per call.
Example: PHP Binding
final class AiCore
{
private FFI $ffi;
private FFI\CData $handle;
public function __construct(string $library)
{
$this->ffi = FFI::cdef('
typedef struct AiCoreFfiHandle AiCoreFfiHandle;
AiCoreFfiHandle *ai_core_handle_new(void);
void ai_core_handle_free(AiCoreFfiHandle *handle);
void ai_core_string_free(char *ptr);
char *ai_core_apply_config_json(
AiCoreFfiHandle *handle,
const char *config_json
);
', $library);
$this->handle = $this->ffi->ai_core_handle_new();
}
public function applyConfig(array $config): array
{
$ptr = $this->ffi->ai_core_apply_config_json(
$this->handle,
json_encode($config, JSON_THROW_ON_ERROR)
);
try {
$env = json_decode(FFI::string($ptr), true, flags: JSON_THROW_ON_ERROR);
if (!$env['ok']) {
throw new RuntimeException($env['error']['message']);
}
return $env['data'];
} finally {
$this->ffi->ai_core_string_free($ptr);
}
}
public function close(): void
{
$this->ffi->ai_core_handle_free($this->handle);
}
}Use the binding from a long-lived PHP worker process or another background execution layer, not directly from latency-sensitive request glue that also owns durable retries or queueing.
Example: Rust Worker Wrapper
use std::sync::Arc;
use tokio::task;
// Assume AiCoreBinding is the thin synchronous wrapper around ai-core-ffi.
struct AiCoreWorker {
core: Arc<AiCoreBinding>,
}
impl AiCoreWorker {
fn new() -> Self {
Self {
core: Arc::new(AiCoreBinding::new()),
}
}
async fn apply_config(&self, config: Vec<u8>) -> anyhow::Result<serde_json::Value> {
let core = Arc::clone(&self.core);
task::spawn_blocking(move || {
let mut out = serde_json::Value::Null;
core.apply_config(&config, &mut out)?;
Ok(out)
})
.await??
}
}This keeps one long-lived handle inside a worker object and moves the blocking FFI call onto tokio::task::spawn_blocking instead of running it on a Tokio worker thread.
Binding Shape
Expose idiomatic host-language methods over the C ABI:
- Python:
core.apply_config(config: dict) -> dict - Go:
core.ApplyConfig(config []byte, out any) error - PHP:
$core->applyConfig(array $config): array
Application code should never manage C pointers directly. Keep pointer copying and ai_core_string_free inside the binding layer.