Языковые привязки
ai-core-ffi — это слой C ABI для SDK не на Rust. Рассматривайте его как стабильную границу для Python, Go, PHP, Swift, Kotlin, Flutter и интеграций на стороне сервера.
Что покрывает этот раздел
Эта страница объясняет:
- какие функции C ABI нужно оборачивать;
- что каждая функция принимает и возвращает;
- как передавать config и request JSON;
- как освобождать возвращаемые строки;
- как строить bindings для Go, Python и PHP.
Обёртка результата
Каждая функция, возвращающая строку, отдаёт выделенную UTF-8 C string с одним из этих JSON envelopes:
{"ok":true,"data":{}}{"ok":false,"error":{"code":"config_error","message":"..."}}Host должен распарсить envelope, скопировать данные в host-owned objects и освободить возвращённую строку через ai_core_string_free.
Владение памятью
ai_core_handle_newвозвращает opaque runtime handle.ai_core_handle_freeосвобождает этот handle.- Каждый возвращённый
char *должен быть освобождён черезai_core_string_free. - Не храните raw C pointers в long-lived host objects.
- Копируйте возвращённый JSON в host-managed memory до освобождения SDK string.
Вызывайте FFI calls вне UI/main async executor thread. Для parallel work держите один long-lived handle на worker или tenant.
Несколько handles в одном host process разделяют native local-model managers под слоем FFI. Сборка builtin-local-llm хранит один resident llama_model на каждый ключ process/model/backend после первого использования. Сборка builtin-speech-whisper хранит resident Whisper context pool на каждый путь process/model. Отдельные OS processes не разделяют эти managers.
Используйте models[].local.max_concurrent_requests и speech.whisper.max_concurrent_requests, чтобы ограничить активную native работу внутри одного process. Используйте max_pending_requests только как небольшую in-process admission queue; durable queues, retries и backpressure остаются в host.
Методы
ai_core_ffi_abi_version
- Inputs: none.
- Returns:
uint32_tABI version. Current value:1. - Use when: binding wants to reject incompatible shared libraries before 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 runtime instance for worker, tenant or process.
- Notes: handle registers built-in core tools and built-in HTTP model providers. Builds with
builtin-local-llmalso register 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 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: host 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: 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: 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
.aipplugin or pipeline package.
ai_core_pipeline_start_json
- Inputs: handle and start request JSON.
- Returns: pipeline run update JSON.
- Use when: starting 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 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 может быть "continue" или "retry". Для "retry" host передаёт заменяющий user_text; повторный запуск всё так же получает skill шага, output schema и предыдущие 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: host already knows 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 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
}Поддерживаемые роли: system, user и assistant. Поддерживаемые форматы ответа: text, json_object и json_schema с обязательной строкой schema. Если модель вернула валидный JSON, content содержит вложенное JSON-значение; любой другой ответ сохраняется исходной строкой.
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"
}Поддерживаемые значения format: wav, pcm16, m4a, mp3, flac, ogg_opus, webm_opus и unknown. Для предсказуемого native behavior host' ы должны декодировать сжатые uploads в WAV PCM16 или raw PCM16 до вызова 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:
Host-owned config snapshot and model-selection fields are documented in Runtime Contracts.
Паттерн worker'а
Примеры ниже показывают тонкие binding layers, а не полную orchestration worker'ов. Application code должна хранить один long-lived handle на worker или tenant и выносить blocking FFI calls с UI thread или async executor thread.
Пример: 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)Вызывайте binding из background thread или process worker. Не запускайте FFI calls напрямую на event-loop thread.
Пример: 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)
}Вызывайте binding из worker goroutine или service worker pool и держите handle живым между requests вместо создания нового handle на каждый call.
Пример: 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);
}
}Вызывайте binding из long-lived PHP worker process или другого background execution layer, а не напрямую из latency-sensitive request glue, которая ещё и владеет durable retries или queueing.
Пример: 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??
}
}Это удерживает один long-lived handle внутри worker object и переносит blocking FFI call в tokio::task::spawn_blocking, а не на Tokio worker thread.
Форма binding'ов
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.