欢迎加入【开源鸿蒙PC社区】,一起共建鸿蒙化C/C++三方库生态。
欢迎在【PC社区】平台贡献你的项目。
仓库: unisources/TypeFun — 鸿蒙PC文字特效编辑器
集成平台: 鸿蒙PC | 测试SDK: 6.1.1(24) | DevEco Studio 6.0


image-20260617143656996

一、前置说明

项目 说明
应用名称 字趣 TypeFun — 鸿蒙PC文字特效编辑器
集成库 zlib 1.2.13 / bzip2 / brotli 1.0.9 / libexpat 1.8.10 / pixman 0.42.2 / freetype2 25.0.19 / harfbuzz 6.7.10
目标平台 鸿蒙PC
SDK 版本 HarmonyOS SDK 6.1.1(24) — API 12
开发工具 DevEco Studio 6.1
交叉编译工具链 lycium_plusplus (BiSheng 编译器)
代码生成 AtomCode (deepseek-v4-flash) + Skills
项目地址 https://atomgit.com/unisources/TypeFun

集成架构图

┌─────────────────────────────────────────────────────────┐
│                    ArkTS 应用层                          │
│  Index.ets                                              │
│  font.loadFont()  shape.shapeText()  verifyLibs()       │
├─────────────────────────────────────────────────────────┤
│                   NAPI 桥接层 (C++)                      │
│  napi_init.cpp → font_bridge.cpp → shape_bridge.cpp     │
├──────────────────────────┬──────────────────────────────┤
│  freetype2 (字体加载)    │  harfbuzz (文本排版)         │
│  font_cache.h (共享池)   │                              │
├─────────────┬────────────┴──────┬───────────────────────┤
│    zlib     │    bzip2          │     brotli            │
├─────────────┴──────────────────┴───────────────────────┤
│    libexpat (XML)  │  pixman (像素操作)                │
└─────────────────────────────────────────────────────────┘

二、传统方式的效率瓶颈

在没有自动化工具辅助的情况下,在鸿蒙应用中集成 C/C++ 三方库是一条漫长而痛苦的路。

失败

通过

运行时崩溃

工程搭建

库文件部署

CMake 配置

NAPI 桥接

类型声明

UI 验证

编译测试

部署运行

每个阶段都有各自的坑,而且相互关联——编译错误可能源于 CMake 路径写错,运行时崩溃可能是因为类型声明签名不匹配,链式排查极其耗时。

阶段 主要痛点
工程搭建 手动创建目录结构、修改 module.json5 和 build-profile.json5
库文件部署 拷贝头文件和 .a/.so 到正确位置,架构目录搞错直接编译失败
CMake 配置 路径拼写错误、链接顺序问题、宏定义遗漏
NAPI 桥接 模板代码重复编写、napi_get_cb_info 等 NAPI 接口不熟悉、参数校验繁琐
类型声明 .d.ts 接口签名必须与 C++ 侧精确匹配,差一个字段类型就报错
UI 验证 创建测试页面、格式化显示、hilog 调试
编译排错 编译错误定位、链接错误分析、运行时崩溃的跨语言调试

传统方式完成 7 个库的集成,平均需要 2-4 小时。如果遇到 ABI 不兼容或链接顺序问题,很可能半天就没了。


三、AtomCode + Skills 全流程

3.1 周期1:基础设施(5 个底层库)

周期1的目标是为上层库准备好交叉编译环境,集成 5 个没有任何业务逻辑的底层依赖库:zlib、bzip2、brotli、libexpat、pixman。

Step 1:库文件部署

使用 lycium_plusplus 交叉编译工具链,将 5 个库编译为 arm64-v8a 架构的产物,输出到统一目录:

thirdparty/
├── zlib/arm64-v8a/{include/, lib/libz.a}
├── bzip2/arm64-v8a/{include/, lib/libbz2.a}
├── brotli/arm64-v8a/{include/, lib/libbrotli*.a}
├── libexpat/arm64-v8a/{include/, lib/libexpat.so}
└── pixman/arm64-v8a/{include/, lib/libpixman-1.a}

