欢迎加入【开源鸿蒙PC社区】,一起共建鸿蒙化C/C++三方库生态。
欢迎在【PC社区】平台贡献你的项目。
仓库: mikke89/RmlUi v6.2 — The C++ GUI library, based on HTML and CSS standards
前置说明
| 项目 | 说明 |
|---|---|
| 集成库 | RmlUi v6.2 |
| 目标平台 | 鸿蒙PC |
| SDK 版本 | OHOS SDK (Clang 15.0.4) |
| 开发工具 | DevEco Studio |
| 交叉编译框架 | lycium_plusplus |
| 三方库静态库 | librmlui.a (7.5MB) + librmlui_debugger.a (521KB) |
| 运行时依赖 | libfreetype.a (1.3MB) |
| 示例仓库 | https://atomgit.com/allincoding/OHOSRmlUiSample |
传统方式的效率瓶颈
在鸿蒙PC生态起步阶段,每个三方库的 NAPI 集成都是一场"踩坑马拉松"。
| 阶段 | 主要痛点 |
|---|---|
| 工程搭建 | 手动创建目录结构、修改 bundleName、deviceTypes |
| 库文件部署 | 拷贝头文件和 .a 到正确位置,依赖链传递 |
| CMake 配置 | 路径拼写错误、链接顺序问题、多库依赖 |
| NAPI 桥接 | napi_get_cb_info / napi_create_string_utf8 等接口重复编写 |
| 类型声明 | 接口签名必须与 C++ 精确匹配,ArkTS 类型约束严格 |
| UI 验证 | 调用测试、格式化显示、异步结果处理 |
| 编译排错 | ArkTS any/unknown 约束、C++ flat_map API 不熟悉 |
RmlUi 的特殊挑战:它是一个完整的 GUI 渲染引擎,依赖 FreeType 字体库,需要实现 SystemInterface 和 RenderInterface。在 NAPI 场景下,我们不能直接渲染到屏幕,而是把它作为 UI 模板引擎使用——解析 RCSS 样式表、创建元素树、管理字体资源。
AtomCode + Skills 全流程
第 1 步:创建 HPKBUILD 交叉编译配置
RmlUi v6.2 使用 CMake 构建系统,通过 lycium_plusplus 框架完成 arm64-v8a 交叉编译。
# 在 lycium_plusplus 框架下创建 HPKBUILD# /lycium_plusplus/thirdparty/RmlUi/HPKBUILDpkgname=RmlUipkgver=6.2pkgrel=0pkgdesc="RmlUi — The C++ GUI library, based on HTML and CSS standards"url="https://github.com/mikke89/RmlUi"archs=("arm64-v8a")license=("MIT")depends=("freetype2")source="https://github.com/mikke89/$pkgname/archive/refs/tags/$pkgver.tar.gz"buildtools="cmake"builddir=$pkgname-$pkgver关键点:RmlUi 依赖 FreeType 字体引擎。编译时通过
depends=("freetype2")让框架自动构建并传递依赖路径。
第 2 步:验证编译产物
交叉编译完成后,产出的静态库位于$LYCIUM_ROOT/usr/RmlUi/arm64-v8a/:
lib/ ├── librmlui.a # 7.5MB — RmlUi 核心库 ├── librmlui_debugger.a # 521KB — 调试器模块 └── cmake/RmlUi/ # CMake 配置(含 FreeType 链接信息) include/ └── RmlUi/ ├── Core.h # 核心 API 入口 ├── Core/*.h # 120+ 头文件 └── Debugger/ # 调试器模块RmlUiTargets.cmake中揭示了关键依赖关系:
set_target_properties(RmlUi::Core PROPERTIES INTERFACE_LINK_LIBRARIES "\$<LINK_ONLY:Freetype::Freetype>" )这意味着链接librmlui.a时必须同时链接libfreetype.a。
第 3 步:创建 NAPI 示例工程
从 OHOSSpdlogSample 模板生成 OHOSRmlUiSample,然后进行定制改造。
3.1 库文件部署
将编译产物复制到工程 thirdparty 目录:
entry/src/main/cpp/thirdparty/ ├── rmlui/ │ ├── include/RmlUi/ # 120+ 头文件 │ └── lib/ │ ├── librmlui.a │ └── librmlui_debugger.a └── freetype/ ├── include/ # ft2build.h + freetype/ └── lib/ └── libfreetype.a3.2 CMakeLists.txt 配置
cmake_minimum_required(VERSION 3.5.0) project(OHOSRmlUiSample) set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}) set(RMLUI_INSTALL_DIR ${NATIVERENDER_ROOT_PATH}/thirdparty/rmlui) set(FREETYPE_INSTALL_DIR ${NATIVERENDER_ROOT_PATH}/thirdparty/freetype) include_directories(${NATIVERENDER_ROOT_PATH} ${RMLUI_INSTALL_DIR}/include ${FREETYPE_INSTALL_DIR}/include) add_library(entry SHARED napi_init.cpp) target_link_libraries(entry PUBLIC libace_napi.z.so) target_link_libraries(entry PUBLIC ${RMLUI_INSTALL_DIR}/lib/librmlui.a) target_link_libraries(entry PUBLIC ${RMLUI_INSTALL_DIR}/lib/librmlui_debugger.a) target_link_libraries(entry PUBLIC ${FREETYPE_INSTALL_DIR}/lib/libfreetype.a)3.3 NAPI 桥接层设计
RmlUi 的 NAPI 桥接分为 4 大模块,共 8 个导出函数:
| 模块 | 函数 | 功能 | C++ API |
|---|---|---|---|
| 引擎管理 | engineInit() | 初始化 RmlUi 引擎 | Rml::SetSystemInterface()+Rml::Initialise() |
engineShutdown() | 安全关闭引擎 | Rml::Shutdown() | |
engineGetState() | 查询引擎版本状态 | Rml::GetVersion() | |
| 样式解析 | styleParseRcss(rcss) | 校验 RCSS 样式表 | Factory::InstanceStyleSheetString() |
| 元素工厂 | elementCreate(tag, attrs) | 创建 UI 元素 | Factory::InstanceElement() |
elementSetStyle(idx, prop, val) | 设置元素样式 | Element::SetProperty() | |
elementGetInfo(idx) | 查询元素属性 | Element::GetTagName()/GetId() | |
| 字体管理 | fontLoadFace(path) | 加载字体文件 | Rml::LoadFontFace() |
核心的引擎初始化代码:
// OHOSSystemInterface — 将 RmlUi 日志桥接到 OHOSclassOHOSSystemInterface:publicRml::SystemInterface{doubleGetElapsedTime()override{return0.0;}boolLogMessage(Rml::Log::Type,constRml::String&)override{returntrue;// 实际部署可桥接到 hilog}};staticOHOSSystemInterface g_systemInterface;staticnapi_valueEngineInit(napi_env env,napi_callback_info info){Rml::SetSystemInterface(&g_systemInterface);boolok=Rml::Initialise();// 返回 JSON: { initialized, version, elementCount }returnCreateString(env,engineStateToJson());}RCSS 样式表解析使用 RmlUi 的原生 CSS 解析器:
staticnapi_valueStyleParseRcss(napi_env env,napi_callback_info info){autostyleSheet=Rml::Factory::InstanceStyleSheetString(rcssContent);if(styleSheet){returnCreateString(env,R"({"success":true})");}}3.4 TypeScript 类型声明
ArkTS 对类型要求严格,必须为每个 NAPI 函数声明签名:
// entry/src/main/cpp/types/libentry/Index.d.tsexportconstengineInit:()=>string;exportconstengineShutdown:()=>string;exportconstengineGetState:()=>string;exportconststyleParseRcss:(rcss:string)=>string;exportconstelementCreate:(tag:string,attributes?:string)=>string;exportconstelementSetStyle:(index:number,property:string,value:string)=>string;exportconstelementGetInfo:(index:number)=>string;exportconstfontLoadFace:(path:string,fallback?:boolean)=>string;3.5 ArkUI 页面设计
UI 采用 5 步工作流设计,每步对应一个业务场景:
// ① 引擎管理 → 启动/查询/关闭Button('启动引擎').onClick(()=>{constresult=testNapi.engineInit();this.engineInfo=result;})// ② RCSS 样式工作台 → 输入+校验TextInput({text:this.rcssInput,placeholder:'输入 RCSS 样式表...'})Button('校验 RCSS 样式表').onClick(()=>{constresult=testNapi.styleParseRcss(this.rcssInput);})// ③ 元素工厂 → 创建+设样式+查询testNapi.elementCreate(this.elementTag,this.elementAttrs);testNapi.elementSetStyle(index,'color','#ff0000');testNapi.elementGetInfo(index);// ④ 字体管理 → 加载字体文件testNapi.fontLoadFace('/system/fonts/NotoSansSC-Regular.otf');// ⑤ 操作日志 → 滚动显示所有操作记录踩坑专区
坑 1:flat_map 没有 Set() 方法
现象:
error: no member named 'Set' in 'itlib::flat_map<std::string, Rml::Variant>' attrs.Set("id", ...);根因:Rml::XMLAttributes实际上是Dictionary,类型链为:
XMLAttributes → Dictionary → SmallUnorderedMap → robin_hood::unordered_flat_map → itlib::flat_mapflat_map是标准库容器的替代品,它的接口与std::map类似——没有.Set()方法,只有operator[]、insert()、emplace()。
修复:
// 错误:flat_map 没有 Set()attrs.Set("id",attrStr.substr(start+1,end-start-1));// 正确:使用 operator[] + 显式 Variant 构造attrs["id"]=Rml::Variant(attrStr.substr(start+1,end-start-1));坑 2:ArkTS 禁止 JSON.parse() 的 any 类型
现象:
Error Message: Use explicit types instead of "any", "unknown" (arkts-no-any-unknown)根因:ArkTS 严格模式禁止any和unknown类型。JSON.parse()返回any,在 DevEco Studio 的 ArkTS 编译器中直接报错。
修复:使用纯字符串解析替代JSON.parse:
// 工具函数:从 JSON 字符串中安全提取结果functionjsonSuccess(result:string):boolean{returnresult.indexOf('"success":true')>=0;}functionjsonExtractInt(result:string,key:string):number{constsearch='"'+key+'":';constpos=result.indexOf(search);if(pos<0)return-1;conststart=pos+search.length;letend=start;while(end<result.length&&result[end]>='0'&&result[end]<='9')end++;if(end===start)return-1;returnparseInt(result.substring(start,end),10);}// 使用if(jsonSuccess(result)){this.elementIndex=jsonExtractInt(result,'elementIndex');}此方案零依赖、零运行时开销,完全符合 ArkTS 类型约束。
坑 3:静态库链接顺序依赖
现象:
ld.lld: error: undefined symbol: FT_Init_FreeType根因:RmlUi 依赖 FreeType,librmlui.a中有对 FreeType 符号的引用。在静态链接中,符号解析是"单向传递"的——被依赖的库必须放在依赖者后面。
修复:
# 错误顺序 target_link_libraries(entry PUBLIC ${FREETYPE_INSTALL_DIR}/lib/libfreetype.a ${RMLUI_INSTALL_DIR}/lib/librmlui.a) # 正确顺序 — RmlUi 依赖 FreeType,FreeType 放后面 target_link_libraries(entry PUBLIC ${RMLUI_INSTALL_DIR}/lib/librmlui.a ${RMLUI_INSTALL_DIR}/lib/librmlui_debugger.a ${FREETYPE_INSTALL_DIR}/lib/libfreetype.a)坑 4:RmlUi 无头模式下的接口适配
现象:
RmlUi 的Initialise()需要至少设置SystemInterface,否则内部日志系统会崩溃。CreateContext()需要一个RenderInterface实现,包含 6 个纯虚方法。
根因:RmlUi 是一个完整的 GUI 引擎,设计上要求提供渲染和系统接口。NAPI 场景下没有 GPU/窗口环境,需要适配为"逻辑层模式"。
修复:提供最小存根实现,绕过需要RenderInterface的 API,专注于Factory层的功能:
// SystemInterface 最小实现classOHOSSystemInterface:publicRml::SystemInterface{doubleGetElapsedTime()override{return0.0;}boolLogMessage(Rml::Log::Type,constRml::String&)override{returntrue;}};// 不使用 CreateContext / LoadDocument — 这些需要 RenderInterface// 使用 Factory 的独立 API:// - Factory::InstanceStyleSheetString() — RCSS 解析(无渲染依赖)// - Factory::InstanceElement() — 元素创建(无渲染依赖)// - Rml::LoadFontFace() — 字体加载(无渲染依赖)通用集成模板(拿来即用)
CMakeLists.txt 模板(带依赖传递检查)
cmake_minimum_required(VERSION 3.5.0) project(YourProject C CXX) set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}) # === 三方库路径 === set(LIB_NAME "yourlib") set(LIB_INSTALL_DIR ${NATIVERENDER_ROOT_PATH}/thirdparty/${LIB_NAME}) # === 安全检查:文件是否存在 === if(NOT EXISTS ${LIB_INSTALL_DIR}/lib/lib${LIB_NAME}.a) message(FATAL_ERROR "${LIB_NAME} static library not found at ${LIB_INSTALL_DIR}/lib/") endif() # === 头文件 === include_directories(${NATIVERENDER_ROOT_PATH} ${LIB_INSTALL_DIR}/include) # === 目标 === add_library(entry SHARED napi_init.cpp) target_link_libraries(entry PUBLIC libace_napi.z.so) # === 链接(注意顺序:被依赖的放后面) === target_link_libraries(entry PUBLIC ${LIB_INSTALL_DIR}/lib/lib${LIB_NAME}.a) target_link_libraries(entry PUBLIC ${LIB_INSTALL_DIR}/lib/lib${LIB_NAME}_debugger.a) # === 运行时依赖 === if(EXISTS ${NATIVERENDER_ROOT_PATH}/thirdparty/freetype/lib/libfreetype.a) include_directories(${NATIVERENDER_ROOT_PATH}/thirdparty/freetype/include) target_link_libraries(entry PUBLIC ${NATIVERENDER_ROOT_PATH}/thirdparty/freetype/lib/libfreetype.a) endif()NAPI 函数 5 步模板
staticnapi_valueMyFunction(napi_env env,napi_callback_info info){// ① 解析参数个数和值size_t argc=2;napi_value argv[2]={nullptr};napi_get_cb_info(env,info,&argc,argv,nullptr,nullptr);// ② 边界检查if(argc<2){napi_throw_error(env,nullptr,"At least 2 arguments required");returnnullptr;}// ③ 类型校验napi_valuetype vt;napi_typeof(env,argv[0],&vt);if(vt!=napi_number){napi_throw_type_error(env,nullptr,"First argument must be a number");returnnullptr;}// ④ 调用 C/C++ APIdoublevalue;napi_get_value_double(env,argv[0],&value);doubleresult=rmlFunction(value);// ⑤ 返回 NAPI 值napi_value ret;napi_create_double(env,result,&ret);returnret;}ArkTS JSON 安全解析模板
// 跨所有 ArkTS 版本的 JSON 安全解析工具// 避免 arkts-no-any-unknown 编译错误functionjsonSuccess(result:string):boolean{returnresult.indexOf('"success":')>=0&&result.indexOf('"success":true')>=0;}functionjsonExtractStr(result:string,key:string):string{constsearch='"'+key+'":"';conststart=result.indexOf(search);if(start<0)return'';constvalStart=start+search.length;constvalEnd=result.indexOf('"',valStart);returnvalEnd<0?'':result.substring(valStart,valEnd);}functionjsonExtractInt(result:string,key:string):number{constsearch='"'+key+'":';constpos=result.indexOf(search);if(pos<0)return-1;letend=pos+search.length;while(end<result.length&&result[end]>='0'&&result[end]<='9')end++;constval=result.substring(pos+search.length,end);returnval?parseInt(val,10):-1;}总结
RmlUi 作为一个完整的 C++ GUI 引擎,在鸿蒙PC上通过 NAPI 集成时面临的最大挑战不是代码编写,而是理解其架构边界:什么功能需要渲染后端(不能用于 NAPI),什么功能可以独立运行(RCSS 解析、元素工厂、字体管理)。
这次集成让我深刻体会到:
理解库的架构边界,比编写一万行 NAPI 桥接代码更重要。
通过将 RmlUi 定位为"UI 模板引擎"而非"渲染引擎",我们在 NAPI 场景下找到了最佳实践:用 RCSS 做样式校验,用 Element Factory 做动态组件创建,用 Font Loader 做字体管理——这些功能不需要 GPU,却能发挥 RmlUi 90% 的工程价值。
你在 NAPI 集成中遇到过什么奇怪的错误?欢迎在评论区分享你的经验。
如果本文对你有帮助,请点赞、收藏、转发支持一下~