青蟒Python编辑器上架鸿蒙PC:把 CPython 静态链进鸿蒙 PC 的 NAPI 运行时

欢迎加入开源鸿蒙PC社区: https://harmonypc.csdn.net/
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

在鸿蒙 PC 上跑 Python,第一反应是「装个解释器」——但沙箱里没有全局 Python,filesDir 是 noexec 挂载,把 python3.12 二进制拷进沙箱再执行这条路从架构上就是死的。青蟒的解法是把整个 CPython 3.12 交叉编译成静态库,链进 NAPI .so,让解释器在应用进程里「活」起来;再把 Thonny 4.1.7 的源码原样放进 site-packages,用真源码做验收门。本文按上架全流程复盘:从运行时打包、嵌入式解释器的 stdout 捕获,到 input() 哨兵机制里那个「SystemExit 会杀掉整个进程」的致命细节。

项目上架信息 / 工程事实
应用名青蟒Python编辑器
包名org.thonny.native
版本1.2.0 (1020000)
设备形态2in1 / tablet(HarmonyOS PC)
上游项目Thonny 4.1.7(Python 入门 IDE,MIT)+ CPython 3.12
技术路线静态 libpython3.12.a 链进 NAPI .so,进程内 PyRun_SimpleString
运行时资产ohos_python_stdlib.zip 随 rawfile 分发,首启解压到沙箱
权限零(module.json5 无 requestPermissions 表)
上架状态已在鸿蒙应用市场正式上架

一、先看效果图

图 1 · 编辑器与 Shell:真解释器在跑

编辑器与Shell

图 1:编辑器米黄底、语法高亮、行号槽,下方 Shell 打印着硬事实——[runtime] sys.base_prefix = /data/storage/el2/base/haps/entry/files/ohos_python,Python 版本 3.12.9 (main, Jul 31 2026) [Clang 15.0.4 ...]:这是一个真的、交叉编译进进程的 CPython。%Run untitled.py 的魔法行与 [1, 2, 3, 4] 的输出,就是 Thonny 的 Shell 交互习惯。

图 2 · 变量面板:运行状态一目了然

变量面板

图 2:多标签页管理多个文件,变量面板实时展示运行中的每一个变量——函数对象、模块、数值一并列出,教学场景里「程序此刻是什么状态」不再靠脑补。

图 3 · 主视觉

应用主视觉

图 3:应用主视觉。界面收敛到新建/打开/保存/运行/停止/变量面板/Shell 面板/自检——认知负担留给代码,不留给界面。


二、需求设计:给新生的「开箱即用」

程序设计入门课的环境痛点很具体:完整 IDE 对新生过于复杂,浏览器方案依赖网络,机房设备装不上解释器。需求因此收敛为三条:

  1. 装得上、开得快:解释器随包分发,不装环境、不连网络;
  2. 能直接运行:编辑即运行,输出在应用内呈现,input() 这类教学最常用的交互必须能用;
  3. 看得懂状态:变量观察面板,让初学者「看见」程序的执行状态。

对应的产品边界:不做调试器、不做包管理器、不做虚拟环境——那是进阶工具的事,入门工具的第一美德是「不会把学生吓跑」。


三、技术方案:静态链进 .so,而不是带一个二进制

3.1 为什么不能「带一个 python 可执行文件」

鸿蒙沙箱有三条与终端/CLI 类应用相关的硬约束,青蟒在立项阶段就全部确认:

  • filesDir 以 noexec 挂载:拷进沙箱的二进制无法 exec;
  • 没有全局解释器:不存在「调系统 Python」的捷径;
  • 应用不能自由创建进程。

结论唯一:把 CPython 交叉编译成静态库 libpython3.12.a,链进 NAPI 模块 libthonny_native.so,解释器成为应用进程的一部分。Thonny 4.1.7 的 Python 源码原样打进运行时 zip 的 site-packages——上游代码一字节不改。

3.2 运行时资产的分发与校验

CPython 标准库 + Thonny site-packages 打成 ohos_python_stdlib.zip 随 HAP 的 rawfile 分发,首启由 ArkTS 解压到 filesDir/ohos_python:

const RUNTIME_ZIP = 'ohos_python_stdlib.zip';
// 每次运行时内容变化必须递增——解压逻辑靠它判断是否需要重铺
const RUNTIME_MARKER = 'READY_P2_1_0_3';