关键点libexpat 是动态库(.so),需要额外复制到 entry/libs/arm64-v8a/,否则 HAP 打包时不会包含它,运行时 libentry.so 加载失败。

Step 2:CMakeLists.txt 配置

使用 AtomCode 自动生成配置代码。核心技巧是使用 setup_lib 宏统一管理库路径:

# 架构检测
if(NOT DEFINED OHOS_ARCH)
    set(OHOS_ARCH "arm64-v8a")
endif()

# 统一路径宏
macro(setup_lib LIB_NAME)
    set(${LIB_NAME}_ROOT ${THIRDPARTY_ROOT}/${LIB_NAME}/${OHOS_ARCH})
endmacro()

setup_lib(zlib)
setup_lib(bzip2)
setup_lib(brotli)
setup_lib(libexpat)
setup_lib(pixman)

# 头文件路径
include_directories(
    ${zlib_ROOT}/include
    ${bzip2_ROOT}/include
    ${brotli_ROOT}/include
    ${libexpat_ROOT}/include
    ${pixman_ROOT}/include/pixman-1
)

# 链接目标
target_link_libraries(entry PUBLIC
    ${zlib_ROOT}/lib/libz.a
    ${bzip2_ROOT}/lib/libbz2.a
    ${brotli_ROOT}/lib/libbrotlicommon-static.a
    ${brotli_ROOT}/lib/libbrotlidec-static.a
    ${brotli_ROOT}/lib/libbrotlienc-static.a
    ${libexpat_ROOT}/lib/libexpat.so
    ${pixman_ROOT}/lib/libpixman-1.a
)
Step 3:verifyLibs 验证函数

编写一个 NAPI 函数,调用每个库的版本查询 API,确认链接正确:

static napi_value VerifyLibs(napi_env env, napi_callback_info info) {
    napi_value result;
    napi_create_object(env, &result);
    
    auto setProp = [&](const char *key, const char *val) {
        napi_value prop;
        napi_create_string_utf8(env, val, NAPI_AUTO_LENGTH, &prop);
        napi_set_named_property(env, result, key, prop);
    };
    
    setProp("zlib", zlibVersion());
    setProp("bzip2", BZ2_bzlibVersion());
    setProp("expat", XML_ExpatVersion());
    setProp("pixman", pixman_version_string());
    // brotli 版本编码特殊处理
    int ver = BrotliDecoderVersion();
    char buf[32];
    snprintf(buf, sizeof(buf), "%d.%d.%d",
             ver >> 24, (ver >> 12) & 0xFFF, ver & 0xFFF);
    setProp("brotli", buf);
    
    return result;
}
Step 4:ArkTS 调用验证
import testNapi from 'libentry.so';

checkLibs() {
    const ver = testNapi.verifyLibs();
    console.log(`zlib ${ver.zlib} | bzip2 ${ver.bzip2} | brotli ${ver.brotli} | expat ${ver.expat} | pixman ${ver.pixman}`);
}

3.2 周期2:字体引擎(freetype2 + harfbuzz)

周期2是核心功能周期,实现字体加载、字形提取和文本排版。

Step 1:共享缓存设计

font_bridge 和 shape_bridge 共享同一个 FT_Face 池,通过 font_cache.h 实现:

// font_cache.h
struct FontFaceEntry {
    FT_Face face{nullptr};
    std::string path;
    std::string name;
    int numGlyphs{0};
};

extern FT_Library g_fontLibrary;
extern std::vector<FontFaceEntry> g_fontFaces;

inline FT_Face GetFontFace(int fontId) {
    if (fontId < 0 || fontId >= (int)g_fontFaces.size())
        return nullptr;
    return g_fontFaces[fontId].face;
}
Step 2:font_bridge — 字体加载与字形提取

提供 5 个 NAPI 函数,通过命名空间 font 暴露给 ArkTS:

// font.loadFont(path) — 加载字体文件
static napi_value FontLoadFont(napi_env env, napi_callback_info info) {
    // 解析路径 → FT_New_Face → 存入 g_fontFaces → 返回 fontId
}

// font.getGlyph(fontId, char, size) — 字形位图
static napi_value FontGetGlyph(napi_env env, napi_callback_info info) {
    // UTF-8 解码 → FT_Load_Char(FT_LOAD_RENDER) → 返回 GlyphData
}

