Cactus 跨语言绑定全指南:Python/Swift/Kotlin/Flutter/Rust/React Native 接入同一个推理引擎
Cactus 跨语言绑定全指南:Python/Swift/Kotlin/Flutter/Rust/React Native 接入同一个推理引擎
Cactus 是一个面向移动端、可穿戴设备、智能家居与机器人的量化、内核、运行时与推理引擎项目。本文以 bindings/README.md 为核心骨架,系统梳理该项目为上层语言提供的六类官方绑定:Python(ctypes)、Swift(C 模块映射)、Kotlin(Android JNI + Kotlin/Native cinterop,即 KMP)、Flutter(Dart FFI)、Rust(原生 extern "C")与 React Native(薄桥接层),并逐一给出集成步骤、可运行的代码示例与流式语音转录的实战写法。读完本文,你将掌握各绑定在 Cactus 中的定位、如何为对应平台构建原生产物、如何在你的应用里完成模型初始化、对话补全、流式转录与资源释放,以及这些绑定背后的统一 C FFI 契约(cactus_engine.h)是如何支撑起全部上层语言的。
绑定总览:一个 C 头文件,六种语言入口
bindings/ 目录不是多个独立 SDK,而是围绕同一个稳定 C ABI(cactus_engine.h)铺开的六套"语言壳"。Cactus 引擎的公开能力——LLM 对话补全、预填充(prefill)、tokenize、打分窗口、语音转录(含流式)、文本/图像/音频嵌入、RAG 查询、向量索引(增删查压)、日志与遥测——全部以 C 函数形式导出(见 cactus_engine.h),各语言绑定只是把这些符号翻译成对应生态的调用方式:
| 绑定目录 | 底层技术 | 目标平台 |
|---|---|---|
| bindings/python/ | ctypes |
Arm Linux、macOS |
| bindings/swift/ | C module map | iOS、macOS |
| bindings/kotlin/ | JNI(Android)+ Kotlin/Native cinterop(iOS,KMP) | Android、iOS |
| bindings/flutter/ | Dart FFI | Android、iOS、macOS |
| bindings/rust/ | 原生 extern "C" 声明 |
Arm Linux、macOS |
| bindings/react-native/ | 基于 Kotlin 与 Swift 原始绑定的薄桥 | iOS、Android |
一个关键的设计决策是:平台相关的构建与打包产物并不放在 bindings/ 内。Android 原生产物的构建由仓库根目录下的 android/ 负责,Apple 原生产物由 apple/ 负责。绑定层只关心"如何调用",不关心"如何编译引擎"——这种解耦让每种语言都可以独立演进,也让所有绑定共享同一份 API 语义。
各绑定覆盖的能力范围
从 docs/choose-bindings.md 的对比表可以看到,六种绑定在能力上完全对齐:LLM 对话、流式输出、视觉(Vision)、音频、转录、函数调用(Function Calling)、RAG/嵌入与云回退(Cloud Fallback)均为全部支持。差异只体现在平台覆盖与安装方式上:
- 移动应用:React Native(已有 RN 工程时最快)、Flutter(跨平台移动 + macOS)、Kotlin(原生 Android 或 KMP)、Swift(iOS/macOS 原生,配合 Metal 加速);
- 服务端与脚本:Python(服务端推理、批量处理、快速原型);
- 系统级嵌入:C++(游戏引擎、原生桌面)、Rust(系统级集成)。
构建前置:先产出原生库,再谈绑定
所有绑定(Python 除外,它有 pip 安装路径)都依赖先构建出引擎原生库。两条构建入口位于绑定目录之外:
- Android:
cactus build --android(或直接bash android/build.sh)。产物包括libcactus_engine.so(JNI 共享库,供 Android App 使用)与libcactus_engine.a(静态库,供原生测试程序使用);构建参数见 android/README.md,其中ANDROID_NDK_HOME可自动探测,ANDROID_PLATFORM默认android-21,CMAKE_BUILD_TYPE默认Release,并默认链接仓库内 vendor 的 libcurl。 - Apple:
cactus build --apple,产出apple/cactus-ios.xcframework与apple/cactus-macos.xcframework(含cactus_engine.h),以及apple/libcactus_engine-device.a/apple/libcactus_engine-simulator.a静态库(详见 apple/README.md)。 - 通用静态库:Rust 与 C++ 场景使用
cactus build,产物位于cactus-engine/build/。
以 Python 为例,库的搜索顺序体现了开发/发布双路径的设计(见 python/cactus/bindings/cactus.py):优先读环境变量 CACTUS_LIB_PATH 覆盖;其次读打包目录 python/cactus/bindings/lib/(随 pip 包捆绑);再次回退到源码树的 cactus-engine/build/(开发构建,由 cactus build --python 填充)。加载失败时错误信息会明确提示常见原因(arm64 架构不匹配、Linux 缺 libcurl、构建过期等),并给出重新构建命令。
Python 绑定:ctypes 直连 cactus_engine.h
Python 绑定位于 python/cactus/bindings/cactus.py,以 ctypes 对 cactus_engine.h 做 FFI。模块加载后立即以 cactus_set_telemetry_environment(b"python", None, None) 声明自身框架身份(参见 cactus.py),随后逐一绑定 cactus_init、cactus_complete、cactus_transcribe、cactus_embed、cactus_rag_query 等入口,并将 cactus_token_callback 映射为 ctypes.CFUNCTYPE 回调类型,使 token 级流式回调可以在 Python 侧直接使用。
库文件命名按平台区分:macOS 为 libcactus_engine.dylib,Linux 为 libcactus_engine.so。安装、高层 API、CLI 与服务端的完整说明见 python/README.md。
import ctypes
import cactus # 高层 API(来自 python/cactus/__init__.py)
# 低层等价写法(ctypes 直接调用)
lib = ctypes.CDLL("libcactus_engine.so")
lib.cactus_init.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_bool]
lib.cactus_init.restype = ctypes.c_void_p
model = lib.cactus_init(b"/path/to/model", None, False)
# ...调用 cactus_complete / cactus_transcribe...
Swift 绑定:C module map + XCFramework
Swift 绑定通过 C module map 导入 cactus_engine.h,官方示例见 bindings/swift/。先执行 cactus build --apple,然后二选一集成:
- XCFramework 方式:把
apple/cactus-ios.xcframework(或apple/cactus-macos.xcframework)拖入 Xcode 并选择 Embed & Sign。框架自带cactus_engine.h,直接import cactus即可; - 静态库方式:链接
apple/libcactus_engine-device.a(真机)或apple/libcactus_engine-simulator.a(模拟器),把 bindings/swift/module.modulemap 拷入工程,并将cactus-engine/加入 Header Search Paths,让 module map 能定位到头文件。
模块映射文件与 import cactus 的桥接关系可见 bindings/swift/module.modulemap 与 bindings/swift/Cactus.swift(后者仅一行 @_exported import cactus,把 C 符号直接暴露给 Swift 调用方)。
对话补全
import cactus
let model = cactus_init("/path/to/model", nil, false)
var buf = CChar
cactus_complete(model, messagesJson, &buf, buf.count, nil, nil, nil, nil, nil, 0)
let response = String(cString: buf)
cactus_destroy(model)
注意 cactus_complete 的 10 个参数与 cactus_engine.h 一一对应:模型句柄、messages JSON、响应缓冲区与大小、可选的 options JSON、可选的 tools JSON、可选的 token 回调与 user_data、可选 PCM 缓冲区与大小。缓冲区由调用方提供并负责长度管理,返回的是以 NUL 结尾的 C 字符串。
流式转录
流式函数以相同方式暴露。按 16 kHz 单声道 PCM16 逐块推入,每次调用读回 {"success":true,"confirmed":...,"pending":...}——confirmed 是已定稿的文本,pending 是易变的尾部:
let stream = cactus_stream_transcribe_start(model, "{\"language\":\"en\"}")
var buf = CChar
for chunk in pcmChunks { // each chunk: 16 kHz mono 16-bit PCM
chunk.withUnsafeBytes { raw in
_ = cactus_stream_transcribe_process(stream, raw.bindMemory(to: UInt8.self).baseAddress, raw.count, &buf, buf.count)
}
// parse String(cString: buf) -> append "confirmed", show "pending" live
}
cactus_stream_transcribe_stop(stream, &buf, buf.count)
Kotlin 绑定:Android JNI 与 KMP 双通道
Kotlin 绑定分两层:CactusJNI(Cactus.kt)是 Android 侧的 JNI 桥,native* 方法签名与 android/cactus_jni.cpp 中 Java_com_cactus_CactusJNI_* 符号一一对应;KMP 侧则是 expect/actual 设计——Cactus.common.kt 声明跨平台 API,Cactus.android.kt 用 JNI 实现,Cactus.ios.kt 用 Kotlin/Native cinterop 实现(见 bindings/kotlin/)。
JNI 桥本身对 JVM 语言无感:Java 消费者只需编写一份 CactusJNI.java,native 声明与 Cactus.kt 保持一致,即可复用同一个 libcactus_engine.so 与同一组符号。
Android 集成
cactus build --android
- 拷贝
android/libcactus_engine.so到app/src/main/jniLibs/arm64-v8a/ - 拷贝
Cactus.kt与CactusCallbacks.kt到你的 Kotlin 源码树
注意 CactusJNI 的 init 块会执行 System.loadLibrary("cactus_engine"),因此 .so 必须按上面路径放置,否则启动即抛 UnsatisfiedLinkError。
Android 用法
val handle = CactusJNI.nativeInit("/path/to/model", null, false)
val buf = ByteArray(65536)
CactusJNI.nativeComplete(handle, messagesJson, buf, null, null, null, null)
val response = String(buf, 0, buf.indexOf(0))
CactusJNI.nativeDestroy(handle)
流式转录——推入 16 kHz 单声道 PCM16,每次调用读回 {"success":true,"confirmed":...,"pending":...}:
val stream = CactusJNI.nativeStreamTranscribeStart(handle, "{\"language\":\"en\"}")
val out = ByteArray(65536)
for (chunk in pcmChunks) { // each: 16 kHz mono PCM16 bytes
CactusJNI.nativeStreamTranscribeProcess(stream, chunk, out)
// parse confirmed/pending from String(out, 0, out.indexOf(0))
}
CactusJNI.nativeStreamTranscribeStop(stream, out)
Kotlin Multiplatform 结构
commonMain/ Cactus.common.kt expect declarations
commonMain/ CactusCallbacks.kt shared callback interfaces
androidMain/ Cactus.android.kt actual via JNI
iosMain/ Cactus.ios.kt actual via cinterop
build.gradle.kts 中配置 iOS cinterop 与链接参数:
kotlin {
androidTarget()
listOf(iosArm64(), iosSimulatorArm64()).forEach { target ->
target.compilations.getByName("main") {
cinterops {
val cactus by creating {
defFile("src/nativeInterop/cinterop/cactus.def")
includeDirs("/path/to/cactus/cactus-engine")
}
}
}
val libSuffix = if (target.name == "iosSimulatorArm64") "simulator" else "device"
target.binaries.framework {
linkerOpts("-L/path/to/cactus/apple", "-lcactus_engine-$libSuffix")
}
}
}
cinterop 的 defFile 指向 bindings/kotlin/cactus.def,includeDirs 指向 cactus-engine/ 以便生成器解析 cactus_engine.h;链接时按目标选择 libcactus_engine-device.a 或 libcactus_engine-simulator.a。
共享代码中的用法:
val model = cactusInit("/path/to/model", null, false)
val result = cactusComplete(model, messagesJson, null, null, null, null)
// streaming transcription: start, feed PCM16 chunks, stop
val stream = cactusStreamTranscribeStart(model, "{\"language\":\"en\"}")
val out = cactusStreamTranscribeProcess(stream, pcmChunk) // {"success":true,"confirmed":...,"pending":...}
val tail = cactusStreamTranscribeStop(stream)
cactusDestroy(model)
KMP 封装还带来了类型层红利:cactusComplete 内部使用 1 MiB 缓冲并把结果 trimEnd('\u0000') 后以 String 返回(见 Cactus.android.kt),cactusTokenize 返回真实 token 数长度的 IntArray,cactusIndexQuery 把原生 id/score 数组组装成 {"results":[...]} JSON——调用方无需手工管理 C 指针。
Flutter 绑定:Dart FFI
Flutter 绑定是 bindings/flutter/cactus.dart,一个完整的 Dart FFI 封装。库的装载策略值得注意:Android/Linux 上用 DynamicLibrary.open('libcactus_engine.so'),iOS/macOS 则用 DynamicLibrary.process() 从进程已加载符号中解析(XCFramework 已把库打进 App,见 cactus.dart)。
集成
cactus build --apple
cactus build --android
- 拷贝
android/libcactus_engine.so到 App 的jniLibs/arm64-v8a/ - iOS:把
apple/cactus-ios.xcframework加入 Xcode 工程 - macOS:把
apple/cactus-macos.xcframework加入 Xcode 工程 - 拷贝
cactus.dart到你的 Dart 源码树 - 在
pubspec.yaml添加ffi依赖
对话补全
import 'dart:ffi';
import 'cactus.dart';
import 'package:ffi/ffi.dart';
final modelPath = '/path/to/model'.toNativeUtf8();
final model = cactusInit(modelPath, nullptr, false);
calloc.free(modelPath);
const messagesJson = '[{"role":"user","content":"Hello"}]';
final msgs = messagesJson.toNativeUtf8();
final buf = calloc<Int8>(65536);
cactusComplete(model, msgs, buf.cast(), 65536, nullptr, nullptr, nullptr, nullptr, nullptr, 0);
final response = buf.cast<Utf8>().toDartString();
calloc.free(msgs);
calloc.free(buf);
cactusDestroy(model);
Dart FFI 的特性决定了内存生命周期必须由调用方显式管理:toNativeUtf8() 与 calloc 分配的每块内存都要在不再使用后 calloc.free,与 C API"调用方提供缓冲区"的约定完全一致。cactus.dart 对全部 C 符号都做了 Native/Dart 双 typedef 并逐一声明 argtypes 与 restype(例如 cactusComplete 的 10 参数签名见 cactus.dart),跨平台类型(Bool/bool、IntPtr/int、Uint8/int)也做了对应映射。
流式转录
final opts = '{"language":"en"}'.toNativeUtf8();
final stream = cactusStreamTranscribeStart(model, opts);
final out = calloc<Int8>(65536);
for (final chunk in pcmChunks) { // each: 16 kHz mono PCM16 bytes
final p = calloc<Uint8>(chunk.length)..asTypedList(chunk.length).setAll(0, chunk);
cactusStreamTranscribeProcess(stream, p, chunk.length, out.cast(), 65536);
calloc.free(p);
// parse {"confirmed":...,"pending":...} from out.cast<Utf8>().toDartString()
}
cactusStreamTranscribeStop(stream, out.cast(), 65536);
calloc.free(opts);
calloc.free(out);
Rust 绑定:原生 extern "C"
Rust 绑定 bindings/rust/cactus.rs 是手写的 unsafe extern "C" 声明块,不依赖任何自动生成工具,且自带 #[link] 属性。注意它在 macOS 上还声明了 Accelerate、CoreML、Metal、MetalPerformanceShaders、Foundation、Security、SystemConfiguration 与 curl 框架/库的链接,并以 #[link(name = "cactus_engine", kind = "static")] 静态链接引擎——这意味着 Rust 侧的产物分发要携带这些系统依赖。
集成
cactus build
把 cactus.rs 拷入工程(它自带 #[link] 属性),并在 build.rs 中把构建目录加入链接搜索路径:
// build.rs
println!("cargo:rustc-link-search=native=/path/to/cactus/cactus-engine/build");
对话补全
use std::ffi::CString;
use std::os::raw::c_char;
mod cactus;
fn main() {
unsafe {
let path = CString::new("/path/to/model").unwrap();
let model = cactus::cactus_init(path.as_ptr(), std::ptr::null(), false);
let messages = CString::new(r#"[{"role":"user","content":"Hello"}]"#).unwrap();
let mut response = vec![0u8; 65536];
cactus::cactus_complete(
model,
messages.as_ptr(),
response.as_mut_ptr() as *mut c_char,
response.len(),
std::ptr::null(), std::ptr::null(),
None, std::ptr::null_mut(),
std::ptr::null(), 0,
);
cactus::cactus_destroy(model);
}
}
流式转录
let opts = CString::new(r#"{"language":"en"}"#).unwrap();
let stream = cactus::cactus_stream_transcribe_start(model, opts.as_ptr());
let mut buf = vec![0u8; 65536];
for chunk in pcm_chunks { // each: 16 kHz mono i16 bytes
cactus::cactus_stream_transcribe_process(
stream, chunk.as_ptr(), chunk.len(),
buf.as_mut_ptr() as *mut c_char, buf.len(),
);
// append "confirmed"; show "pending" as a live preview
}
cactus::cactus_stream_transcribe_stop(stream, buf.as_mut_ptr() as *mut c_char, buf.len());
React Native 绑定:架在原始绑定之上的薄桥
React Native 绑定(bindings/react-native/)不是独立的 FFI,而是调用前面 Kotlin 与 Swift 原始绑定的 JS 桥:Android 侧由 CactusModule.kt 与 CactusPackage.kt 实现原生模块 Cactus,Apple 侧由 Cactus.m 与 Cactus.swift 实现。类型层也因此发生变化:C 指针句柄在 JS 侧表现为字符串句柄,PCM 数据以 base64 字符串传递。
集成
cactus build --apple
cactus build --android
- 把本目录的桥接文件加入 React Native 工程
- 加入它们依赖的原始绑定:bindings/kotlin/(Android)、bindings/swift/(Apple)
- 在
MainApplication.kt注册 Android 包:
override fun getPackages() = PackageList(this).packages.apply {
add(com.cactus.reactnative.CactusPackage())
}
index.ts 在原生模块缺失时会抛出带指引的 LINKING_ERROR(见 index.ts),提醒先完成桥接文件与注册步骤。
对话补全
import Cactus from './index';
const handle = await Cactus.init('/path/to/model', null, false);
const result = await Cactus.complete(handle, messagesJson, null, null, null, false);
await Cactus.destroy(handle);
CactusNativeModule 接口(index.ts)几乎完整镜像了 C API:init/destroy/reset/stop、prefill/complete/tokenize/scoreWindow、transcribe/streamTranscribeStart/Process/Stop、embed/imageEmbed/audioEmbed、ragQuery、indexInit/Add/Delete/Get/Query/Compact/Destroy,以及 logSetLevel、setTelemetryEnvironment、setAppId、telemetryFlush/Shutdown、getLastError。另有 CactusEvents(NativeEventEmitter)用于接收 TokenEvent({token, tokenId})类型的 token 流事件。
流式转录
const stream = await Cactus.streamTranscribeStart(handle, '{"language":"en"}');
for (const chunkBase64 of pcmChunks) {
const json = await Cactus.streamTranscribeProcess(stream, chunkBase64);
// parse json -> append "confirmed", show "pending" as a live preview
}
const tail = await Cactus.streamTranscribeStop(stream);
横切要点:统一 C ABI 下的常见模式
六种绑定虽然语法各异,但行为模式高度一致,理解这些模式可以显著降低切换语言时的学习成本:
- 句柄生命周期:
cactus_init返回不透明句柄,用毕必须cactus_destroy。KMP 的cactusInit与cactusStreamTranscribeStart在返回0L时还会抛出带cactus_get_last_error信息的异常(见 Cactus.android.kt),Swift/Rust/Dart 侧则由调用方自行判断空指针。 - 调用方提供缓冲区:
cactus_complete、cactus_transcribe等函数要求调用方传入char*缓冲与长度(各绑定示例中普遍使用 65536 字节,KMP 的完整对话缓冲为 1 MiB),返回值是 NUL 结尾的 C 字符串或 JSON。 - 返回码约定:C 函数返回非负整数表示成功、负数表示失败,失败详情通过
cactus_get_last_error()获取;cactus_log_set_level的日志级别为 0=DEBUG、1=INFO、2=WARN(默认)、3=ERROR、4=NONE(见 cactus_engine.h)。 - 流式转录协议:
cactus_stream_transcribe_start(model, optionsJson)创建流句柄,逐块喂入 16 kHz 单声道 PCM16,process每次返回{"success":true,"confirmed":...,"pending":...},最后stop收尾;confirmed段可安全追加到已展示文本,pending段用于实时预览且随时可能被改写。该协议在 Swift、Kotlin、Dart、Rust、TypeScript 五份文档中完全一致,属于引擎层面的稳定契约。 - 错误与调试:所有绑定都暴露
cactus_get_last_error;日志可通过cactus_log_set_callback注册回调接入应用日志系统;遥测通过cactus_set_telemetry_environment(framework, cacheLocation, version)与cactus_set_app_id声明运行环境(Python 绑定在加载时即以b"python"声明自身,见 cactus.py),并可在退出前调用cactus_telemetry_flush落盘。
选型建议
结合 docs/choose-bindings.md 的推荐:
- 已有 React Native 工程 → React Native 桥,
npm install即可接入; - 需要跨 Android/iOS/macOS 的统一 UI 层 → Flutter;
- 原生 Android 或要写共享 Kotlin 逻辑 → Kotlin/JNI 或 KMP;
- 原生 iOS/macOS 且看重 Metal 加速 → Swift XCFramework;
- 服务端推理、批量任务、快速原型 → Python;
- 系统级、嵌入式或对 FFI 边界要求极致的场景 → Rust 或直接使用 C++ 引擎 API(docs/cactus_engine.md)。
所有绑定共享同一份 C ABI 与相同的能力集,选型本质上只取决于目标平台与工程栈;无论选哪条路,模型的构建、对话、转录与索引能力都是同一套引擎在提供。