欢迎光临
我们一直在努力

把科学计算 IDE Spyder 搬到鸿蒙 PC:Qt C++ 宿主 + libpython embed 的三阶段实战

把科学计算 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。

在这里插入图片描述

题图:demo_case.py 跑通后的控制台输出——SPYDER ASCII 艺术字 + Initializing 动画 + ✓ System initialized / ✓ Python environment ready / ✓ Welcome to the terminal


一、为什么 Spyder 移植比想象的难

Spyder 上游是 Python 写的 PyQt IDE,发布形态是「Python 包 + Qt 应用代码 + 一堆 .ui/.py」。正常人在 PC 上是这么装它:

pip install spyder
spyder # 启动

到鸿蒙 PC 上,三件事全部不成立:

  • 没有 Python 解释器(普通 HAP 应用沙箱里只有 ArkTS 运行时)
  • 没有 PyQt(PyQt5/QtWidgets 那一套 native binding 需要 CPython + Qt 头文件 + sip 编译器交叉编译)
  • 没有 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 接到 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)

    底部状态栏的关键日志(看到就说明链路通):

    [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)。

    DevEco 工程视图 + 部署日志:Build task 9s 542ms → Launching → hdc file send → bm install 4s → aa start 153ms → successfully launched within 19s 995ms

    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)可选

    选择真机,Run 一次即装机启动,整套 Qt 资源 + libpython + stdlib 一并推到设备沙箱。

    6.2 核心成功验收

    (见 §三 Phase 3 图)—— demo.py 跑完,5 个真实变量 + [Phase3] SUCCESS。这是验收清单里唯一硬性指标。

    6.3 多标签 + 错误状态

    Phase 3 多标签 4 个 tab(demo.py / untitled1.py / spyder_script.py / __fd_debug.txt)+ SyntaxError 错误状态

    QTabWidget 实现的标签页,左下红点表示该 tab 有未保存改动。运行时某次输入语法错,Console 显示完整 traceback,Variable Explorer 还能看到上一次成功运行的 globals——这点和真实 Spyder 一致。

    6.4 关于弹窗(About)

    Phase 3 主界面 + About 弹窗:Qt Widgets + embedded CPython 3.12 + Variable Explorer introspects globals + Multi-tab editor + nostrip iron law + Avoid QString::arg + PYTHONHOME 路径 + Success 标准

    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 → 效果如下:

    demo_case.py 完美跑通:Console 显示完整 SPYDER ASCII 艺术字 logo(box drawing 字符)+ Initializing./Initializing.. → Initializing.... 动画 + ✓ System initialized successfully / ✓ Python environment ready / ✓ Welcome to the terminal

    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 应用 + 嵌入式脚本解释器"都可以照搬:

  • XComponent 是 Qt 进 OHOS 的正门——ArkTS 端薄壳只负责创建 NativeWindow 并交给 QPA,不做任何渲染
  • QPA 插件层是能力适配的"差量补丁"位——OHOS 没实现的 Qt 平台调用都在这里补(剪贴板、输入法、字体库)
  • embed 解释器走 fork + pipe + dlopen——比子进程更可控(能拿到 Py_BytesMain 入口),但路径上必须 nostrip
  • nostrip 是动态 dlopen 链的硬约束——HAP 内任何会被 dlopen 的 .so 都得 strip=false
  • 跨 Qt 版本编译永远先 grep 头版本特有符号——5.12→5.15、5.15→6.x,每个版本都有一批过时/新增的 inline 函数
  • ArkTS bootstrap 是二级曲线救国的好地方——任何"业务 so 里写死但改不了"的东西,都可以在 bootstrap 里"先 filesDir 写文件再让 so 用"
  • 平台权限墙摸清再动 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)
    未经允许不得转载:171主机测评 » 把科学计算 IDE Spyder 搬到鸿蒙 PC:Qt C++ 宿主 + libpython embed 的三阶段实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址