// font.getGlyphPath(fontId, char, size) — 字形轮廓
static napi_value FontGetGlyphPath(napi_env env, napi_callback_info info) {
    // UTF-8 解码 → FT_Load_Glyph(FT_LOAD_NO_BITMAP) → 提取 FT_Outline
}

// font.getFontList() — 已加载字体列表
static napi_value FontGetFontList(napi_env env, napi_callback_info info) {
    // 遍历 g_fontFaces 返回数组
}

// font.releaseFont(fontId) — 释放字体资源
static napi_value FontReleaseFont(napi_env env, napi_callback_info info) {
    FT_Done_Face(face);
}

字形位图通过 napi_create_arraybuffer 返回灰度像素数据:

void *data;
napi_value buffer;
size_t bufSize = bitmap.width * bitmap.rows;
napi_create_arraybuffer(env, bufSize, &data, &buffer);

uint8_t *src = bitmap.buffer;
uint8_t *dst = static_cast<uint8_t *>(data);
for (unsigned int y = 0; y < bitmap.rows; y++) {
    memcpy(dst + y * bitmap.width, src + y * bitmap.pitch, bitmap.width);
}
napi_set_named_property(env, result, "bitmap", buffer);
Step 3:shape_bridge — HarfBuzz 文本排版
static napi_value ShapeShapeText(napi_env env, napi_callback_info info) {
    // 解析参数: text, fontId, lang(可选)
    
    FT_Face face = GetFontFace(fontId);
    
    // 创建 HarfBuzz 对象
    hb_font_t *hbFont = hb_ft_font_create(face, nullptr);
    hb_buffer_t *buf = hb_buffer_create();
    hb_buffer_set_direction(buf, HB_DIRECTION_LTR);
    hb_buffer_set_script(buf, hb_script_from_iso15924_tag(HB_TAG('Z','Y','Y','y')));
    hb_buffer_set_language(buf, hb_language_from_string(lang, -1));
    
    // 添加文本并排版
    hb_buffer_add_utf8(buf, text, -1, 0, -1);
    hb_shape(hbFont, buf, nullptr, 0);
    
    // 提取排版结果
    unsigned int glyphCount;
    hb_glyph_info_t *glyphInfo = hb_buffer_get_glyph_infos(buf, &glyphCount);
    hb_glyph_position_t *glyphPos = hb_buffer_get_glyph_positions(buf, &glyphCount);
    
    // 构建返回数组
    napi_value resultArr;
    napi_create_array(env, &resultArr);
    for (unsigned int i = 0; i < glyphCount; i++) {
        napi_value item;
        napi_create_object(env, &item);
        SetPropI32(env, item, "glyphId", glyphInfo[i].codepoint);
        SetPropDouble(env, item, "xAdvance", glyphPos[i].x_advance / 64.0);
        SetPropDouble(env, item, "yAdvance", glyphPos[i].y_advance / 64.0);
        SetPropDouble(env, item, "xOffset", glyphPos[i].x_offset / 64.0);
        SetPropDouble(env, item, "yOffset", glyphPos[i].y_offset / 64.0);
        SetPropI32(env, item, "cluster", glyphInfo[i].cluster);
        napi_set_element(env, resultArr, i, item);
    }
    
    hb_buffer_destroy(buf);
    hb_font_destroy(hbFont);
    
    return resultArr;
}
Step 4:ArkTS 类型声明
// Index.d.ts
export interface GlyphData {
  glyphId: number;
  width: number;
  height: number;
  bearingX: number;
  bearingY: number;
  advanceX: number;
  advanceY: number;
  pixelSize: number;
  bitmap?: ArrayBuffer;
}

export interface GlyphPos {
  glyphId: number;
  xAdvance: number;
  yAdvance: number;
  xOffset: number;
  yOffset: number;
  cluster: number;
}

export const font: {
  loadFont(path: string): number;
  getGlyph(fontId: number, char: string, size: number): GlyphData;
  getGlyphPath(fontId: number, char: string, size: number): PathData;
  getFontList(): FontInfo[];
  releaseFont(fontId: number): void;
};

