欢迎加入开源鸿蒙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 的资质:

  1. 测试面自包含:上游 test/ 用自带测试框架 test_manager,22 个测试源文件、CASE_EXPECT_* 断言合计 1618 条,纯 CPU,不依赖设备节点、网络服务或 GPU。
  2. 运行期依赖极小:只有 Threads(pthread)这一层 libc 级依赖,没有一串需要逐个先适配的上游库。
  3. 通过率能真跑出来: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

这张表里,第一、二行是会直接把产物带偏的差异;第四、五行是"环境噪声",必须和真问题分开记,否则排查会被带跑。

三、适配流程总览

  1. W0 依赖就绪性先证:运行期只有 Threads(pthread);测试侧经审计确认不需要任何外部测试框架(依据见 4.7)。因此本版的依赖清单最终是空的——不是"先假设要 gtest,编过了再删"。
  2. 源码两个源:主源是上游 tag 归档(codeload 实测 GET 200、269771B、sha256 4f7831a8…),副源是 tag 归档里缺失的 git 子模块快照(见 4.2)。
  3. 配置期注入平台变量:LIBCOPP_FCONTEXT_OS_PLATFORM=arm64(armv8 ⇒ arm64),并同时打开 PROJECT_ENABLE_UNITTEST=ON / BUILD_TESTING=ON、关闭示例 PROJECT_ENABLE_SAMPLE=OFF(交付面最小化)。
  4. 构建期真跑上游测试:ctest --output-on-failure --no-tests=error --timeout 600,日志全量留档。
  5. 打包面收口:删掉上游自带的 CMake config(见 4.3),只让 Conan 生成的 config 生效。
  6. 消费者视角自证: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 的退码,这就是一次彻底的假绿。

修法:三层一起上。

  1. --no-tests=error:注册数为 0 直接判红,堵住"空跑";
  2. 日志留档核验:要求日志里出现二进制内部的真实执行尾部,而不是只有聚合行;
  3. 产物核验: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(rev 0fc0a439)成功,pre-submit 复跑通过,正文校验 0 问题(5466/8000 字符),已合并(2026/10/15)。
  • 一条排障留痕:本地首跑 CI 时 JC1/JC5/JC6 报红,逐条查下去全是陈旧基线与他人存量假阳性(报出的 3 个 .staging-verified 属于另外三个包,JC6 一次报了 939 个包),改用本包自己的 diff(24638B、恰好 8 个文件)对齐口径后全绿。门禁报大量无关项时,先怀疑基线,而不是先改自己的东西。

B 机鸿蒙 PC 完整桌面与 libcopp 测试结果

图:B 机原始完整桌面截图显示 ctest 聚合目标 1/1 通过及消费者六行运行轨迹。上文的 64 条内部用例来自独立的 LastTest.log,不以这张图代证。

六、方法论小结

  1. 架构探测类问题要"看探测输出",不能只看编译报错。本次首错落在汇编文件的 invalid operand 上,真正的病灶却在 CMake 探测阶段——先去把 CMAKE_HOST_SYSTEM_PROCESSOR / 探测中间变量打出来,比在汇编上反复试快得多。
  2. 归档 ≠ 源码树:依赖 git 子模块的项目,tag 归档里往往是空目录,必须按 gitlink 钉住的 commit 单独取源,并同样校验 sha256。
  3. 零源码补丁也可以是一次真适配。能用上游预留的显式变量(-DLIBCOPP_FCONTEXT_OS_PLATFORM=arm64)解决问题,就不要去改上游的探测逻辑;改一行看起来更"彻底",但会把可维护性交出去。
  4. 打包面有四个点只在消费者侧暴露:config 权威性、库清单与顺序、头文件、测试。库自己编过跑过,不等于消费者能编过。
  5. 假绿要主动去造:--no-tests=error + 真实日志留档 + 产物核验,三件套缺一不可;并且要写清每道防线挡不住什么。
  6. 依赖最小化靠证据,不靠直觉:宏名对不上、link.txt 没有、符号计数为 0,这三条一起才能判"这个依赖是死门"。
  7. 跨层级口径分列:声明面 / 注册面 / 执行面各自写清数字与含义,否则"我到底有没有少报"根本说不清。

七、参考

  • 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(已合并)

本项目的适配配方与验证读数随协作仓交付,构建与测试命令均可复现。

Logo

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

更多推荐