把科学计算 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 上,三件事全部不成立:
但 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 接到 XComponent |
| libqohos.so | Qt 平台抽象:把 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。
![Phase 3 核心成功:demo.py 跑完,Console 显示 Hello/count/Done + ,Variable Explorer 展示 5 个真实全局变量(name='HarmonyOS' / count=3 / items=[1,2,3] / mapping={'a':1,'b':2} / i=2)](https://www.171host.com/wp-content/uploads/2026/09/20260914235203-6aa888a3bf42d.jpg)
底部状态栏的关键日志(看到就说明链路通):
[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 部署一键式
![DevEco 设备选择器:HUAWEI MateBook Pro[3QC0124C20000733] 7.0.0(26.0.0) 已连接,Huawei模拟器(Mate 80 Pro / MateBook Pro / Mate X7 / Pura 90 / MatePad Pro 13)可选](https://www.171host.com/wp-content/uploads/2026/09/20260914235208-6aa888a82d42b.png)
选择真机,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 + argToQString | 5.15 头 + QString::arg | 用 1.0.3+;禁 .arg();build_ohos.sh ABI 检查 |
| 仍是 Phase2 标题 / 旧回显 | 未装 1.0.3 | 卸载旧包再 Run |
| stdlib not ready / missing os.py | 解压未完成 | 等 5–15s 再 F5 |
| dlopen / SIGSEGV | libpython 被 strip | 查 HAP 内 so 是否仍 ≈7063424;strip=false |
| Variable Explorer 空 | runner 未产出 JSON | 看 Console 是否有 traceback |
| exit=0 但无 Hello | bare exit | 不算成功 |
| Ctrl+V 无内容 | READ_PASTEBOARD grantStatus=-1 | 受限权限,普通签名拿不到;暂无解 |
| 想换默认 demo | demo 嵌在 so 二进制 | 改 ArkTS bootstrap 写 filesDir/demo_case.py |
| libpasteboard.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 应用 + 嵌入式脚本解释器"都可以照搬:
写完这篇回头看,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 字样或输出旧回显,先卸载旧包再装,升级安装偶尔有元数据缓存。






