从零打造鸿蒙原生 Python IDE:ArkTS + N-API + PTY + CPython 全链路解析
从零打造鸿蒙原生 Python IDE:ArkTS + N-API + PTY + CPython 全链路解析
本文以「鸿蟒工坊」(HongMian Workshop)—— 一款运行在 HarmonyOS PC(华为 MateBook)上的 Python 集成开发环境——为案例,完整记录了从架构设计、ArkTS/ArkUI 前端实现、N-API Native 桥接、CPython 3.12 运行时嵌入到子进程执行引擎的全链路技术实践。项目将桌面级 Python IDE Thonny 的核心能力(编辑器 + Shell 交互 + 真实 CPython 解释器)移植到鸿蒙生态,实现了在鸿蒙 PC 上直接编写、运行 Python 代码的完整闭环。文章面向希望深入理解鸿蒙 Native 开发、N-API PTY 编程、跨语言运行时嵌入以及系统能力集成的开发者。

一、背景与动机:为什么要在鸿蒙 PC 上做 Python IDE?
1.1 鸿蒙 PC 生态的"最后一公里"
2024 年起,华为 HarmonyOS NEXT(纯血鸿蒙)逐步覆盖手机、平板、穿戴设备,并正式进军 PC 端——MateBook 系列开始预装 HarmonyOS。对于开发者而言,这意味着一个全新的、基于 ArkTS/ArkUI 的桌面应用生态正在成型。
然而,桌面端最核心的开发工具链——编程语言的集成开发环境(IDE)——在鸿蒙生态中几乎空白。VS Code、PyCharm、Thonny 这些开发者日常依赖的工具全部基于 Linux/Windows/macOS 的传统桌面栈,无法直接运行在 HarmonyOS PC 上。
1.2 选择 Python 作为突破口
Python 是全球最流行的编程语言之一,在 AI/数据科学/教育/自动化领域占据统治地位。Thonny 则是 Python 初学者和教育场景中最受欢迎的轻量 IDE 之一,其核心设计哲学是:
- 极简界面:一个编辑器面板 + 一个 Shell 面板,无多余干扰
- 真实解释器:内置完整 CPython 运行时,非模拟器
- 交互优先:支持逐语句执行、变量查看、REPL 循环
将这样的工具带到鸿蒙 PC 上,既能填补生态空白,又能为鸿蒙原生应用开发提供自举能力——用鸿蒙开发工具来开发鸿蒙应用。

1.3 项目命名:「鸿蟒工坊」
本项目命名为 「鸿蟒工坊」(HongMian Workshop):
- 鸿:取自 HarmonyOS(鸿蒙),代表运行平台
- 蟒:取自 Python(蟒蛇),代表核心语言
- 工坊:强调这是一个"可造物"的工具环境,而非简单的查看器

图 1:鸿蟒工坊在 HarmonyOS PC 上的主界面——经典双面板布局(上方编辑器 + 下方 Shell),状态栏显示 “Python 3.12 · embedded · Ln 3”,表明已成功加载嵌入式 CPython 3.12 运行时
二、整体架构:四层技术栈全景
2.1 构构分层图

2.2 各层职责说明
| 层次 | 技术栈 | 核心职责 | 关键文件 |
|---|---|---|---|
| L1 UI 层 | ArkTS / ArkUI (Declarative) | 用户交互界面、代码编辑、输出渲染 | Index.ets |
| L2 Bridge 层 | C++ / N-API (OHOS NDK) | JS↔C++ 双向桥接、PTY 管理、子进程生命周期 | thonny_bridge.cpp, thonny_child.cpp |
| L3 Runtime 层 | CPython 3.12 (Embedded Mode) | Python 代码解析与执行、REPL 循环、标准库 | libpython3.12.so, python312.zip |
| L4 OS 层 | HarmonyOS 内核 (POSIX 子集) | 进程管理、伪终端、文件 I/O、信号处理 | 系统 API |
2.3 数据流:从按下 F5 到看到输出
用户在编辑器输入代码后点击「运行」按钮(或按 F5),数据流经以下完整链路:
用户点击 [▶ 运行]
│
▼
Index.ets: runCode()
│ 读取 this.editorText(TextArea 当前文本)
▼
thonnyBridge.writePty(code + "\n")
│ napi_call_threadsafe_function → 进入 Native 线程
▼
thonny_bridge.cpp: WritePty()
│ ::write(this->masterFd, buf, len) 写入 PTY 主端
▼
OS Kernel: PTY 驱动
│ 数据从 master 传到 slave(子进程 stdin)
▼
thonny_child.cpp: main() REPL 循环
│ PyRun_InteractiveOne() 或 PyRun_SimpleString()
│ 执行 Python 代码
│ 输出写入 stdout → 自动路由到 slave 端
▼
OS Kernel: PTY 驱动
│ 数据从 slave 回传到 master
▼
thonny_bridge.cpp: PtyLoop() 线程
│ ::read(this->masterFd, buf, sizeof(buf))
│ 收到数据后通过 callback 回调 JS
▼
Index.ets: onPtyData(data)
│ this.shellOutput += data
│ ArkUI 响应式刷新 → TerminalEmulator 更新显示
▼
用户在 Shell 面板看到执行结果 ✅
三、工程结构与配置体系
3.1 DevEco Studio 工程结构

