鸿蒙PC应用集成SQLite指南:从编译到NAPI踩坑实战
欢迎加入【开源鸿蒙PC社区】,一起共建鸿蒙化C/C++三方库生态。
欢迎在【PC社区】平台贡献你的项目。
仓库: sqlite/sqlite v3.47.x — 自包含、零配置、事务型 SQL 数据库引擎
集成平台: 鸿蒙PC| 测试SDK: 6.1.0(23)
源代码:https://atomgit.com/unisources/OHOSSqliteSample

前置说明
| 项目 | 说明 |
|---|---|
| 集成库 | sqlite3 v3.47.x |
| 目标平台 | 鸿蒙PC |
| SDK 版本 | OHOS SDK 6.1.0(23) |
| 兼容 SDK | OHOS SDK 6.0.0(20) |
| 开发工具 | DevEco Studio 5.x |
| 原生编译器 | BiSheng(毕昇,OHOS 定制 clang) |
| 交叉编译工具链 | lycium_plusplus |
| 三方库静态库 | libsqlite3.a (1.5MB, arm64-v8a) |
| 应用包名 | com.unisources.sqlite |
| 设备类型 | phone / 2in1 |
传统方式的效率瓶颈
手动将 sqlite3 集成到鸿蒙应用,需要经历以下完整链路:
每个环节都有明确的痛点:
| 阶段 | 主要痛点 | 典型耗时 |
|---|---|---|
| 工程搭建 | 手动创建目录结构、修改 module.json5 / build-profile.json5 | 10分钟 |
| 库文件部署 | 拷贝 sqlite3.h / sqlite3ext.h 和 libsqlite3.a 到正确位置 | 5分钟 |
| CMake 配置 | 路径拼写错误、链接顺序问题、abiFilters 遗漏 | 15分钟 |
| NAPI 桥接 | 模板代码重复、napi_get_cb_info 等接口不熟悉、指针传递 | 60分钟 |
| 类型声明 | Index.d.ts 签名必须与 C++ 精确匹配 | 15分钟 |
| UI 验证 | 调用测试、SQL 结果格式化显示 | 20分钟 |
| 编译排错 | 编译错误定位、跨语言调试、BiSheng 兼容性 | 30-120分钟 |
最大的痛点是 NAPI 桥接。sqlite3 的 C API 有 200+ 个函数,手动逐个写 NAPI 包装函数,每个都要处理参数解析、类型转换、错误处理、返回值构造——工作量巨大且极易出错。
传统方式总计 2-4 小时,而且每次新增接口都要重复这套流程。
AtomCode + Skills 全流程
接下来,我们用 AtomCode 走一遍完整集成流程。核心思路:让 AtomCode 自动生成重复性代码,人工只做关键决策。
步骤 1:工程搭建
鸿蒙 Stage 模型应用的标准目录结构:
OHOSSqliteSample/
├── AppScope/
│ ├── app.json5 # 应用全局配置
│ └── resources/base/
├── entry/
│ ├── build-profile.json5 # 模块构建配置(含 abiFilters)
│ ├── src/main/
│ │ ├── cpp/ # ← C/C++ 原生代码
│ │ │ ├── CMakeLists.txt
│ │ │ ├── napi_init.cpp # ← NAPI 桥接层
│ │ │ └── thirdparty/sqlite/ # ← 三方库部署位置
│ │ │ ├── include/
│ │ │ │ ├── sqlite3.h
│ │ │ │ └── sqlite3ext.h
│ │ │ └── libs/arm64-v8a/
│ │ │ └── libsqlite3.a # 1.5MB 静态库
│ │ ├── ets/ # ← ArkTS 业务代码
│ │ │ ├── entryability/
│ │ │ │ └── EntryAbility.ets
│ │ │ └── pages/
│ │ │ └── Index.ets # ← 主界面
│ │ ├── resources/
│ │ └── module.json5 # 模块配置
│ └── libs/arm64-v8a/ # 编译产物输出
└── build-profile.json5 # 工程级构建配置
关键配置项:
build-profile.json5(工程级)中指定 BiSheng 编译器和 SDK 版本:
{
"app": {
"products": [{
"targetSdkVersion": "6.1.0(23)",
"compatibleSdkVersion": "6.0.0(20)",
"runtimeOS": "HarmonyOS",
"buildOption": {
"nativeCompiler": "BiSheng" // ← 关键:使用毕昇编译器
}
}]
}
}
entry/build-profile.json5(模块级)中指定 abiFilters:
{
"buildOption": {
"externalNativeOptions": {
"path": "./src/main/cpp/CMakeLists.txt",
"abiFilters": ["arm64-v8a"] // ← 关键:只编译 arm64
}
}
}
module.json5 中指定 deviceTypes:
{
"module": {
"deviceTypes": ["phone", "2in1"] // ← phone + PC 双端
}
}
步骤 2:库文件部署
将 lycium_plusplus 交叉编译产物部署到工程内:
entry/src/main/cpp/thirdparty/sqlite/
├── include/
│ ├── sqlite3.h # 233KB,核心头文件
│ └── sqlite3ext.h # 扩展 API 头文件
└── libs/arm64-v8a/
└── libsqlite3.a # 1.5MB,arm64-v8a 静态库
部署要点:
- 头文件放在
include/子目录,与 CMake 的include_directories对应 - 静态库按 ABI 分目录存放(
libs/arm64-v8a/),后续扩展 x86 时只需新增目录 - 不要把 .a 放到
entry/libs/arm64-v8a/,那是编译产物输出目录
步骤 3:CMake 配置
这是集成中最容易出错的环节。完整 CMakeLists.txt 如下:
cmake_minimum_required(VERSION 3.5.0)
project(OHOSSqliteSample)
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
set(SQLITE_ROOT ${NATIVERENDER_ROOT_PATH}/thirdparty/sqlite)
if(DEFINED PACKAGE_FIND_FILE)
include(${PACKAGE_FIND_FILE})
endif()
# ① 头文件搜索路径
include_directories(${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include
${SQLITE_ROOT}/include)
# ② 定位静态库
set(SQLITE_A ${SQLITE_ROOT}/libs/arm64-v8a/libsqlite3.a)
# ③ 编译目标
add_library(entry SHARED napi_init.cpp)
# ④ 链接顺序:系统库 → 三方库 → 系统辅助库
target_link_libraries(entry PUBLIC libace_napi.z.so)
target_link_libraries(entry PUBLIC ${SQLITE_A})
target_link_libraries(entry PUBLIC m pthread dl)
逐行解析:
| 行 | 说明 | 易错点 |
|---|---|---|
include_directories |
三个路径:自身、cpp/include、sqlite头文件 | 路径拼错导致 sqlite3.h: No such file |
set(SQLITE_A ...) |
用变量持有库路径,便于后续条件扩展 | 硬编码路径不利于多 ABI |
add_library(entry SHARED) |
必须为 SHARED(动态库),NAPI 要求 | 写成 STATIC 会导致 dlopen 失败 |
libace_napi.z.so |
NAPI 运行时库,必须第一个链接 | 遗漏则所有 napi_* 符号 undefined |
${SQLITE_A} |
sqlite3 静态库 | 放在 m/pthread 前面可能触发链接顺序问题 |
m pthread dl |
数学库、线程库、动态链接库 | sqlite3 依赖 pthread 和 dl,遗漏会报 undefined symbol |
步骤 4:NAPI 桥接(核心)
这是整个集成的核心环节。我们需要把 sqlite3 的 C API 包装成 JS 可调用的 NAPI 函数。
4.1 辅助函数
#include "napi/native_api.h"
#include <sqlite3.h>
#include <string>
#include <sstream>
#include <cstring>
// 创建 JS 字符串的快捷方式
static napi_value Str(napi_env env, const std::string& s) {
napi_value r;
napi_create_string_utf8(env, s.c_str(), s.length(), &r);
return r;
}
// 创建 JS number 的快捷方式
static napi_value Num(napi_env env, double v) {
napi_value r;
napi_create_double(env, v, &r);
return r;
}
4.2 dbOpen —— 打开/创建数据库
static napi_value DbOpen(napi_env env, napi_callback_info info) {
// ① 解析参数
size_t argc = 1; napi_value a[1] = {};
napi_get_cb_info(env, info, &argc, a, nullptr, nullptr);
// ② 边界检查
if (argc < 1) return Str(env, "ERROR:need path");
// ③ 提取字符串参数
size_t sz = 0; std::string path;
napi_get_value_string_utf8(env, a[0], nullptr, 0, &sz);
path.resize(sz);
napi_get_value_string_utf8(env, a[0], &path[0], sz + 1, &sz);
// ④ 调用 sqlite3 C API
sqlite3 *db = nullptr;
int rc = sqlite3_open(path.c_str(), &db);
if (rc != SQLITE_OK) {
std::string e = sqlite3_errmsg(db);
sqlite3_close(db);
return Str(env, "ERROR:" + e);
}
// ⑤ 返回指针的 hex 字符串(JS 侧用 string 持有)
char buf[32];
snprintf(buf, sizeof(buf), "%p", (void*)db);
return Str(env, buf);
}
设计决策:为什么用 hex 字符串传指针,而不是 napi_external?
| 方案 | 优点 | 缺点 |
|---|---|---|
| hex 字符串 | JS 侧可打印调试、可持久化 | 不安全(可伪造) |
napi_external |
类型安全、带 finalize 回收 | 跨调用传递复杂、调试困难 |
对于演示项目,hex 字符串更直观。生产环境建议用 napi_external + napi_wrap。
4.3 dbClose —— 关闭数据库
static napi_value DbClose(napi_env env, napi_callback_info info) {
size_t argc = 1; napi_value a[1] = {};
napi_get_cb_info(env, info, &argc, a, nullptr, nullptr);
if (argc < 1) return Str(env, "");
// hex 字符串 → 指针
size_t sz = 0; std::string ptrStr;
napi_get_value_string_utf8(env, a[0], nullptr, 0, &sz);
ptrStr.resize(sz);
napi_get_value_string_utf8(env, a[0], &ptrStr[0], sz + 1, &sz);
sqlite3 *db = (sqlite3*)strtoull(ptrStr.c_str(), nullptr, 16);
if (db) sqlite3_close(db);
return Str(env, "");
}
4.4 dbExec —— 执行 SQL 并返回 JSON
这是最复杂的桥接函数,需要用 sqlite3_exec 的回调机制收集查询结果:
static napi_value DbExec(napi_env env, napi_callback_info info) {
size_t argc = 2; napi_value a[2] = {};
napi_get_cb_info(env, info, &argc, a, nullptr, nullptr);
if (argc < 2) return Str(env, "ERROR:need dbPtr,sql");
// 解析两个字符串参数
size_t sz = 0; std::string ptrStr, sql;
napi_get_value_string_utf8(env, a[0], nullptr, 0, &sz);
ptrStr.resize(sz);
napi_get_value_string_utf8(env, a[0], &ptrStr[0], sz + 1, &sz);
napi_get_value_string_utf8(env, a[1], nullptr, 0, &sz);
sql.resize(sz);
napi_get_value_string_utf8(env, a[1], &sql[0], sz + 1, &sz);
sqlite3 *db = (sqlite3*)strtoull(ptrStr.c_str(), nullptr, 16);
if (!db) return Str(env, "ERROR:db is null");
// 用回调收集查询结果,构造 JSON
std::ostringstream json;
json << "{\"changes\":0,\"rows\":[]}";
char *err = nullptr;
struct CbData { std::ostringstream *j; bool first; };
CbData cbd = { &json, true };
auto callback = [](void *data, int ncols, char **vals, char **names) -> int {
auto *d = (CbData*)data;
if (d->first) {
*d->j << "{\"changes\":0,\"rows\":[";
d->first = false;
} else {
*d->j << ",";
}
*d->j << "{";
for (int i = 0; i < ncols; i++) {
if (i > 0) *d->j << ",";
*d->j << "\"" << (names[i] ? names[i] : "") << "\":\""
<< (vals[i] ? vals[i] : "") << "\"";
}
*d->j << "}";
return 0;
};
int rc = sqlite3_exec(db, sql.c_str(), callback, &cbd, &err);
if (rc != SQLITE_OK) {
std::string e = err ? err : sqlite3_errmsg(db);
if (err) sqlite3_free(err);
return Str(env, "ERROR:" + e);
}
if (!cbd.first) { json << "]}"; }
else {
json.str("");
json << "{\"changes\":" << sqlite3_changes(db) << ",\"rows\":[]}";
}
return Str(env, json.str());
}
返回值格式:
// SELECT 查询
{"changes":0,"rows":[{"id":"1","name":"alpha","value":"3.14"}]}
// INSERT/UPDATE/DELETE
{"changes":1,"rows":[]}
4.5 sqliteVersion —— 版本号
static napi_value SqliteVersion(napi_env env, napi_callback_info info) {
return Str(env, SQLITE_VERSION); // 宏,编译期展开为 "3.47.2"
}
4.6 模块注册
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor d[] = {
{ "dbOpen", nullptr, DbOpen, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "dbClose", nullptr, DbClose, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "dbExec", nullptr, DbExec, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "sqliteVersion", nullptr, SqliteVersion, nullptr, nullptr, nullptr, napi_default, nullptr },
};
napi_define_properties(env, exports, sizeof(d) / sizeof(d[0]), d);
return exports;
}
EXTERN_C_END
static napi_module demoModule = {
.nm_version = 1, .nm_flags = 0, .nm_filename = nullptr,
.nm_register_func = Init, .nm_modname = "entry",
.nm_priv = ((void *)0), .reserved = {0},
};
extern "C" __attribute__((constructor)) void RegisterEntryModule(void) {
napi_module_register(&demoModule);
}
关键点:
nm_modname必须为"entry",与模块名一致__attribute__((constructor))确保动态库加载时自动注册- 4 个函数通过
napi_property_descriptor数组批量导出
步骤 5:类型声明(Index.d.ts)
ArkTS 侧需要类型声明才能调用 NAPI 函数:
// entry/src/main/cpp/types/libentry/Index.d.ts
export const dbOpen: (path: string) => string;
export const dbClose: (dbPtr: string) => string;
export const dbExec: (dbPtr: string, sql: string) => string;
export const sqliteVersion: () => string;
配套的 oh-package.json5:
{
"name": "libentry.so",
"types": "./Index.d.ts",
"version": "1.0.0",
"description": "SQLite3 NAPI bridge"
}
类型声明必须与 C++ 侧严格对应:
| C++ 侧 | JS 类型 | d.ts 签名 |
|---|---|---|
napi_value (string) |
string |
(path: string) => string |
napi_value (number) |
number |
() => number |
| 多参数 | 按顺序 | (dbPtr: string, sql: string) => string |
步骤 6:UI 页面(Index.ets)
ArkTS 侧的调用方式极其简洁:
import db from 'libentry.so';
import { common } from '@kit.AbilityKit';
@Entry
@Component
struct Index {
@State sqlInput: string = 'SELECT 1 + 1 AS result';
@State dbPtr: string = '';
@State result: string = '';
@State resultOk: boolean = false;
@State ver: string = '';
private abilityCtx: common.UIAbilityContext | null = null;
aboutToAppear(): void {
this.abilityCtx = getContext(this) as common.UIAbilityContext;
if (db) this.ver = db.sqliteVersion(); // ← 直接调用 NAPI
this.openDb();
}
private openDb(): void {
this.dbPath = `${this.abilityCtx.filesDir}/sqlite_demo.db`;
const ptr = db.dbOpen(this.dbPath); // ← 打开数据库
if (ptr.startsWith('ERROR:')) { /* 错误处理 */ return; }
this.dbPtr = ptr;
db.dbExec(this.dbPtr, 'CREATE TABLE IF NOT EXISTS kv(k TEXT PRIMARY KEY, v TEXT)');
}
private execSql(): void {
const r = db.dbExec(this.dbPtr, this.sqlInput); // ← 执行 SQL
if (r.startsWith('ERROR:')) { this.result = r; this.resultOk = false; return; }
this.result = r; this.resultOk = true;
}
}
调用链路:
ArkTS: db.dbExec(ptr, sql)
→ NAPI: DbOpen() → sqlite3_exec()
→ C API: sqlite3_exec(db, sql, callback, ...)
→ 返回 JSON 字符串
← NAPI: Str(env, json.str())
← ArkTS: this.result = r
踩坑专区
坑 1:CMake 链接顺序导致 undefined symbol
现象:
ld.lld: error: undefined symbol: sqlite3_open
ld.lld: error: undefined symbol: sqlite3_exec
根因:静态库链接有顺序依赖。LLD 链接器从左到右扫描,如果 libsqlite3.a 在 m pthread dl 之后,而 sqlite3 内部依赖这些系统库,链接器在扫描 sqlite3 时还不知道后续有这些库,就会报 undefined。
修复:
# ✗ 错误 —— sqlite3 依赖 pthread,但 pthread 在前面
target_link_libraries(entry PUBLIC m pthread dl)
target_link_libraries(entry PUBLIC ${SQLITE_A})
# ✓ 正确 —— 被依赖的库放后面
target_link_libraries(entry PUBLIC ${SQLITE_A})
target_link_libraries(entry PUBLIC m pthread dl)
经验法则:依赖别人的库放前面,被依赖的库放后面。即 A 依赖 B → 链接顺序 A B。
坑 2:NAPI 字符串提取的两次调用模式
现象:
napi_get_value_string_utf8 failed: napi_invalid_arg
或者提取到的字符串是乱码、截断。
根因:napi_get_value_string_utf8 需要调用两次——第一次获取长度,第二次获取内容。很多初学者只调用一次,或者 buffer 大小不够(没有 +1 给 \0)。
修复:
// ✗ 错误 —— 只调用一次,buffer 大小未知
char buf[256];
napi_get_value_string_utf8(env, argv[0], buf, 256, &sz);
// ✓ 正确 —— 两次调用模式
size_t sz = 0;
// 第一次:获取字符串长度
napi_get_value_string_utf8(env, argv[0], nullptr, 0, &sz);
// 分配精确大小的 buffer
std::string path;
path.resize(sz);
// 第二次:获取字符串内容,buf_size = sz + 1(含 \0)
napi_get_value_string_utf8(env, argv[0], &path[0], sz + 1, &sz);
为什么必须两次:NAPI 的字符串是 UTF-16 内部表示,转换到 UTF-8 后长度可能变化。第一次调用返回的是 UTF-8 编码后的字节数,不是 JS 字符串的 .length。
坑 3:abiFilters 与静态库架构不匹配
现象:
dlopen failed: "libentry.so" is 64-bit instead of 32-bit
# 或
dlopen failed: cannot locate symbol "sqlite3_open"
根因:build-profile.json5 中的 abiFilters 与实际编译的静态库架构不匹配。例如 abiFilters 设为 ["x86_64"],但 libsqlite3.a 是 arm64-v8a 编译的。
修复:
// build-profile.json5 —— abiFilters 必须与静态库架构一致
{
"buildOption": {
"externalNativeOptions": {
"abiFilters": ["arm64-v8a"] // ← 与 libsqlite3.a 的架构一致
}
}
}
多架构支持:如果需要同时支持 arm64 和 x86_64,需要为每个架构分别编译静态库:
thirdparty/sqlite/libs/
├── arm64-v8a/
│ └── libsqlite3.a
└── x86_64/
└── libsqlite3.a
CMake 中按架构选择:
if(${CMAKE_SYSTEM_PROCESSOR} STREQUAL "aarch64")
set(SQLITE_A ${SQLITE_ROOT}/libs/arm64-v8a/libsqlite3.a)
elseif(${CMAKE_SYSTEM_PROCESSOR} STREQUAL "x86_64")
set(SQLITE_A ${SQLITE_ROOT}/libs/x86_64/libsqlite3.a)
endif()
坑 4:BiSheng 编译器 vs 标准 Clang 的 ABI 差异
现象:
undefined reference to '__aeabi_uidiv'
undefined reference to '__aeabi_d2lz'
根因:OHOS SDK 使用 BiSheng 编译器(基于 clang 的定制版,见 build-profile.json5 中 "nativeCompiler": "BiSheng")。某些三方库用标准 clang 或 GCC 编译后,与 BiSheng 运行时存在 ABI 差异,主要体现在:
- 内置函数命名:
__aeabi_*系列软除法函数的符号名不同 - 异常处理:C++ 异常的 unwind 表格式可能不同
- TLS 实现:线程局部存储的访问方式不同
修复:
# 确保使用 lycium_plusplus(OHOS 官方交叉编译工具链)编译所有三方库
# 不要混用系统 clang / GCC 与 BiSheng
lycium++ -target aarch64-linux-ohos -c sqlite3.c -o sqlite3.o
lycium++ -target aarch64-linux-ohos -shared -o libsqlite3.a sqlite3.o
在 CMake 中显式指定编译器(如果不用 lycium_plusplus):
set(CMAKE_C_COMPILER "$ENV{OHOS_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang")
set(CMAKE_CXX_COMPILER "$ENV{OHOS_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang++")
判断依据:如果 build-profile.json5 中 nativeCompiler 为 "BiSheng",则所有原生代码必须用 BiSheng 工具链编译。
坑 5:指针传递的安全性——dbPtr 伪造导致 UAF
现象:
应用崩溃,日志显示 SIGSEGV 或 SIGABRT,但没有任何有意义的错误信息。
根因:本方案用 hex 字符串传递 sqlite3* 指针。JS 侧可以伪造一个指针字符串,导致 C++ 侧解引用非法地址。更隐蔽的是:如果 dbClose 后 JS 侧仍持有旧 ptrStr 并继续调用 dbExec,就会触发 Use-After-Free。
修复(生产环境方案):
// 方案 A:用 napi_external + 全局 map 管理生命周期
static std::unordered_map<std::string, sqlite3*> g_dbMap;
static int g_dbSeq = 0;
static napi_value DbOpen(napi_env env, napi_callback_info info) {
// ... sqlite3_open ...
std::string handle = "db_" + std::to_string(++g_dbSeq);
g_dbMap[handle] = db;
return Str(env, handle); // 返回 "db_1" 而非裸指针
}
static napi_value DbExec(napi_env env, napi_callback_info info) {
// ... 解析 handle ...
auto it = g_dbMap.find(handle);
if (it == g_dbMap.end()) return Str(env, "ERROR:invalid db handle");
sqlite3 *db = it->second;
// ... sqlite3_exec ...
}
// 方案 B:napi_wrap 自动回收
static napi_value DbOpen(napi_env env, napi_callback_info info) {
// ... sqlite3_open ...
napi_value handle;
napi_create_external(env, db, [](napi_env, void* data, void*) {
sqlite3_close(static_cast<sqlite3*>(data)); // GC 时自动关闭
}, nullptr, &handle);
return handle;
}
本项目选择 hex 字符串的原因:演示项目追求代码简洁和可调试性。生产项目务必使用方案 A 或 B。
通用集成模板(拿来即用)
CMakeLists.txt 模板
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})
set(LIB_A ${LIB_ROOT}/libs/arm64-v8a/lib{lib}.a)
if(NOT EXISTS ${LIB_A})
message(FATAL_ERROR "{lib} not found: ${LIB_A}")
endif()
if(DEFINED PACKAGE_FIND_FILE)
include(${PACKAGE_FIND_FILE})
endif()
include_directories(${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include
${LIB_ROOT}/include)
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 dl)
NAPI 桥接函数 5 步模板
static napi_value MyFunction(napi_env env, napi_callback_info info) {
// ① 解析参数
size_t argc = 2; napi_value argv[2];
napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
// ② 边界检查
if (argc < 2) {
napi_value err;
napi_create_string_utf8(env, "ERROR:need 2 args", NAPI_AUTO_LENGTH, &err);
return err;
}
// ③ 类型校验
napi_valuetype vt;
napi_typeof(env, argv[0], &vt);
if (vt != napi_string) {
napi_value err;
napi_create_string_utf8(env, "ERROR:arg0 must be string", NAPI_AUTO_LENGTH, &err);
return err;
}
// ④ 提取参数值 + 调用 C API
size_t sz = 0;
napi_get_value_string_utf8(env, argv[0], nullptr, 0, &sz);
std::string arg0;
arg0.resize(sz);
napi_get_value_string_utf8(env, argv[0], &arg0[0], sz + 1, &sz);
std::string result = c_api_call(arg0.c_str());
// ⑤ 返回 NAPI 值
napi_value ret;
napi_create_string_utf8(env, result.c_str(), result.length(), &ret);
return ret;
}
NAPI 字符串提取宏(减少重复代码)
// 一次性从 argv[idx] 提取 std::string
#define NAPI_GET_STRING(env, argv, idx, out) do { \
size_t _sz = 0; \
napi_get_value_string_utf8(env, argv[idx], nullptr, 0, &_sz); \
(out).resize(_sz); \
napi_get_value_string_utf8(env, argv[idx], &(out)[0], _sz + 1, &_sz); \
} while(0)
// 使用示例
NAPI_GET_STRING(env, a, 0, ptrStr);
NAPI_GET_STRING(env, a, 1, sql);
异步 NAPI 函数模板(napi_create_async_work)
适用场景:耗时 SQL 操作(大数据量查询、复杂聚合)不阻塞 UI 线程。
struct AsyncData {
std::string dbPtr;
std::string sql;
std::string result;
std::string error;
napi_async_work work;
napi_deferred deferred; // Promise
};
static void AsyncExecute(napi_env env, void *data) {
auto *async = static_cast<AsyncData*>(data);
// 在 worker 线程执行耗时 SQL
sqlite3 *db = (sqlite3*)strtoull(async->dbPtr.c_str(), nullptr, 16);
if (!db) { async->error = "ERROR:db is null"; return; }
char *err = nullptr;
int rc = sqlite3_exec(db, async->sql.c_str(), nullptr, nullptr, &err);
if (rc != SQLITE_OK) {
async->error = err ? std::string(err) : sqlite3_errmsg(db);
if (err) sqlite3_free(err);
} else {
async->result = "{\"changes\":" + std::to_string(sqlite3_changes(db)) + "}";
}
}
static void AsyncComplete(napi_env env, napi_status status, void *data) {
auto *async = static_cast<AsyncData*>(data);
if (!async->error.empty()) {
napi_value err;
napi_create_string_utf8(env, async->error.c_str(), NAPI_AUTO_LENGTH, &err);
napi_reject_deferred(env, async->deferred, err);
} else {
napi_value ret;
napi_create_string_utf8(env, async->result.c_str(), NAPI_AUTO_LENGTH, &ret);
napi_resolve_deferred(env, async->deferred, ret);
}
napi_delete_async_work(env, async->work);
delete async;
}
static napi_value DbExecAsync(napi_env env, napi_callback_info info) {
size_t argc = 2; napi_value a[2];
napi_get_cb_info(env, info, &argc, a, nullptr, nullptr);
auto *async = new AsyncData();
NAPI_GET_STRING(env, a, 0, async->dbPtr);
NAPI_GET_STRING(env, a, 1, async->sql);
napi_value promise;
napi_create_promise(env, &async->deferred, &promise);
napi_value work_name;
napi_create_string_utf8(env, "DbExecAsync", NAPI_AUTO_LENGTH, &work_name);
napi_create_async_work(env, nullptr, work_name,
AsyncExecute, AsyncComplete, async, &async->work);
napi_queue_async_work(env, async->work);
return promise; // JS 侧: const r = await db.dbExecAsync(ptr, sql)
}
Index.d.ts 模板
export const dbOpen: (path: string) => string;
export const dbClose: (dbPtr: string) => string;
export const dbExec: (dbPtr: string, sql: string) => string;
export const dbExecAsync: (dbPtr: string, sql: string) => Promise<string>;
export const sqliteVersion: () => string;
总结
sqlite3 是鸿蒙应用中最常集成的三方库之一——本地数据存储、配置管理、离线缓存,都离不开它。但 NAPI 桥接的模板代码、CMake 的链接陷阱、BiSheng 的 ABI 差异,让很多开发者在集成环节反复踩坑。
你在 NAPI 集成中遇到过什么奇怪的错误?欢迎在评论区分享你的经验。
如果本文对你有帮助,请 点赞、收藏、转发 支持一下~
更多推荐




所有评论(0)