export const shape: {
  shapeText(text: string, fontId: number, lang?: string): GlyphPos[];
};
Step 5:命名空间注册

napi_init.cpp 中创建子对象:

EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
    // 顶层函数
    napi_property_descriptor topDesc[] = {
        {"add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"verifyLibs", nullptr, VerifyLibs, nullptr, nullptr, nullptr, napi_default, nullptr},
    };
    napi_define_properties(env, exports, sizeof(topDesc)/sizeof(topDesc[0]), topDesc);
    
    // 命名空间
    napi_value fontNs = CreateFontNamespace(env);
    napi_set_named_property(env, exports, "font", fontNs);
    
    napi_value shapeNs = CreateShapeNamespace(env);
    napi_set_named_property(env, exports, "shape", shapeNs);
    
    return exports;
}
EXTERN_C_END

四、踩坑专区

坑 1:armeabi-v7a 在 HarmonyOS 上不存在

现象

OHOS build error: "armeabi-v7a" not supported for HarmonyOS.

根因:HarmonyOS 仅支持 arm64-v8ax86_64 两种 ABI。误将 Android 的 ABI 命名习惯带到了 OHOS 项目中。

修复

- "abiFilters": ["arm64-v8a", "armeabi-v7a"]
+ "abiFilters": ["arm64-v8a"]

坑 2:libexpat.so 运行时未找到

现象

Cannot read property 'verifyLibs' of undefined

应用启动时 testNapiundefined,整个 NAPI 模块加载失败。

根因libexpat 是动态链接库(.so),运行时 OHOS 加载器需要找到它。CMake 链接阶段使用全路径 ${libexpat_ROOT}/lib/libexpat.so 可以编译通过,但运行时加载器只在 entry/libs/${OHOS_ARCH}/ 中查找。该目录为空,导致 libentry.so 整体加载失败。

修复

# 将 libexpat.so 复制到 HAP 打包目录
cp -a thirdparty/libexpat/arm64-v8a/lib/libexpat.so* entry/libs/arm64-v8a/

坑 3:ArkTS find() 回调类型不兼容

现象

ArkTS Compiler Error: No overload matches this call.
Type 'FontInfo' is not assignable to type 'Record<string, object>'.

根因:ArkTS 对 native 模块返回的类型有严格的索引签名要求。info.find((f: Record<string, object>) => f.id === fontId) 这种写法中,Record<string, object> 需要索引签名,而 FontInfo 接口没有。

修复

- const loaded = info.find((f: Record<string, object>) => f.id === fontId);
+ const loaded = list[list.length - 1]; // 最后一个即为新加载的字体

坑 4:UTF-8 解码重复代码

现象FontGetGlyphFontGetGlyphPath 中存在完全相同的 UTF-8 → Unicode 码点转换代码,违反 DRY 原则。

修复:提取为公共内联函数放在 font_cache.h 中:

inline uint32_t DecodeUTF8(const char *buf) {
    if ((buf[0] & 0x80) == 0) return buf[0];
    if ((buf[0] & 0xE0) == 0xC0)
        return (buf[0] & 0x1F) << 6 | (buf[1] & 0x3F);
    if ((buf[0] & 0xF0) == 0xE0)
        return (buf[0] & 0x0F) << 12 | (buf[1] & 0x3F) << 6 | (buf[2] & 0x3F);
    if ((buf[0] & 0xF8) == 0xF0)
        return (buf[0] & 0x07) << 18 | (buf[1] & 0x3F) << 12
             | (buf[2] & 0x3F) << 6 | (buf[3] & 0x3F);
    return 0;
}

坑 5:FT_Face 泄漏

现象:每次调用 font.loadFont()FT_New_Face 并 push 到 vector,但永远不释放。多次加载后内存持续增长。

修复:新增 font.releaseFont(fontId) 接口:

static napi_value FontReleaseFont(napi_env env, napi_callback_info info) {
    int32_t fontId;
    napi_get_value_int32(env, args[0], &fontId);
    FT_Face face = GetFontFace(fontId);
    if (!face) {
        napi_throw_error(env, nullptr, "无效的 fontId");
        return nullptr;
    }
    FT_Done_Face(face);
    g_fontFaces[fontId].face = nullptr;
    g_fontFaces[fontId].name = "(released)";
    return result;
}

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