RUNTIME_MARKER 是一个容易低估的工程点:zip 内容变了但文件名没变,设备上旧解压结果不会自动失效——运行时内容与标记版本必须绑定递增,否则「改了代码但设备上没生效」这类灵异问题会消耗数天排查时间。
在这里插入图片描述

3.3 NAPI 层:四个导出函数

原生层只导出四个函数:getBuildInfo / setPythonHome / runCode / runTest。初始化链路的关键代码:

std::string site = lib + "/site-packages";
setenv("PYTHONHOME", g_pyHome.c_str(), 1);
setenv("PYTHONPATH", (lib + ":" + site).c_str(), 1);  // 显式列出,不依赖 site 模块
setenv("PYTHONUNBUFFERED", "1", 1);
setenv("PYTHONDONTWRITEBYTECODE", "1", 1);   // 沙箱内不写 .pyc
setenv("PYTHONNOUSERSITE", "1", 1);          // 禁用用户目录,路径全部收在沙箱

Py_SetPythonHome(wHome);
Py_InitializeEx(0);
// site 模块受限时,把 site-packages 显式插到 sys.path 首位
PyRun_SimpleString("import sys; sys.path.insert(0, site_packages_path)");

setPythonHome 会先做一次结构探针:检查 lib/python3.12/os.py 与 site-packages/thonny/VERSION 是否存在,并把目录列表打进 hilog。这个探针看似多余,实则是排障利器——「解释器起不来」的九成原因(zip 没解压、解压了一半、路径拼错)在探针日志里一眼可判。


四、核心难点:嵌入式解释器的输入与输出

在这里插入图片描述

4.1 stdout/stderr 捕获:文件中转

PyRun_SimpleString 里的 print 不会自己跑到 UI 上。做法是给每段用户代码包一层 Python 侧的捕获壳:替换 sys.stdout/sys.stderr 为写文件的捕获类,执行完由 C++ 读回文件、拼装报告(含 PyRun_SimpleString 返回码)回传 ArkTS。

用文件而不是内存管道中转,是因为嵌入式场景下 io 层的可靠性经过多项目验证不如「落盘再读」直接——而 os.write 层面的文件操作在沙箱内是确定性行为。

4.2 input():哨兵机制与 SystemExit 之死

教学场景 input() 是刚需,但嵌入式解释器没有交互 stdin。解法分三步:

  1. 运行前注入:把用户在 Shell 里已输入的行放进 __stdin_lines__,并覆盖 builtins.input;
  2. 有存货就弹出一行返回;没有存货则打印哨兵并抛自定义异常:
if len(__stdin_lines__) == 0:
    print('__NEED_INPUT__:' + str(prompt))
    raise _NeedInput()
  1. ArkTS 侧识别哨兵:看到 __NEED_INPUT__: 就把 Shell 切到等待输入态,用户回车后带着新行重跑。

这里埋着一个差点致命的细节:等待输入的异常绝不能用 SystemExit 表达。PyRun_SimpleString 的包装层用 except Exception 兜底,而 SystemExit 继承自 BaseException,会逃逸出这个兜底——逃逸后 CPython 走 handle_system_exit → Py_Exit → exit(),直接杀掉整个应用进程,现象是「一点运行就闪退」。换成普通 Exception 子类 _NeedInput 后,异常被包装层正常捕获,哨兵机制成立。同理,脚本里的 exit()/quit() 也用哨兵 __SCRIPT_EXIT__ 在 ArkTS 层优雅收尾。

这个案例的普适教训:在嵌入式解释器里,「让进程退出」的表达力是一种危险能力,一切「停止/等待/退出」语义都要在宿主层用协议表达,而不是交给异常类型。

4.3 编码探测:中文教学环境的基本功

源文件打开链路实现了三级探测:UTF-8 BOM → UTF-8 严格解码 → 出现替换符则回退 GB18030。中国高校机房的历史文件几乎必有一批 GB 编码的 .py,这一层不做,用户第一印象就是「打开的文件是乱码」。


五、验收门:用上游真源码当测试标准

「解释器能跑」和「Thonny 真的在跑」是两回事。工程设了一个 P2 验收门,runTest 在真机执行一段固定脚本:

