鸿蒙PC迁移:kiwi C++ 约束求解三方库鸿蒙PC适配全记录
欢迎加入鸿蒙PC开发者社区,共同打造开发者工具生态:鸿蒙PC开发者社区:https://harmonypc.csdn.net/
项目开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_kiwi
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
这篇文章记录的是一次把 C++ 约束求解三方库 kiwi 接入 HarmonyOS PC / 鸿蒙 PC 应用的完整过程。
和 fontTools、ImageIO、jieba 这些 Python 三方库不同,kiwi 不是 Python 库,也不是一个现成的 GUI 软件。它是 Cassowary 约束求解算法 的一个高效 C++ 实现,常用于 UI 自动布局(类似 iOS 的 AutoLayout、各类界面约束系统底层都用到这类求解器)。更关键的是,它是一个 header-only(纯头文件) 的 C++ 库:没有需要单独编译的 .c/.cpp 源文件,把头文件 #include 进来就能用。
也就是说,这次适配要回答的问题和 Python 库那几篇完全不一样:
当三方库本身就是 C++ 时,鸿蒙 PC 应用要不要再绕一圈 Python?还是直接把它编译进 Native 层?
最终我们在项目里新增了一个 examples/harmony_pc/ 示例工程。这个工程不走 HNP Python,而是把 kiwi 的头文件直接编译进鸿蒙应用的 Native/N-API 模块,通过 JSON 协议把求解能力暴露给 ArkTS 页面。应用自包含、不依赖设备上的任何运行时,安装即用。

