GenieX Android SDK 四层 JNI 桥接架构解析:端侧 LLM 与 VLM 的完整调用链路
【免费下载链接】GenieXRun frontier LLMs and VLMs locally on Qualcomm devices across NPU, GPU, and CPU with a few lines of code项目地址: https://gitcode.com/GitHub_Trending/ne/GenieX
本文以 bindings/android/README.md 为主线,结合 GenieX 仓库中 Kotlin、C++ 与核心库源码,系统拆解 Android 端 SDK 的“四层 JNI 桥接”架构:从面向协程的高层 Kotlin API,到声明
external方法的 JNI 接口、实现类型转换与回调桥接的 C++ 层,再到最终执行推理的核心libgeniex.so。读者将掌握 LLM/VLM 在 Android 端的创建、流式生成、停止、聊天模板等完整调用链路,理解插件注册与原生库加载机制,并学会如何构建与发布 Android AAR。
一、整体架构:四层 JNI 桥接模式
GenieX 的 Android 绑定通过JNI(Java Native Interface)桥接将 Kotlin 上层 API 与 C 语言核心推理库连接起来,整体划分为四个职责清晰的层级:
┌─────────────────────────────────────────────────────────┐ │ Layer 4: Public API (Kotlin) │ │ ModelWrapper.kt - Coroutine-based high-level API │ │ (LlmWrapper, VlmWrapper) │ └────────────────┬────────────────────────────────────────┘ │ calls ┌────────────────▼────────────────────────────────────────┐ │ Layer 3: JNI Interface (Kotlin) │ │ Model.kt - Declares external native methods │ │ (Llm, Vlm) │ └────────────────┬────────────────────────────────────────┘ │ JNI boundary ┌────────────────▼────────────────────────────────────────┐ │ Layer 2: JNI Bridge (C++) │ │ model_bridge_jni.cpp - Implements JNI methods │ │ Java_com_geniex_sdk_jni_Model_* functions │ └────────────────┬────────────────────────────────────────┘ │ calls C API ┌────────────────▼────────────────────────────────────────┐ │ Layer 1: Core Library (C) │ │ libgeniex.so - Provides ml_* API from ml.h │ │ (ml_llm_create, ml_llm_generate, etc.) │ └─────────────────────────────────────────────────────────┘四层的职责划分非常清晰:Kotlin 层只面向业务、C 层只面向推理,中间由 JNI 边界完成语言屏障的跨越。每一层都可以独立演进——更换底层推理引擎不影响上层 API,调整 Kotlin API 形态也无须改动 C 核心。
需要说明的是,原文档 Layer 1 中标注的ml.h/ml_*命名属于早期设计约定。从当前仓库源码看,核心 C API 的实际头文件位于 sdk/include/geniex.h,对外暴露的是geniex_LLM、geniex_llm_create()、geniex_llm_generate()等geniex_*前缀函数(详见下文“Layer 1”一节),架构分层思想本身保持不变。
二、Layer 4:面向协程的高层 Kotlin API
最高层是业务开发者直接接触的 Kotlin API,位于bindings/android/app/src/main/java/com/geniex/sdk/下,代表是 LlmWrapper.kt 与 VlmWrapper.kt。
2.1 构建器模式:Builder + suspend build()
两个 Wrapper 都采用Builder 模式 + 协程的设计:Builder负责收集输入参数,build()是挂起函数,在指定调度器上完成原生句柄的创建。
// LlmWrapper 构建入口 class LlmWrapper private constructor( private val dispatcher: CoroutineDispatcher ) : Closeable { private val llm = Llm() private var handle: Long = 0 companion object { @JvmStatic fun builder() = Builder() } class Builder { private var llmCreateInput: LlmCreateInput? = null private var dispatcher: CoroutineDispatcher = Dispatchers.IO fun llmCreateInput(llmCreateInput: LlmCreateInput) = apply { this.llmCreateInput = llmCreateInput } fun dispatcher(dispatcher: CoroutineDispatcher) = apply { this.dispatcher = dispatcher } suspend fun build(): Result<LlmWrapper> = withContext(dispatcher) { try { val input = llmCreateInput ?: throw IllegalArgumentException("modelPath required") val wrapper = LlmWrapper(dispatcher) wrapper.handle = wrapper.llm.create(input) Result.success(wrapper) } catch (e: Exception) { Result.failure(e) } } } }关键设计点:
- 默认调度器为
Dispatchers.IO,保证模型创建这类耗时操作不阻塞主线程;调用方可通过dispatcher(...)覆盖。 - 句柄(
handle: Long)是原生对象在 Kotlin 侧的“令牌”,所有后续操作都以它作为参数传给 JNI。 build()以Result包装结果,模型路径缺失等参数问题会以异常形式捕获并转为Result.failure。- Wrapper 实现
Closeable,close()内部委托destroy()释放原生资源。
VlmWrapper 的构建方式完全对称(VlmWrapper.kt):
val wrapper = VlmWrapper.builder() .vlmCreateInput(input) .dispatcher(Dispatchers.IO) .build()2.2 流式生成:callbackFlow 包装原生回调
两个 Wrapper 都提供了generateStreamFlow(prompt, config): Flow<LlmStreamResult>,用callbackFlow把原生回调模型翻译成 Kotlin Flow,上层可以像收集普通 Flow 一样处理流式输出:
fun generateStreamFlow(prompt: String, config: GenerationConfig): Flow<LlmStreamResult> = callbackFlow { withContext(dispatcher) { val callback = object : LLMTokenCallback { override fun onToken(token: String): Boolean { trySend(LlmStreamResult.Token(token)) return true } override fun onComplete(result: LlmGenerateResult) { trySend(LlmStreamResult.Completed(result.profileData)) close() } } try { val result = llm.generate(handle, prompt, config, callback) Log.d(TAG, "llm result:$result") } catch (e: Exception) { trySend(LlmStreamResult.Error(e)) close() } } awaitClose { close() } }流经这个 Flow 的事件分为三类:
| 事件 | 含义 |
|---|---|
LlmStreamResult.Token(token) | 每次产出的新 token 文本 |
LlmStreamResult.Completed(profileData) | 生成结束,携带 ProfilingData 性能统计 |
LlmStreamResult.Error(e) | 生成过程发生异常 |
onToken返回Boolean是原生端用来控制“是否继续生成”的语义:返回false即可让 C 侧停止吐 token(详见 jni_cb.cpp 的 stop_flag 机制)。
2.3 生命周期与辅助方法
LlmWrapper 还提供以下方法(VlmWrapper 结构相同):
applyChatTemplate(messages, tools, enableThinking, addGenerationPrompt):把多轮ChatMessage用模型的聊天模板格式化成单条提示词;tools为可选的工具调用 JSON,enableThinking控制是否开启“思考模式”。stopStream():挂起函数,请求终止正在进行的流式生成。reset():重置模型状态(对应原生geniex_llm_reset)。destroy():释放原生句柄并清零,幂等安全。
VlmWrapper 额外提供injectMediaPathsToConfig(messages, config),它调用原生extractMediaPaths从多模态消息里抽取图片/音频路径,回填到GenerationConfig的imagePaths、audioPaths字段中,方便直接构造带媒体的生成请求。
三、Layer 3:JNI 接口声明层
第三层位于bindings/android/app/src/main/java/com/geniex/sdk/jni/,是声明external原生方法的内部类,业务代码不应直接触碰。
LLm.kt 声明的原生方法:
internal class Llm { external fun create(llmCreateInputObj: LlmCreateInput): Long external fun reset(handle: Long): Int external fun destroy(handle: Long): Int external fun stopStream(handle: Long) external fun applyChatTemplate( handle: Long, messages: Array<ChatMessage>, tools: String?, enableThinking: Boolean, addGenerationPrompt: Boolean = true ): LlmApplyChatTemplateOutput external fun generate( handle: Long, prompt: String, config: GenerationConfig, cb: LLMTokenCallback ): LlmGenerateResult }Vlm.kt 与之对应,并多出两个多模态专属方法:
internal class Vlm { external fun create(vlmCreateInput: VlmCreateInput): Long external fun destroy(handle: Long): Int external fun reset(handle: Long): Int external fun getCapabilities(handle: Long): VlmCapabilities external fun generate(handle: Long, prompt: String, config: GenerationConfig, cb: LLMTokenCallback): LlmGenerateResult external fun applyChatTemplate(handle: Long, messages: Array<VlmChatMessage>, tools: String?, enableThinking: Boolean): LlmApplyChatTemplateOutput external fun stopStream(handle: Long) external fun extractMediaPaths(messages: Array<VlmChatMessage>): Pair<Array<String>, Array<String>> }getCapabilities用于查询 VLM 的能力信息(VlmCapabilities.kt),extractMediaPaths则负责解析多模态消息中的媒体引用。这一层是 JNI 命名约定的枢纽:Kotlin 类com.geniex.sdk.jni.Llm的create方法,对应 C++ 侧的Java_com_geniex_sdk_jni_Llm_create函数。
四、Layer 2:C++ JNI 桥接层
这是整个桥接最核心的一层,位于bindings/android/app/src/main/cpp/,负责将 Java/Kotlin 数据类型转换为 C 结构体,并调用核心库 API。
4.1 LLM 桥接:llm_bridge_jni.cpp
JNI 函数与核心 C API 的映射关系如下:
| JNI 函数 | 调用的核心 API | 职责 |
|---|---|---|
Java_com_geniex_sdk_jni_Llm_create | geniex_llm_create() | 解析LlmCreateInput为geniex_LlmCreateInput,创建geniex_LLM*句柄并以jlong返回 |
Java_com_geniex_sdk_jni_Llm_destroy | geniex_llm_destroy() | 释放句柄对应资源 |
Java_com_geniex_sdk_jni_Llm_generate | geniex_llm_generate() | 发起生成,注册 token/完成回调 |
Java_com_geniex_sdk_jni_Llm_stopStream | 置位停止标志 | 通过std::atomic<bool>通知生成线程停止 |
Java_com_geniex_sdk_jni_Llm_applyChatTemplate | geniex_llm_apply_chat_template() | 聊天模板格式化 |
Java_com_geniex_sdk_jni_Llm_reset | geniex_llm_reset() | 重置模型状态 |
以create为例,它展示了完整的类型转换流程:
extern "C" JNIEXPORT jlong JNICALL Java_com_geniex_sdk_jni_Llm_create( JNIEnv* env, jobject thiz, jobject llm_create_input_obj) { geniex_LlmCreateInput create_input = extract_llm_create_input(env, llm_create_input_obj); geniex_LLM* handle = nullptr; int32_t result = geniex_llm_create(&create_input, &handle); if (result != GENIEX_SUCCESS || !handle) { throw_runtime_exception(env, "Llm create failed: %s", geniex_get_error_message(static_cast<geniex_ErrorCode>(result))); return 0; } return reinterpret_cast<jlong>(handle); }类型转换工具集中在 jniutils.cpp(extract_llm_create_input、extract_generation_config、jstring2str等),回调处理集中在 jni_cb.cpp 与 jni_cb.h。
4.2 流式回调的线程模型
generate的实现揭示了流式回调的关键细节:
- 每次生成前,在全局
g_stopFlags表中为句柄登记一个std::atomic<bool>停止标志; - C 侧每产出一个 token,通过
jni_cb_emit_token回调 Kotlin 的onToken; stopStream只是把对应标志置为true,下一次回调即被拦截;- 回调完成后清理全局引用与停止标志,保证
stop_flag的生命周期安全。
jni_cb.cpp里还有一个值得注意的工程细节——token 编码转换。原生 SDK 输出标准 UTF-8(可能包含 4 字节 emoji),而NewStringUTF期望 JNI 修改版 UTF-8,会破坏增补平面字符。因此jni_cb.cpp实现了utf8_to_jstring:手动解码 UTF-8 序列为 Unicode 码点,再转为 UTF-16 代理对,最后用NewString构造jstring。这正是流式中文、emoji 输出不乱码的底层保障。
4.3 SDK 初始化桥接:geniex_sdk.cpp
JNI_OnLoad是原生库的入口,完成三件事:
- 重定向
stdout/stderr到 logcat; - 通过
geniex_set_log(android_sdk_log_to_logcat)把 SDK 日志路由到GenieXSdk标签; - 调用
geniex_init()完成核心库初始化。
此外它还实现了插件注册与 QAIRT 运行时切换的原生方法:
registerPlugin(pluginLibPath):dlopen加载插件.so,dlsym取出plugin_id与create_plugin两个符号,交给geniex_register_plugin()注册;setQairtRuntimePath(path):切换 QAIRT 运行时路径。文档与源码注释特别强调:必须在init()之前调用,因为 QNN 库每个进程只会加载一次、从不卸载;路径不可用时错误会在创建模型时才暴露。
五、Layer 1:核心 C 库 libgeniex.so
最底层是核心推理库,对应仓库 sdk 目录。README 描述的核心库 API 来自ml.h/ml_*约定,当前仓库中实际实现为:
- 头文件:sdk/include/geniex.h,定义
geniex_LLM、geniex_ModelConfig、geniex_GenerationConfig、geniex_llm_create/generate/destroy/reset等符号; - 实现:sdk/src/llm.cpp、sdk/src/vlm.cpp(以及 device.cpp、ml.cpp、registry.cpp 等);
- 插件体系:推理后端通过插件动态注册,当前仓库内置 llama_cpp 与 qairt 两个插件,前者提供 CPU/GPU/HYBRID 计算单元,后者基于 Qualcomm QNN 提供 NPU 加速。
从源码结构可以推断,核心库通过插件注册表(registry)+ 运行时标识(runtime_id)做后端分发:上层只提交“模型路径 + 配置”,runtime_id决定由哪个插件实际执行,从而实现同一套 JNI 桥接层支持多种推理后端的可扩展设计。
六、LLM 完整调用链:从 Kotlin 一行调用到 C 推理
将四层串联起来,一次 LLM 流式生成的生命周期如下:
- 初始化:
GenieXSdk.getInstance().init(context),注册插件并初始化模型管理器; - 创建:
LlmWrapper.builder().llmCreateInput(LlmCreateInput(...)).build()→Llm.create(external)→Java_..._Llm_create→geniex_llm_create(),获得原生句柄; - 生成:
generateStreamFlow(prompt, config)→llm.generate→Java_..._Llm_generate→geniex_llm_generate(),C 侧逐 token 回调onToken,经 UTF-8→UTF-16 转换后由 Flow 发射给上层; - 停止/重置:
stopStream()置位原子标志中断生成;reset()调用geniex_llm_reset; - 释放:
close()/destroy()→geniex_llm_destroy(),句柄清零。
6.1 关键输入配置
LlmCreateInput.kt 定义模型创建输入:
data class LlmCreateInput( override val model_path: String, // 必填:模型路径 val tokenizer_path: String? = null, // tokenizer 路径(可选) override val config: ModelConfig, // 模型配置 override val runtime_id: String? = null, // 后端选择(llama_cpp / qairt) override val compute_unit: String? = null // 计算单元别名 ) : CreateInputBasecompute_unit为null时选择各后端默认值:llama_cpp默认HYBRID,qairt默认NPU;也可显式指定CPU/GPU/NPU/HYBRID。
ModelConfig.kt 与原生geniex_ModelConfig结构体一一对应,核心字段:
| 字段 | 默认值 | 说明 |
|---|---|---|
nCtx | 2048 | 文本上下文大小,0 = 使用模型默认 |
nThreads | 8 | 文本生成线程数 |
nThreadsBatch | 8 | 批量处理线程数 |
nBatch | 2048 | 提交给llama_decode的最大逻辑批量 |
nUBatch | 512 | 后端支持的最大物理批量 |
nSeqMax | 1 | 最大并行序列数 |
nGpuLayers | -1 | 卸载到 GPU/NPU 的层数;-1 = 全部(CPU 计算单元下 JNI 会强制为 0) |
spec_type | "" | 投机解码类型(llama_cpp 专属),如draft-mtp、ngram-* |
GenerationConfig.kt 控制单次生成行为:
data class GenerationConfig( var maxTokens: Int = 32, var stopWords: Array<String>? = null, var stopCount: Int = 0, var samplerConfig: SamplerConfig? = null, var imagePaths: Array<String>? = null, // 多模态输入 var imageCount: Int = 0, var audioPaths: Array<String>? = null, var audioCount: Int = 0, var slidingWindow: Boolean = false, // 环缓冲上下文淘汰(qairt) var slidingWindowNKeep: Int = 0 // 0 = 插件默认(4) )七、SDK 初始化、插件注册与原生库加载
GenieXSdk.kt 是 SDK 入口,其companion object中的静态初始化块负责加载 JNI 桥接库:
companion object { init { System.loadLibrary("npu_jni") // 加载 Layer 2 JNI 桥接 } }init(context, callback)完成两层初始化,并通过@Volatile标志保证幂等(Activity 重建后重复调用是安全的):
- 插件注册:遍历
RuntimeIdValue.LLAMA_CPP与RuntimeIdValue.QAIRT,在nativeLibraryDir下查找libgeniex_plugin_<name>.so,存在则调用原生registerPlugin动态注册; - 模型管理器初始化:在
context.filesDir/geniex创建数据目录,调用 ModelManager 初始化模型管理 FFI(错误码 -100008 对应GENIEX_ERROR_COMMON_ALREADY_INITIALIZED,视为幂等成功)。
所有异常被收集后统一通过InitCallback.onFailure(reason)上报,无异常则回调onSuccess()。README 中提到的动态插件加载目标产物libgeniex_plugin.so(NPU 后端,内部基于 QNN)也由此机制完成注册,从而在不改动桥接层的前提下扩展新的推理后端。
八、构建系统与产物
8.1 原生库编译
Android 工程的 CMake 入口为 bindings/android/app/src/main/cpp/CMakeLists.txt。它并不在本地编译核心库,而是链接预编译的 SDK 包:
# 使用预构建 SDK 包 sdk/pkg-geniex get_filename_component(PROJECT_ROOT "${CMAKE_SOURCE_DIR}/../../../../../.." ABSOLUTE) set(SDK_INSTALL_DIR "${PROJECT_ROOT}/sdk/pkg-geniex") set(geniex_bridge "${SDK_INSTALL_DIR}/lib/libgeniex.so") set(SDK_INCLUDE_DIR "${SDK_INSTALL_DIR}/include") add_subdirectory(qnn) # NPU 后端构建配置(QNN 库)qnn/CMakeLists.txt 负责 NPU 后端的构建配置(QNN 库),仓库同时提供了 arm64-android-llvm.cmake 等交叉编译工具链,用于产出 arm64-v8a 目标(app/extLibs/arm64-v8a/libomp.so为预置的 OpenMP 运行时)。
最终构建产物包括:
| 产物 | 说明 |
|---|---|
libnpu_jni.so | JNI 桥接包装库,由GenieXSdk通过System.loadLibrary("npu_jni")加载 |
libgeniex.so | 核心 ML 库,被 JNI 包装库链接 |
libgeniex_plugin_<runtime>.so | 插件库,如 NPU 后端,供registerPlugin动态加载 |
8.2 发布到 Maven
README 记录的 Android SDK AAR 发布流程(在bindings/android目录下执行):
- 修改
app/update.gradle中的tmpVersion为新版本号; - 使用 Gradle 同步工程;
- 执行
./gradlew assembleRelease构建 release AAR; - 执行
./gradlew publish生成 Maven 格式的repo目录。
仓库同时提供了完整的 Gradle 工程骨架:settings.gradle.kts、gradle/libs.versions.toml、gradle.properties 与 gradlew 包装器。README 还标注了“发布到 Maven Central”与“发布到 GitHub”两个 TODO 章节,说明这两条公网发布路径尚待补充详细文档。
九、目录结构与模块定位
bindings/android/ ├── app/ │ ├── src/main/ │ │ ├── cpp/ # Layer 2: JNI Bridge (C++) │ │ │ ├── llm_bridge_jni.cpp # LLM JNI 实现 │ │ │ ├── vlm_bridge_jni.cpp # VLM JNI 实现 │ │ │ ├── geniex_sdk.cpp # SDK 初始化与插件注册 │ │ │ ├── jniutils.cpp/.h # JNI 类型转换工具 │ │ │ ├── jni_cb.cpp/.h # 流式回调处理 │ │ │ ├── qnn/CMakeLists.txt # NPU 后端构建配置(QNN) │ │ │ └── CMakeLists.txt │ │ └── java/com/geniex/sdk/ │ │ ├── jni/ # Layer 3: JNI Interface │ │ │ ├── Llm.kt │ │ │ └── Vlm.kt │ │ ├── LlmWrapper.kt # Layer 4: LLM 公共 API │ │ ├── VlmWrapper.kt # Layer 4: VLM 公共 API │ │ ├── GenieXSdk.kt # SDK 入口 │ │ └── bean/ # 数据类(配置、输入、结果、回调) │ └── build.gradle.kts # 构建配置 └── README.md # 本文档bean/目录下的数据类是 JNI 边界的“契约”——CreateInputBase.kt、ModelConfig.kt、GenerationConfig.kt、LlmGenerateResult.kt、LlmStreamResult.kt 等,它们的字段布局必须与 C++ 侧extract_*解析逻辑严格对应。
十、日志与调试
SDK 日志被统一路由到 logcat 的GenieXSdk标签,优先级与 Android 日志级别一一对应(TRACE→VERBOSE、DEBUG→DEBUG、INFO→INFO、WARN→WARN、ERROR→ERROR)。桥接层源码中大量使用LOGd/LOGe输出句柄、错误码与调用过程,排查问题时可直接过滤:
adb logcat -s GenieXSdk此外JNI_OnLoad会把 C 侧的stdout/stderr一并重定向到 logcat,因此原生层的打印也能在同一标签下看到,配合错误码(如geniex_get_error_message返回的描述)即可快速定位创建失败、生成中断等问题的根因。
小结
GenieX 的 Android 绑定用一套严谨的四层 JNI 桥接架构,把“Kotlin 协程 API”与“C 推理内核”干净地解耦:上层开发者只面对 LlmWrapper / VlmWrapper 的构建器与 Flow,原生层通过 llm_bridge_jni.cpp / vlm_bridge_jni.cpp 完成类型转换与回调桥接,最终由libgeniex.so调度 llama_cpp / qairt 插件在 CPU、GPU 或 Qualcomm NPU 上执行推理。理解这条从 Kotlin 到 C 的完整链路,是排查问题、定制后端和扩展多模态能力的基础。
【免费下载链接】GenieXRun frontier LLMs and VLMs locally on Qualcomm devices across NPU, GPU, and CPU with a few lines of code项目地址: https://gitcode.com/GitHub_Trending/ne/GenieX
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考