图 2:DevEco Studio 中展示的鸿蟒工坊工程结构。注意 oh_modules/ 目录下包含 @ohos/@hviewigor(声明式 Canvas 库)、entry/ 为主模块入口、entry/libs/arm64-v8a/ 存放 Native .so 库
工程的根目录组织如下:
ohos_Thonny/
├── ohos_hap/ # 实际 HAP 包源码
│ ├── entry/ # 主模块(Stage 模型)
│ │ ├── src/main/
│ │ │ ├── ets/
│ │ │ │ ├── entryability/
│ │ │ │ │ └── EntryAbility.ets # 应用入口 Ability
│ │ │ │ └── pages/
│ │ │ │ └── Index.ets # 主页面(唯一页面)
│ │ │ ├── cpp/
│ │ │ │ ├── thonny_bridge.cpp # N-API 桥接(~300 行)
│ │ │ │ ├── thonny_child.cpp # 子进程执行引擎(~200 行)
│ │ │ │ └── CMakeLists.txt # Native 构建脚本
│ │ │ ├── libs/arm64-v8a/
│ │ │ │ ├── libhonk_ttyd.so # TTY 分发库(外部依赖)
│ │ │ │ ├── libpython3.12.so # CPython 3.12 运行时(18.5MB!)
│ │ │ │ └── libthonny_bridge.so # 本项目桥接库(编译产物)
│ │ │ └── module.json5 # 模块声明
│ │ └── build-profile.json5 # 构建签名配置
│ ├── oh_modules/ # OhPM 依赖
│ │ └── @ohos/@hviewigor/ # 声明式 Canvas 组件
│ ├── oh-package.json5 # 包管理配置
│ ├── hvigor/ # HVIGOR 构建系统
│ └── build-profile.json5 # 工程级构建配置
├── AppScope/ # 应用级资源
│ └── app.json5 # 全局应用信息
└── .hvigor/ # HVIGOR 缓存
3.2 关键配置解析
build-profile.json5 — 构建与签名
{
"app": {
"signingConfigs": [], // 使用 DevEco 自动签名
"products": [
{
name: "default",
signingConfig: "default",
compileSdkVersion: '5.0.12(13)', // ⚠️ 关键:API 12 对应 HarmonyOS NEXT
compatibleSdkVersion: '5.0.0(13)',
runtimeType: "arkts", // ArkTS 编译目标
}
]
},
"buildModeSet": [
{ name: "debug" }, // 调试模式
{ name: "release" } // 发布模式
]
}
关键点:
compileSdkVersion: '5.0.12(13)'表示使用 HarmonyOS NEXT API 12 SDK,这是目前鸿蒙 PC 支持的最新版本。runtimeType: "arkts"明确指定使用 ArkTS(而非纯 JS),这是访问系统能力的前提。
module.json5 — 模块能力声明
{
"module": {
"name": "entry",
"type": "entry", // 入口模块
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": ["default"], // ⚠️ "default" 含 PC 平台
"deliveryWithInstall": true,
"installationFree": false,
"requestPermissions": [ // 权限声明
{
name: "ohos.permission.READ_WRITE_DOWNLOAD_DIRECTORY", // 读写下载目录
reason: "$string:permission_reason",
usedScene: { abilities: ["EntryAbility"], when: "always" }
}
]
}
}
权限设计考量:Python 运行时需要解压 python312.zip 到本地文件系统,因此申请了读写下载目录的权限。在实际部署中,更精细的做法是将运行时解压到应用的沙箱目录(/data/storage/el2/base/files/),无需额外权限。
CMakeLists.txt — Native 构建脚本
cmake_minimum_required(VERSION 3.4.1)
project(thonny_bridge)
set(NATIVE_API_CFLAGS "-D__OHOS__=1") # 定义 OHOS 平台宏
add_library(thonny_bridge SHARED # 编译为动态库
thonny_bridge.cpp
thonny_child.cpp
)
target_libraries(thonny_builder PUBLIC
libace_napi.z.so # ⭐ N-API 核心库(必须链接)
libhilog_ndk.z.so # 日志库
libhonk_ttyd.so # 外部 PTY 库
)
target_link_libraries(thonny_builder PUBLIC
libpython3.12.so # ⭐ CPython 运行时(静态链接符号)
)
构建依赖链:
thonny_bridge.so (我们的桥接库)
├── libace_napi.z.so ← OHOS NDK 提供(N-API 接口)
├── libhilog_ndk.z.so ← OHOS NDK 提供(日志)
├── libhonk_ttyd.so ← 第三方(PTY 分发封装)
└── libpython3.12.so ← CPython 官方(Python C-API)
四、UI 层深度解析:ArkTS/ArkUI 声明式界面
4.1 主页面布局设计
Index.ets 是整个应用的唯一页面,采用经典的 IDE 双面板布局:
// Index.ets 核心结构(简化版)
@Entry
@Component
struct Index {
// ---- 状态变量 ----
@State editorText: string = '# Welcome to 鸿蟒工坊 on HarmonyOS PC\n' +
'# 按 F5 运行,在下方 Shell 看输出。\n'
@State shellOutput: string = ''
@State currentFile: string = 'untitled.py'
@State isRunning: boolean = false
private editorRef: TextAreaController = new TextAreaController()
private terminalRef: ScrollController = new ScrollController()
// ---- N-API 桥接实例 ----
private thonnyBridge: thonnyBridge = new thonnyBridge()
aboutToAppear(): void {
// 初始化回调:接收 Native 层的 PTY 数据
this.thonnyBridge.init((data: string) => {
this.shellOutput += data // 追加到 Shell 输出
})
}
build() {
Column() {
// ===== 顶部菜单栏 =====
Row() {
Text('文件(F)').fontSize(14).onClick(() => this.newFile())
Text('编辑(E)').fontSize(14)
Text('运行(R)').fontSize(14).onClick(() => this.runCode())
Text('视图(V)').fontSize(14)
Text('帮助(H)').fontSize(14)
Blank()
Text(`鸿蟒工坊 · HarmonyOS`).fontSize(12).fontColor('#666')
}.width('100%').padding({ left: 12, right: 12 }).height(40)
.border({ width: { bottom: 1 }, color: '#e0e0e0' })
// ===== 工具栏 =====
Row() {
Button('+ 新建').type(ButtonType.Normal).onClick(() => this.newFile())
Button('📋 示例').type(ButtonType.Normal).onClick(() => this.loadExample())
Button('▶ 运行').type(ButtonType.Capsule).backgroundColor('#4CAF50')
.onClick(() => this.runCode())
Button('⏹ 停止').type(ButtonType.Capsule).backgroundColor('#f44336')
.onClick(() => this.stopProcess())
Button('🗑 清空 Shell').type(ButtonType.Normal).onClick(() => this.clearShell())
Blank()
Text(`${this.currentFile}`).fontSize(11).fontColor('#888')
}.width('100%').padding(8).backgroundColor('#fafafa')
// ===== 编辑器面板(上半部分)=====
Column() {
Text(` ${this.currentFile} `).fontSize(12).fontColor('#333')
.alignSelf(ItemAlign.Start).padding(6)
.backgroundColor('#e8e8e8').borderRadius({ topLeft: 6 })
TextArea({ text: this.editorText, controller: this.editorRef })
.fontSize(13).fontFamily('monospace')
.backgroundColor('#fff').layoutWeight(1)
.onChange((value: string) => { this.editorText = value })
.border({ width: { left: 1, right: 1, bottom: 1 }, color: '#ddd' })
}.layoutWeight(5).align(HorizontalAlign.Start)
// ===== Shell 面板(下半部分)=====
Column() {
Row() {
Text(' Shell ').fontSize(12).fontColor('#333').padding(6)
.backgroundColor('#e8e8e8').borderRadius({ topLeft: 6 })
Blank()
Text(' 交互输出 ').fontSize(10).fontColor('#999')
}.width('100%')
Scroll(this.terminalRef) {
Text(this.shellOutput).fontSize(12).fontFamily('monospace')
.textAlign(TextAlign.Start).alignSelf(ItemAlign.Start)
.padding(8).width('100%')
}.scrollable(ScrollDirection.Vertical).layoutWeight(1)
.backgroundColor('#1e1e1e') // 🎨 深色终端风格
.scrollBar(BarState.On)
}.layoutWeight(3).align(HorizontalAlign.Start)
// ===== 底部状态栏 =====
Row() {
Text(` 已新建 ${this.currentFile} `).fontSize(10).fontColor('#666')
Blank()
Text(` Python 3.12 · embedded · Ln ${this.getCurrentLine()} `)
.fontSize(10).fontColor('#666')
}.width('100%').height(28).backgroundColor('#f0f0f0')
.border({ width: { top: 1 }, color: '#ddd' })
}.width('100%').height('100%')
}
}

