【开源软件移植】把 CopyQ 搬上鸿蒙 PC:主窗口启动的适配实录—实战适配笔记记录

欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/

注意(本部分必看):
开始本文工作前需要完成:
从0创建项目指南,新手先看这篇,都写到这篇文章内了,重复的步骤不过多重复写,后续工作都在用HAP 壳工程:
https://blog.csdn.net/weixin_52908342/article/details/161343743
准备 OpenHarmony SDK
准备 Qt for HarmonyOS
复现最小 Qt Widgets Demo
准备 DevEco HAP 壳工程

完成上面步骤,即可跟着本文进行手把手教学适配CopyQ!
在这里插入图片描述
项目开源仓库:https://atomgit.com/weixin_52908342/OH-CopyQ

项目信息

内容
项目名称 CopyQ
上游版本 9.1.0
上游仓库 https://github.com/hluk/CopyQ
开源协议 GPL-3.0-or-later(适配产物同协议开源)
主要语言 C++17
GUI 框架 Qt 5 Widgets(无 QML / 无 OpenGL)
源码规模 ~10 万行,98 个 .cpp 翻译单元
项目类型 跨平台剪贴板管理器(client/server 单进程双角色)
内容
操作系统 HarmonyOS Next PC
设备形态 PC(2in1)
CPU 架构 aarch64 (ARM64)
ABI aarch64-linux-ohos
运行时 qt-ohos(Qt 5.12.12 鸿蒙移植版)

编译环境

内容
构建主机 OpenCloudOS Server 9(x86_64,腾讯云 4C8G)
OHOS NDK /root/ohos-sdk/ohos-sdk/linux/native/(自带 Clang 15.0.4)
Qt-OHOS /opt/qt-ohos/qt-5.12.12-ohos/qt-5.12.12-ohos/
构建工具 CMake 3.x + Ninja/Make
HAP 打包工具 DevEco Studio(Mac 端)
真机 HarmonyOS Next PC(hdc 调试)

本文记录的是通用 Qt 桌面应用 → 鸿蒙 PC的适配实战。如果你是:

  • 🎯 想把自家/开源 Qt 5 应用移植到鸿蒙 PC 的开发者
  • 🎯 想了解 HAP dlopen 模型与传统 Qt 可执行文件区别的研究者
  • 🎯 在排查鸿蒙 PC 上 dlopen failedNapi::Error: too frequentlymain returned 1 等崩溃问题的运维 / 测试工程师

一、为什么选 CopyQ?

我们清单里 100 多个 Qt 开源软件中,CopyQ 属于"中等偏难"档次:

  • 优势:Qt5 Widgets 纯 GUI,没有 OpenGL/QML 重度依赖,源码 ~10 万行规模可控
  • 难点:典型 client/server 架构(一个进程同时是 server 和 client),启动逻辑高度依赖命令行参数;用了 QSystemTrayIcon(系统托盘);用了 QSettings 读 ini

后来证明,这三个"难点"分别在启动流程的三个不同阶段把我们绊倒了。


二、技术总路线

鸿蒙 PC 没有原生 Qt runtime,业界目前的主流方案是 qt-ohos 平台插件

在这里插入图片描述

也就是说,Qt 应用必须先被改造成动态库.so),让 HAP 用 dlopen + dlsym("main") 反向调起来。这跟传统的"独立可执行文件"模型完全相反。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述


三、Phase 1:把 CopyQ 改造成 .so

3.1 交叉编译环境

服务器是 OpenCloudOS Server 9(x86_64),工具链路径如下:

项目 路径
OHOS NDK Clang++ /root/ohos-sdk/ohos-sdk/linux/native/llvm/bin/aarch64-unknown-linux-ohos-clang++
OHOS sysroot /root/ohos-sdk/ohos-sdk/linux/native/sysroot
Qt-OHOS 5.12.12 /opt/qt-ohos/qt-5.12.12-ohos/qt-5.12.12-ohos/
Target triple aarch64-linux-ohos

3.2 CMake toolchain

CopyQ 用 CMake,写一个标准的 ohos-toolchain.cmake,关键开关:

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER   ${OHOS_SDK}/llvm/bin/aarch64-unknown-linux-ohos-clang)
set(CMAKE_CXX_COMPILER ${OHOS_SDK}/llvm/bin/aarch64-unknown-linux-ohos-clang++)

# 关键:visibility 默认 default + export-dynamic
# 否则 HAP 壳 dlsym("main") 会找不到符号
add_compile_options(-fvisibility=default -fPIC)
add_link_options(-Wl,--export-dynamic)

3.3 把可执行目标改成共享库

CopyQ 原本生成 copyq 可执行文件。我们改 src/CMakeLists.txt

- add_executable(copyq main.cpp ...)
+ add_library(copyq SHARED main.cpp ...)

