最近在折腾项目的时候碰到了这个知识点,查了不少资料,索性整理出来分享给大家。
把科学计算 IDE Spyder 搬到鸿蒙 PC:Qt C++ 宿主 + libpython embed 的三阶段实战
欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/
欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
适配开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_spyder
写在前面
本文要走一条完全不同的路:
- 不在 Electron 里跑前端
- 不在浏览器里跑 Python
- 而是用 Qt C++ 宿主 + 内嵌 libpython 在鸿蒙 PC 上真真正正渲出一个 Qt Widgets 原生 GUI、一个原生 QTextEdit 编辑器、一个能调 Python 解释器跑的 Console,外加一个真实的 Variable Explorer。
一、为什么 Spyder 移植比想象的难
Spyder 上游是 Python 写的 PyQt IDE,发布形态是「Python 包 + Qt 应用代码 + 一堆 .ui/.py」。正常人在 PC 上是这么装它:
pip install spyder
spyder # 启动
到鸿蒙 PC 上,三件事全部不成立:
1. 没有 Python 解释器(普通 HAP 应用沙箱里只有 ArkTS 运行时)
2. 没有 PyQt(PyQt5/QtWidgets 那一套 native binding 要 CPython + Qt 头文件 + sip 编译器交叉编译)
3. 没有 X Server(Qt 5.x 默认走 XCB,离屏也得有平台抽象)
但 Qt for OpenHarmony 已经把"在 OHOS 上渲染 Qt Widgets"这条路打通——libqohos.so 是 Qt 的 OHOS QPA 平台插件,把 QWindow/QWidget 画到 OHOS 的 NativeWindow,并把 OHOS 的输入事件翻译成 QEvent。剩下要做的是把 Qt 应用本身 + Python 解释器塞进 HAP。
这条路被 Thonny(另一个 Python IDE)走过一遍并验证可行,Spyder 在它的基础上加了三件事:
- 三栏布局(Editor / Variable Explorer / Console)
- F5 → 真实 Python 执行
- Variable Explorer 展示运行后的真实全局变量
二、整体架构
四个层次的分工:
层职责ArkTS 薄壳Qt 应用启动入口;解压 stdlib;把 NativeWindow 接到 XComponentlibqohos.soQt 平台抽象:把 OHOS 原生 API 适配成 Qt 平台调用libspyder_shell.so业务宿主:Qt 渲染 + Python 子进程 + runner 协议libpython3.12.so真 CPython 解释器,跑在子进程里执行用户脚本
三、三阶段迭代
整个移植分三阶段,每一阶段在真机上独立可验收:
Phase 1:三栏 UI + Run 回显(最简链路)
目标:验证 Qt 嵌入 Python 跑通。
ArkTS QAbility → libqohos.so → libspyder_shell.so → fork → libpython3.12
stdout → Qt Console
跑通了 Run 命令能看到 hello 字样,就说明:Qt 平台抽象、Python 子进程、stdout 回流三条链路全部 OK。
Phase 2:F5 → Hello/count/Done + [Phase2] SUCCESS
目标:完整执行链 + 内嵌默认 demo 字符串。
so 里固化了一段 demo(方便第一次启动就有东西可跑):
name = 'HarmonyOS'
count = 3
items = [1, 2, 3]
mapping = {'a': 1, 'b': 2}
print('Hello from Spyder')
for i in range(count):
print('count', i)
print('Done')
F5 → 把这段内容写到 <filesDir>/spyder_script.py → fork 子进程跑 → 抓 stdout。
Phase 3:Variable Explorer 真实变量 + 多标签/Open
目标:真正"能看到运行状态"。
引入 spyder_runner.py(so 内部嵌入):
# Runner 概要(伪代码)
src = open(sys.argv[1]).read()
g = {"__name__": "__main__"}
exec(compile(src, sys.argv[1], "exec"), g)
rows = []
for k, v in g.items():
if k.startswith("__") or isinstance(v, types.ModuleType):
continue
rows.append({"name": k, "type": type(v).__name__,
"value": repr(v)[:300], "size": safe_len(v)})
print("SPYDER_VARS_BEGIN")
print(json.dumps(rows, ensure_ascii=False))
print("SPYDER_VARS_END")
子进程跑完用户脚本 → 输出 SPYDER_VARS_BEGIN/END 包裹的 JSON → C++ 侧 ExtractVarJson 抠出 → QTableWidget 渲染成 Variable Explorer。
底部状态栏的关键日志(看到就说明链路通):
[spyder-1.0.3-phase3-introspect-tabs] ready.
PYTHONHOME=/data/storage/el2/base/haps/entry/files/python
--- Run @ 16:45:08 (Phase3 introspect) ---
[spyder-bootstrap] stdout bound to fd=51
Hello from Spyder
count 0
count 1
count 2
Done
[Phase3] SUCCESS: Hello/count/Done observed.
四、ABI 铁律(必踩一坑)
HAP 内 Qt runtime 是 5.12.12(壳模板自带),本机交叉编译常用 Homebrew qt@5 5.15 头文件。禁止在业务 so 里使用 QString::arg(...)。
原因:QString::arg(...) 在 5.15 头里会展开为 QtPrivate::argToQString(QStringView,...),这个符号 5.12 的 libQt5Core 里没有 → 业务 so dlopen 进来时找不到符号 → SIGABRT。
护栏:native 工程的 build_ohos.sh 会在产物里 grep argToQString,命中就 fail。
教训:跨大版本编译 Qt 业务代码,永远要 grep 头版本特有的符号。这是 Qt 5.12 → 5.15 / 5.15 → 6.x 迁移时最隐蔽的杀手。
五、nostrip iron law(动态 dlopen 链的硬约束)
libpython3.12.so 务必不 strip。原因:
// libspyder_shell.so 启动时:
dlopen("libpython3.12.so", RTLD_NOW);
Py_BytesMain(argc, argv);
strip 后的 libpython 会:
- 丢失动态符号(
Py_BytesMain、Py_Main等) dlopen失败 → SIGABRT
build-profile.json5 里 nativeLib.debugSymbol.strip=false;HAP 内 libpython3.12.so ≈ 7063424 字节。验收时如果发现大小不对,立刻停止排查 dlopen 链。
六、真机验收:七个功能点逐一过
设备:HUAWEI MateBook Pro,HarmonyOS 7.0.0。安装产物 entry-default-signed.hap ≈ 478 MB(含 Qt runtime + libpython + stdlib zip + 全部 .so)。
6.1 DevEco 部署一键式
选择真机,Run 一次即装机启动,整套 Qt 资源 + libpython + stdlib 一并推到设备沙箱。
6.2 核心成功验收
(见 §三 Phase 3 图)—— demo.py 跑完,5 个真实变量 + [Phase3] SUCCESS。这是验收清单里唯一硬性指标。
6.3 多标签 + 错误状态
QTabWidget 实现的标签页,左下红点表示该 tab 有未保存改动。运行时某次输入语法错,Console 显示完整 traceback,Variable Explorer 还能看到上一次成功运行的 globals——这点和真实 Spyder 一致。
6.4 说说弹窗(About)
About 弹窗文字本身就是项目核心约束的小抄:
- Qt Widgets + embedded CPython 3.12 (libpython):技术栈
- Same nostrip iron law: strip=false:复盘 ABI 铁律
- Avoid
QString::arg(Qt 5.15 ABI vs HAP 5.12 runtime):另一个 ABI 铁律 - Success = Console shows Hello / count / Done + variable rows:唯一验收标准
七、剪贴板权限墙(额外发现)
移植做完后用户提了一个朴素问题:「为什么应用里 Ctrl+V 粘贴不了?」
以为是快捷键绑定问题,结果是平台权限墙:
$ hdc shell "atm dump -t -b org.spyder.ide.ohos" | grep READ_PASTEBOARD
"permissionName": "ohos.permission.READ_PASTEBOARD",
"grantStatus": -1, ← 未授权
"grantFlag": 0
READ_PASTEBOARD 是 system_basic 级别的受限权限,普通 debug 签名(normal APL)的 HAP 装上后运行时授权直接被拒。
链路:
Ctrl+V
→ Qt QClipboard (QPlatformClipboard)
→ libqohos.so (qohosclipboardobject.cpp)
→ OH_Pasteboard_GetData() ← 每次被权限拦截
→ 永远返回空
→ 剪贴板菜单可点,无内容可贴
module.json5 里其实已经声明了这个权限(这是正常做法),但声明 ≠ 授权。受限开放权限在普通应用上不会自动批准。
这是个对所有鸿蒙 PC 工程都通用的发现:任何 Qt/Qt-like 应用,只要用到了系统剪贴板读取功能,都面临同一堵墙。
八、默认案例的"二级曲线救国"
另一个朴素问题:「能不能把默认的 demo.py 换成我想要的那段 SPYDER logo 代码?」
第一反应:改 so 内嵌字符串。
钻进 so(libspyder_shell.so)一看——demo.py 确实以 C 字符串常量形式嵌在二进制里:
"# Spyder for HarmonyOS PC — Phase 3 (introspect + tabs)\n
# Qt C++ shell · Ability + XComponent + QPA\n
# Run (F5): executes this file with CPython 3.12 and\n
# introspects the globals into the Variable Explorer.\n\n
name = 'HarmonyOS'\n
count = 3\n
items = [1, 2, 3]\n
mapping = {'a': 1, 'b': 2}\n
print('Hello from Spyder')\n
for i in range(count):\n
print('count', i)\n
print('Done')\n"
但 native 源码(spyder_shell_main.cpp + build_ohos.sh)在本开发区里没保留,本地无法重编 so。改 so 二进制字符串要等长覆盖,新代码(含 emoji 🚀、box-drawing ╔═╗║╚╝)UTF-8 长度远超旧串,做不了原地替换。
绕路:ArkTS 侧的 SpyderPythonBootstrap.ets 在 ensureSpyderPythonRuntime 启动时执行——而 stdlib 解压完了大家就有了 filesDir 写权限。在那一步把 demo_case.py 的代码常量写到 filesDir/demo_case.py:
const DEMO_CASE_PY = [
'import time',
'import os',
'',
'logo = r"""',
' ██████╗ ██████╗ ██████╗ ███████╗',
// ... 用户给的整段代码
'if __name__ == "__main__":',
' main()'
].join('\n');
function ensureDemoCase(filesDir: string): void {
const target = `${filesDir}/${DEMO_CASE_NAME}`;
if (readTextFile(target) === DEMO_CASE_PY) return;
writeTextFile(target, DEMO_CASE_PY);
}
应用启动后用户 File → Open… 选择 demo_case.py → F5 → 效果如下:
Variable Explorer 自动捕获 logo (str, 229 字符)、clear (function)、main (function)——说明 Spyder 的 introspect 协议对任何合法 Python 脚本都生效。
这个"二级曲线"的可推广性:任何鸿蒙 PC 应用,如果遇到「功能在 so 里、不在源码里、改不了」的痛点,都可以问一句:ArkTS bootstrap 阶段能补一层吗? 能补就补。
九、踩坑对照表
现象根因处理启动 SIGABRT + argToQString5.15 头 + QString::arg用 1.0.3+;禁 .arg();build_ohos.sh ABI 检查仍是 Phase2 标题 / 旧回显未装 1.0.3卸载旧包再 Runstdlib not ready / missing os.py解压未完成等 5–15s 再 F5dlopen / SIGSEGVlibpython 被 strip查 HAP 内 so 是否仍 ≈7063424;strip=falseVariable Explorer 空runner 未产出 JSON看 Console 是否有 tracebackexit=0 但无 Hellobare exit不算成功Ctrl+V 无内容READ_PASTEBOARD grantStatus=-1受限权限,普通签名拿不到;暂无解想换默认 demodemo 嵌在 so 二进制改 ArkTS bootstrap 写 filesDir/demo_case.pylibpasteboard.so 缺失系统库找不到QPA 降级为空实现;复制/粘贴双重失效
十、已知限制(如实记录)
限制说明内嵌 demo 不可热替换demo 字符串固化在 so 二进制里;已用 ArkTS bootstrap 写 demo_case.py 绕路剪贴板读权限被拒READ_PASTEBOARD 是 system_basic 受限权限,普通 debug 包拿不到GPU 合成未开OHOS QPA 上 GPU 路径还在调,避免整页崩溃进程内 libpython 体积stdlib zip ≈ 14 MB,全部解压到 filesDir单 tab 内的 stdin 交互当前仅 stdout 回流(runner 一次性执行);无 REPL
十一、复现命令
# 1. 重编业务 so(需要 native 源码,本工程未保留)
cd /Users/zhubo/Downloads/harmony-pc/ohos_Spyder/native
./build_ohos.sh
# 2. 重打 HAP
cd /Users/zhubo/Downloads/harmony-pc/ohos_Spyder/harmony_pc
export NODE_HOME=/Applications/DevEco-Studio.app/Contents/tools/node
export DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk
export PATH=$NODE_HOME/bin:$PATH
hvigorw --mode module -p module=entry@default -p product=default assembleHap --no-daemon
# 3. DevEco 自动签名后,检查并修正 products[].signingConfig = "default"
# 4. 安装
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a QAbility -b org.spyder.ide.ohos
# 5. 启动后等 5-15 秒(stdlib 解压),然后 Open → demo_case.py → F5
十二、给 Qt 移植工程的方法论
这套路径不只适用于 Spyder。任何"鸿蒙 PC 上的 Qt 应用 + 嵌入式脚本解释器"都可以照搬:
1. XComponent 是 Qt 进 OHOS 的正门——ArkTS 端薄壳只负责创建 NativeWindow 并交给 QPA,不做任何渲染
2. QPA 插件层是能力适配的"差量补丁"位——OHOS 没实现的 Qt 平台调用都在这里补(剪贴板、输入法、字体库)
3. embed 解释器走 fork + pipe + dlopen——比子进程更可控(能拿到 Py_BytesMain 入口),但路径上务必 nostrip
4. nostrip 是动态 dlopen 链的硬约束——HAP 内任何会被 dlopen 的 .so 都得 strip=false
5. 跨 Qt 版本编译永远先 grep 头版本特有符号——5.12→5.15、5.15→6.x,每个版本都有一批过时/新增的 inline 函数
6. ArkTS bootstrap 是二级曲线救国的好地方——任何"业务 so 里写死但改不了"的东西,都可以在 bootstrap 里"先 filesDir 写文件再让 so 用"
7. 平台权限墙摸清再动 UI——READ_PASTEBOARD 这种 system_basic 受限权限,写 UI 之前先在 atm dump -t -b 确认是否可达,否则剪贴板类功能做出来也是摆设
写完这篇回头看,Qt 应用在鸿蒙 PC 上跑通的核心不是 Qt 本身——Qt for OHOS 团队已经把大部分脏活干完了。真正的难点在于:ABI 跨版本、动态 dlopen 链的符号可用性、进程间 stdlib 共享、还有那些你想改但改不了的二进制资源。后两项这次都靠 ArkTS bootstrap 兜住了。
如果你也在做鸿蒙 PC 的 Qt 应用移植,欢迎拿这七个坑对照——大概率能少走一到两周的弯路。
常见问题 FAQ
Q1:启动后马上 F5,报 stdlib not ready 或 missing os.py?
首次启动时应用正在后台解压 14MB 的 stdlib zip 到沙箱,要 5–15 秒。等状态栏出现 ready. 再 F5。后续启动有 marker 文件跳过解压,秒开。
Q2:应用一启动就闪退?
九成是 ABI 断链:业务 so 里用了 QString::arg(),编译头是 5.15,HAP 里跑的 Qt 是 5.12,符号找不到直接 SIGABRT。检查 libspyder_shell.so 里是否残留 argToQString 符号,命中就回源码改掉重编。
Q3:F5 跑了,但 Variable Explorer 是空的?
看 Console 有没有 traceback——最常见是脚本本身语法错或运行时异常。只要 Console 打出了 SPYDER_VARS_BEGIN/END 包裹的 JSON,Explorer 就一定有内容;没有就是 runner 没跑完。
Q4:为什么编辑器里 Ctrl+V 粘贴不了代码?
READ_PASTEBOARD 是 system_basic 受限权限,普通 debug 签名应用授权直接被拒(grantStatus: -1),Qt 剪贴板桥接层每次读取都被拦。临时绕路:File → Open… 打开沙箱里的 .py 文件。
Q5:想把默认的 demo.py 换成自己的代码?
默认 demo 以 C 字符串固化在 libspyder_shell.so 二进制里,本地没有 native 源码改不了。已在 SpyderPythonBootstrap.ets 里内置了曲线方案:启动时自动把 demo_case.py 写到沙箱,Open… 打开即可。想换内容就改那个 ArkTS 常量重编 HAP。
Q6:怎么确认装的是新版本?
看窗口标题栏——Spyder (OHOS) — Phase 3 · introspect + tabs。如果还是 Phase 2 字样或输出旧回显,先卸载旧包再装,升级安装偶尔有元数据缓存。
暂时整理到这里。以上都是个人理解,可能有疏漏,欢迎指正。
评论 (0)
暂无评论