一、项目背景:kiwi 是什么,为什么它和 Python 库不一样
kiwi 是 Cassowary 约束求解算法的 C++ 实现,由 Nucleic 团队维护(也就是 Python 里那个常用的 kiwisolver 的底层)。它的定位是“快、轻”:官方说法是比原始 Cassowary 求解器快 10x~500x,内存占用降到 1/5 以下。
它解决的是这样一类问题——给定一组线性约束,求出一组变量的值。最直观的例子就是界面布局:
- 「侧栏宽度 + 间隔 + 内容区宽度 = 窗口宽度」
- 「侧栏至少 180,最多 320」
- 「内容区至少 240」
- 「侧栏最好占 30%,但这是软约束,空间不够时可以让步」
kiwi 会在这些约束里求出一组同时满足(或尽可能满足)的解。这正是 UI 自适应布局的核心。
这次鸿蒙 PC 示例就把 kiwi 包装成三个直观的模块:
- 运行环境检查:确认原生 kiwi 求解器已经编译进应用,并显示版本;
- 线性方程组求解:用约束求解一个二元一次方程组
x + y = a、x − y = b; - 约束布局演示:拖动滑块改变容器宽度,kiwi 实时求解侧栏 / 内容区宽度,直观展示它在布局里的本职用途。
这里要强调 kiwi 和 Python 库最本质的区别:Python 库(fontTools/ImageIO/jieba)必须有一个 Python 解释器去跑,所以鸿蒙端要准备 HNP Python;而 kiwi 是 C++,编译完就是机器码,可以直接塞进 .so,根本不需要解释器。
二、路线选择:不绕 HNP Python,直接把 C++ 编译进 Native 层
适配前对比了两条路线。
第一条路线是“沿用 Python 库的做法”:在鸿蒙 PC 上装 HNP Python,再 pip install kiwisolver,应用通过 N-API 启动 Python 去调用。这条路线和 fontTools 那篇一模一样。但对 kiwi 来说它有几个明显的浪费:
- kiwisolver 的 Python 绑定本身又是 C++ 扩展,装到 HNP Python 里要解决 wheel 的 ABI / NDK 编译问题;
- 用户必须先装「Python 安装器」、初始化 HNP Python、再装包,门槛高;
- 明明是 C++,却为了调用它额外背上一个 Python 解释器。
第二条路线是“顺着 kiwi 的本性来”:既然它是 header-only C++,那就把它的头文件直接编译进鸿蒙应用的 Native 模块,N-API 层直接 #include <kiwi/kiwi.h> 构建求解器。
最终采用的是第二条路线:
鸿蒙 ArkTS 页面
-> KiwiSolverClient 构建 JSON 约束问题
-> libkiwi_bridge.so (N-API)
-> #include <kiwi/kiwi.h> 在 C++ 里构建 Solver / Variable / Constraint 并求解
-> 返回 JSON { 变量值 }
-> ArkTS 解析结果、渲染界面
两条路线放在一起对比,差异很清楚:
| Python 库(如 fontTools) | kiwi | |
|---|---|---|
| 库本体 | 纯 Python | header-only C++ |
| 鸿蒙后端 | HNP Python(设备要装 Python 安装器 + pip 装包) | 直接编译进 .so 的 C++ |
| 运行时依赖 | 依赖设备上的 HNP Python 环境 | 无,自包含在 HAP 内 |
| ABI / NDK 风险 | 需要纯 Python wheel 规避 | 无(头文件随应用一起编译) |
这条路线的好处是:
- 不需要重写 kiwi,上游头文件一行不改;
- 应用自包含,用户安装即用,不用准备任何运行环境;
- 没有 Python wheel 的 ABI 适配风险;
- 原生性能,求解在 C++ 里直接完成;
- Native 层只做 JSON ↔ kiwi 的桥接,业务逻辑清晰。
三、先把 C++ 求解核心在开发机上验证通过
三方库适配有个很实用的习惯:先验证最核心、最容易出错的那一层,再去搭 UI。对 kiwi 来说,最核心的就是“接收一个 JSON 约束问题 → 调用 kiwi 求解 → 返回 JSON 结果”这段纯 C++ 逻辑。
所以这一层我故意写成和 N-API 完全解耦的一个头文件 kiwi_bridge.h,里面不引用任何 OpenHarmony 头文件。这样它在开发机上用普通 clang++/g++ 就能编译运行,不需要鸿蒙 SDK 就能先把求解逻辑测对。
JSON 请求协议设计得很直接,一条约束就是 Σ(coeff · var) <op> rhs:
{
"op": "solve",
"variables": ["x", "y"],
"constraints": [
{ "terms": [{ "var": "x", "coeff": 1 }, { "var": "y", "coeff": 1 }], "op": "==", "rhs": 10 },
{ "terms": [{ "var": "x", "coeff": 1 }, { "var": "y", "coeff": -1 }], "op": "==", "rhs": 4 }
]
}
在开发机上直接编译这个核心逻辑(同时把 kiwi 头文件和 cpp 目录加进 include 路径):
c++ -std=c++17 \
-Iexamples/harmony_pc/entry/src/main/cpp \
-I. \
host_test.cpp -o host_test && ./host_test
x + y = 10、x − y = 4 的求解结果应当是 x = 7, y = 3:
{"ok":true,"op":"solve","kiwi_version":"1.4.2","variable_count":2,"constraint_count":2,"values":{"x":7,"y":3}}
我还顺手把三个 UI 模块会真实下发的请求都跑了一遍:方程组多组取值、布局在不同容器宽度下的解、无解约束的报错、坏 JSON 的容错——全部通过后,再去搭鸿蒙 UI 才有意义。如果这一层就错了,后面 ArkTS 和 Native 写得再漂亮也没用。
四、新增的鸿蒙 PC 示例工程长什么样
适配完成后,项目里新增了鸿蒙 PC 示例目录,结构和其他几篇文章里的工程类似,但 cpp/ 目录是这次的重点:
examples/harmony_pc/
├── AppScope/app.json5
├── build-profile.json5 / oh-package.json5
├── sample_request.json 一个示例求解请求
└── entry/
└── src/main/
├── module.json5
├── cpp/
│ ├── CMakeLists.txt 链接上游 kiwi 头文件,编出 libkiwi_bridge.so
│ ├── napi_init.cpp N-API 入口(字符串进 / 字符串出)
│ ├── kiwi_bridge.h 纯 C++ 求解逻辑(与 N-API 解耦,可单测)
│ ├── mini_json.h 无依赖的迷你 JSON 解析 / 序列化
│ └── types/libkiwi_bridge/ .so 的 TS 声明
└── ets/
├── native/KiwiSolverNative.ets .so 的 ArkTS 封装
├── kiwi/KiwiSolverClient.ets 构建请求 / 解析结果
└── pages/Index.ets 演示页(三个模块)
各文件职责很清楚:
Index.ets:鸿蒙 PC 页面,三个模块的输入、按钮、滑块和结果展示;KiwiSolverClient.ets:ArkTS 侧封装,把约束问题组织成 JSON 请求、解析返回值;KiwiSolverNative.ets:声明.so暴露的solve()、getKiwiVersion()方法;napi_init.cpp:N-API 入口,只负责字符串进字符串出;kiwi_bridge.h:真正#include <kiwi/kiwi.h>构建并求解的 C++ 逻辑;mini_json.h:一个零依赖的迷你 JSON 读写,避免引入额外库。
这里同样没有改写上游 kiwi/ 头文件,只是在外面加了一层鸿蒙 PC 调用壳。边界清晰:kiwi 保持原有 C++ 库形态,鸿蒙适配逻辑全部集中在 examples/harmony_pc/。