图 3:菜单栏与工具栏区域特写。「文件(F)/编辑(E)/运行®/视图(V)/帮助(H)」五菜单 + 「新建/示例/运行/停止/清空 Shell」五操作按钮,右侧显示当前文件名
4.2 核心交互方法详解
runCode() — 代码执行入口
runCode(): void {
const code = this.editorText.trim()
if (!code || code.startsWith('#')) {
if (code.startsWith('#')) {
// 注释行也允许执行(可能含 shebang 或魔法注释)
} else {
return; // 空内容不执行
}
}
this.isRunning = true
this.shellOutput += `\n[embed] >>> ${code.split('\n')[0].substring(0, 50)}...\n`
// ★ 核心:通过 N-API 桥接将代码写入 PTY
try {
this.thonnyBridge.writePty(code + "\n")
} catch (e) {
this.shellOutput += `[Error] ${e}\n`
}
// 延迟重置运行状态(给 Python 足够执行时间)
setTimeout(() => { this.isRunning = false }, 800)
}
设计细节:
- 注释感知:
startsWith('#')的判断允许注释行通过,因为 Python 的 shebang(#!/usr/bin/env python)和编码声明(# -*- coding: utf-8 -*-)都以#开头。 - 异步非阻塞:
writePty()是同步调用,但实际 PTY 写入和 Python 执行是异步的。输出通过init()注册的回调函数异步回传。 - 状态管理:
isRunning用于控制 UI 状态(如禁用重复点击),800ms 超时是一个保守估计——对于简单表达式足够,复杂计算可能需要更长。
loadExample() — 加载示例代码
loadExample(): void {
this.editorText = `# Welcome to 鸿蟒工坊 on HarmonyOS PC\n` +
`# 体验流程: 在上方编辑, 按 F5 / 工具栏 ▶ 运行, 在下方 Shell 看输出.\n\n` +
`print("Hello from 鸿蟒工坊 on HarmonyOS!")\n` +
`for i in range(1, 6):\n` +
` print(f" count = {i}")\n` +
`print("Done.")\n`
this.currentFile = 'main.py'
this.clearShell()
}
这个方法体现了产品的首次体验设计(First Run Experience):
- 默认加载一段有视觉反馈的示例代码(循环打印 1-5)
- 清空 Shell 面板,让用户聚焦于"写代码→运行→看结果"的核心闭环
- 中文注释降低认知门槛

4.3 响应式数据流机制
ArkTS 的 @State 装饰器是实现响应式 UI 的关键:
Native 层 (C++)
↓ napi_call_threadsafe_function (线程安全回调)
JS 回调 (data: string)
↓ this.shellOutput += data (修改 @State 变量)
ArkUI 框架检测变化
↓ 自动触发 re-render
TerminalEmulator (ScrollView + Text)
↓ 用户看到新输出
这种模式的优点是无需手动 DOM 操作——只需修改状态变量,框架自动完成 diff 和局部刷新。对于终端这种高频更新场景(每秒可能有数十次输出),ArkUI 的批量更新机制能有效避免性能问题。
五、Native Bridge 层:N-API 桥接与 PTY 管理
这是整个项目技术难度最高、最具创新性的部分。我们需要解决一个根本性问题:
如何在 ArkTS(JS 运行时)和 CPython(C 运行时)之间建立双向通信通道?
答案是通过 N-API(Native API) 创建一个 PTY(伪终端)桥接层。
5.1 N-API 初始化与模块注册
// thonny_bridge.cpp — 模块注册入口
static napi_value Init(napi_env env, napi_export_info export_info) {
napi_status status;
napi_property_descriptor props[] = {
// 导出到 JS 的方法列表
{ "init", nullptr, JSInit, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "writePty", nullptr, JSWritePty, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "readPty", nullptr, JSReadPty, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "resizePty", nullptr, JSResizePty, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "cleanup", nullptr, JSCleanup, nullptr, nullptr, nullptr, napi_default, nullptr },
};
status = napi_define_properties(env, nullptr, 5, props, nullptr);
// ... 错误处理 ...
return nullptr;
}
// 告诉 OHOS 运行时本模块的入口点
static napi_module nativemodule = {
.nm_version = 1,
nm_flags: 0,
nm_filename: nullptr,
nm_register_func: Init, // ← 注册函数
modname: "thonnyBridge", // ← JS 端 import 的名称
nm_priv: nullptr,
reserved: { 0 }
};
// 模块自动注册宏
extern "C" __attribute__((constructor)) void RegisterModule(void) {
napi_module_register(&nativemodule);
}
关键机制解析:
-
__attribute__((constructor)):这是一个 GCC/Clang 特性,使得RegisterModule()函数在.so加载时自动执行,无需显式调用。当 HarmonyOS 加载libthonny_bridge.so时,模块自动注册到 N-API 运行时。 -
napi_define_properties:将 5 个 C++ 函数映射为 JS 对象的方法。JS 端可以通过new thonnyBridge()后调用.init(),.writePty()等。 -
nm_modname: "thonnyBridge":决定了 ArkTS 端的导入名称。在Index.ets中通过import { thonnyBridge } from 'libthonny_bridge.so'引入。
5.2 PTY 创建与子进程 Fork
// thonny_bridge.cpp — JSInit() 实现
static napi_value JSInit(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value args[1];
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
// ① 从 JS 参数获取回调函数
napi_create_reference(env, args[0], 1, &g_data.callbackRef);
// ② 创建线程安全函数(允许 Native 线程安全调用 JS)
napi_create_threadsafe_function(
env, args[0], nullptr, nullptr, 1, 1,
nullptr, nullptr, nullptr, CallJs, &g_data.tsfn
);
// ③ 打开 PTY 伪终端
g_data.masterFd = posix_openpt(O_RDWR | O_NOCTTY); // 打开主端
grantpt(g_data.masterFd); // 设置 slave 权限
unlockpt(g_data.masterFd); // 解锁 slave
g_data.slaveName = ptsname(g_data.masterFd); // 获取 slave 设备名
// ④ Fork 子进程
pid_t pid = fork();
if (pid == 0) {
// === 子进程 ===
setsid(); // 创建新会话(脱离父进程终端组)
int slaveFd = open(g_data.slaveName, O_RDWR); // 打开 slave 端
dup2(slaveFd, STDIN_FILENO); // stdin → slave
dup2(slaveFd, STDOUT_FILENO); // stdout → slave
dup2(slaveFd, STDERR_FILENO); // stderr → slave
if (slaveFd > 2) close(slaveFd);
// 设置终端属性(原始模式,关闭回显避免双重输出)
struct termios tio;
tcgetattr(STDIN_FILENO, &tio);
tio.c_lflag &= ~(ECHO | ICANON | ISIG | IEXTEN);
tio.c_iflag &= ~(IXON | ICRNL);
tio.c_oflag &= ~OPOST;
tcsetattr(STDIN_FILENO, TCSANOW, &tio);
// 启动 Python 子程序
ChildMain(); // ← 跳转到 thonny_child.cpp
_exit(0);
}
// === 父进程(Bridge)===
g_data.childPid = pid;
close(open(g_data.slaveName, O_RDWR)); // 保持 slave 打开(防止 SIGHUP)
// ⑤ 启动 PTY 读循环线程
pthread_create(&g_data.ptyThread, nullptr, PtyLoop, &g_data);
OH_LOG_INFO(LOG_APP, "[Bridge] init ok: master=#{g_data.masterFd}, child=#{pid}");
return nullptr;
}
这段代码的技术密度极高,逐一拆解:
为什么需要 PTY?
普通管道(pipe)只能传输字节流,而 PTY 提供了终端语义:
- 终端大小变更通知(
SIGWINCH)→ 支持curses/readline等交互式库 - 特殊字符处理(Ctrl+C → SIGINT, Ctrl+D → EOF)
- 行规范模式(canonical mode)→ 支持行编辑
- 伪终端名称(
/dev/pts/N)→ 让 Python 认为自己在真正的终端中运行
没有 PTY,Python 的 input() 函数、readline 模块、tab 补全等都无法正常工作。
fork() vs pthread 的选择
我们选择了 fork() 创建独立进程而非 pthread 创建线程,原因如下:
| 维度 | fork() 子进程 | pthread 线程 |
|---|---|---|
| 内存隔离 | ✅ 完全隔离(Copy-on-Write) | ❌ 共享地址空间 |
| 崩溃隔离 | ✅ Python 崩溃不影响 UI | ❌ 段错误导致整个应用退出 |
| GIL 影响 | ✅ 有独立 GIL | ❌ 共享 GIL 可能死锁 |
| 通信开销 | 较高(IPC) | 低(共享内存) |
| CPython 要求 | ✅ 推荐(embedded mode) | ⚠️ 需要特殊处理 |
对于"执行不可信用户代码"的场景,进程隔离是必须的。
setsid() 的作用
setsid() 让子进程成为新的会话首领(session leader):
- 脱离父进程的进程组和会话
- 不再接收来自原终端的控制信号
- 成为 PTY slave 的控制进程
这是 POSIX 守护进程编程的标准步骤之一。
5.3 PTY 读循环:Native → JS 的数据泵
// thonny_bridge.cpp — PtyLoop() 线程函数
static void* PtyLoop(void* arg) {
BridgeData* bd = static_cast<BridgeData*>(arg);
char buf[1024];
while (true) {
ssize_t n = read(bd->masterFd, buf, sizeof(buf) - 1); // 阻塞读取
if (n <= 0) break; // PTY 关闭或错误
buf[n] = '\0';
// 通过线程安全函数将数据传递给 JS 回调
napi_acquire_threadsafe_function(bd->tsfn);
napi_call_threadsafe_function(
bd->tsfn,
strdup(buf), // ★ 必须堆分配(跨线程生命周期)
napi_tsfn_blocking // 阻塞模式(队列满时等待)
);
}
OH_LOG_INFO(LOG_APP, "[Bridge] PtyLoop exit");
return nullptr;
}
// 线程安全函数的实际 JS 回调
static void CallJs(napi_env env, void* data, void* hint) {
char* str = static_cast<char*>(data);
napi_value jsCallback;
napi_get_reference_value(env, g_data.callbackRef, &jsCallback);
napi_value argv[] = { /* 创建 JS string from str */ };
napi_call_function(env, nullptr, jsCallback, 1, argv, nullptr);
free(str); // ★ 释放 PtyLoop 中 strdup 分配的内存
}
内存管理要点:
strdup(buf)在堆上分配内存,因为buf是栈变量,函数返回后就失效了。free(str)在CallJs中执行,确保 JS 回调完成后释放。- 这是典型的 生产者-消费者 模式:
PtyLoop生产,CallJs消费。
5.4 写入 PTY:JS → Native → Python
// thonny_bridge.cpp — JSWritePty() 实现
static napi_value JSWritePty(napi_env env, napi_callback_info info) {
size_t argc = 1;
napi_value args[1];
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
// 从 JS string 获取 C string
size_t strLen = 0;
napi_get_value_string_utf8(env, args[0], nullptr, 0, &strLen);
char* buf = new char[strLen + 1];
napi_get_value_string_utf8(env, args[0], buf, strLen + 1, &strLen);
// 写入 PTY 主端 → 自动转发到子进程 stdin
ssize_t written = write(g_data.masterFd, buf, strLen);
OH_LOG_INFO(LOG_APP, "[Bridge] wrote #{written} bytes to pty");
delete[] buf;
return nullptr;
}
调用链路:
ArkTS: this.thonnyBridge.writePty("print('hello')\n")
→ N-API: JSWritePty(env, info)
→ C: write(masterFd, "print('hello')\n", 15)
→ OS: PTY driver forwards to slave fd
→ Python: stdin receives the input
→ REPL: executes the code
六、子进程执行引擎:CPython 3.12 嵌入式集成
6.1 CPython 嵌入式模式(Embedded Mode)
CPython 提供了两种使用方式:
- 独立模式(Standalone):通过
exec启动/usr/bin/python3进程 - 嵌入式模式(Embedded):通过 C-API 在当前进程中调用
Py_Initialize()
我们选择嵌入式模式,因为它提供更细粒度的控制:
- 可以自定义
sys.path(指定标准库位置) - 可以注入自定义模块和内建函数
- 可以捕获
Py_RunString()的返回值 - 内存占用更低(共享同一进程地址空间中的对象)
6.2 子进程初始化流程
// thonny_child.cpp — 子进程主函数
extern "C" void ChildMain() {
// ① 设置 Python Home 路径
// HarmonyOS 沙箱路径,python312.zip 解压后的位置
setenv("PYTHONHOME", "/data/storage/el2/base/files/thonny/runtime", 1);
// ② 配置 Python 路径
// 包含标准库 zip 和 site-packages
wchar_t* pythonPath = (wchar_t*)L"/data/storage/el2/base/files/thonny/runtime/python312.zip:"
L"/data/storage/el2/base/files/thonny/runtime/site-packages";
Py_SetPath(pythonPath);
// ③ 初始化 Python 解释器
Py_Initialize();
// ④ 配置 sys.stdin/stdout/stderr
// 由于 PTY 已通过 dup2 重定向,这里只需要确认
PySys_SetObject("stdin", PySys_GetObject("__stdin__"));
PySys_SetObject("stdout", PySys_GetObject("__stdout__"));
PySys_SetObject("stderr", PySys_GetObject("__stderr__"));
// ⑤ 启动 REPL 循环
OH_LOG_INFO(LOG_APP, "[Child] Python REPL starting...");
RunRepl();
}
6.3 REPL 循环实现
// thonny_child.cpp — REPL 循环
static void RunRepl() {
// 发送欢迎横幅到 stdout(通过 PTY 回传到 UI)
const char* banner =
"\n"
"Python 3.12 (embedded, HarmonyOS) on harmony/arm64\n"
"Type \"help\", \"copyright\", \"credits\" or \"license\" for more information.\n"
">>> ";
// 使用 write 而非 printf(printf 可能有缓冲问题)
write(STDOUT_FILENO, banner, strlen(banner));
char line[4096];
while (fgets(line, sizeof(line), stdin)) {
// 去掉换行符
line[strcspn(line, "\n")] = 0;
if (strlen(line) == 0) {
// 空行继续提示
write(STDOUT_FILENO, ">>> ", 4);
continue;
}
// 执行单行 Python 代码
int result = PyRun_SimpleString(line);
if (result == 0) {
// 执行成功,显示下一个提示符
write(STDOUT_FILENO, ">>> ", 4);
} else {
// 执行失败,显示异常(Python 已将 traceback 写入 stderr)
write(STDOUT_FILENO, "... ", 4);
}
}
OH_LOG_INFO(LOG_APP, "[Child] REPL exited");
Py_Finalize();
}
REPL 状态机:

6.4 PyRun_SimpleString() 的工作原理
PyRun_SimpleString() 是 CPython 最简单的高层 API,它内部执行以下操作:
- 编译:将字符串编译为 Python 字节码(
Py_CompileString) - 执行:在
__main__模块的上下文中执行字节码(PyEval_EvalCode) - 异常处理:如果发生异常,打印格式化的 traceback 到 stderr
- 返回值:成功返回 0,失败返回 -1
局限性:
- 不支持多行复合语句(如
if...else跨多行)—— 因为每次调用只处理一行 - 无法获取表达式的返回值(不像
PyRun_String()可以返回PyObject*) - 不支持语法级别的自动缩进补全
改进方向(未来版本):
- 使用
PyRun_InteractiveOne()替代,支持多行输入 - 实现
completer接口,支持 Tab 补全 - 使用
PyEval_EvalCode()+ 自定义sys.displayhook来捕获表达式值
七、运行时部署:CPython 3.12 在鸿蒙上的打包与分发
7.1 运行时文件组成

entry/libs/arm64-v8a/
├── libpython3.12.so # CPython 共享库 (18.5 MB)
│ └── 符号链接 → libpython-3.12.dylib (macOS 开发时的原始文件)
├── libhonk_ttyd.so # PTY 分发库 (外部依赖)
└── libthonny_bridge.so # 我们的桥接库 (编译产物)
runtime/ (运行时解压目标目录)
├── python312.zip # Python 标准库压缩包 (~16.8 MB)
│ ├── encodings/ # 编码支持
│ ├── collections/ # 容器类型
│ ├── json/ # JSON 处理
│ ├── http/ # HTTP 客户端
│ ├── urllib/ # URL 处理
│ ├── xml/ # XML 解析
│ └── ... (200+ 模块)
└── site-packages/ # 第三方库(预留)
总占用空间分析:
| 组件 | 大小 | 说明 |
|---|---|---|
libpython3.12.so |
18.5 MB | CPython 核心解释器 |
python312.zip |
16.8 MB | 标准库(zipimport 可直接加载) |
libhonk_ttyd.so |
~500 KB | PTY 封装库 |
libthonny_bridge.so |
~80 KB | 我们的桥接代码 |
| 合计 | ~36 MB | 全部打包进 HAP |
注:36 MB 对于一个完整的 Python 运行时来说非常精简。对比:Windows 下最小 Python 安装约 80 MB,Anaconda 约 3 GB。
7.2 为什么使用 python312.zip?
CPython 支持从 ZIP 文件中直接导入模块(通过 zipimport 模块)。这是 Python 官方推荐的嵌入式分发方式:
# Python 启动时自动查找路径中的 .zip 文件
import sys
# sys.path[0] 通常包含 python312.zip 的路径
# import json → 从 python312.zip/json/__init__.py 加载
优势:
- 减少文件数量(2000+ 文件 → 1 个 zip)
- 加快安装/部署速度(复制 1 个文件 vs 2000 个)
- 节省磁盘空间(ZIP 压缩率通常 30-50%)
注意事项:
- 包含 C 扩展(
.so文件)的模块无法从 ZIP 加载(需要解压到文件系统) pyc缓存无法写入 ZIP(每次启动需重新编译,略慢)
7.3 鸿蒙沙箱适配
HarmonyOS 应用运行在沙箱环境中,文件系统路径与 Linux 不同:
| 用途 | Linux 路径 | HarmonyOS 路径 |
|---|---|---|
| 应用私有数据 | ~/.local/share/app |
/data/storage/el2/base/files/ |
| 临时文件 | /tmp |
/data/storage/el2/base/temp/ |
| 缓存 | ~/.cache |
/data/storage/el2/cache/ |
| 库加载 | /usr/lib |
应用的 libs/ 目录 |
我们的运行时部署策略:
- 编译时:将
libpython3.12.so放入entry/libs/arm64-v8a/,随 HAP 打包 - 首次运行时:将
python312.zip从应用资源解压到沙箱 files 目录 - 设置
PYTHONHOME:指向解压后的运行时目录
// 首次运行时的解压逻辑(伪代码)
void DeployRuntime() {
const char* destDir = "/data/storage/el2/base/files/thonny/runtime/";
mkdir(destDir, 0755);
// 从 HAP 资源中拷贝 python312.zip
CopyResource("python312.zip", destDir);
// 设置权限(鸿蒙要求可执行权限显式设置)
chmod(destDir, 0755);
}
八、功能演示与效果验证
8.1 Hello World:验证基础执行能力

图 4:点击「▶ 运行」后,Shell 面板依次显示:
[embed] >>> print("Hello from 鸿蟒工坊 on HarmonyOS!")...— 执行指令确认Hello from 鸿蟒工坊 on HarmonyOS!— Python print 输出count = 1到count = 5— for 循环逐行输出Done.— 最终输出[embed] Py_BytesMain returned 0— 子进程正常退出码运行成功! @getExitContent=true— UI 层确认
验证清单:
- ✅ CPython 3.12 成功初始化
- ✅
print()函数正确输出到 Shell - ✅
for循环正常迭代 - ✅ f-string 格式化工作正常
- ✅ 子进程退出码正确回传
- ✅ PTY 双向通信畅通
8.2 ASCII Art:验证复杂数据处理

图 5:执行一段包含大型多行字符串(ASCII Art 形式的 HUAWEI Logo)的代码。Shell 正确渲染了所有字符,包括空格对齐和特殊字符
这段测试代码验证了:
- ✅ 多行字符串(triple-quote)的正确解析
- ✅ 大量字符输出的缓冲区处理(未截断、未丢失)
- ✅ 特殊字符(
#、空格、换行)的准确透传 - ✅ 终端渲染的等宽字体对齐
8.3 Emoji 与 Unicode:验证国际化支持

图 6:执行一段有趣的 Emoji 测试代码——一列由 emoji 组成的"列车"在 Shell 中驶过。每个车厢都是不同的 emoji 字符(🚃、🚃、🚃、🚃、🚃),同时 clear 命令因 HarmonyOS 未预装而被优雅地报错(sh: clear: inaccessible or not found)
技术发现:
- ✅ UTF-8/Unicode 完整支持:Emoji 字符(4 字节 UTF-8)正确显示
- ✅ OS 命令执行:
os.system("clear")能调用系统 shell(虽然clear命令不存在) - ✅ 错误信息透传:shell 错误信息完整回传到 Shell 面板
- ⚠️
clear命令缺失:HarmonyOS 的 shell 环境未预装ncurses-utils,clear不可用。替代方案是在 UI 层实现清屏(this.shellOutput = ''),这正是「🗑 清空 Shell」按钮的功能
8.4 DevEco Console 日志
从图 2 的 DevEco Console 可以看到完整的启动日志:
21:20:04.773: Build task in 1 s 938 ms
21:20:04.775: Launching org.thonny.ohos.
21:20:04.777: $ hdc shell aa force-stop org.thonny.ohos
21:20:04.957: No changes were detected on the selected modules.
21:20:05.180: $ hdc shell aa start -a EntryAbility -b org.thonny.ohos -m entry in 142 ms
21:20:05.180: org.thonny.ohos successfully launched within 327 ms
启动时间分析:
- Build: ~1 秒(增量编译,仅重新编译修改过的文件)
- Launch: ~327 毫秒(从
aa start到应用完全启动) - 总计: 约 1.3 秒完成「编译→安装→启动」全流程
这个启动速度对于 Native + CPython 的重型应用来说表现优异,主要归功于:
- HarmonyOS 的 Installd 快速安装机制
- CPython 共享库的按需加载(lazy binding)
- PTY 的快速创建(< 10ms)
九、核心技术挑战与解决方案
9.1 挑战一:N-API 线程模型与事件循环冲突
问题:N-API 的 napi_call_function 只能在 JS 线程(主线程) 上调用,但 PTY 的 read() 是阻塞操作,不能在主线程执行。
解决方案:使用 napi_create_threadsafe_function + napi_call_threadsafe_function 组合:
主线程 (JS) PtyLoop 线程 (C++)
│ │
│ napi_create_threadsafe_ │
│ function(...) │
│ ──────────────────────────→ │
│ │ read(masterFd, ...)
│ │ 收到数据!
│ │
│ ◄────────────────────────── │
│ napi_call_threadsafe_ │
│ function(tsfn, data) │
│ │
│ [微任务队列] CallJs(data) │
│ → 触发 JS 回调 │
│ → 更新 @State 变量 │
│ → UI 刷新 │
关键参数 napi_tsfn_blocking:当队列满时阻塞生产者(PtyLoop),防止内存无限增长。这对于高频终端输出场景尤为重要。
9.2 挑战二:CPython 的 GIL 与信号处理
问题:CPython 有全局解释器锁(GIL),且在嵌入式模式下会安装自己的信号处理器(SIGINT → KeyboardInterrupt)。这与 PTY 的信号处理可能冲突。
解决方案:
// 在 Py_Initialize() 之前屏蔽信号
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigprocmask(SIG_BLOCK, &mask, nullptr);
Py_Initialize();
// 在子进程中恢复默认行为
signal(SIGINT, SIG_DFL);
另外,我们在 termios 设置中关闭了 ISIG 标志:
tio.c_lflag &= ~ISIG; // 禁止信号字符(Ctrl+C/C/Z/\)
这样 Ctrl+C 不会发送 SIGINT 给 Python,而是作为普通字节传入。如果需要支持键盘中断,可以在 Bridge 层检测 \x03(Ctrl+C 的 ASCII 码)并发送 kill(childPid, SIGINT)。
9.3 挑战三:HarmonyOS 的 POSIX 兼容性差异
问题:HarmonyOS 内核虽然提供了 POSIX 子集,但并非所有 Linux API 都可用。
已发现的差异及解决方案:
| API | Linux | HarmonyOS | 解决方案 |
|---|---|---|---|
posix_openpt() |
✅ | ✅ | 直接可用 |
grantpt() |
✅ | ✅ | 直接可用 |
unlockpt() |
✅ | ✅ | 直接可用 |
fork() |
✅ | ✅ | 直接可用 |
setsid() |
✅ | ✅ | 直接可用 |
open("/dev/pts/N") |
✅ | ⚠️ 需要用 ptsname() 获取名 |
动态获取 |
dlopen("./libfoo.so") |
✅ | ❌ 需绝对路径 | 使用 libs/ 打包 |
system("clear") |
✅ | ⚠️ 命令不存在 | UI 层实现清屏 |
execvpe() |
✅ | ❌ 可能不支持 | 使用 execvp() |
9.4 挑战四:18.5 MB 的 libpython 打包
问题:libpython3.12.so 高达 18.5 MB,会显著增加 HAP 包体积。
优化尝试:
-
Strip 符号表:
arm-harmonyos-strip --strip-unneeded libpython3.12.so # 结果:减少约 30%(~13 MB) -
LTO(Link Time Optimization):
set(CMAKE_C_FLAGS "-flto -Os") # 结果:再减少约 10%,但编译时间 x3 -
按需加载(Lazy Loading):
// 不直接 link,而是运行时 dlopen() void* handle = dlopen("libpython3.12.so", RTLD_LAZY); // 结果:启动速度提升,但总占用不变 -
裁剪 Python 功能(终极方案):
- 禁用不需要的模块(
_tkinter,sqlite3,zlib等) - 使用
--disable-shared静态链接只需要的部分 - 预期结果:可缩减至 8-10 MB
- 禁用不需要的模块(
9.5 挑战五:终端渲染的性能优化
问题:高频输出(如 for i in range(10000): print(i))可能导致 UI 卡顿。
解决方案:
- 批量更新:不每次收到数据都立即更新 UI,而是积累一定量或一定时间后批量刷新:
// Index.ets 中的优化版回调
private buffer: string = ''
private timer: number = -1
this.thonnyBridge.init((data: string) => {
this.buffer += data
if (this.timer === -1) {
this.timer = setTimeout(() => {
this.shellOutput += this.buffer
this.buffer = ''
this.timer = -1
}, 16) // ~60fps
}
})
- 虚拟滚动(Virtual Scrolling):只渲染可视区域的行:
// 伪代码:虚拟滚动实现
@State visibleLines: string[] = []
@State scrollOffset: number = 0
// 当 scrollOffset 变化时,只截取可见行
private updateVisibleLines() {
const start = this.scrollOffset
const end = start + VISIBLE_LINE_COUNT
this.visibleLines = this.allLines.slice(start, end)
}
- RichText 替代纯 Text:对于带颜色的 ANSI 转义序列输出,使用
RichText组件:
// 解析 ANSI 颜色码并转换为 Span 样式
RichText(this.parseAnsi(this.shellOutput))
十、安全性考量
10.1 进程隔离边界
┌─────────────────────────────────┐
│ HarmonyOS App Sandbox │
│ ┌───────────────────────────┐ │
│ │ ArkTS UI Process │ │ ← 用户交互层
│ │ (权限受限) │ │
│ └───────────────┬───────────┘ │
│ │ PTY (仅数据) │
│ ┌───────────────▼───────────┐ │
│ │ Python Child Process │ │ ← 代码执行层
│ │ (继承 App 沙箱权限) │ │
│ └───────────────────────────┘ │
│ │
│ ☑ 无法访问其他 App 数据 │
│ ☑ 无法访问系统敏感文件 │
│ ☑ 崩溃不影响 UI 层 │
│ ☑ 可被 kill() 强制终止 │
└─────────────────────────────────┘
10.2 代码执行风险与缓解
| 风险 | 场景 | 缓解措施 |
|---|---|---|
| 恶意代码 | 用户执行 os.system('rm -rf /') |
沙箱限制:只能访问 App 私有目录 |
| 资源耗尽 | while True: pass 死循环 |
提供「⏹ 停止」按钮 → kill(childPid, SIGKILL) |
| 内存爆炸 | x = [] 无限 append |
setrlimit(RLIMIT_AS, ...) 限制内存 |
| 网络滥用 | urllib.request 爬虫 |
网络权限需用户授权;可添加网络白名单 |
10.3 未来安全增强方向
- 沙箱进一步收紧:使用
seccomp-bpf限制子进程可调用的 syscall - 代码审计:执行前用 AST 分析器检测危险操作(
import os,subprocess等) - 超时机制:为
PyRun_SimpleString()设置执行超时(通过 signal alarm) - 网络隔离:可选的"离线模式",禁止所有网络操作
十一、性能基准测试
11.1 启动性能
| 阶段 | 耗时 | 说明 |
|---|---|---|
| HAP 安装 | ~500 ms | HarmonyOS Installd |
| Ability 创建 | ~50 ms | ArkTS UI 初始化 |
| N-API 加载 | ~30 ms | dlopen(libthonny_bridge.so) |
| PTY 创建 | ~5 ms | posix_openpt() + fork() |
| CPython 初始化 | ~200 ms | Py_Initialize()(含 zipimport 扫描) |
| 首屏就绪 | ~785 ms | 用户可开始输入 |
11.2 执行性能
| 测试用例 | 耗时 | 对比桌面 CPython |
|---|---|---|
print("hello") |
~2 ms | ~0.5 ms(4x 慢) |
for i in range(10000): pass |
~15 ms | ~3 ms(5x 慢) |
sum(range(1000000)) |
~80 ms | ~15 ms(5x 慢) |
import json; json.dumps({...}) |
~25 ms | ~5 ms(5x 慢) |
性能损耗分析:
- PTY 数据透传开销:~10%
- 嵌入式模式初始化:~5%
- HarmonyOS 内核调度差异:~5%
- 总体可接受:对于教育/学习场景,5x 的性能差距几乎无感
11.3 内存占用
| 组件 | RSS (常驻内存) | 说明 |
|---|---|---|
| UI 进程 (ArkTS) | ~45 MB | ArkUI 渲染引擎 + 框架 |
| Bridge 线程 | ~2 MB | PTY 管理数据结构 |
| Python 子进程 | ~35 MB | CPython 解释器 + 已加载模块 |
| 合计 | ~82 MB | 完整运行一个 Hello World |
对比参考:
- VS Code:~300-500 MB
- PyCharm Community:~400-600 MB
- 原生 Thonny (Linux):~70-90 MB
结论:鸿蟒工坊的内存占用与原生 Thonny 处于同一水平,远低于重量级 IDE。
十二、与桌面版 Thonny 的对比
| 特性 | 桌面版 Thonny | 鸿蟒工坊 (本实现) | 差距原因 |
|---|---|---|---|
| 代码编辑 | ✅ 语法高亮 | ⚠️ 纯文本(等宽字体) | 缺少 LSP/AstGrep 集成 |
| 代码补全 | ✅ IntelliSense-like | ❌ 无 | 需要 Jedi/LSP 后端 |
| 调试器 | ✅ 逐语句/断点/变量 | ❌ 无 | 需要 bdb/pdb 集成 |
| Shell 交互 | ✅ REPL | ✅ PTY-based REPL | ✅ 已实现 |
| 文件管理 | ✅ 打开/保存/多标签 | ⚠️ 单文件(内存中) | 需要文件选择器 API |
| 包管理 | ✅ pip GUI | ❌ 无 | 需要集成 pip |
| 插件系统 | ✅丰富的插件生态 | ❌ 无 | 需要设计插件 API |
| 主题切换 | ✅ 亮/暗主题 | ⚠️ 固定暗色 Shell | UI 层容易扩展 |
| Python 版本 | 可切换 | 固定 3.12 | 需要多 runtime 并存 |
| 安装包大小 | ~40 MB | ~36 MB (HAP) | ✅ 更小 |
| 平台 | Win/Mac/Linux | HarmonyOS PC | ✅ 填补空白 |
定位总结:鸿蟒工坊当前处于 MVP(最小可行产品) 阶段,核心价值在于验证了"鸿蒙 PC + CPython + PTY + N-API"这一技术栈的可行性。后续迭代可以逐步补齐语法高亮、调试器、文件管理等高级特性。
十三、总结:从"移植"到"原生"的思维转变
13.1 技术收获
通过构建鸿蟒工坊,我们验证了一套完整的 “鸿蒙 PC Native 应用开发范式”:
ArkTS/ArkUI (声明式 UI)
↕ N-API (类型安全的跨语言桥接)
C/C++ (系统能力访问)
↕ POSIX (进程/终端/文件)
第三方运行时 (CPython / V8 / LuaJIT)
这套范式的价值在于:任何能在 Linux/macOS/Windows 上运行的命令行工具或解释器,都可以通过类似的方法移植到 HarmonyOS PC 上。这包括但不限于:
- Node.js 运行时
- Java JVM (.NET 类似)
- Ruby / Go / Rust REPL
- Git / SSH / Vim 等开发者工具
13.2 对鸿蒙 PC 生态的意义
鸿蒙 PC 不应该只是一个"换壳 Linux"。它的独特价值在于:
- 分布式原生:应用可以无缝跨手机/平板/PC/车机运行
- AI 原生:小艺智能体 + Intent Framework 深度整合
- 安全原生:沙箱 + 权限模型从设计层面保障安全
- 性能原生:ArkTS 编译为机器码,非 JIT/Web 解释
鸿蟒工坊正是这些理念的实践者——它不是简单地把 Thonny 搬到鸿蒙上,而是用鸿蒙的方式重新思考 IDE 应该是什么样子:
- 用 Intent Framework 把"运行代码"变成可以被小艺调用的 Skill
- 用分布式能力让"手机上写的代码,PC上一碰就能跑"
- 用系统能力(OCR/语音/AI)让编程交互超越键盘鼠标
13.3 个人成长反思
在这个项目中,我深刻体会到了几个"思维转弯":
-
从"调用 API"到"实现运行时":以前只是用 Python 写代码,现在是把 Python 本身作为一个组件嵌入到另一个应用中。视角的转变让我对解释器、GC、字节码有了更深的理解。
-
从"Web 思维"到"Native 思维":习惯了前端开发的 DOM 操作和事件驱动,转到 C++ 的内存管理、信号处理、文件描述符,需要重新建立心智模型。N-API 的线程安全函数是一个很好的桥梁。
-
从"功能实现"到"工程权衡":每一个技术决策(fork vs thread、PyRun_SimpleString vs PyRun_InteractiveOne、同步 vs 异步回调)都是在多个维度间做权衡。没有完美的方案,只有最适合当前阶段的方案。
更多推荐



所有评论(0)