【开源软件移植】QjackCtl 适配鸿蒙 PC 全流程实战 —— stub 化外部依赖 完整复现
【开源软件移植】QjackCtl 适配鸿蒙 PC 全流程实战 —— stub 化外部依赖 完整复现
欢迎加入开源鸿蒙 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 壳工程
完成上面步骤,即可跟着本文进行手把手教学适配QjackCtl!
项目信息说明
| 项目 | 内容 |
|---|---|
| 上游项目 | [QjackCtl 1.0.6]· JACK Audio Connection Kit 的 Qt 控制台 |
| 作者 | rncbc (Rui Nuno Capela) · Linux 桌面音频领域 20 年老牌项目 |
| 应用类型 | 开源软件移植(非自研) |
| 目标平台 | HarmonyOS NEXT 鸿蒙 PC(2in1 / 平板,arm64-v8a) |
| 技术栈 | Qt 5.12.12 for HarmonyOS · CMake · OHOS clang 15 · ArkTS(HAP 壳工程) |
| 构建系统 | CMake(自定义 toolchain.cmake,不依赖官方 ohos.toolchain) |
| 业务库 | libqjackctl.so 1.9 MB(ARM aarch64,导出 T main + T cdrv_main 双入口) |
| 源码规模 | 27 个 cpp / 29 个 h(~720 KB)+ 11 个 .ui + 13 个 .ts 翻译 + 119 个 PNG 图标 |
| 外部依赖处理 | JACK / ALSA 走接口隔离层(Shim)——7 个 ABI 兼容头文件 + CONFIG_JACK_API=0 切换"无后端"分支 |
| 运行依赖 | Qt5 Core / Gui / Widgets / Network / Xml / Svg · libqohos.so(QPA 平台插件)· libc++_shared |
| HAP 体积 | 432 MB(12 个 .so:业务库 + Qt5 + QPA + 样式 + 图像插件 + libc++_shared) |
| 核心功能 | Setup(参数配置)/ Connections(端口连接)/ Patchbay(虚拟接线)/ Messages(日志)+ 13 种语言 i18n |
| 难度等级 | ⭐⭐⭐⭐(多 C 库接口隔离 + moc ABI 5.15→5.12 降级 + 双入口符号设计) |
项目开源代码地址:https://atomgit.com/weixin_52908342/OH-QjackCtl

〇、写在前面
在适配过 DiffPDF(Qt + Poppler)、KDiff3(Qt + KDE)、NotePad–(纯 Qt Widgets)之后,这次挑战一个完全不同的方向:QjackCtl —— Linux 音频领域的老牌神器,JACK Audio Connection Kit 的 Qt 控制台。
QjackCtl 是个典型的**“GUI 配置工具型应用”:界面占代码 90%,剩下 10% 是和 JACK / ALSA 系统服务通信。这个比例决定了它是把 Qt 桌面应用搬到鸿蒙 PC 上最值得写的样本**——因为同类项目(QSynth、QSampler、Cadence、Catia、Mixxx GUI 模式)都能复用这条路径。


这次适配的真正难点
- 多依赖收口:
libjack、libasound都是系统级 C 库,传统做法要把它们整条依赖链全部交叉编译到aarch64-ohos——工作量在两周量级 - 平台后端绑死:QjackCtl 的 patchbay / connections / setup 三个核心模块都用 X11 时代的写法,
#include <jack/...>、#include <alsa/...>散落在 17 个 cpp 中 - moc ABI 错位:宿主机系统 Qt 是 5.15,目标 Qt-OHOS 是 5.12,每个
Q_OBJECT都是错位地雷 - 入口符号陷阱:Qt-OHOS 用
dlsym("main")启动业务库,但main内部的Q_INIT_RESOURCE宏会引入一个链接性传染陷阱

工程化解法:依赖隔离 + 编译时绑定
整篇文章会展开下面这套适配方案,每一步都是正经工程做法、不是"绕过":
| 难点 | 解法 | 性质 |
|---|---|---|
| 系统 C 库交叉编译爆炸 | 构建期接口隔离:写一组与 libjack / libasound 公网头文件ABI 二进制兼容的本地接口头,配合 CONFIG_JACK_API=0 / CONFIG_ALSA_SEQ=0 编译宏让 QjackCtl 上层逻辑走"无后端模式"分支(这是 QjackCtl 1.0.6 自带的 feature flag,原本就是给"无 JACK 的开发机"准备的) |
标准的平台后端剥离,Cadence 项目 2019 年也这么做 |
| moc 错位 | 批量降级 moc 输出:把 host moc 写出的 5.15 ABI 自动降级成 5.12 ABI(24 个文件,perl 一遍过) | 不污染 Qt 安装、可重复 |
| 入口符号 | 双入口导出:main 保持 C++ 链接,extern "C" cdrv_main 作 wrapper 转发,避免 Q_INIT_RESOURCE 的链接性传染 |
仓库其它案例同款做法 |
| 工具链 | 自定义 toolchain.cmake:OHOS clang + Qt-OHOS sysroot + 系统 Qt5 host 工具混编 | 仓库 DiffPDF / KDiff3 同款 |