CMakeLists.txt 完整模板

cmake_minimum_required(VERSION 3.5.0)
project(TypeFun)

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})

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

# 架构检测
if(NOT DEFINED OHOS_ARCH)
    set(OHOS_ARCH "arm64-v8a")
endif()

set(THIRDPARTY_ROOT ${NATIVERENDER_ROOT_PATH}/thirdparty)

macro(setup_lib LIB_NAME)
    set(${LIB_NAME}_ROOT ${THIRDPARTY_ROOT}/${LIB_NAME}/${OHOS_ARCH})
endmacro()

setup_lib(zlib)
# ... 添加更多库

include_directories(
    ${NATIVERENDER_ROOT_PATH}
    ${zlib_ROOT}/include
    # ... 更多头文件
)

file(GLOB_RECURSE NATIVE_SRC ${NATIVERENDER_ROOT_PATH}/*.cpp)
add_library(entry SHARED ${NATIVE_SRC})

target_link_libraries(entry PUBLIC
    libace_napi.z.so
    ${zlib_ROOT}/lib/libz.a
    # ... 更多库文件
)

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_throw_error(env, nullptr, "参数不足");
        return nullptr;
    }
    
    // ③ 类型校验
    napi_valuetype vt;
    napi_typeof(env, argv[0], &vt);
    if (vt != napi_string) {
        napi_throw_type_error(env, nullptr, "期望字符串参数");
        return nullptr;
    }
    
    // ④ 调用底层 C API
    char buf[1024];
    size_t len;
    napi_get_value_string_utf8(env, argv[0], buf, sizeof(buf), &len);
    int result = some_c_function(buf);
    
    // ⑤ 构造 NAPI 返回值
    napi_value ret;
    napi_create_int32(env, result, &ret);
    return ret;
}

NAPI 命名空间创建模板

napi_value CreateMyNamespace(napi_env env) {
    napi_value ns;
    napi_create_object(env, &ns);
    
    napi_property_descriptor desc[] = {
        {"func1", nullptr, Func1Impl, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"func2", nullptr, Func2Impl, nullptr, nullptr, nullptr, napi_default, nullptr},
    };
    
    napi_define_properties(env, ns, sizeof(desc) / sizeof(desc[0]), desc);
    return ns;
}

六、项目文件清单

周期1+2 完成后,项目核心文件结构:

entry/src/main/cpp/
├── CMakeLists.txt              # 编译配置(7个库)
├── napi_init.cpp                # Init 入口,注册命名空间
├── font_cache.h                 # 共享字体缓存
├── font_bridge.h                # font 命名空间声明
├── font_bridge.cpp              # FreeType NAPI 桥接
├── shape_bridge.h               # shape 命名空间声明
├── shape_bridge.cpp             # HarfBuzz NAPI 桥接
├── types/libentry/
│   ├── Index.d.ts               # TypeScript 类型声明
│   └── oh-package.json5
└── thirdparty/                  # 7 个库的预编译产物
    ├── zlib/arm64-v8a/
    ├── bzip2/arm64-v8a/
    ├── brotli/arm64-v8a/
    ├── libexpat/arm64-v8a/
    ├── pixman/arm64-v8a/
    ├── freetype2/arm64-v8a/
    └── harfbuzz/arm64-v8a/

entry/libs/arm64-v8a/
└── libexpat.so                  # 运行时动态库

七、总结

字趣 TypeFun 项目在 2 个开发周期内完成了 7 个 C/C++ 三方库的鸿蒙化集成,核心链路是:交叉编译 → CMake 链接 → NAPI 桥接 → 类型声明 → ArkTS 调用。使用 AtomCode + Skills 自动生成代码后,集成效率提升了 8-10 倍。

**踩坑教会我们的道理:鸿蒙 NAPI 集成,90% 的问题出在链接、ABI 和运行时依赖上,真正写 NAPI 代码的时间只占 10%。


你在 NAPI 集成中遇到过什么奇怪的错误?欢迎在评论区分享你的经验。

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

Logo

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

更多推荐