欢迎加入【开源鸿蒙PC社区】,一起共建鸿蒙化C/C++三方库生态。
欢迎在【PC社区】平台贡献你的项目。
仓库: google/leveldb v1.23 — 高性能 KV 存储引擎
集成平台: 鸿蒙PC| 测试SDK: HarmonyOS 6.1.0(23)
源代码:https://atomgit.com/allincoding/OHOSLeveldbSample

在这里插入图片描述
在这里插入图片描述

前置说明

项目 说明
集成库 LevelDB v1.23
库类型 静态库(.a),零外部依赖
目标平台 鸿蒙PC
SDK 版本 HarmonyOS 6.1.0(23),兼容 6.0.0(20)
开发工具 DevEco Studio
原生编译器 BiSheng(build-profile.json5 中 nativeCompiler: BiSheng
交叉编译工具链 lycium_plusplus
三方库静态库 libleveldb.a for arm64-v8a
NAPI 接口数 6 个(ldbOpen/ldbClose/ldbPut/ldbGet/ldbDelete/ldbVersion)
UI 框架 ArkTS Stage 模式,三栏桌面布局

LevelDB 与前面集成的 libharu/capstone 的关键区别:它是 C++ 库但提供了 C APIleveldb/c.h),且是静态库。静态库意味着无需 .so 双重部署,但链接顺序和 m pthread 依赖不可遗漏。


传统方式的效率瓶颈

链接错误

运行错误

工程搭建

库文件部署

CMake 配置

NAPI 桥接

类型声明

UI 页面

编译测试

阶段 主要痛点 传统耗时
工程搭建 手动建目录、改 module.json5 10分钟
库文件部署 15 个头文件 + .a,目录嵌套 5分钟
CMake 配置 静态库路径、链接顺序(m/pthread) 15分钟
NAPI 桥接 C API 指针传递、options 生命周期、错误处理 40分钟
类型声明 Index.d.ts 签名匹配 10分钟
UI 页面 KV 输入、PUT/GET/DELETE 交互 25分钟
编译排错 链接符号缺失、运行时参数错误 20-60分钟

总计 2-3 小时,NAPI 桥接占大头——LevelDB 的 C API 需要 options/readoptions/writeoptions 等临时对象,生命周期管理容易出错。


AtomCode + Skills 全流程

环节 1:工程创建与模板复用

基于 OHOSCapstoneSample 模板(零依赖 .so 库),改造为静态库模式:

cp -r /home/hoapp/OHOSCapstoneSample /home/hoapp/OHOSLeveldbSample
rm -rf .git entry/build entry/.cxx oh_modules
rm -rf entry/src/main/cpp/thirdparty/capstone entry/libs

静态库 vs 动态库的关键差异:静态库不需要 entry/libs/ 部署,也不需要 IMPORTED 目标,直接在 CMake 中用完整路径链接即可。

环节 2:库文件部署

头文件 → cpp/thirdparty/leveldb/include/leveldb/

15 个头文件,核心是 c.h(C API)和 db.h(C++ API)。我们只用 c.h——C API 避免 C++ ABI 兼容性问题,且 NAPI 桥接更简单。

静态库 → cpp/thirdparty/leveldb/libs/arm64-v8a/

只需 libleveldb.a 一个文件,无需 .so、无需 entry/libs/ 部署。

环节 3:CMake 配置

静态库的 CMake 比动态库更简单,但有一个容易遗漏的点——m 和 pthread

cmake_minimum_required(VERSION 3.5.0)
project(OHOSLeveldbSample)

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
set(LDB_ROOT ${NATIVERENDER_ROOT_PATH}/thirdparty/leveldb)

if(DEFINED PACKAGE_FIND_FILE)
    include(${PACKAGE_FIND_FILE})
endif()

include_directories(${NATIVERENDER_ROOT_PATH}
                    ${NATIVERENDER_ROOT_PATH}/include
                    ${LDB_ROOT}/include)

set(LDB_LIB ${LDB_ROOT}/libs/arm64-v8a/libleveldb.a)

