欢迎加入【开源鸿蒙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 集成到鸿蒙应用,需要经历以下完整链路:

失败

工程搭建

库文件部署

CMake 配置

NAPI 桥接

类型声明

UI 验证

编译测试

每个环节都有明确的痛点:

阶段 主要痛点 典型耗时
工程搭建 手动创建目录结构、修改 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.am 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 差异,主要体现在:

  1. 内置函数命名__aeabi_* 系列软除法函数的符号名不同
  2. 异常处理:C++ 异常的 unwind 表格式可能不同
  3. 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.json5nativeCompiler"BiSheng",则所有原生代码必须用 BiSheng 工具链编译。

坑 5:指针传递的安全性——dbPtr 伪造导致 UAF

现象

应用崩溃,日志显示 SIGSEGVSIGABRT,但没有任何有意义的错误信息。

根因:本方案用 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 集成中遇到过什么奇怪的错误?欢迎在评论区分享你的经验。

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

Logo

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

更多推荐