鸿蒙 PC 适配 libcopp 2.2.0:72 份汇编与零源码补丁的验证实践
欢迎加入开源鸿蒙PC社区: https://harmonypc.csdn.net/
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
鸿蒙 PC 适配 libcopp 2.2.0:72 份汇编与零源码补丁的验证实践
一次路径 C 级移植复盘。libcopp 的适配难点不在代码量,而在它的"心脏"是一叠按 ABI 分文件的手写上下文切换汇编。它最终以零源码补丁交付——全部必要改动收敛成一个 CMake 变量的显式注入。但为了确认"该注入的确实生效、而不是被上游的架构探测悄悄改回去了",本文把过程里的每一条实测读数和每一段首错原文都留了下来。
一、为什么值得做:协程库的"心脏"是一段汇编
libcopp(owent/libcopp)是一个 C++ 协程库,提供两层能力:底层的协程上下文切换,以及其上的协程任务调度器;同时支持 C++20 coroutine 与手写汇编两套实现路径。它常被当作轻量级任务/协程底座嵌进服务端与端侧程序,属于"底座型"依赖。
它真正难的地方,是上下文切换这一层无法用 C 重写:切换要按目标 ABI 成组保存与恢复 callee-saved 寄存器、切换栈指针、搬运返回地址,所以上游把每一种架构、每一种 ABI 都写成一份独立的汇编文件,集中放在 src/libcopp/fcontext/asm/ 下。本次清点:该目录共 91 个文件,其中 .S 汇编 72 份(jump / make / ontop 三件套 × 各架构 ABI,例如 aarch64 用的 jump_arm64_aapcs_elf_gas.S),另有 18 份 Windows 侧 .asm 与 1 份 .cpp。也就是说:平台识别错一步,编出来的就不是"目标平台跑不了",而是"选错了架构的汇编"——这正好是本次适配撞上的第一颗雷。
除了"架构敏感",它还有三个适合做路径 C 的资质:
- 测试面自包含:上游
test/用自带测试框架test_manager,22 个测试源文件、CASE_EXPECT_*断言合计 1618 条,纯 CPU,不依赖设备节点、网络服务或 GPU。 - 运行期依赖极小:只有
Threads(pthread)这一层 libc 级依赖,没有一串需要逐个先适配的上游库。 - 通过率能真跑出来:CI 配置下实测 100%,满足路径 C「≥90% 通过率」的硬门槛。
一句话:这是一个"依赖很少、但机制很深"的包。它的价值在于把跨平台架构探测这一类机制摊开给你看。
二、平台差异一览(先说清"哪里真的不一样")
| 差异面 | 鸿蒙 PC 实测读数 | 影响 |
|---|---|---|
| CMake 平台识别 | System is unknown to cmake, create: Platform/HarmonyOS ...;最小项目实测 CMAKE_HOST_SYSTEM_PROCESSOR=unknown / CMAKE_SYSTEM_NAME=HarmonyOS | 上游架构探测把 unknown 按指针宽度硬回退成 x86_64 |
| 指针宽度 | CMAKE_SIZEOF_VOID_P=8(aarch64) | 恰好命中上游"8 字节 ⇒ x86_64"的回退分支 |
| 工具链 | clang 15.0.4 / LLVM,os=OHOS,os.version=6.0,arch=armv8,compiler.cppstd=17,libcxx=libc++ | 只有 clang;本包零源码补丁,未触发 musl 专有接口问题 |
| make 作业服务 | 原文形态 make: jobserver mkfifo: <家目录>/GMfifo60848: Operation not permitted(主机路径已略去) | jobserver 通道建不起来;本次两次构建均照常完成,属于"看着吓人但不拦路"的一类 |
/proc 可读性 | Problem parsing /proc/meminfo | 只影响探测类信息读取,不影响构建与测试 |
| 静态库链接语义 | ld.lld: error: undefined symbol: cotask::v2002::this_task::get_task() | 包内交付两个静态库,清单漏报或顺序颠倒都会在消费者侧炸 |
| 汇编源选择 | 正确后实测构建行为 Building ASM object .../fcontext/asm/make_arm64_aapcs_elf_gas.S.obj(同批 ontop / jump) | 平台变量对了,ABI 才回到 aapcs/elf/gas |
这张表里,第一、二行是会直接把产物带偏的差异;第四、五行是"环境噪声",必须和真问题分开记,否则排查会被带跑。
三、适配流程总览
- W0 依赖就绪性先证:运行期只有
Threads(pthread);测试侧经审计确认不需要任何外部测试框架(依据见 4.7)。因此本版的依赖清单最终是空的——不是"先假设要 gtest,编过了再删"。 - 源码两个源:主源是上游 tag 归档(codeload 实测
GET 200、269771B、sha2564f7831a8…),副源是 tag 归档里缺失的 git 子模块快照(见 4.2)。 - 配置期注入平台变量:
LIBCOPP_FCONTEXT_OS_PLATFORM=arm64(armv8 ⇒ arm64),并同时打开PROJECT_ENABLE_UNITTEST=ON/BUILD_TESTING=ON、关闭示例PROJECT_ENABLE_SAMPLE=OFF(交付面最小化)。 - 构建期真跑上游测试:
ctest --output-on-failure --no-tests=error --timeout 600,日志全量留档。 - 打包面收口:删掉上游自带的 CMake config(见 4.3),只让 Conan 生成的 config 生效。
- 消费者视角自证:
test_package真实走一次「创建协程 → 启动 → 协程内 yield → resume → 取回返回码」。
四、逐难点:现象 / 根因 / 修法
4.1 ★最核心的一条:unknown 被硬回退成 x86_64,aarch64 设备去编 x86_64 汇编
现象(首错原文):
src/libcopp/fcontext/asm/jump_x86_64_sysv_elf_gas.S:138:9: error: invalid operand
jmp *%r8
根因:鸿蒙 PC 上 CMake 报不出处理器名(CMAKE_HOST_SYSTEM_PROCESSOR=unknown),而上游 src/libcopp/fcontext/detect/fcontext.detect.cmake 里有这么一段:
if(LIBCOPP_FCONTEXT_OS_PLATFORM MATCHES "unknown|i386|i686|x86_64|amd64")
if(CMAKE_SIZEOF_VOID_P MATCHES 8)
set(CMAKE_SYSTEM_PROCESSOR "x86_64")
set(LIBCOPP_FCONTEXT_OS_PLATFORM "x86_64")
else()
set(CMAKE_SYSTEM_PROCESSOR "i386")
它把 unknown 和 x86_64 放进了同一个分支,再按指针宽度判:8 字节 ⇒ x86_64。于是 aarch64 的机器认真地去编 x86_64 的上下文切换汇编,第 138 行的 jmp *%r8 在 aarch64 上自然无效。注意这条分支的写法本身没有错——它在"探测不出处理器名"时选择了一个默认值;只是这个默认值在异构平台上代价很大。
修法:不改上游一行,改用上游自己预留的显式变量,在配方里注入:
tc.cache_variables["LIBCOPP_FCONTEXT_OS_PLATFORM"] = "arm64" # armv8/armv8.3 ⇒ arm64
修后实测:探测输出回到 arm64 / aapcs / elf / gas,构建日志出现前面那三条 make_/ontop_/jump_arm64_aapcs_elf_gas.S.obj;打包产物的静态库成员顺序(ar t 实读)为:
jump_arm64_aapcs_elf_gas.S.obj
make_arm64_aapcs_elf_gas.S.obj
ontop_arm64_aapcs_elf_gas.S.obj
并且 x86_64 成员计数为 0。这三行读数很关键:它证明的不只是"编过了",而是包里躺着的确实是 arm64 的切换实现。
4.2 tag 归档不含 git 子模块,configure 阶段直接找不到 CMake 函数
现象(首错原文):
include could not find requested file: IncludeDirectoryRecurse
include could not find requested file: EchoWithColor
根因:上游 tag 归档不打 submodule:atframework/cmake-toolset 在 tarball 里是个空目录,而它提供了上面那些 CMake 工具函数;该 tag 也没有 release 资产可下。补一个容易说错的细节:.gitmodules 只写了 path 与 url,真正的版本钉在 git 树的 gitlink 上(审计独立复核为 mode 160000、commit c911fe34b494dcb3200408803f25787772e5d8c6,该 commit 在子模块仓确实存在)。
修法:conandata.yml 里加第二个源,按钉住的 commit 从 codeload 取快照(sha256 ede39873…),在 source() 里铺回 atframework/cmake-toolset/ 原路径。
这条的教训:“源码归档"不等于"源码树”。依赖 git 子模块的项目,必须把 pin 住的 commit 当作一个独立源来对待,并和主源一样校验 sha256。
4.3 消费者 find_package 命中了上游自带的那份 config
现象:消费者解析到包后,报找不到 libcopp-targets.cmake。
根因:上游 install 规则会把自家用的 libcopp-config.cmake 一并装进包目录,而那份 config 内部 include(libcopp-targets.cmake)——这个 targets 文件是 Conan 生成器不会产出的(它只产出自己的 libcopp-config.cmake)。于是同一个包目录里出现两份 config,消费者按搜索顺序命中了"错的"那一份。
修法:package() 里把上游那份连目录一起删掉:
rmdir(self, os.path.join(self.package_folder, "lib", "cmake"))
rmdir(self, os.path.join(self.package_folder, "share"))
让 Conan 生成的 config 成为唯一权威。
4.4 只报了一个库名:消费者链接期炸 undefined symbol
现象(首错原文):
ld.lld: error: undefined symbol: cotask::v2002::this_task::get_task()
根因:上游 install 交付的是两个静态库——libcopp.a(上下文切换/栈/工具)与 libcotask.a(协程任务调度器,依赖 copp),而配方最初只声明了 copp。审计做了三组实链对照:只 -lcopp ⇒ 上述未定义符号稳定复现;加上 -lcotask ⇒ 链接通过。
修法:
self.cpp_info.libs = ["cotask", "copp"] # 顺序不可反
self.cpp_info.system_libs = ["pthread"]
顺序为什么不能反:静态链接按命令行顺序解析符号,被依赖者要排在依赖者之后(cotask 依赖 copp,所以 cotask 在前)。
4.5 配方里用错了 API:AttributeError
现象(首错原文):
AttributeError: 'CMake' object has no attribute 'build_folder'
根因:在 build() 里写了 cmake.build_folder——CMake 工具对象没有这个属性,构建目录挂在 ConanFile 自己身上。
修法:改用 self.build_folder。这类错误属于"配方自身的 API 误用",与平台无关;记下来是因为它和 4.1 撞在同一轮,很容易被误归因成平台问题。
4.6 ctest 的"假绿":用例注册了,但可执行文件不在,照样 Test Passed.
现象(原文,来自本包探针目录的 LastTest.log):
Unable to find executable: .../build/test/libcopp_unit_test
1/1 Test: libcopp.unit_test
Command: ""
Test time = 0.00 sec
Test Passed.
根因:这套测试在 CMake 侧只注册了一个聚合目标(libcopp.unit_test),真正的 64 个子用例在那个二进制里跑。二进制没被构建出来时,ctest 找不到可执行文件,却仍然给了一个 Test Passed.——如果只看 ctest 的退码,这就是一次彻底的假绿。
修法:三层一起上。
--no-tests=error:注册数为 0 直接判红,堵住"空跑";- 日志留档核验:要求日志里出现二进制内部的真实执行尾部,而不是只有聚合行;
- 产物核验:
ar t看静态库成员、看消费者测试是否真跑。
并且要如实披露这道防线的边界:--no-tests=error 只挡"0 注册",不挡"注册了但二进制缺失"——后者只能靠第 2、3 层。把防护声明写宽了,下次就会有人以为它万能。
4.7 gtest 依赖其实是死门:删掉依赖,测试一条不少
读数(审计独立实测):链接命令行文件 link.txt 里没有 gtest 库;nm -C libcopp_unit_test | grep -c "testing::" 结果为 0。
根因:上游 test/CMakeLists.txt 里确实有 find_package(GTest),但它是非 REQUIRED 的可选门;而代码真正检查的宏是 UTILS_TEST_MACRO_TEST_ENABLE_GTEST(test/frame/test_macros.h:26),CMake 里定义的却是 COPP_MACRO_TEST_ENABLE_GTEST——两个名字不同,全仓没有任何地方给前者赋值。所以 gtest 分支在上游就是死代码,真正在跑的是上游自带的 test_manager。
修法:从配方里删除 googletest 依赖,并同步改正四处口径(配方注释、notes.md、manifest.yaml 的依赖表、test_package/MANIFEST.yml),避免"文档说依赖 gtest、源代码说不依赖"这种自相矛盾。
这条最有普适价值:依赖最小化必须靠证据(宏名、link.txt、符号计数),不能靠"源码里出现了 find_package(GTest) 就以为要 gtest",也不能靠"删了试试看还能不能编"。
4.8 测试口径要分三层,别混填进同一行
同一个包,会有三个不同的数字:
| 口径层级 | 本包读数 | 含义 |
|---|---|---|
| 静态声明面 | 22 个测试源文件 / CASE_EXPECT_* 断言 1618 条 | 全 test/ 树的源与断言合计(其中 test/case 16 个源 / 1594 条,test/frame 24 条) |
| 构建系统注册面 | ctest 注册 1 个聚合目标(libcopp.unit_test) | 当前配置下 CMake 真正 add_test 的条数 |
| 真实执行面 | CI 配置 compiler.cppstd=17 下 64/64;同源码在 C++20 coroutine 路径启用的原生配置下 144/144 | 二进制内部 [ RUN ]/[ OK ] 的真实通过数 |
三层不是互相矛盾的"少报/多报",而是三个不同层级。把 64 与 144 并存的原因也一并写清楚:差异只在 C++20 coroutine 那部分子用例是否被 cppstd=17 的 profile 关掉,通过率两种配置都是 100%。
五、真机验证与结果
- 构建:
conan create通过(clang 15.0.4 / armv8 / cppstd 17 /os=OHOS 6.0/ libc++),全过程零源码补丁。 - 上游测试(真机执行):聚合目标
100% tests passed, 0 tests failed out of 1,Test #1: libcopp.unit_test ... Passed 8.44 sec;日志尾部为二进制内部的真实统计:
[==========] 64 test(s) ran. (40849.6 ms total)
[ PASSED ] 64 test case(s).
(上面两行为 LastTest.log 原文,仅去掉了终端颜色转义码。)
- 另一种配置的对照:把 C++20 coroutine 路径打开后,同一份源码实测
144 test(s) ran/[ PASSED ] 144 test case(s).,两种配置通过率均为 100%(耗时逐次波动,属量级参考)。 - 消费者自证:
test_package真跑一次完整上下文切换,输出五步并按返回码断言:
LIBCOPP_STEP created
LIBCOPP_STEP inside-runner
LIBCOPP_STEP started-and-yielded
LIBCOPP_STEP resumed
LIBCOPP_STEP finished
LIBCOPP_TEST PASSED
- 产物:
lib/libcopp.a+lib/libcotask.a+include/libcopp、include/libcotask;静态库首三成员是 arm64 汇编目标文件,x86_64成员为 0。 - 本地 CI:
exit_code=0、integrity=8461a511cee2fde2;L0/L1/L4/L5 全 pass,L2 skip(本包无bin/);另有一道"破坏性反向测试":把lib/与include/移走后消费者测试确实失败,证明测试真的依赖本包产物、不是空跑。 - 平台侧:PR #15925(标题
【高难度挑战】feat(libcopp): 2.2.0 适配 HarmonyOS PC),CI build #92462(rev0fc0a439)成功,pre-submit 复跑通过,正文校验 0 问题(5466/8000 字符),已合并(2026/10/15)。 - 一条排障留痕:本地首跑 CI 时 JC1/JC5/JC6 报红,逐条查下去全是陈旧基线与他人存量假阳性(报出的 3 个
.staging-verified属于另外三个包,JC6 一次报了 939 个包),改用本包自己的 diff(24638B、恰好 8 个文件)对齐口径后全绿。门禁报大量无关项时,先怀疑基线,而不是先改自己的东西。