注意 main.cpp 里的 int main(int argc, char **argv) 一行不动——HAP 壳就是按这个签名 dlsym("main") 然后调用的。

3.4 CMake 配置阶段实测

在这里插入图片描述

几个关键信号:

  • Clang 15.0.4 —— OHOS NDK 自带的 LLVM
  • aarch64-unknown-linux-ohos-clang —— target triple 正确,是鸿蒙
  • OHOS build: using platform/dummy backend —— 让 CopyQ 用 dummy 平台后端(无 X11/Win32 依赖)
  • ⚠️ CMAKE_DISABLE_FIND_PACKAGE_X11 警告 —— 无视,这是我们主动塞的

3.5 编译阶段实测(98 个 .cpp 翻译单元)

在这里插入图片描述

100% 干净链接,零警告零错误产出 libcopyq.so


四、Phase 2:第一次崩溃 —— 符号缺失

打包成 HAP,推到鸿蒙 PC,点启动……闪退。抓 hilog:

05-29 23:11:35.559 W MUSL-LDSO: relocating failed: symbol not found.
                   dso=/data/storage/el1/bundle/libs/arm64/libcopyq.so
                   s=qt_resourceFeatureZlib
05-29 23:11:35.559 F dlopen() failed to open library 'libcopyq.so':
                   Error relocating libcopyq.so: qt_resourceFeatureZlib: symbol not found

根因

CopyQ 链了 Qt5::Core,CMake 自动把 Qt 的 zlib feature 探测代码也拉了进来,引用了 qt_resourceFeatureZlib。但鸿蒙的 Qt-OHOS 5.12.12 库里 没有这个符号——这是 Qt 5.13+ 才引入的内部 API。

修复

在 link 阶段加 -Wl,--unresolved-symbols=ignore-in-shared-libs,并清理 .qrc 资源里的压缩开关:

target_link_options(copyq PRIVATE
    -Wl,--unresolved-symbols=ignore-in-shared-libs)

重新链接后验证(真实服务器输出):

$ /root/ohos-sdk/ohos-sdk/linux/native/llvm/bin/llvm-nm -D --undefined-only \
    /root/CopyQGOGO/artifacts/libcopyq.so | grep -c qt_resourceFeatureZlib
0

✅ 符号引用清零。


五、Phase 3:第二次崩溃 —— main 跑了 1 秒就 return 1

第一坑修完,再启动。这次 dlopen 成功、main 也调到了,但不到 1 秒应用窗口就消失。日志:

05-29 23:20:07.046 D opened library '/data/storage/el1/bundle/libs/arm64/libcopyq.so' with main function
05-29 23:20:07.046 D 'main' function argv[0]='/data/storage/el1/bundle/libs/arm64/libcopyq.so'
05-29 23:20:07.046 D 'main' function argv[1]=''                              ← 注意!空字符串
05-29 23:20:08.065 D 'main' function in library returned 1                   ← 1 秒后退出
05-29 23:20:08.065 I Qt: asynchronously terminating remaining QAbility instances

根因

读 CopyQ 源码 src/main.cpp

int startApplication(int argc, char **argv) {
    const QStringList arguments = platformNativeInterface()->getCommandLineArguments(argc, argv);
    int skipArguments = 0;
    const QString sessionName = getSessionName(arguments, &skipArguments);
    // ...
    // 关键判断:没有任何参数 → 启动 server(GUI 模式)
    if (skipArguments == arguments.size())
        return startServer(argc, argv, sessionName);
    // 有参数 → 当作命令传给 client
    return startClient(argc, argv, arguments.mid(skipArguments), sessionName);
}

HAP 壳调 main 时 argv[1] = ""(空串),但 argc=2。CopyQ 把空串当成了一个有效 argument,于是 arguments.size()=1, skipArguments=0走进了 client 分支——尝试连接已运行的 server,连不上,return 1。

这是 OHOS HAP 壳的"特色"——它会给 main 塞一个空串占位,普通 Qt 应用对此免疫,但 CopyQ 这种带命令行解析的应用就栽了。

修复

main() 入口先过滤空串:

int main(int argc, char **argv) {
    // OHOS HAP shell may pass empty string args; filter them out
    int newArgc = 0;
    for (int i = 0; i < argc; ++i) {
        if (argv[i] && argv[i][0] != '\0') {
            argv[newArgc++] = argv[i];
        }
    }
    argc = newArgc;
    try {
        return startApplication(argc, argv);
    } ...
}

服务器上验证 patch 已落地(真实输出):

