Cactus 跨语言绑定全指南:Python/Swift/Kotlin/Flutter/Rust/React Native 接入同一个推理引擎

原创2026-10-09 11:29:021,907 阅读
文章标签:推理引擎模型推理服务算子库模型量化边缘计算

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
  1. 拷贝 android/libcactus_engine.so 到 app/src/main/jniLibs/arm64-v8a/
  2. 拷贝 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
  1. 拷贝 android/libcactus_engine.so 到 App 的 jniLibs/arm64-v8a/
  2. iOS:把 apple/cactus-ios.xcframework 加入 Xcode 工程
  3. macOS:把 apple/cactus-macos.xcframework 加入 Xcode 工程
  4. 拷贝 cactus.dart 到你的 Dart 源码树
  5. 在 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
  1. 把本目录的桥接文件加入 React Native 工程
  2. 加入它们依赖的原始绑定:bindings/kotlin/(Android)、bindings/swift/(Apple)
  3. 在 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 下的常见模式

六种绑定虽然语法各异,但行为模式高度一致,理解这些模式可以显著降低切换语言时的学习成本:

  1. 句柄生命周期:cactus_init 返回不透明句柄,用毕必须 cactus_destroy。KMP 的 cactusInit 与 cactusStreamTranscribeStart 在返回 0L 时还会抛出带 cactus_get_last_error 信息的异常(见 Cactus.android.kt),Swift/Rust/Dart 侧则由调用方自行判断空指针。
  2. 调用方提供缓冲区:cactus_complete、cactus_transcribe 等函数要求调用方传入 char* 缓冲与长度(各绑定示例中普遍使用 65536 字节,KMP 的完整对话缓冲为 1 MiB),返回值是 NUL 结尾的 C 字符串或 JSON。
  3. 返回码约定:C 函数返回非负整数表示成功、负数表示失败,失败详情通过 cactus_get_last_error() 获取;cactus_log_set_level 的日志级别为 0=DEBUG、1=INFO、2=WARN(默认)、3=ERROR、4=NONE(见 cactus_engine.h)。
  4. 流式转录协议:cactus_stream_transcribe_start(model, optionsJson) 创建流句柄,逐块喂入 16 kHz 单声道 PCM16,process 每次返回 {"success":true,"confirmed":...,"pending":...},最后 stop 收尾;confirmed 段可安全追加到已展示文本,pending 段用于实时预览且随时可能被改写。该协议在 Swift、Kotlin、Dart、Rust、TypeScript 五份文档中完全一致,属于引擎层面的稳定契约。
  5. 错误与调试:所有绑定都暴露 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 与相同的能力集,选型本质上只取决于目标平台与工程栈;无论选哪条路,模型的构建、对话、转录与索引能力都是同一套引擎在提供。

登录后查看全文
cactus