图:B 机原始完整桌面截图显示 ctest 聚合目标 1/1 通过及消费者六行运行轨迹。上文的 64 条内部用例来自独立的 LastTest.log,不以这张图代证。
六、方法论小结
- 架构探测类问题要"看探测输出",不能只看编译报错。本次首错落在汇编文件的
invalid operand上,真正的病灶却在 CMake 探测阶段——先去把CMAKE_HOST_SYSTEM_PROCESSOR/ 探测中间变量打出来,比在汇编上反复试快得多。 - 归档 ≠ 源码树:依赖 git 子模块的项目,tag 归档里往往是空目录,必须按 gitlink 钉住的 commit 单独取源,并同样校验 sha256。
- 零源码补丁也可以是一次真适配。能用上游预留的显式变量(
-DLIBCOPP_FCONTEXT_OS_PLATFORM=arm64)解决问题,就不要去改上游的探测逻辑;改一行看起来更"彻底",但会把可维护性交出去。 - 打包面有四个点只在消费者侧暴露:config 权威性、库清单与顺序、头文件、测试。库自己编过跑过,不等于消费者能编过。
- 假绿要主动去造:
--no-tests=error+ 真实日志留档 + 产物核验,三件套缺一不可;并且要写清每道防线挡不住什么。 - 依赖最小化靠证据,不靠直觉:宏名对不上、
link.txt没有、符号计数为 0,这三条一起才能判"这个依赖是死门"。 - 跨层级口径分列:声明面 / 注册面 / 执行面各自写清数字与含义,否则"我到底有没有少报"根本说不清。
七、参考
- libcopp 上游仓库与文档:https://github.com/owent/libcopp (本次用 tag
v2.2.0) - 开源鸿蒙 PC 社区:https://harmonypc.csdn.net/
- 项目托管与协作(AtomGit):https://atomgit.com/OpenHarmonyPCDeveloper/build_in_harmonyos
- 本次适配 PR:https://atomgit.com/OpenHarmonyPCDeveloper/build_in_harmonyos/merge_requests/15925(已合并)
本项目的适配配方与验证读数随协作仓交付,构建与测试命令均可复现。
更多推荐

所有评论(0)