五、Native/N-API:直接在 C++ 里调用 kiwi 求解
ArkTS 不能直接用 C++ 的 kiwi,所以中间需要 Native/N-API 做桥接。和 Python 库那几篇“N-API 启动 Python 进程”不同,这里 N-API 层自己就是求解器——它直接把 kiwi 编译进来调用,没有任何子进程。
napi_init.cpp 只暴露两个方法,而且只做字符串的进出:
solve(requestJson: string): string // 传入 JSON 约束问题,返回 JSON 结果
getKiwiVersion(): string // 返回编译进 .so 的 kiwi 版本
真正的求解在 kiwi_bridge.h 里。它把 JSON 里的每条约束翻译成 kiwi 的对象。kiwi 的约束是“表达式与 0 比较”,所以 Σ(coeff·var) <op> rhs 要把 rhs 挪到左边变成 Σ(coeff·var) − rhs:
std::vector<kiwi::Term> terms;
for (/* JSON 里每个 term */) {
terms.emplace_back(pool.get(varName), coeff);
}
kiwi::Expression expression(std::move(terms), -rhs); // 把 rhs 挪到左边
kiwi::Constraint constraint(expression, op, strength); // op: == / <= / >=
solver.addConstraint(constraint);
强度(strength)支持 kiwi 的四档 required / strong / medium / weak,对应“必须满足 / 强烈倾向 / 一般倾向 / 弱倾向”。布局演示里“侧栏最好占 30%”就是 weak,而“侧栏 + 间隔 + 内容 = 总宽”是 required。
求解完成后,把每个变量的值拼成 JSON 返回:
{
"ok": true,
"op": "solve",
"kiwi_version": "1.4.2",
"variable_count": 2,
"constraint_count": 2,
"values": { "x": 7, "y": 3 }
}
如果约束无解(比如同时要求 x == 1 和 x == 2 且都是 required),kiwi 会抛出 UnsatisfiableConstraint 异常。bridge 把这些异常都接住,转成结构化的错误 JSON,方便页面展示:
{ "ok": false, "op": "solve", "error_type": "UnsatisfiableConstraint", "message": "..." }
CMake 这边也只做一件事:把上游 kiwi 头文件的目录加进 include 路径,然后编出 libkiwi_bridge.so。因为 kiwi 是 header-only,所以不需要编译任何上游源文件:
# kiwi 头文件在仓库根目录,header-only,只需加入 include 路径
set(KIWI_INCLUDE_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../..")
include_directories(${KIWI_INCLUDE_ROOT})
add_library(kiwi_bridge SHARED napi_init.cpp)
target_compile_features(kiwi_bridge PRIVATE cxx_std_17)
target_link_libraries(kiwi_bridge PUBLIC libace_napi.z.so)
六、ArkTS 页面:把求解器做成能上手玩的三个模块
页面 Index.ets 没有写成“一个验证按钮”,而是拆成三个用户能直接理解、能动手的模块。
模块一:运行环境检查。 调用 getKiwiVersion(),显示 kiwi 版本,确认原生求解器已经加载。和 Python 库不同,这里不需要检查 Python、不需要检查 pip 装没装包——求解器就在 .so 里。
模块二:线性方程组求解。 用户输入 a、b,应用构造两条约束 x + y = a、x − y = b,调用 solve() 求出 x、y。KiwiSolverClient 负责把它拼成 JSON:
const request: KiwiRequest = {
op: 'solve',
variables: ['x', 'y'],
constraints: [
{ terms: [{ var: 'x', coeff: 1 }, { var: 'y', coeff: 1 }], op: '==', rhs: a },
{ terms: [{ var: 'x', coeff: 1 }, { var: 'y', coeff: -1 }], op: '==', rhs: b }
]
};
const result = this.client.solve(request);
模块三:约束布局演示。 这是最能体现 kiwi 价值的模块。一个滑块控制容器宽度,每次拖动都把当前宽度作为“建议值”喂给一个编辑变量(edit variable),kiwi 实时求解侧栏和内容区宽度,再用两个色块按比例画出来。约束是这样一组:
侧栏 + 间隔 + 内容 = 总宽(required)间隔 = 16(required)侧栏 ≥ 180、侧栏 ≤ 320、内容 ≥ 240(strong)侧栏 = 30% × 总宽(weak,软偏好)
容器够宽时侧栏按 30% 走,窗口拉大到一定程度侧栏会被钳在 320 不再变宽,空间不够时软约束让步——这正是真实自适应布局的行为。


七、构建:用 hvigor 把 ArkTS 和 Native 一起编出来
工程可以在 DevEco Studio 里直接构建,也可以用命令行 hvigor 构建。先装依赖,再 assembleHap:
ohpm install
hvigorw clean assembleHap --mode module -p product=default -p buildMode=debug --no-daemon
这里遇到了一个很典型的 SDK 版本问题。我最初照搬别的工程,把 compatibleSdkVersion 写成了 6.0.2(22),但这台机器命令行 SDK 实际装的是 API 21(6.0.1.112),构建直接报错:
Configuration Error: Unable to find the compatibleSdkVersion 6.0.2(22) in SDK Manager.
解决办法是把 build-profile.json5 里的版本改成与本机 SDK 匹配的 6.0.1(21):
"compatibleSdkVersion": "6.0.1(21)",
"targetSdkVersion": "6.0.1(21)",
改完重新构建就通过了。从日志里能清楚看到 Native 和 ArkTS 两条线都编了:BuildNativeWithCmake / BuildNativeWithNinja 把 C++ 的 libkiwi_bridge.so 编出来,CompileArkTS 编页面,最后签名打包:
Finished :entry:default@BuildNativeWithCmake
Finished :entry:default@BuildNativeWithNinja
Finished :entry:default@CompileArkTS
Finished :entry:default@PackageHap
Finished :entry:default@SignHap
Finished :entry:assembleHap
BUILD SUCCESSFUL in 5 s 759 ms
构建产物里能看到 libkiwi_bridge.so 同时编出了 arm64-v8a 和 x86_64 两个架构,已经打进 HAP。

八、装到真机并实测三个模块
构建出签名 HAP 后,用 hdc 装到连着的鸿蒙 PC 上并启动:
HDC=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc
$HDC install -r entry/build/default/outputs/default/entry-default-signed.hap
$HDC shell aa start -a EntryAbility -b org.kiwisolver.harmony.example
装好后在设备上把三个模块都点了一遍,实测都正常:
- 模块一:状态显示「已就绪」,kiwi 版本
1.4.2,说明原生.so已加载、求解器可用; - 模块二:a = 10、b = 4 点求解,得到 x = 7、y = 3;把输入换成 a = 1030、b = 48 再求解,得到 x = 539、y = 491(验算 (1030±48)/2 完全正确),说明是真在求解而不是写死结果;
- 模块三:拖动滑块从 720 到 1090,侧栏被正确钳制在上限 320,内容区拿到 754,软约束的钳制逻辑在设备上工作正常。
整条链路——从 C++ 求解核心 → Native .so 编译 → ArkTS → 签名打包 → 真机安装 → 运行时交互——全部跑通。
九、这次适配后的结果与一点总结
完成后,这个示例是一个自包含、安装即用的鸿蒙 PC 约束求解器:
- 不依赖设备上的 Python 或任何运行时,安装即用;
- 上游 kiwi 头文件一行未改,适配逻辑全部集中在
examples/harmony_pc/; - Native 层直接
#include <kiwi/kiwi.h>求解,没有子进程、没有 ABI 风险; - 求解核心与 N-API 解耦,开发机上用普通 C++ 编译器就能先测对;
- 三个模块普通用户和开发者都能直接上手;
- DevEco / hvigor 构建通过,真机实测三个模块都正常。
从这次适配可以总结出一条和 Python 库不一样的接入思路——当三方库本身就是 C/C++ 时,不要习惯性地再套一层 Python:
先把纯 C++ 求解核心与 N-API 解耦,在开发机上测对
-> 用一个迷你 JSON 协议约定请求 / 响应
-> N-API 层直接 #include 头文件调用,不起子进程
-> CMake 只把头文件加进 include 路径(header-only 无需编译上游)
-> ArkTS Client 封装业务方法,页面做成能上手的模块
-> 最后用真机和 DevEco / hvigor 构建验证闭环
对 kiwi 这种 header-only C++ 库来说,这条路线比绕 HNP Python 更轻、更稳,也更适合推广到其他 C/C++ 三方库(比如各类几何、数值、解析类的纯算法库)。后续如果要继续扩展,可以在这个基础上增加更多约束能力,比如批量布局求解、约束的增删与重解、把求解结果直接驱动 ArkUI 组件做一套真正的约束布局引擎。
更多推荐


所有评论(0)