add_library(entry SHARED napi_init.cpp)
target_link_libraries(entry PUBLIC libace_napi.z.so)
target_link_libraries(entry PUBLIC ${LDB_LIB})
target_link_libraries(entry PUBLIC m pthread)   # ← 不可遗漏!

为什么需要 m 和 pthread:LevelDB 内部使用了 std::atomic(依赖 pthread)和 std::sqrt(依赖 libm)。静态链接时这些符号不会自动带入,必须显式链接。

环节 4:NAPI 桥接

6 个接口,核心设计决策是指针传递方式

指针编码为 hex 字符串

LevelDB 的 leveldb_t* 是不透明指针,无法直接传给 ArkTS。方案:将指针地址编码为 hex 字符串,ArkTS 侧作为 dbPtr: string 保存,C++ 侧用 strtoull 还原:

// 打开数据库,返回指针的 hex 编码
static napi_value LdbOpen(napi_env env, napi_callback_info info) {
    // ... 读取 path 参数 ...
    leveldb_t *db = nullptr;
    char *err = nullptr;
    leveldb_options_t *options = leveldb_options_create();
    leveldb_options_set_create_if_missing(options, 1);  // ← 坑1!
    db = leveldb_open(options, path.c_str(), &err);
    leveldb_options_destroy(options);                    // ← 记得释放!
    if (err) { std::string e = err; leveldb_free(err); return Str(env, "ERROR:" + e); }
    // 指针 → hex 字符串
    char buf[32]; snprintf(buf, sizeof(buf), "%p", (void*)db);
    return Str(env, buf);
}

// 其他接口从 hex 字符串还原指针
leveldb_t *db = (leveldb_t*)strtoull(ptrStr.c_str(), nullptr, 16);

为什么不返回 ArrayBuffer 或 External:ArkTS 的 NAPI 侧操作 External 较复杂,hex 字符串方案简单可靠,且对 KV 存储场景性能足够。

ldbPut / ldbGet / ldbDelete — KV 操作
static napi_value LdbPut(napi_env env, napi_callback_info info) {
    // 读取 dbPtr, key, value
    leveldb_t *db = (leveldb_t*)strtoull(ptrStr.c_str(), nullptr, 16);
    char *err = nullptr;
    leveldb_writeoptions_t *wo = leveldb_writeoptions_create();
    leveldb_put(db, wo, key.c_str(), key.size(), val.c_str(), val.size(), &err);
    leveldb_writeoptions_destroy(wo);   // ← 必须释放!
    if (err) { std::string e = err; leveldb_free(err); return Str(env, "ERROR:" + e); }
    return Str(env, "");
}

static napi_value LdbGet(napi_env env, napi_callback_info info) {
    // 读取 dbPtr, key
    leveldb_readoptions_t *ro = leveldb_readoptions_create();
    val = (char*)leveldb_get(db, ro, key.c_str(), key.size(), &vlen, &err);
    leveldb_readoptions_destroy(ro);
    if (!val) return Str(env, "");  // key 不存在,返回空串
    std::string result(val, vlen);
    leveldb_free(val);              // ← leveldb_get 返回的内存必须 free!
    return Str(env, result);
}

options 生命周期leveldb_writeoptions_tleveldb_readoptions_t 是临时对象,用完必须 _destroy 释放。leveldb_get 返回的 val 指针由 leveldb 内部 malloc,必须 leveldb_free

环节 5:类型声明

export const ldbOpen: (path: string) => string;
export const ldbClose: (dbPtr: string) => string;
export const ldbPut: (dbPtr: string, key: string, value: string) => string;
export const ldbGet: (dbPtr: string, key: string) => string;
export const ldbDelete: (dbPtr: string, key: string) => string;
export const ldbVersion: () => string;

返回值约定:成功返回空串(ldbPut/ldbDelete/ldbClose)或数据(ldbGet),失败返回 "ERROR:..." 前缀字符串。ArkTS 侧用 startsWith('ERROR:') 判断。

环节 6:UI 页面

三栏布局(数据库状态 + 操作/数据/代码 + API 参考),核心交互:

