Kotlin Multiplatform 三方库 SQLDelight 的 OpenHarmony 鸿蒙化适配实战
Kotlin Multiplatform 三方库 SQLDelight 的 OpenHarmony 鸿蒙化适配实战(RDB C API + Kotlin/Native + NAPI 桥接 ArkTS)
库版本:SQLDelight 2.x(自建 ohos 切片)|验证环境:Kotlin Multiplatform 2.1.x(含 ohosArm64/ohosX64 target)|DevEco Studio 5.x|DevEco 模拟器 x86_64|OpenHarmony API 12

SQLDelight 是 Cash App 开源的 Kotlin 数据库框架:写 .sq 文件,编译期生成强类型 Kotlin API,底层靠 SqlDriver 接口对接各平台 SQLite。官方覆盖 Android / iOS / JVM / Native / JS,唯独没有 HarmonyOS。本文记录把 SQLDelight 跑上鸿蒙的全过程:cinterop 接入系统 RDB C API、Kotlin/Native 编双 ABI 的 .so、NAPI 桥接 ArkTS,并逐个排掉 6 个坑(9568347 ABI 不匹配、spawn java ENOENT、OH_Values_Create: symbol not found、main: symbol not found、14800001 RDB_E_INVALID_ARGS、opaque config 内部 token 校验失败),最终在 DevEco 模拟器上建表/写入/查询/版本号全绿。
*先睹为快:DevEco 模拟器实测,状态徽章 PASSED,Console 输出 dlopen → 驱动版本 → 沙箱路径 → OH_Rdb_GetOrOpen → CREATE/INSERT/SELECT 全绿*

一、适配目标与整体链路
目标:在鸿蒙模拟器里跑一个 ArkTS 应用,点按钮真实调用 Kotlin/Native 里的 SQLDelight 驱动,走完「打开 RDB → 建表 → 写入 → 查询」全链路并渲染结果——证明整条链路真正打通,而不是 UI 摆几个写死的字符串。
整体链路:
ArkTS (Index.ets)
│ import sqldelightdemo from 'libsqldelightdemo.so'
▼ NAPI
napi_init.cpp (C++)
│ dlopen("libsqldelight_driver_ohos.so", RTLD_LAZY)
▼
libsqldelight_driver_ohos.so ← Kotlin/Native (ohosArm64 / ohosX64)
│ cinterop
▼
libnative_rdb_ndk.z.so (系统 RDB,底层 SQLite)
关键设计决策:驱动编成独立的 Kotlin/Native 共享库,不进 DevEco 的 CMake 链接,而是运行时 dlopen。这样 Kotlin/Native 的链接参数、运行时初始化与 hap 的原生构建完全解耦——CMake 不需要知道 Kotlin 的存在。