$ grep -A2 "OHOS HAP" /root/CopyQGOGO/CopyQ-9.1.0/src/main.cpp | head -10
    // OHOS HAP shell may pass empty string args; filter them out
    int newArgc = 0;
    for (int i = 0; i < argc; ++i) {

增量重编:

$ cd /root/CopyQGOGO/build && make -C src copyq -j4
[  2%] Building CXX object src/CMakeFiles/copyq.dir/main.cpp.o
[  3%] Linking CXX shared library libcopyq.so
[100%] Built target copyq

六、Phase 4:第三次崩溃 —— SystemTrayIcon API 限流

修完前两个坑,再启动。这次进度条又往前推了一大步,已经能看到 ArkUI 把窗口画出来了……然后又崩。这次有完整的 cppcrash 文件:

LastFatalMessage: terminating due to uncaught exception of type
                  Napi::Error: The API is being called too frequently.

Tid:55895, Name:mple.qtohosdemo
#11 pc 002c9e50  libcopyq.so  MainWindow::loadSettings(QSettings&, AppConfig*)+5020
#12 pc 0020d124  libcopyq.so  ClipboardServer::loadSettings(AppConfig*)+2648
#13 pc 0020ae34  libcopyq.so  ClipboardServer::ClipboardServer(QApplication*, QString const&)+2460
#14 pc 00324540  libcopyq.so  main+4212

完整调用链(向上展开):

main                              ← Phase 3 修过,已能进入
  └─ ClipboardServer 构造
      └─ loadSettings()
          └─ MainWindow::loadSettings()
              └─ QSystemTrayIcon::setIcon(QIcon&)   ← 设置托盘图标
                  └─ libqohos.so / libQt5Widgets.so
                      └─ runInJsThreadAndWait       ← 桥到 ArkTS JS 线程
                          └─ NAPI: statusBarManager.setIcon(...)
                              └─ throw Napi::Error("The API is being called too frequently")
                                  └─ 没人 catch
                                      └─ __cxa_rethrow → abort

根因

鸿蒙 PC 没有"系统托盘"概念,Qt-OHOS 平台插件用顶部状态栏 statusBarManager API 模拟 QSystemTrayIcon。该 API 有调用频率限制,CopyQ 启动时短时间内连发多次 setIcon()(主图标 + 小图标 + 监听图标)→ 触发限流 → 抛异常 → 没 catch → abort。

修复

src/gui/mainwindow.cpp:2802,在 loadSettings() 中:

- setTrayEnabled( !appConfig->option<Config::disable_tray>() );
+ setTrayEnabled(false); // OHOS: disable tray to avoid statusBarManager rate limit

理由:

  1. CopyQ 主窗口(剪贴板历史 + 工具栏)不依赖托盘,照样能用
  2. 鸿蒙 PC 已有自己的 dock 栏管理图标,托盘是 "Linux/Windows 桌面"概念,不适用
  3. 最小侵入:1 行改动

服务器上验证(真实输出):

$ grep "OHOS:" /root/CopyQGOGO/CopyQ-9.1.0/src/gui/mainwindow.cpp
    setTrayEnabled(false); // OHOS: disable tray to avoid statusBarManager rate limit

七、最终成果验证

在这里插入图片描述

上图浓缩了下面 7.1~7.3 全部验证项,可作整体一览。下面分项展开:

7.1 服务器产物(真实命令输出)

$ ls -la /root/CopyQGOGO/artifacts/
-rwxr-xr-x 1 root root 4883776 May 29 23:28 libcopyq.so

$ /root/ohos-sdk/ohos-sdk/linux/native/llvm/bin/llvm-readelf -h \
    /root/CopyQGOGO/artifacts/libcopyq.so | head -10
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           AArch64
  Version:                           0x1

7.2 依赖共享库(真实输出)

$ /root/ohos-sdk/ohos-sdk/linux/native/llvm/bin/llvm-readelf -d \
    /root/CopyQGOGO/artifacts/libcopyq.so | grep NEEDED
  0x0000000000000001 (NEEDED)       Shared library: [libQt5Svg.so]
  0x0000000000000001 (NEEDED)       Shared library: [libQt5Xml.so]
  0x0000000000000001 (NEEDED)       Shared library: [libQt5Qml.so]
  0x0000000000000001 (NEEDED)       Shared library: [libQt5Widgets.so]
  0x0000000000000001 (NEEDED)       Shared library: [libQt5Gui.so]
  0x0000000000000001 (NEEDED)       Shared library: [libQt5Network.so]
  0x0000000000000001 (NEEDED)       Shared library: [libQt5Core.so]
  0x0000000000000001 (NEEDED)       Shared library: [libc++_shared.so]
  0x0000000000000001 (NEEDED)       Shared library: [libc.so]

干净!9 个动态库依赖,全部为 Qt-OHOS 标准件 + libc。无任何 desktop 残留(如 libGL、libX11、libdbus)。

7.3 main 符号成功导出(真实输出)

$ /root/ohos-sdk/ohos-sdk/linux/native/llvm/bin/llvm-nm -D --defined-only \
    /root/CopyQGOGO/artifacts/libcopyq.so | grep -E " T main$"
0000000000323484 T main

$ /root/ohos-sdk/ohos-sdk/linux/native/llvm/bin/llvm-nm -D --undefined-only \
    /root/CopyQGOGO/artifacts/libcopyq.so | grep -c qt_resourceFeatureZlib
0

main 符号导出(HAP 壳能 dlsym 到)
qt_resourceFeatureZlib 未引用计数 = 0(Phase 2 patch 仍生效)

📸 Phase 3 增量重编日志(patch 后只重编 main.cpp + 重链接,6 秒搞定)

在这里插入图片描述

7.4 HAP 工程产物(真实 Mac 端输出)

在这里插入图片描述

$ ls -la /Users/zhubo/Downloads/QtOnHarmonyOSPC/CopyQ-OHOS/CopyQ-HAP/entry/libs/arm64-v8a/
-rw-r--r--  35506944  libQt5Core.so
-rw-r--r--  39324352  libQt5Gui.so
-rw-r--r--  12139200  libQt5Network.so
-rw-r--r--   5339904  libQt5OhosExtras.so
-rw-r--r--  42080712  libQt5Qml.so
-rw-r--r--   2561816  libQt5Svg.so
-rw-r--r--  37788352  libQt5Widgets.so
-rw-r--r--   1202968  libQt5Xml.so
-rw-r--r--   1267392  libc++_shared.so
-rwxr-xr-x   4883776  libcopyq.so      ← 我们的产物
-rw-r--r-- 156203776  libqohos.so      ← qt-ohos 平台插件
drwxr-xr-x            platforms/
drwxr-xr-x            styles/

$ file libcopyq.so
libcopyq.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV),
             dynamically linked, with debug_info, not stripped