import thonny
ver = thonny.get_version()          # 必须以 4. 开头
import thonny.common as common      # 公共模块可导入、可调用
assert ver and str(ver).startswith('4.')

判定条件包括 rc=0、Hello 输出、thonny 4.x、common_ok True、计数与 Done 全部命中。验收标准锚在上游真源码上,而不是自己写的 demo——这保证了「演示能跑」与「产品可用」是同一件事。

一个诚实的限制也写进了注释:静态 libpython 嵌入下,lib-dynload/*.so 的 dlopen 通常会失败(缺共享 libpython),所以 thonny.backend(拉起 math.so 等 C 扩展)只做软探针、失败不判死——这是 P1/P2 阶段的已知边界,不是「源码没带上」。


六、调试与真机实测

真机验收覆盖四条链路:冷启动解压运行时 → Shell 就绪横幅(base_prefix 指向沙箱路径)→ 编辑/运行/输出闭环 → input() 交互往返。实测要点:

  • Shell 横幅即体检报告:base_prefix、Python 版本串(含交叉编译工具链信息)直接可见,真机上一眼判定「跑的是不是目标运行时」;
  • 变量面板:运行结束后枚举全局命名空间,函数、模块、数值分类呈现,配合多标签文件管理覆盖完整教学会话;
  • 嵌入模式的边界如实呈现:已提交给解释器的代码无法强制中断(应用内提示「嵌入模式限制」),UI 的「停止」按钮用于提交前状态——把做不到的事明说,好过做一个点了没反应的按钮;
  • 编码探测用 GB18030 历史文件实测;%Run 魔法行、提示符配色(紫 prompt / 蓝 stdin / 红 stderr)逐项对照 Thonny 上游主题还原。

上架材料同样从工程事实出发:三张预览图全为真机实拍;权限表整张缺席——解释器、文件、变量面板全部工作在沙箱内,无任何系统能力申请。


在这里插入图片描述

七、上架后迭代

从 1.0.x 到 1.2.0(versionCode 1020000)的迭代主线是「从运行时验证到 IDE 体验」:

  1. Shell 交互升级:%Run 魔法行、输入等待、exit 哨兵,把 Thonny 的 Shell 习惯搬过来;
  2. 变量面板:从「输出对不对」进到「状态看得见」,这是教学工具的核心增值;
  3. 运行时版本化:RUNTIME_MARKER 机制固化,任何运行时内容变更都走「改标记 → 全量重铺」的受控路径。

后续路线:交叉编译高频 C 扩展补齐 lib-dynload、教学示例工程随包、变量面板接入「表达式求值」。所有路线共享同一条红线:不申请权限、不引入网络。


八、踩坑总结

  1. 静态链进 .so 是沙箱跑解释器的唯一正解,filesDir noexec 决定了「带二进制」路线全灭。
  2. 运行时 zip 必须带版本标记,内容变更与标记递增绑定,否则真机灵异问题吃掉数天。
  3. PYTHONDONTWRITEBYTECODE 必须设——沙箱内写 .pyc 既浪费又可能与只读目录冲突。
  4. PyRun_SimpleString 包装层的 except Exception 挡不住 SystemExit,它会一路走到 Py_Exit 杀进程;一切停止/等待语义用普通异常 + 哨兵协议表达。
  5. 结构探针打进日志:解释器起不来的原因九成在文件布局,探针让它是「一眼可见」而不是「猜」。
  6. 验收门锚在上游真源码(import thonny + version + common),自己的 demo 全绿不算数。
  7. 编码探测(BOM → UTF-8 → GB18030)是中文教学场景的基本功,不是可选项。

九、结语

青蟒的技术主线只有一句话:别把解释器当外部程序,把它当库。CPython 静态链进 NAPI 的那一刻,进程边界、exec 限制、noexec 挂载这些约束全部失效——代价是要自己接管解释器的 stdio 与生命周期,而接管得越彻底,能实现的交互语义(input 哨兵、退出哨兵、变量观察)就越完整。

对同类「内嵌运行时」应用(Python、Lua、JS 引擎皆然),本文的可复用结论是:运行时资产要版本化、探针要进日志、验收门要锚在上游真源码、一切退出语义要协议化。剩下的事,就是像青蟒这样,把上游 IDE 的体验一点点搬到真机上。

源码与构建说明见开源仓库;本文代码摘录均来自工程实际提交。

Logo

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

更多推荐