
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 绑定通过JNIJava 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(): ResultLlmWrapper 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 实现Closeableclose()内部委托destroy()释放原生资源。VlmWrapper 的构建方式完全对称VlmWrapper.ktval wrapper VlmWrapper.builder() .vlmCreateInput(input) .dispatcher(Dispatchers.IO) .build()2.2 流式生成callbackFlow 包装原生回调两个 Wrapper 都提供了generateStreamFlow(prompt, config): FlowLlmStreamResult用callbackFlow把原生回调模型翻译成 Kotlin Flow上层可以像收集普通 Flow 一样处理流式输出fun generateStreamFlow(prompt: String, config: GenerationConfig): FlowLlmStreamResult 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为可选的工具调用 JSONenableThinking控制是否开启“思考模式”。stopStream()挂起函数请求终止正在进行的流式生成。reset()重置模型状态对应原生geniex_llm_reset。destroy()释放原生句柄并清零幂等安全。VlmWrapper 额外提供injectMediaPathsToConfig(messages, config)它调用原生extractMediaPaths从多模态消息里抽取图片/音频路径回填到GenerationConfig的imagePaths、audioPaths字段中方便直接构造带媒体的生成请求。三、Layer 3JNI 接口声明层第三层位于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: ArrayChatMessage, 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: ArrayVlmChatMessage, tools: String?, enableThinking: Boolean): LlmApplyChatTemplateOutput external fun stopStream(handle: Long) external fun extractMediaPaths(messages: ArrayVlmChatMessage): PairArrayString, ArrayString }getCapabilities用于查询 VLM 的能力信息VlmCapabilities.ktextractMediaPaths则负责解析多模态消息中的媒体引用。这一层是 JNI 命名约定的枢纽Kotlin 类com.geniex.sdk.jni.Llm的create方法对应 C 侧的Java_com_geniex_sdk_jni_Llm_create函数。四、Layer 2C JNI 桥接层这是整个桥接最核心的一层位于bindings/android/app/src/main/cpp/负责将 Java/Kotlin 数据类型转换为 C 结构体并调用核心库 API。4.1 LLM 桥接llm_bridge_jni.cppJNI 函数与核心 C API 的映射关系如下JNI 函数调用的核心 API职责Java_com_geniex_sdk_jni_Llm_creategeniex_llm_create()解析LlmCreateInput为geniex_LlmCreateInput创建geniex_LLM*句柄并以jlong返回Java_com_geniex_sdk_jni_Llm_destroygeniex_llm_destroy()释放句柄对应资源Java_com_geniex_sdk_jni_Llm_generategeniex_llm_generate()发起生成注册 token/完成回调Java_com_geniex_sdk_jni_Llm_stopStream置位停止标志通过std::atomicbool通知生成线程停止Java_com_geniex_sdk_jni_Llm_applyChatTemplategeniex_llm_apply_chat_template()聊天模板格式化Java_com_geniex_sdk_jni_Llm_resetgeniex_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_castgeniex_ErrorCode(result))); return 0; } return reinterpret_castjlong(handle); }类型转换工具集中在 jniutils.cppextract_llm_create_input、extract_generation_config、jstring2str等回调处理集中在 jni_cb.cpp 与 jni_cb.h。4.2 流式回调的线程模型generate的实现揭示了流式回调的关键细节每次生成前在全局g_stopFlags表中为句柄登记一个std::atomicbool停止标志C 侧每产出一个 token通过jni_cb_emit_token回调 Kotlin 的onTokenstopStream只是把对应标志置为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.cppJNI_OnLoad是原生库的入口完成三件事重定向stdout/stderr到 logcat通过geniex_set_log(android_sdk_log_to_logcat)把 SDK 日志路由到GenieXSdk标签调用geniex_init()完成核心库初始化。此外它还实现了插件注册与 QAIRT 运行时切换的原生方法registerPlugin(pluginLibPath)dlopen加载插件.sodlsym取出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.createexternal→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默认HYBRIDqairt默认NPU也可显式指定CPU/GPU/NPU/HYBRID。ModelConfig.kt 与原生geniex_ModelConfig结构体一一对应核心字段字段默认值说明nCtx2048文本上下文大小0 使用模型默认nThreads8文本生成线程数nThreadsBatch8批量处理线程数nBatch2048提交给llama_decode的最大逻辑批量nUBatch512后端支持的最大物理批量nSeqMax1最大并行序列数nGpuLayers-1卸载到 GPU/NPU 的层数-1 全部CPU 计算单元下 JNI 会强制为 0spec_type投机解码类型llama_cpp 专属如draft-mtp、ngram-*GenerationConfig.kt 控制单次生成行为data class GenerationConfig( var maxTokens: Int 32, var stopWords: ArrayString? null, var stopCount: Int 0, var samplerConfig: SamplerConfig? null, var imagePaths: ArrayString? null, // 多模态输入 var imageCount: Int 0, var audioPaths: ArrayString? 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.soNPU 后端内部基于 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.soJNI 桥接包装库由GenieXSdk通过System.loadLibrary(npu_jni)加载libgeniex.so核心 ML 库被 JNI 包装库链接libgeniex_plugin_runtime.so插件库如 NPU 后端供registerPlugin动态加载8.2 发布到 MavenREADME 记录的 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),仅供参考