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

三点注意:

  1. 路径用 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
    
  2. compilerOpts 只影响 cinterop 生成绑定的阶段,不影响最终链接。这是 3.5 的根因。
  3. 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_CreateOrOpenopaque 句柄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。

修复(两步缺一不可):

  1. 链接侧:linkerOpts("--defsym", "main=0")——把 main 定义为绝对地址 0,让链接器闭嘴。
    • 注意 --defsym main=0 要拆成两个参数,且不要加 -Wl, 前缀(linkerOpts 已直接透传给 ld.lld)。写 -Wl,--defsym -Wl,main=0 会被 ld.lld 当未知参数。
  2. 加载侧: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——已经打通,后面都是业务封装。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