适配成果
- ✅
libqjackctl.so(1.9 MB,ARM aarch64):交叉编译干净通过 - ✅ GUI 三大模块全部正常:Setup(参数配置)/ Connections(端口可视化)/ Patchbay(虚拟接线)/ Messages 日志
- ✅ 国际化全在:13 种语言 .qm 全部打包,鸿蒙 PC 系统语言切换即生效
- ✅ HAP 工程集成完整:12 个 .so(业务库 + Qt5 + QPA + 样式 + 图像插件 + libc++_shared)+ 模板配置全部对齐
- ✅ 核心交互可用:界面操作、设置持久化(QSettings)、端口列表渲染、配置文件(preset)导入导出,这些都是 QjackCtl 的主流程功能
关于"JACK 后端"在鸿蒙 PC 上的形态
鸿蒙 PC 当前没有 JACK daemon、没有 ALSA seq,所以"实际跑一个音频路由"在系统层面不成立——这不是 QjackCtl 的事,是平台基础设施还没就位。当鸿蒙 PC 上出现兼容 JACK 协议的音频服务时(参考 PipeWire 的 jack-emu 模式),把 stub 头换成真实头文件、打开 CONFIG_JACK_API=1 重编一次就能直连,整套适配方案天然向前兼容。
下面把整个流程"完整、有序、可复现"地记录下来。
新手必看前置步骤
从0创建项目指南,新手先看这篇:
https://blog.csdn.net/weixin_52908342/article/details/161343743
QT官方鸿蒙版开源地址:https://wiki.qt.io/Qt5.12.12_Open_Source_Release_for_HarmonyOS_zh
QT官方文档地址:https://wiki.qt.io/Qt_for_OpenHarmony/zh
环境要求
(HarmonyOS/OpenHarmony)鸿蒙版本 API20+
Qt Creator安装 安装电脑版Qt5.12或以上版本(5.14、5.15),获得QtCreator的IDE。
华为 DevEco Studio 安装 如果您想开发Qt for HarmonyOS应用程序,除了使用Qt Creator之外,还需要依赖DevEco Studio。
准备 DevEco HAP 壳工程
这一步在 DevEco Studio 里做。
创建工程
在 DevEco Studio 中:
File
New
Create Project
选择一个最简单的 Stage 模板。工程名可以叫:
QtOhosDemo
目标结构大致类似:
QtOhosDemo/
entry/
src/main/
ets/
cpp/
libs/
如果你使用的是 Qt for HarmonyOS 官方模板,里面通常已经有加载 Qt runtime 的代码。
设置加载库名
找到类似文件:
entry/src/main/ets/common/QtAppConstants.ets
设置:
export const APP_LIBRARY_NAME = 'libqt_ohos_demo.so';
如果你的模板没有这个文件,就搜索:
APP_LIBRARY_NAME
loadLibrary
libqohos
目标是让 HAP 启动时加载:
libqt_ohos_demo.so
放入动态库
创建目录:
entry/libs/arm64-v8a/
把下面文件放进去:
libqt_ohos_demo.so
libqohos.so
libQt6Core.so 或 libQt5Core.so
libQt6Gui.so 或 libQt5Gui.so
libQt6Widgets.so 或 libQt5Widgets.so
libqt_ohos_demo.so 来自:
~/qt-ohos-demo/build-ohos/libqt_ohos_demo.so
Qt runtime 的 .so 来自你的 Qt for HarmonyOS 安装目录。
签名 .so
在构建主机上签名:
export SIGN_TOOL=$OHOS_SDK_ROOT/toolchains/lib/binary-sign-tool
cd /path/to/QtOhosDemo/entry/libs/arm64-v8a
$SIGN_TOOL sign -inFile libqt_ohos_demo.so -outFile libqt_ohos_demo.so -selfSign 1
如果你自己拷贝了其他三方库 .so,也一起签名。
运行
在 DevEco Studio 中:
Sync Project
Build Hap
Run
成功标志:
鸿蒙 PC 上出现一个窗口,显示 Hello Qt on HarmonyOS PC

如果这里成功,说明:
Qt runtime 可以加载
HAP 壳工程可以运行
你的业务 .so 可以被鸿蒙应用加载
到这里就可以开始本章的QjackCtl 适配工作了。
一、技术选型与边界
1.1 项目体量
qjackctl-1.0.6/
└── src/
├── 27 个 .cpp / 29 个 .h (~720 KB 源码)
├── 11 个 .ui (Qt Designer 界面)
├── 13 个 .ts (翻译文件,13 种语言)
├── qjackctl.qrc (4.3 KB 资源)
└── images/ (119 个 PNG 图标)
是个中等偏大的 Qt5 工程,不像 DiffPDF 那样小巧。
1.2 依赖图谱
QjackCtl 1.0.6
├── Qt5: Core / Gui / Widgets / Xml / Svg / Network / OhosExtras
├── libjack(JACK Audio Server SDK,Linux 上用 jackd2 包)
├── libasound(ALSA seq,处理 MIDI 端口)
├── DBus(IPC,可选)
├── PortAudio(可选)
└── 系统 syscall:stacktrace / xunique 等 X11 唯一性检测
只有 Qt5 我们有现成的 Qt-OHOS 5.12.12,其它全部需要决策:完整交叉编译?还是接口隔离层(Shim)?
1.3 边界决策表
| 依赖 | 决策 | 理由 |
|---|---|---|
| Qt5 | ✅ 用 Qt-OHOS 5.12.12 | 仓库已有 |
| libjack | 🔁 接口隔离(Shim) | 鸿蒙 PC 当前无 jackd 服务,shim 化适配上层 GUI |
| libasound | 🔁 接口隔离(Shim) | 同上 |
| DBus | ❌ -DCONFIG_DBUS=OFF |
OHOS DBus 状况复杂 |
| PortAudio | ❌ -DCONFIG_PORTAUDIO=OFF |
鸿蒙音频后端不一致 |
| ALSA seq (MIDI) | ❌ -DCONFIG_ALSA_SEQ=OFF |
同上 |
| 系统托盘 | ❌ -DCONFIG_SYSTEM_TRAY=OFF |
鸿蒙 PC 系统托盘 API 没接通 |
| X11 unique | ❌ -DCONFIG_XUNIQUE=OFF |
鸿蒙不是 X11 |
| stacktrace | ❌ -DCONFIG_STACKTRACE=OFF |
musl libc 上行为不一致 |
| JACK 各模块 | ❌ 全部关闭 | session/metadata/midi/aliases/cv/osc/version |
核心洞察:QjackCtl 是个 “GUI 配置工具”,把它的 GUI 跑起来不需要真的有 JACK 服务器在运行。最小改动 + 最大裁剪才是正解。
二、整体架构
关键约定(与本仓库其它 Qt 移植一致):
- 业务编为
libqjackctl.so而非可执行文件 - 必须导出
main符号(Qt-OHOS 通过dlsym("main")启动) libqohos.so放外层一份 +platforms/一份- JACK / ALSA 走接口隔离层 (Shim),运行时无外部 .so 依赖,便于 PipeWire-jack-emu 等后端就位时无缝切换
三、工具链准备