打开数据库 → PUT/GET/DELETE → 数据表展示

aboutToAppear(): void {
  this.abilityCtx = getContext(this) as common.UIAbilityContext;
  this.openDb();  // 启动时自动打开
}

private openDb(): void {
  this.dbPath = `${this.abilityCtx.filesDir}/leveldb_demo`;
  const ptr = ldb.ldbOpen(this.dbPath);
  if (ptr.startsWith('ERROR:')) { /* 错误处理 */ return; }
  this.dbPtr = ptr;  // 保存指针,后续操作都用它
}

环节 7:C API vs C++ API 的选择

LevelDB 同时提供 C++ API(db.h)和 C API(c.h)。我们选择 C API 有三个原因:

  1. ABI 稳定性:C API 的符号签名是固定的,不受 C++ 编译器版本、name mangling、异常处理等影响。BiSheng 编译器与标准 clang 的 C++ ABI 可能存在差异,C API 完全规避此风险
  2. NAPI 桥接简单:C API 全部是函数调用,无类/模板/STL,桥接代码量比 C++ 少 50%+
  3. 错误处理统一:C API 通过 char** errptr 返回错误,NAPI 统一转为 "ERROR:..." 字符串
// C++ API(不推荐用于 NAPI)
#include <leveldb/db.h>
leveldb::DB* db;
leveldb::Options options;
options.create_if_missing = true;
leveldb::Status status = leveldb::DB::Open(options, path, &db);
// Status 对象、string 引用、虚函数表 → ABI 风险

// C API(推荐)
#include <leveldb/c.h>
leveldb_t* db;
leveldb_options_t* options = leveldb_options_create();
leveldb_options_set_create_if_missing(options, 1);
char* err = nullptr;
db = leveldb_open(options, path, &err);
// 纯函数调用、char* 错误 → 零 ABI 风险

踩坑专区

坑 1:leveldb_open 默认 create_if_missing=false 导致首次打开失败

现象
应用启动报错 Invalid argument: /data/storage/.../leveldb_demo: does not exist (create_if_missing is false)

根因
leveldb_options_create() 返回的 options 对象,create_if_missing 默认值为 0(false)。首次运行时数据库目录不存在,leveldb 拒绝创建,直接报错。

修复

// 错误 —— 默认不创建,首次打开必失败
db = leveldb_open(leveldb_options_create(), path.c_str(), &err);

// 正确 —— 显式设置 create_if_missing
leveldb_options_t *options = leveldb_options_create();
leveldb_options_set_create_if_missing(options, 1);
db = leveldb_open(options, path.c_str(), &err);
leveldb_options_destroy(options);

LevelDB C API 的默认值陷阱create_if_missingerror_if_existsparanoid_checks 等选项的默认值都是 0/false,不会自动创建数据库。这与 Python/Rust 绑定中默认 create_if_missing=True 的行为不同。

坑 2:静态库链接遗漏 m 和 pthread 导致 undefined symbol

现象
链接期报 undefined reference to 'pthread_mutex_lock'undefined reference to 'sqrt'

根因
LevelDB 内部使用了 C++ <thread>(依赖 pthread)和 <cmath>(依赖 libm)。作为静态库,这些依赖不会自动传递给最终链接目标,必须显式声明。

修复

# 错误 —— 遗漏 m 和 pthread
target_link_libraries(entry PUBLIC libace_napi.z.so)
target_link_libraries(entry PUBLIC ${LDB_LIB})

# 正确 —— 显式链接 m 和 pthread
target_link_libraries(entry PUBLIC libace_napi.z.so)
target_link_libraries(entry PUBLIC ${LDB_LIB})
target_link_libraries(entry PUBLIC m pthread)

静态库链接规则:静态库(.a)不像动态库(.so)那样自动传递依赖。静态库用到的系统库(m/pthread/dl 等)必须在最终链接目标上显式声明。这是 CMake 集成静态库最常见的遗漏。

坑 3:leveldb_get 返回值未用 leveldb_free 释放导致内存泄漏

现象
多次 GET 操作后应用内存持续增长,不回落。