二、工程结构
sqldelight-ohos/
├── sqldelight-runtime/ # SQLDelight 多平台运行时(SqlDriver/SqlCursor/Transacter)
├── sqldelight-driver-ohos/ # ★ OHOS 驱动:实现 SqlDriver on RDB C API
│ ├── build.gradle.kts # 双 ABI + 链接参数
│ └── src/
│ ├── ohosMain/kotlin/app/cash/sqldelight/driver/ohos/OhosSqlDriver.kt
│ └── nativeInterop/cinterop/rdb.def
└── deveco-demo/ # DevEco 工程,加载驱动 .so 验证
└── entry/src/main/
├── cpp/napi_init.cpp # NAPI 桥 + dlopen
├── ets/pages/Index.ets # 点按钮触发
└── libs/{arm64-v8a,x86_64}/libsqldelight_driver_ohos.so
sqldelight-runtime:SQLDelight 的SqlDriver/SqlCursor/Transacter接口,commonMain 无平台代码;sqldelight-driver-ohos:实现SqlDriver,通过 cinterop 调 OHOS RDB C API;deveco-demo:ArkTS UI + NAPI 桥,dlopen驱动.so做真机验证。
三、适配过程:六个关键步骤
3.1 cinterop 接入 RDB C API
OHOS 的关系型存储 NDK 是纯 C API,头文件在 <OHOS_SDK>/native/sysroot/usr/include/database/rdb/。用 Kotlin/Native cinterop 生成绑定:
# sqldelight-driver-ohos/src/nativeInterop/cinterop/rdb.def
headers = database/rdb/relational_store.h database/rdb/oh_cursor.h \
database/rdb/oh_value_object.h database/rdb/oh_values_bucket.h \
database/rdb/oh_rdb_types.h database/rdb/oh_rdb_transaction.h \
database/rdb/relational_store_error_code.h \
database/data/oh_data_values.h database/data/oh_data_value.h
headerFilter = database/rdb/*.h database/data/*.h
compilerOpts = -I C:/PROGRA~1/Huawei/DEVECO~1/sdk/default/OPENHA~1/native/sysroot/usr/include
excludedFunctions = OH_Rdb_CreateOrOpenWithCryptoParam
excludedStructs = OH_Rdb_CryptoParam
三点注意:
- 路径用 Windows 8.3 短路径。clang 对带空格和括号的路径(
C:\Program Files\Huawei\DevEco Studio\...)解析不可靠,会被截断成C:\Program。查短路径:(New-Object -ComObject Scripting.FileSystemObject).GetFolder("C:\Program Files\Huawei").ShortPath # -> C:\PROGRA~1\Huawei compilerOpts只影响 cinterop 生成绑定的阶段,不影响最终链接。这是 3.5 的根因。OH_Rdb_CryptoParam系列在 API 12 头文件里声明与结构体定义不一致,直接排除,否则 cinterop 编译失败。
3.2 用 Kotlin/Native 编译双 ABI 的 .so
鸿蒙的 Kotlin/Native target 是 ohosArm64(真机)和 ohosX64(模拟器),两个都要:
// sqldelight-driver-ohos/build.gradle.kts
kotlin {
val ohosSysrootLib = "C:/PROGRA~1/Huawei/DEVECO~1/sdk/default/OPENHA~1/native/sysroot/usr/lib"
listOf(
ohosArm64() to "aarch64-linux-ohos",
ohosX64() to "x86_64-linux-ohos",
).forEach { (target, abiTriple) ->
target.binaries {
sharedLib {
linkerOpts(
"-L$ohosSysrootLib/$abiTriple",
"-lnative_rdb_ndk.z",
"-lhilog_ndk.z",
"--defsym", "main=0", // 见 3.6
)
}
}
target.compilations.getByName("main") {
cinterops.create("rdb") {
defFile = project.file("src/nativeInterop/cinterop/rdb.def")
}
}
}
}
用 JDK 21 作为 JAVA_HOME 执行:
$env:JAVA_HOME = "C:/Users/nwu/Desktop/z_pig/jdk-21.0.2"
.\gradlew.bat :sqldelight-driver-ohos:linkReleaseSharedOhosArm64 `
:sqldelight-driver-ohos:linkReleaseSharedOhosX64
产物:
sqldelight-driver-ohos/build/bin/ohosArm64/releaseShared/libsqldelight_driver_ohos.so
sqldelight-driver-ohos/build/bin/ohosX64/releaseShared/libsqldelight_driver_ohos.so
sqldelight-driver-ohos/build/bin/ohosArm64/releaseShared/libsqldelight_driver_ohos_api.h
_api.h 是 Kotlin/Native 导出符号表头文件,C++ 侧消费用。
3.3 打开数据库:用公开结构体,不用 opaque config
这是最容易踩的坑。 RDB NDK 同时提供两套配置 API:
| API | 风格 | 状态 |
|---|---|---|
OH_Rdb_CreateConfig + OH_Rdb_SetDatabaseDir/SetArea/SetSecurityLevel + OH_Rdb_CreateOrOpen | opaque 句柄 | API 12 模拟器上 CreateOrOpen 稳定返回 14800001 = RDB_E_INVALID_ARGS,所有 setter 都返回 0(成功),问题出在 opaque config 的内部 token 校验,外部无法绕过 |
alloc<OH_Rdb_Config>() 直接填字段 + OH_Rdb_GetOrOpen | 公开结构体(@since 10) | ✅ 可用 |
用后者:
memScoped {
val config = alloc<OH_Rdb_Config>()
config.selfSize = sizeOf<OH_Rdb_Config>().toInt()
config.dataBaseDir = databaseDir.cstr.ptr
config.storeName = databaseName.cstr.ptr
config.bundleName = bundleName.cstr.ptr
config.moduleName = "entry".cstr.ptr
config.isEncrypt = false
config.securityLevel = 1 // RDB_SECURITY_LEVEL_S1
config.area = 2 // RDB_SECURITY_AREA_EL2
val errCode = alloc<IntVar>()
store = OH_Rdb_GetOrOpen(config.ptr, errCode.ptr)
?: throw IllegalStateException("open failed: ${errCode.value}")
}
经验:OHOS NDK 同时存在"旧公开结构体"和"新 opaque 句柄"两套 API 时,模拟器上优先试旧的那套——opaque 版本常带运行时版本/权限校验,模拟器镜像不一定满足。
3.4 NAPI 桥接:dlopen 进 Kotlin 世界
完整实现见 deveco-demo/entry/src/main/cpp/napi_init.cpp,骨架:
#include <dlfcn.h>
#include "libsqldelight_driver_ohos_api.h"
static void* g_kotlinLib;
static libsqldelight_driver_ohos_ExportedSymbols* g_kotlinSymbols;
static bool LoadKotlinLibrary() {
if (g_kotlinSymbols) return true;
// 必须 RTLD_LAZY,见 3.6
g_kotlinLib = dlopen("libsqldelight_driver_ohos.so", RTLD_LAZY | RTLD_LOCAL);
if (!g_kotlinLib) return false;
auto init = (libsqldelight_driver_ohos_ExportedSymbols* (*)())
dlsym(g_kotlinLib, "libsqldelight_driver_ohos_symbols");
g_kotlinSymbols = init();
return g_kotlinSymbols != nullptr;
}
// 调用 Kotlin 构造函数
auto& pkg = g_kotlinSymbols->kotlin.root.app.cash.sqldelight.driver.ohos;
auto driver = pkg.OhosSqlDriver.OhosSqlDriver(dbDir, dbName, bundleName);
if (driver.pinned == nullptr) { /* 创建失败 */ }
Kotlin/Native 共享库导出一个 lib<name>_symbols() 函数,返回一张巨大的结构体表:Kotlin 里每个包、类、函数都对应表里的一个字段/函数指针。Kotlin 的 null 用 { .pinned = nullptr } 字面量构造。
ArkTS 侧:
import sqldelightdemo from 'libsqldelightdemo.so';
const ctx = getContext(this);
const dbDir = ctx.databaseDir; // 真沙箱路径,不能硬编码
const testResult: string = sqldelightdemo.testDatabase(dbDir) as string;
UI 是深色现代化界面:渐变背景(#0B1020 → #101A33)+ 状态徽章(IDLE/RUNNING/PASSED/FAILED 四态圆点)+ 卡片式 Console(✓/✕/· 前缀日志)。
*初始页:深蓝渐变 + IDLE 徽章 + 发光主按钮,点击「Test Database」触发 NAPI 调用*
3.5 排掉链接阶段的坑:cinterop def 的 linkerOpts 不进最终链接
驱动编出来,dlopen 时报:
OH_Values_Create: symbol not found
根因:最初把 -lnative_rdb_ndk.z 写进了 rdb.def 的 linkerOpts,但那只影响 cinterop 工具自身(生成 klib),不透传到最终 .so 的链接命令,导致 .so 的 DT_NEEDED 里没有 libnative_rdb_ndk.z.so。
修复:链接参数挪到 sharedLib { linkerOpts(...) }(见 3.2)。
3.6 排掉加载阶段的坑:main: symbol not found
链接修好后,dlopen 报:
main: symbol not found
根因:Kotlin/Native 在 ohos target 上链接时会拉入 musl 的 Scrt1.o(可执行文件启动文件),其中引用 main,但共享库没有 main。
修复(两步缺一不可):
- 链接侧:
linkerOpts("--defsym", "main=0")——把main定义为绝对地址 0,让链接器闭嘴。- 注意
--defsym main=0要拆成两个参数,且不要加-Wl,前缀(linkerOpts已直接透传给 ld.lld)。写-Wl,--defsym -Wl,main=0会被 ld.lld 当未知参数。
- 注意
- 加载侧:
dlopen(..., RTLD_LAZY)——函数符号延迟到首次调用才解析,main永远不会被调用,0 地址也就永远不会被解引用。
错误方向:--defsym main=_init 能链接通过,但运行时一旦有路径真的跳到 main,会把 ELF 初始化函数当 main 跑,直接段错误。别用符号当 main 的替身,用绝对地址 0 + LAZY 绑定。
四、运行效果(DevEco 模拟器实测)
应用已正常安装到 DevEco 模拟器并可拉起:
*点「Test Database」后:状态徽章 PASSED,Console 输出 dlopen 成功 → 驱动版本 v1.0.0 → 沙箱路径 /data/storage/el2/database/entry → OH_Rdb_GetOrOpen 成功 → CREATE TABLE / INSERT / SELECT 全绿*
点「Get Version」也能正常返回:
*点「Get Version」:Console 追加 Version: SQLDelight OHOS Driver v1.0.0*
五、FAQ
Q1:DevEco 点 Run 装模拟器报 code:9568347 — install parse native so failed — Abi type ... does not match?模拟器是 x86_64,但 hap 里 libs/ 只有 arm64-v8a。修复:① 驱动加 ohosX64() target;② entry/build-profile.json5 的 abiFilters 加 "x86_64"。
Q2:命令行 hvigorw assembleHap 报 Invalid value of 'DEVECO_SDK_HOME'?先 export DEVECO_SDK_HOME=<DevEco 安装目录>/sdk,再 hvigorw --stop-daemon 后重试。
Q3:PackageHap 任务报 spawn java ENOENT?hvigor 的打包节点进程是 node,它通过 spawn('java', ...) 起 jar 包做最终打包,读的是 PATH 里的 java,不是 JAVA_HOME。把 JDK 的 bin 目录加进 PATH。在 DevEco 内部点 Run 没这问题(IDE 会注入 java),只有命令行 hvigorw 有。
Q4:dlopen 报 OH_Values_Create: symbol not found?cinterop def 文件的 linkerOpts 不进最终链接。把 -lnative_rdb_ndk.z 挪到 sharedLib { linkerOpts(...) }。
Q5:dlopen 报 main: symbol not found?Kotlin/Native 链接 Scrt1.o 残留 main 引用。链接侧 --defsym main=0 + 加载侧 dlopen(RTLD_LAZY)。别用 main=_init,会段错误。
Q6:OH_Rdb_CreateOrOpen 返回 14800001?OHOS 错误码基数 E_BASE = 14800000,+1 即 RDB_E_INVALID_ARGS。两个可能根因:① 用了 opaque config API(模拟器上内部 token 校验失败),换公开结构体 OH_Rdb_Config + OH_Rdb_GetOrOpen;② 数据库路径硬编码(如 /data/storage/el2/database),不是合法沙箱路径,改用 getContext().databaseDir。
Q7:模拟器能用吗?完全可用——RDB 是纯本地引擎,无外设依赖。这是与蓝牙/NFC 等受限场景相反的"完美验证"类别。
Q8:hdc 自动化点击按钮没反应?别凭截图比例估算坐标,用 hdc shell uitest dumpLayout 查控件真实 bounds,再按中心点 uitest uiInput click x y。
六、总结与参考
把 SQLDelight 跑上鸿蒙,本质是打通 KMP → Kotlin/Native(ohos target) → .so → dlopen → NAPI → ArkTS → RDB C API 这条链。关键不在某一步多难,而在于每一步都有"看起来对但其实不对"的细节:cinterop 的 linkerOpts 不进最终链接、Scrt1.o 残留 main 符号、opaque config 在模拟器上的内部校验、沙箱路径不能硬编码。这次全绿,证明整条链路真实可用。
还没做的:.sq 文件集成(现在 SQL 是硬编码字符串,没走 SQLDelight 编译器生成强类型 API)、异步驱动(RDB 异步回调 → 协程)、加密(OH_Rdb_CryptoParam)、分布式同步。但最难的部分——Kotlin/Native 共享库在 OHOS 上 dlopen 起来、初始化成功、调通系统 C API——已经打通,后面都是业务封装。
更多推荐


所有评论(0)