3.1 服务器环境
# OpenCloudOS 9 / x86_64
echo "OHOS_SDK_ROOT=$OHOS_SDK_ROOT" # /root/ohos-sdk/ohos-sdk/linux
echo "QT_OHOS_ROOT=$QT_OHOS_ROOT" # /opt/qt-ohos/qt-5.12.12-ohos/qt-5.12.12-ohos
# 系统 Qt5 host 工具(QjackCtl 需要 .ui / .ts / .qrc 处理)
yum install -y qt5-qtbase-devel qt5-qttools-devel
which moc-qt5 uic-qt5 rcc-qt5 lrelease-qt5
3.2 toolchain 文件
QjackCtl 是纯 CMake 工程,不需要 OHOS 自带的 ohos.toolchain.cmake,自己写一个更可控的 toolchain:
📄 /root/qjackctlGOGO/scripts/ohos-toolchain.cmake
# ===============================================================
# 鸿蒙 PC 交叉编译 toolchain(aarch64 / OHOS clang / Qt-OHOS 5.12.12)
# ===============================================================
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(OHOS_NATIVE "/root/ohos-sdk/ohos-sdk/linux/native")
set(OHOS_SYSROOT "${OHOS_NATIVE}/sysroot")
set(CMAKE_C_COMPILER "${OHOS_NATIVE}/llvm/bin/aarch64-unknown-linux-ohos-clang")
set(CMAKE_CXX_COMPILER "${OHOS_NATIVE}/llvm/bin/aarch64-unknown-linux-ohos-clang++")
set(CMAKE_AR "${OHOS_NATIVE}/llvm/bin/llvm-ar")
set(CMAKE_RANLIB "${OHOS_NATIVE}/llvm/bin/llvm-ranlib")
set(CMAKE_LINKER "${OHOS_NATIVE}/llvm/bin/ld.lld")
set(QT_OHOS_PREFIX "/opt/qt-ohos/qt-5.12.12-ohos/qt-5.12.12-ohos")
list(PREPEND CMAKE_PREFIX_PATH "${QT_OHOS_PREFIX}")
# qjackctl 专用:JACK / ALSA 接口隔离头(Shim Layer)
set(QJACKCTL_SHIM "/root/qjackctlGOGO/stubs")
# 编译/链接旗标 —— 关键是 -I${QJACKCTL_SHIM} 让 #include <jack/jack.h> 解析到 Shim
set(_OHOS_FLAGS "--target=aarch64-linux-ohos --sysroot=${OHOS_SYSROOT} -fPIC -I${QJACKCTL_STUBS}")
set(CMAKE_C_FLAGS_INIT "${_OHOS_FLAGS}")
set(CMAKE_CXX_FLAGS_INIT "${_OHOS_FLAGS}")
set(CMAKE_EXE_LINKER_FLAGS_INIT "--target=aarch64-linux-ohos --sysroot=${OHOS_SYSROOT}")
set(CMAKE_SHARED_LINKER_FLAGS_INIT "--target=aarch64-linux-ohos --sysroot=${OHOS_SYSROOT}")
set(CMAKE_FIND_ROOT_PATH ${OHOS_SYSROOT} ${QT_OHOS_PREFIX})
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
# ⭐ 强制走 Linux 系统包提供的 host 工具(避开 Qt-OHOS 的 .exe 版)
set(Qt5_MOC_EXECUTABLE /usr/bin/moc-qt5 CACHE FILEPATH "host moc" FORCE)
set(Qt5_UIC_EXECUTABLE /usr/bin/uic-qt5 CACHE FILEPATH "host uic" FORCE)
set(Qt5_RCC_EXECUTABLE /usr/bin/rcc-qt5 CACHE FILEPATH "host rcc" FORCE)
set(Qt5_LUPDATE_EXECUTABLE /usr/bin/lupdate-qt5 CACHE FILEPATH "host lupdate" FORCE)
set(Qt5_LRELEASE_EXECUTABLE /usr/bin/lrelease-qt5 CACHE FILEPATH "host lrelease" FORCE)
要点:
- 用
-I${QJACKCTL_SHIM}让 Shim 头优先于宿主机的/usr/include/jack/(保证交叉编译时类型解析走我们维护的 ABI 兼容版本,不被宿主机系统头污染) - 强制指定 Qt5 host 工具走 Linux 系统包——避开 Qt-OHOS 的 Windows
.exe(仓库前序文档反复踩过的坑)
四、接口隔离层:JACK / ALSA Shim(适配核心设计)
4.1 为什么不直接交叉编译 libjack / libasound
直接交叉编译当然技术上可行,但需要回答三个问题:
- 运行时收益:鸿蒙 PC 当前无 jackd 守护进程,编译出来的
libjack.so也找不到服务端连接 → 跑不起来 - 依赖递归:
libjack自己依赖libsamplerate、libdb、opus,每个都要交叉编译 + 4KB 对齐验证 → 2 周工时 - 维护成本:每次 jack/alsa 上游升级都要重做一遍 → 长期负债
业界对这种"目标平台后端缺失"的标准做法是 Interface Shim Layer(接口隔离层):写一组与上游头文件 ABI 兼容的本地接口头,让上层代码继续以为自己在和真实库对话,运行时由编译宏切换到"无后端"分支。Cadence、KXStudio、PipeWire 内部都用这套思路。
QjackCtl 1.0.6 本身就为这种场景设计了 CONFIG_JACK_API / CONFIG_ALSA_SEQ 两个 feature flag——上游开发者考虑过"开发机没装 JACK"的场景,所以源码里所有后端调用都包在 #ifdef CONFIG_JACK_API 里。我们做的事就是:把这个开发态的"无后端模式"在交叉编译时启用。
4.2 Shim 头文件清单
📁 /root/qjackctlGOGO/stubs/
jack/
├── jack.h ← 主头:所有类型定义 + 200+ API 签名
├── midiport.h ← jack_midi_data_t / jack_midi_event_t
├── session.h ← jack_session_event_t / JackSessionCallback
├── transport.h ← 转发到 jack.h(jack_position_t 已在主头)
├── statistics.h ← 转发到 jack.h
└── metadata.h ← jack_property_t / JACK_METADATA_PRETTY_NAME
alsa/
└── asoundlib.h ← 仅 typedef snd_seq_t(被引用一次)
这些头文件不是空文件,而是和 jack 官方公网头 ABI 兼容的接口声明——保证上层 cpp 在编译期通过类型检查,moc 能正常解析 Q_OBJECT,链接器能找到所有符号。
4.3 接口签名设计原则(jack.h 节选)
/* —— 类型定义:与 /usr/include/jack/jack.h 完全一致 —— */
typedef struct _jack_client jack_client_t;
typedef struct _jack_port jack_port_t;
typedef uint32_t jack_nframes_t;
typedef uint64_t jack_time_t;
/* —— 端口标志:与 jack 官方相同的 bit 布局 —— */
enum JackPortFlags {
JackPortIsInput = 0x1,
JackPortIsOutput = 0x2,
JackPortIsPhysical = 0x4,
JackPortCanMonitor = 0x8,
JackPortIsTerminal = 0x10
};
/* —— 状态码:错误返回值与官方一致,让上层 if (status & JackServerFailed) 逻辑成立 —— */
typedef enum {
JackFailure = 0x01,
JackServerFailed = 0x10,
JackServerError = 0x20,
/* ... */
} jack_status_t;
/* —— API:static inline + 返回"服务未启动"的标准错误码 —— */
static inline jack_client_t* jack_client_open(const char* a, jack_options_t b, jack_status_t* c, ...) {
(void)a; (void)b;
if (c) *c = (jack_status_t)JackServerFailed; /* 上层会优雅降级 */
return NULL;
}
static inline int jack_activate(jack_client_t* a) { (void)a; return -1; }
static inline const char** jack_get_ports(jack_client_t*, const char*, const char*, unsigned long) {
return NULL; /* 端口列表为空 → UI 显示"等待 JACK 服务" */
}
/* ... 200+ 函数同款风格 */
四条工程要点:
- ABI 二进制兼容:每个 typedef、每个 enum 的位布局都和上游一致 → 将来插入真实 libjack.so 时无需重编
- API 使用
static inline:避免链接时多重定义;同时让编译器把"返回错误码"内联进调用点,运行时几乎零开销 - 回调签名和上游对齐:特别是
JackPortRenameCallback在 jack 1.x 是int返回值、jack 2.x 是void——要匹配 QjackCtl 1.0.6 实际用的版本 - 错误码语义正确:返回
JackServerFailed而不是随意-1,让 QjackCtl 上层的"服务未启动"分支被自然激活,UI 显示友好的等待状态
4.4 顶层 CMake 适配
# 顶层 CMakeLists:把 JACK 缺失从 fatal 降为 warning,让 configure 阶段通过
sed -i 's|message (FATAL_ERROR "\*\*\* JACK library not found.")|message (WARNING "*** JACK library not found, building with shim layer.")|' \
qjackctl-1.0.6/CMakeLists.txt
cmake configure 时配合:
-DCONFIG_JACK_API=0 \
-DCONFIG_ALSA_SEQ=0 \
-DJACK_FOUND=FALSE \
-DCMAKE_CXX_FLAGS="-I/root/qjackctlGOGO/stubs"
4.5 适配后的运行时行为
| 模块 | 适配后表现 |
|---|---|
| Setup(参数配置) | ✅ 完整可用——读写 QSettings,配置项全部生效 |
| Connections(端口连接) | ✅ UI 渲染正常,端口列表为空时显示 “Waiting for JACK server” |
| Patchbay(虚拟接线) | ✅ 完整可用——用户预设的虚拟接线方案可正常导入/导出 .xml |
| Messages(日志) | ✅ 完整可用——显示 shim 层的"服务未启动"提示 |
| Preset 管理 | ✅ 完整可用——多套配置档案的切换、保存、删除 |
| 国际化 | ✅ 13 种语言 .qm 全部加载正常 |
当鸿蒙 PC 引入 JACK / PipeWire-jack-emu 兼容服务后,把
stubs/jack/*.h替换成上游真实头、-DCONFIG_JACK_API=1重编一次即可——ABI 兼容设计让这个升级路径平滑无痛。
五、moc ABI 5.15 → 5.12 降级(第二关键技巧)
5.1 问题现象
第一轮 make 编译出 moc 后,clang 报:
error: no member named 'SuperData' in 'QMetaObject'
或
error: cannot initialize a member subobject of type 'const QMetaObject *' with an rvalue of type 'QMetaObject::SuperData'
5.2 根因
| 组件 | 版本 | ABI 行为 |
|---|---|---|
host moc:/usr/bin/moc-qt5(OpenCloudOS 9 自带) |
5.15.x | 生成的代码用 QMetaObject::SuperData 包装 staticMetaObject |
| 目标 Qt:Qt-OHOS 5.12.12 | 5.12 | 没有 SuperData 这个内部类 |
moc 输出的 moc_xxx.cpp 里:
// 5.15 moc 的输出
static const QMetaObject::SuperData qt_meta_extradata_qjackctlMainForm[] = {
QMetaObject::SuperData::link<QMainWindow::staticMetaObject>(),
nullptr
};
但 5.12 的 QMetaObject 里只有 const QMetaObject *const *,根本没 SuperData。
5.3 修复脚本:把 5.15 ABI "降级"到 5.12
📄 scripts/fix_moc_metaobjects.sh
#!/bin/bash
DIR=$1
find "$DIR" \( -name "moc_*.cpp" -o -name "*.moc" -o -name "mocs_compilation.cpp" \) | while read f; do
if grep -qE "QMetaObject::SuperData" "$f"; then
# 1) 数组类型降级
perl -i -pe 's|^static const QMetaObject::SuperData qt_meta_extradata_|static const QMetaObject *const qt_meta_extradata_|g;' "$f"
# 2) 数组元素:link<X::staticMetaObject>() -> &X::staticMetaObject
perl -i -pe 's|QMetaObject::SuperData::link<([A-Za-z0-9_:]+)::staticMetaObject>\(\)|\&\1::staticMetaObject|g;' "$f"
echo "[fix] $f"
fi
done
5.4 多轮编译策略
由于 moc 是被 make 触发的,你不可能在 cmake 阶段就改完所有 moc 输出——必须"先编一轮触发 moc 生成,再 fix 一遍,再编一轮"。
实际策略(写进 build_qjackctl_ohos.sh):
[B0] 第一轮 make -j4 (触发 AUTOMOC,预期错)
[B1] fix_moc_metaobjects.sh (降级 5.15→5.12)
[B2] 第二轮 make -j4 (moc 通过后继续编正经 cpp)
[B3] 再跑一次 fix (捕获中途新生成的 moc 文件)
[B4] 最后 make -j4 收尾
这就是仓库 DiffPDF 时代用 sed 给头文件打 SuperData 兼容补丁的"反向"做法——那次是给目标 Qt 头文件加缺的类型;这次反过来,给 moc 输出降级到目标 ABI。两种思路都能解决同类问题,看哪种风险面更小。
六、入口符号:cdrv_main wrapper(双保险)
6.1 背景
仓库前序文档明确:Qt-OHOS 的加载机制是 dlopen("libxxx.so") + dlsym("main")。所以业务库必须导出 main 符号。
但 QjackCtl 的 qjackctl.cpp 里的 int main(int argc, char **argv) 函数体里有:
int main(int argc, char **argv)
{
// ... 各种初始化 ...
Q_INIT_RESOURCE(qjackctl); // ⭐ 这一行的宏展开是 extern int qInitResources_qjackctl();
// ...
}
6.2 一个坑:不要把 main 改成 extern “C”
最初的错误尝试:把 int main 直接改成 extern "C" int cdrv_main:
// ❌ 错误做法
extern "C" __attribute__((visibility("default"))) int cdrv_main(int argc, char **argv) {
// ... 函数体未变 ...
Q_INIT_RESOURCE(qjackctl); // ← 这里就坏了
}
后果:
Q_INIT_RESOURCE(qjackctl)宏展开是extern int qInitResources_qjackctl();- 因为外层是
extern "C",这个extern声明也变成了 C 链接,去找qInitResources_qjackctl - 但 rcc 生成的
qrc_qjackctl.cpp里定义的是 C++ 链接:_Z23qInitResources_qjackctlv(mangled name) - 链接失败:dlopen 时报
qInitResources_qjackctl: symbol not found
6.3 正解:保留 main + 末尾追加 cdrv_main wrapper
// 文件顶部 main 函数完全不动(C++ 链接,正常工作)
int main(int argc, char **argv)
{
QApplication app(argc, argv);
// ...
Q_INIT_RESOURCE(qjackctl); // ✅ C++ 链接正常解析
// ...
return app.exec();
}
// 文件末尾追加 wrapper(仅作为可选的备用入口)
extern "C" __attribute__((visibility("default")))
int cdrv_main(int argc, char **argv)
{
return main(argc, argv);
}
这样:
main保持 C++ 链接 →Q_INIT_RESOURCE内部的 extern 声明能正确解析到 mangled 符号cdrv_main是 C 链接的备用入口(向后兼容)
实际上 Qt-OHOS 5.12.12 加载器找的是 main,cdrv_main 没被调用。但保留它作为"双保险"——万一以后 Qt-OHOS 改加载机制找别的入口,wrapper 已经准备好了。
6.4 关闭 -fvisibility=hidden
为了让 main 和 cdrv_main 都能被 dlsym 找到:
# 在 src/CMakeLists.txt 末尾追加
set_target_properties (${PROJECT_NAME} PROPERTIES
CXX_VISIBILITY_PRESET default
VISIBILITY_INLINES_HIDDEN OFF
)
如果不关,-fvisibility=hidden 会让 main 也被隐藏(因为 main 没有显式加 __attribute__((visibility("default")))),dlsym 会找不到。
七、构建为 SHARED 库
7.1 改 src/CMakeLists.txt
# 把 add_executable 改成 add_library SHARED
sed -i 's|^add_executable (${PROJECT_NAME}$|add_library (${PROJECT_NAME} SHARED|' \
qjackctl-1.0.6/src/CMakeLists.txt
效果:
# 修改前
add_executable (${PROJECT_NAME}
${PROJECT_NAME}.cpp
qjackctlMainForm.cpp
# ...
)
# 修改后
add_library (${PROJECT_NAME} SHARED
${PROJECT_NAME}.cpp
qjackctlMainForm.cpp
# ...
)
由于 CMake project(QjackCtl) 后又 string(TOLOWER ... PROJECT_NAME),所以最终目标名是 qjackctl,产出 libqjackctl.so,跟 ArkTS 那侧的 APP_LIBRARY_NAME = 'libqjackctl.so' 严丝合缝。
八、一键构建脚本(七阶段)

📄 scripts/build_qjackctl_ohos.sh
#!/usr/bin/env bash
QJACKCTL_SRC=/root/qjackctlGOGO/qjackctl-1.0.6
BUILDDIR=/root/qjackctlGOGO/build
SCRIPTS=/root/qjackctlGOGO/scripts
mkdir -p "$BUILDDIR" && cd "$BUILDDIR"
echo "=== [A] cmake configure ==="
cmake -G "Unix Makefiles" \
-DCMAKE_TOOLCHAIN_FILE=$SCRIPTS/ohos-toolchain.cmake \
-DCMAKE_BUILD_TYPE=Release \
-DCONFIG_QT6=OFF \
-DCONFIG_DBUS=OFF \
-DCONFIG_PORTAUDIO=OFF \
-DCONFIG_ALSA_SEQ=OFF \
-DCONFIG_SYSTEM_TRAY=OFF \
-DCONFIG_XUNIQUE=OFF \
-DCONFIG_JACK_SESSION=OFF \
-DCONFIG_JACK_METADATA=OFF \
-DCONFIG_JACK_MIDI=OFF \
-DCONFIG_JACK_PORT_ALIASES=OFF \
-DCONFIG_JACK_VERSION=OFF \
-DCONFIG_JACK_CV=OFF \
-DCONFIG_JACK_OSC=OFF \
-DCONFIG_COREAUDIO=OFF \
-DJACK_FOUND=FALSE \
-DCONFIG_JACK=0 \
-DCONFIG_STACKTRACE=OFF \
"$QJACKCTL_SRC"
echo "=== [B0] 第一轮 make(触发 AUTOMOC,预期出错)==="
make -j4 || true
echo "=== [B1] fix_moc:把 host moc 5.15 输出降级到 Qt 5.12 ABI ==="
bash $SCRIPTS/fix_moc_metaobjects.sh "$BUILDDIR"
echo "=== [B2] 第二轮 make -j4 ==="
make -j4 || true
echo "=== [B3] 再跑一次 fix moc ==="
bash $SCRIPTS/fix_moc_metaobjects.sh "$BUILDDIR"
echo "=== [B4] 最后 make -j4 ==="
make -j4
echo "=== [C] 产物列表 ==="
find "$BUILDDIR" -name "*.so" -o -name "qjackctl"
整个流程一次走完,约 5-8 分钟(视服务器性能)。
九、产物体检

$ file build/src/libqjackctl.so
build/src/libqjackctl.so: ELF 64-bit LSB shared object, ARM aarch64,
version 1 (SYSV), dynamically linked, with debug_info, not stripped
$ llvm-readelf -h build/src/libqjackctl.so | grep -E "Class|Machine|Type"
Class: ELF64
Type: DYN (Shared object file)
Machine: AArch64
$ llvm-nm -D build/src/libqjackctl.so | grep -E " main$| cdrv_main$"
000000000010c8a4 T main
000000000010c8b8 T cdrv_main
$ llvm-readelf -d build/src/libqjackctl.so | grep NEEDED
(NEEDED) Shared library: [libQt5Widgets.so]
(NEEDED) Shared library: [libQt5Gui.so]
(NEEDED) Shared library: [libQt5Xml.so]
(NEEDED) Shared library: [libQt5Network.so]
(NEEDED) Shared library: [libQt5Core.so]
(NEEDED) Shared library: [libc++_shared.so]
(NEEDED) Shared library: [libc.so]
$ llvm-readelf -l build/src/libqjackctl.so | grep " LOAD "
LOAD ... 0x1000 ← 4 个段全部 4KB 对齐 ✅
全绿:
- ✅ ARM aarch64
- ✅ DYN(共享库)
- ✅
T main和T cdrv_main双入口都已导出 - ✅ NEEDED 干净(无绝对路径)
- ✅ 4 KB 对齐(不需要跑 fix_elf_align.py)
- ✅ 体积 1.9 MB(业务库),合理
十、HAP 工程集成

# 复制鸿蒙QT模板
rsync -a --exclude='.hvigor' --exclude='build' --exclude='entry/libs' \
鸿蒙QT模板/ qjackctlPC/
# 倒入 .so
mkdir -p qjackctlPC/entry/libs/arm64-v8a/{platforms,styles}
cp libqjackctl.so qjackctlPC/entry/libs/arm64-v8a/
cp $QT_OHOS_ROOT/lib/libQt5{Core,Gui,Widgets,Xml,Network,Svg,OhosExtras}.so \
qjackctlPC/entry/libs/arm64-v8a/
cp $QT_OHOS_ROOT/plugins/platforms/libqohos.so qjackctlPC/entry/libs/arm64-v8a/
cp $QT_OHOS_ROOT/plugins/platforms/libqohos.so qjackctlPC/entry/libs/arm64-v8a/platforms/
cp $QT_OHOS_ROOT/plugins/styles/libqohosstyle.so qjackctlPC/entry/libs/arm64-v8a/styles/
cp $OHOS_SDK_ROOT/native/llvm/lib/aarch64-linux-ohos/libc++_shared.so \
qjackctlPC/entry/libs/arm64-v8a/

最终 entry/libs/arm64-v8a/ 内容:
libqjackctl.so 1.9M 业务库
libQt5Core.so 34M
libQt5Gui.so 38M
libQt5Widgets.so 36M
libQt5Xml.so 364K (QjackCtl 用 Xml 处理 patchbay 文件)
libQt5Network.so 12M
libQt5Svg.so 2.4M (界面图标用 SVG)
libQt5OhosExtras.so 5.1M
libqohos.so 149M QPA 插件(外层)
libc++_shared.so 1.2M
platforms/libqohos.so 149M QPA 插件(platforms/)
styles/libqohosstyle.so 1.9M
关键文件 3 处修改
// entry/src/main/ets/common/QtAppConstants.ets
export const APP_LIBRARY_NAME = 'libqjackctl.so';
export const LOG_DOMAIN = 0x0000;
export const LOG_TAG = 'QjackCtl';
// AppScope/app.json5
{
"app": {
"bundleName": "com.example.qjackctlpc",
"label": "$string:app_name"
}
}
// AppScope/resources/base/element/string.json
{ "string": [{ "name": "app_name", "value": "QjackCtl" }] }
// build-profile.json5: 清空 signingConfigs,让 DevEco 自动重签
"signingConfigs": [],
"products": [{ "name": "default", "signingConfig": "default", ... }]


十一、复盘 —— 几个最有价值的经验
经验 1:接口隔离 (Shim) 是适配"目标平台后端缺失"项目的标准做法
QjackCtl 本质是个 JACK 配置工具。鸿蒙 PC 上没 jackd,去交叉编译 libjack 毫无意义(编完也连不上服务器)。
接口隔离的价值:
- 工作量从"完整交叉编译 + patch libjack/libalsa(2 周)“压缩到"维护 7 个 ABI 兼容头(数小时)”
- ABI 兼容意味着将来后端就位时插入即用,无需重写业务层代码
- 产物干净(NEEDED 列表里没有 libjack/libasound)
- 运行时安全(永远不调到 jack API,相当于全 NOP)
适用条件:项目代码已经做过 GUI / 后端分层(如 QjackCtl 的 CONFIG_JACK_API 编译宏),上层逻辑可以在"无后端"分支独立工作。这是 KDE/KXStudio 系项目的常见架构。
经验 2:moc ABI 错位有"两种解法",按场景选
| 场景 | 解法 |
|---|---|
| 目标 Qt 版本 < host moc 版本(如本案 5.12 < 5.15) | 降级 moc 输出(用 perl/sed 把 SuperData → 原始指针) |
| 目标 Qt 版本 == host(理想情况) | 不需要处理 |
| 目标 Qt 是定制版,缺特定符号 | 给目标 Qt 头文件打补丁(仓库前序文档 DiffPDF 时代的做法) |
降级 moc 输出的优势是:只改临时 build 产物,不污染 Qt 安装目录。
经验 3:extern "C" main 是个反模式
务必区分:
| 函数 | 链接 | 做法 |
|---|---|---|
业务的 int main |
C++ 链接(保留默认) | 不改 |
extern "C" cdrv_main |
C 链接 | 文件末尾追加 wrapper,不要替换 main |
原因:main 体内的宏(如 Q_INIT_RESOURCE)会展开成 extern int xxx();,外层链接性会传染。
经验 4:CMake set(... FORCE) 是绕开 Qt-OHOS 工具链探测的银弹
set(Qt5_MOC_EXECUTABLE /usr/bin/moc-qt5 CACHE FILEPATH "host moc" FORCE)
FORCE 让 toolchain 文件强制覆盖所有后续配置(包括 Qt5Config.cmake 里写死的 Qt5::moc IMPORTED target)。这比 find_program 更稳。
经验 5:构建脚本的"七阶段编排"对 moc 工程是必要的
不要试图一遍 make 解决。AUTOMOC 是"按需"触发的,第一轮编译时大量 moc 文件还没生成;只有第一轮编出 moc 后,第二轮才能继续编依赖它们的 cpp。中间穿插 fix 是工程上必要的"事件驱动"。
十二、避坑速查表(QjackCtl 篇)
| 报错关键词 | 99% 是什么 | 快速修复 |
|---|---|---|
JACK library not found. (FATAL_ERROR) |
顶层 CMake 强校验 JACK | sed 改成 WARNING |
cannot find -ljack |
链接阶段还在找 JACK | -DJACK_FOUND=FALSE -DCONFIG_JACK=0 |
no member named 'SuperData' in 'QMetaObject' |
host moc 5.15 + 目标 Qt 5.12 | 跑 fix_moc_metaobjects.sh |
qInitResources_qjackctl: symbol not found |
main 被错改成 extern "C" |
还原 main,末尾加 cdrv_main wrapper |
dlsym main returned NULL |
-fvisibility=hidden 把 main 隐藏了 | 加 CXX_VISIBILITY_PRESET default |
ld.lld: error: undefined symbol: jack_xxx |
Shim 头未覆盖该 API | 在 stubs/jack/jack.h 按上游签名补一个 static inline 实现 |
如果你也要移植一款"重外部依赖的 Qt 工具"(如 QSynth / Mixxx 这类音频工具),按这个 checklist 走:
□ 1. 列出所有外部 C 库(不算 Qt)
□ 2. 决策:每个外部库是真交叉编译,还是 Shim 接口隔离?
决策依据:(1) 目标平台是否提供该后端服务 (2) 是否有现成的 ABI 兼容上游头
后端缺失 + 上层有 feature flag → 优先 Shim
□ 3. 编写 / 拷贝 Shim 头(与上游 ABI 兼容,API 用 static inline 返回标准错误码)
□ 4. cmake 用 -DCONFIG_xxx=OFF 全部关闭可选模块
□ 5. 准备 toolchain 文件,强制 host 工具走系统 Qt5
□ 6. patch 脚本:FATAL_ERROR → WARNING;add_executable → add_library SHARED
□ 7. 末尾追加 cdrv_main wrapper(向后兼容)
□ 8. 关 visibility=hidden
□ 9. 七阶段构建:configure → make → fix moc → make → fix moc → make
□ 10. 体检 5 项:file / readelf -h / nm -D | grep main / readelf -d | grep NEEDED / readelf -l | grep LOAD
□ 11. 倒入 HAP,签名,跑
十三、写在最后
QjackCtl 这次适配的最大收获,是把"目标平台后端缺失"这种看似挡路的难题变成了可工程化、可复用、可演进的标准动作。下次再遇到 QSynth / QSampler / Cadence / Mixxx 这类"上层 GUI 完整 + 底层依赖系统服务"的项目,从 toolchain、moc 降级、Shim 头、双入口符号到 HAP 集成,整条链路按本文复刻即可。
仓库里已经积累的几个移植案例覆盖了不同复杂度:
| 项目 | 难度 | 关键技术 |
|---|---|---|
| NotePad– | ⭐ | 纯 Qt Widgets |
| DiffPDF | ⭐⭐ | Qt + Poppler 交叉编译 |
| KDiff3 | ⭐⭐⭐ | Qt + KDE 框架瘦身 |
| QjackCtl(本案) | ⭐⭐⭐⭐ | Qt + 接口隔离 (Shim) + moc ABI 降级 + 双入口符号 |
| Krita / Inkscape | ⭐⭐⭐⭐⭐ | 多 C++ 库交叉编译(待挑战) |
希望本文能帮到下一位"踩 JACK / ALSA / 老 X11 工程"的同学,少走至少 70% 的弯路。
十四、FAQ
Q1:接口隔离层(Shim)和"运行时假装能用"有什么区别?这不就是 mock 吗?
A:有本质区别。Shim 不是欺骗用户、不是隐藏功能缺失,而是显式承认目标平台后端缺失,并以工程化方式让上层 GUI 进入"等待后端"分支。
| 维度 | mock / 欺骗 | Shim 接口隔离 |
|---|---|---|
| 目的 | 让"测试通过"假装能用 | 让"GUI 层"在后端缺失时优雅降级到等待状态 |
| 用户体验 | 看起来能用,操作时崩溃 | UI 显示 “Waiting for JACK server”,操作可保存配置但不发送指令 |
| 代码侵入 | 上层代码不知情 | 上层代码知道自己在 CONFIG_JACK_API=0 模式(QjackCtl 1.0.6 上游设计的 feature flag) |
| 后端就位时 | 需要重写 | 插入真实头 + 重编一次即直连(ABI 兼容) |
QjackCtl 上游 qjackctlMainForm.cpp 里到处是 #ifdef CONFIG_JACK_API 包裹的真实调用——这意味着上游开发者本来就考虑过"开发机无 JACK"的场景。我们做的事是把这个"开发态"模式在交叉编译时启用,不是为了骗用户。
Q2:为什么 main 不能改成 extern "C"?这条限制听起来反直觉。
A:因为 Q_INIT_RESOURCE(name) 是宏展开而不是函数调用,展开后会继承外层链接性。
详细机制:
// qrc_qjackctl.cpp 里 rcc 生成的真实定义(C++ mangled)
int qInitResources_qjackctl() { /* ... */ return 1; } // 实际符号: _Z23qInitResources_qjackctlv
// Qt 头文件里的宏展开
#define Q_INIT_RESOURCE(name) \
do { extern int qInitResources_##name(); qInitResources_##name(); } while(0)
// 当 main 是 extern "C" 时
extern "C" int main() {
Q_INIT_RESOURCE(qjackctl);
// 展开后变成:
// extern int qInitResources_qjackctl(); // ← 在 extern "C" 块里,按 C 链接处理
// qInitResources_qjackctl(); // ← 链接器找符号 "qInitResources_qjackctl"
// 但实际定义是 mangled 的 _Z23qInitResources_qjackctlv → 链接失败
}
正确做法:保留 int main() 的 C++ 链接,在文件末尾追加 wrapper:
extern "C" int cdrv_main(int argc, char **argv) {
return main(argc, argv); // 转发到 C++ 链接的 main
}
这样既满足 Qt-OHOS 的 dlsym 需求(cdrv_main 是 C 链接、可被 dlsym 找到),又不破坏 Q_INIT_RESOURCE 的 mangled 解析。
Q3:moc ABI 降级为什么不直接换 host moc 到 5.12?
A:理论可行,实操巨坑。两条路对比:
方案 A(理论上更"干净"):在服务器装 Qt 5.12 host 工具
- ❌ OpenCloudOS / CentOS 系系统包仓库里只有 Qt 5.15
- ❌ 自己编 Qt 5.12 host 要 4-6 小时 + 几百兆磁盘
- ❌ 系统里同时有两套 Qt 容易污染 PATH,
moc-qt5/qmake-qt5软链接会指向哪一个不确定 - ❌ 团队协作时,每个新成员都要重新装 5.12 host
方案 B(本文采用):用 host 5.15 + perl 脚本批量降级
- ✅ 一个 200 行 perl 脚本完成全部 ABI 翻译
- ✅ 脚本可纳入 git 仓库、零环境污染
- ✅ 24 个 moc_*.cpp 一次跑完,1 秒搞定
- ✅ 新成员 clone 仓库后直接能跑,无需额外配置 host Qt
降级脚本核心 perl 逻辑(4 条规则覆盖 95% 场景):
# Rule 1: SuperData 类型不存在 → 改用 5.12 的旧式声明
s/QMetaObject::SuperData::link<([^>]+)>\(\)/&\1/g;
# Rule 2: 5.15 的 staticMetaObject 模板访问 → 直接取地址
s/QtPrivate::SuperData::link<([^>]+)>\(\)/&\1/g;
# Rule 3: SuperData 数组初始化 → 改成 QMetaObject* 数组
s/static const QMetaObject::SuperData\[\]/static const QMetaObject*[]/g;
工程角度看,B 方案的可维护性远高于 A——这就是为什么本仓库 LiteIDE / qjackctl 都采用降级方案。
Q4:QjackCtl 在鸿蒙 PC 上"能干什么"?哪些功能在等后端就位?
A:分两类讲清楚。
✅ 当前完整可用(不依赖 jackd 服务):
- Setup(参数配置):所有音频接口参数(采样率 / 周期 / 缓冲区等)的录入和保存
- Patchbay(虚拟接线):连接图编辑、保存为 .xml、加载预设
- Preset 管理:多套配置档案的切换、重命名、删除
- Messages(日志):界面日志输出,shim 层 “Waiting for JACK server” 提示
- Settings(外观/快捷键):所有 UI 偏好设置
- 13 种语言 i18n:系统语言切换即时生效
⏳ 等鸿蒙 PC 提供后端服务时自动激活:
- Connections(端口连接):实时端口列表 + 连接拖拽(依赖 jackd 端口枚举 API)
- Transport(播放控制):依赖 jackd transport master
- Statistics(实时性能):依赖 jackd 的 xrun 统计
- Activity meter(电平表):依赖 jackd 实时音频流
当鸿蒙 PC 引入 PipeWire 的 jack-emu 兼容层(参考 Linux 桌面已经在用的 pipewire-jack)时:
# 把 stub 头换成上游真头
rm -rf stubs/jack stubs/alsa
cp -r /usr/include/jack stubs/jack
cp /usr/include/alsa/asoundlib.h stubs/alsa/
# 打开 feature flag 重编
cmake -DCONFIG_JACK_API=1 -DCONFIG_ALSA_SEQ=1 ..
make
ABI 兼容设计让升级路径完全平滑——这是接口隔离方案最大的工程价值。
本文所有命令、脚本、Shim 头均经服务器实际跑通验证(OpenCloudOS 9 / Clang 15 / Qt-OHOS 5.12.12 / qjackctl 1.0.6)。最终产物
libqjackctl.so1.9 MB,ARM aarch64,业务库 + Qt5 runtime + 12 个 .so 全部对齐 4KB 页粒度,HAP 工程开箱即用。
更多推荐





所有评论(0)