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

一、前置说明
| 项目 | 说明 |
|---|---|
| 应用名称 | 字趣 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 路径写错,运行时崩溃可能是因为类型声明签名不匹配,链式排查极其耗时。
| 阶段 | 主要痛点 |
|---|---|
| 工程搭建 | 手动创建目录结构、修改 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-v8a 和 x86_64 两种 ABI。误将 Android 的 ABI 命名习惯带到了 OHOS 项目中。
修复:
- "abiFilters": ["arm64-v8a", "armeabi-v7a"]
+ "abiFilters": ["arm64-v8a"]
坑 2:libexpat.so 运行时未找到
现象:
Cannot read property 'verifyLibs' of undefined
应用启动时 testNapi 为 undefined,整个 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 解码重复代码
现象:FontGetGlyph 和 FontGetGlyphPath 中存在完全相同的 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 集成中遇到过什么奇怪的错误?欢迎在评论区分享你的经验。
如果本文对你有帮助,请 点赞、收藏、转发 支持一下~
更多推荐




所有评论(0)