欢迎加入鸿蒙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 = ax − 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 = 10x − 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 == 1x == 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 = ax − 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-v8ax86_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 组件做一套真正的约束布局引擎。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