根因
leveldb_get 返回的 char* 指针由 leveldb 内部 malloc 分配,调用方必须用 leveldb_free 释放。如果直接构造 std::string 后忘记 free,每次 GET 都泄漏一块内存。

修复

// 错误 —— 忘记释放,内存泄漏
val = (char*)leveldb_get(db, ro, key.c_str(), key.size(), &vlen, &err);
std::string result(val, vlen);  // ← val 未释放!

// 正确 —— 构造 string 后释放
val = (char*)leveldb_get(db, ro, key.c_str(), key.size(), &vlen, &err);
if (!val) return Str(env, "");
std::string result(val, vlen);
leveldb_free(val);  // ← 必须释放!
return Str(env, result);

LevelDB C API 内存所有权规则leveldb_get 返回的值、leveldb_openerrptr 指向的字符串,都由 leveldb 内部分配,必须用 leveldb_free 释放。leveldb_options_t 等 options 对象用 _destroy 释放。两者不可混用。


通用集成模板(拿来即用)

静态库 CMakeLists.txt 模板

适用于 LevelDB 这类零依赖静态库:

cmake_minimum_required(VERSION 3.5.0)
project({Project} C CXX)

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
set(LIB_ROOT ${NATIVERENDER_ROOT_PATH}/thirdparty/{lib})

if(DEFINED PACKAGE_FIND_FILE)
    include(${PACKAGE_FIND_FILE})
endif()

include_directories(${NATIVERENDER_ROOT_PATH}
                    ${NATIVERENDER_ROOT_PATH}/include
                    ${LIB_ROOT}/include)

set(LIB_A ${LIB_ROOT}/libs/arm64-v8a/lib{lib}.a)

add_library(entry SHARED napi_init.cpp)
target_link_libraries(entry PUBLIC libace_napi.z.so)
target_link_libraries(entry PUBLIC ${LIB_A})
target_link_libraries(entry PUBLIC m pthread)  # 静态库的系统依赖

LevelDB C API 指针传递模板

// 打开 → 指针编码为 hex
static napi_value LdbOpen(napi_env env, napi_callback_info info) {
    // ... 读取 path ...
    leveldb_options_t *opt = leveldb_options_create();
    leveldb_options_set_create_if_missing(opt, 1);
    char *err = nullptr;
    leveldb_t *db = leveldb_open(opt, path.c_str(), &err);
    leveldb_options_destroy(opt);
    if (err) { std::string e = err; leveldb_free(err); return Str(env, "ERROR:" + e); }
    char buf[32]; snprintf(buf, sizeof(buf), "%p", (void*)db);
    return Str(env, buf);
}

// 关闭 → hex 还原为指针
static napi_value LdbClose(napi_env env, napi_callback_info info) {
    // ... 读取 ptrStr ...
    leveldb_t *db = (leveldb_t*)strtoull(ptrStr.c_str(), nullptr, 16);
    if (db) leveldb_close(db);
    return Str(env, "");
}

NAPI 错误处理约定模板

// 约定:成功返回空串或数据,失败返回 "ERROR:..."
if (err) {
    std::string e = err;
    leveldb_free(err);        // 释放 leveldb 分配的错误字符串
    return Str(env, "ERROR:" + e);
}
return Str(env, "");           // 成功
// ArkTS 侧判断
const result = ldb.ldbPut(this.dbPtr, key, value);
if (result.startsWith('ERROR:')) {
    // 失败:result = "ERROR:IO error: ..."
} else {
    // 成功
}

总结

LevelDB 集成鸿蒙的难点不在库本身(零依赖、C API 简洁),而在 静态库链接的系统依赖传递C API 默认参数的隐含假设create_if_missing=false 是设计上的保守选择,但对应用开发者来说是第一个必踩的坑;m/pthread 的遗漏是静态库集成的通病,CMake 不会自动帮你补。


你在 NAPI 集成中遇到过什么奇怪的错误?是链接符号缺失、C API 默认值还是内存泄漏?欢迎在评论区分享你的经验。

如果本文对你有帮助,请 点赞、收藏、转发 支持一下~

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