八、踩坑沉淀:Qt → 鸿蒙 PC 适配方法论

经过 CopyQ + 之前 9 个项目,我把"通用三连击坑"总结成:

阶段 现象 根因 修复
dlopen 阶段 symbol not found Qt 版本符号差异 --unresolved-symbols=ignore-in-shared-libs
main 入口阶段 main returned 1/直接退出 argv[1]=“” 空串占位 入口处过滤空字符串 argv
GUI 初始化阶段 Napi::Error: too frequently 调用 OHOS 受限 API 过快 禁用 QSystemTrayIcon / 加节流

只要把这三关都过了,剩下的问题基本都是业务逻辑层面的、可控的细节。

不通用的坑:CopyQ 特色

  • client/server 单 main 入口:必须先过 argv 过滤,否则进 client 分支必崩
  • 托盘启动顺序MainWindow::loadSettings 里就调 setTrayEnabled,比创建主窗口还早
  • ⚠️ session 名单:CopyQ 用 QSettings 持久化 session,鸿蒙沙箱目录权限要确认(本次没踩到)

九、最终启动效果

在这里插入图片描述

修完三个 patch、重编 4.66MB 的 libcopyq.so、推到鸿蒙 PC:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

十、常见问题(FAQ)

Q1:argv[1]="" 这个空串到底是谁塞进去的?能在 HAP 层修复吗?

A:是 Qt-OHOS 平台插件的实现细节:它会把 QAbility 拉起时拿到的 Want 参数转成 argv 传给 main,即使上层没传任何参数,为了保证 argc>=1 且与 POSIX 习惯一致,插件会主动填一个 argv[0]=".so 路径" + argv[1]=""。HAP 层动不了(除非你同时修改 qt-ohos 插件源码并重编 libqohos.so,代价太大)。最佳实践是在应用的 main() 入口头加三行空串过滤,这不仅适用于 CopyQ,凡是用了 QCommandLineParser / getopt 的 Qt 应用都建议默认上这个 patch。

Q2:禁掉 QSystemTrayIcon 之后,用户怎么快速唤出 CopyQ 历史面板?

A:原生 Linux/Windows 上是点托盘图标或全局快捷键(Ctrl+Shift+V)。鸿蒙 PC 上:

  • 代替方案 1:dock 栏。HAP 安装后默认在鸿蒙 PC 的应用抽屉 / dock 里可点,不需额外入口。
  • 代替方案 2:HAP 侧注册全局快捷键。鸿蒙提供 inputMonitor / globalShortcut API(需声明权限),可以在 ArkTS 侧抓 Ctrl+Shift+V 后通过 NAPI 跳回 C++ 层调 MainWindow::showAt(),这样不需要托盘 API 也能实现原本习惯。本次适配为了快速交付,第一期只做了方案 1;方案 2 量大金在处,会放到 v1.1 迫加。
Logo

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

更多推荐