Files
mtgodot-poc/docs/PYTHON-EMBED-EVAL.md
T
shenleiandClaude Opus 5 b02c49bb2f spike(python): CPython 2.7.18 runs the 40250 root scripts on Android arm64
libpython2.7.a cross-builds with NDK 27.2 (API 24) after disabling
HAVE_LANGINFO_H; 30 C modules built in. On a OnePlus 13 (API 36) the
harness runs system.py -> prototype.RunApp() and imports the root modules
with results identical to macOS (66/74; the 8 failures are stub values or
dev-only scripts). py_embed_spike.py now isolates __main__ like
CPythonLauncher and reports stdlib usage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 18:16:44 +09:00

103 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 内嵌 Python 运行 40250 UI 的评估
日期:2026-09-22。目的:判断能否不翻译 40250 的 Python UI,而是内嵌解释器原样运行
`Client/Eternexus/root/*.py``uiscript/`,只移植它调用的 C++ 模块。
## 40250 的实际情况
| 项 | 数据 |
| --- | --- |
| 解释器 | CPython 2.7.6 内嵌(`extern/include/Python-2.7``python27.lib` |
| 启动 | `ScriptLib/PythonLauncher.cpp``CPythonLauncher::Create` 注入 `TRUE`/`FALSE``__builtin__`,然后把 `system.py` 作为 `__main__` 运行(`__main__``__builtins__` 是模块,不是 dict);`system.py` 替换 `__import__`,从 pack 读源码并 `compile` |
| 脚本 | `root/*.py` 90 个(其中 74 个 `.py` 模块),`uiscript/uiscript/*.py` 94 个 |
| 语法 | 154 个文件在 Python 2.7.18 下全部能编译;Python 3 下有 56 个编译失败;约 292 行只在 Python 2 下成立(`print` 语句、`except X, e``has_key``iteritems` 等) |
| 语义风险 | 约 185 行在整数之间用 `/`Python 2 是整除,Python 3 是浮点);字符串是 CP949 字节串 |
| C++ 暴露的模块 | 35 个(`app` `net` `player` `chr` `chrmgr` `wndMgr` `grp` `item` `skill` 等),约 1160 个函数、约 994 个常量 |
| 需要移植的 C++ | `UserInterface/*Module.cpp` + `EterPythonLib`(窗口系统)+ `ScriptLib`,共约 2.44 万行 |
| 脚本实际引用 | 1627 个不同的 `模块.属性`(含常量) |
对比:如果逐个翻译成 GDScript,需要翻译 `root` 下的 3350 个函数,外加 uiscript 数据文件;而这些 C++
模块在任何方案里都要移植(它们就是 UI 和游戏逻辑的接口)。现在仓库里 `project/ui/*.gd` 有 60 个文件、
2.39 万行,当初依据的是错误版本的脚本(m2dev `assets/root`),无论选哪条路都要重做。
## 原型验证(已完成)
`.agents/skills/metin2-40250-parity-audit/scripts/py_embed_spike.py`,在本机 Python 2.7.18 下:
-`CPythonLauncher::Create` 注入 `TRUE`/`FALSE`,所有 C++ 模块用返回 0 的桩代替,`pack` 用目录代替;
- 原样运行 40250 的 `system.py`,再通过它自己的 `__pack_import` 导入全部 74 个 root 模块。
结果:**不改任何脚本**`system.py` 引导后 `prototype.py` 完整执行了 `RunApp()`:创建 MainStream、进入 logo
阶段、加载 `UIScript/PopupDialog.py``app.Loop`、Destroy。之后逐个导入全部 74 个 root 模块,66 个成功。
剩下 8 个失败都不是解释器或脚本的问题:
- `prototype`:已经由 `system.py` 执行过,再导入时 `RunApp` 结束触发 `SystemExit`,属正常;
- `game``interfacemodule``chr.MOTION_SKILL + 121` 这类常量需要真实整数(真实模块用 `PyModule_AddIntConstant` 提供);
- `introloading``consolemodule`locale 数据路径(`app.GetLocaleServiceName` 等返回值决定走哪个 locale 目录);
- `system`:只能作为 `__main__` 运行;`rootlibcythonizer``test_affect` 是开发用脚本,40250 运行时也不加载。
引导过程中脚本调用了 33 个 C++ 模块函数(`app.Create``wndMgr.Register``net.SetHandler` 等)。
结论:在 Python 2.7 上,40250 的脚本和导入机制可以原封不动运行;工作量集中在 C++ 模块本身。
## 方案对比
| 方案 | 脚本改动 | 与 40250 一致性 | 平台风险 | 结论 |
| --- | --- | --- | --- | --- |
| A. 内嵌 CPython 2.7.18(静态链接) | 无 | 最高:解释器语义、`/` 整除、字节串、C API`Py_InitModule``PyTuple_GetInteger`)都和 40250 相同,`*Module.cpp` 可以几乎原样复制 | 需要自己为 Android/iOS 交叉编译;2.7 已停止维护 | **推荐** |
| B. 内嵌 CPython 3.13+ | 56 个文件要转换,约 185 处 `/` 需逐一判断 | 中:字符串/整除/字典顺序语义不同,转换本身会引入偏差;C API 要改写 | 3.13 起官方支持 Android/iOS | A 失败时的备选 |
| C. pocketpy 等轻量解释器 | 需要大量改动 | 低:只支持 Python 3 子集 | 小 | 不推荐 |
| D. 翻译成 GDScript | 全部重写 3350 个函数 | 取决于翻译质量,难以机械核对 | 无 | 不推荐 |
### 方案 A 的平台问题
- macOS / Linux / Windows:直接编译,无风险。
- Androidarm64NDK 27):CPython 2.7 能用 NDK 编译(python-for-android、Kivy 曾长期支持),但需要补丁:
关闭 `dlopen` 扩展、把用到的标准库 C 模块静态编进去、`pyconfig.h` 按 NDK 调整。
- iOS:同样静态链接(Kivy-ios 曾支持 2.7)。App Store 允许包内自带的解释型代码(不下载代码即可)。
- 标准库只需要极少一部分:脚本用到的是 `sys` `os` `marshal` `imp` `types` `copy` `math` `traceback`
`_weakref` `__builtin__` `locale``psyco` 有 try 保护。可以冻结为内置模块,不需要带整个 `Lib/`
- 2.7 停止维护的风险:解释器只运行包内自带的脚本;服务器数据先经过 C++ 解析再以普通值交给 Python,
不会执行远端代码。
## 采用方案 A 后的结构
```
extension/src/port/ScriptLib/PythonLauncher.cpp # CPythonLauncher,几乎原样
extension/src/port/EterPythonLib/PythonWindow*.cpp # 窗口树、焦点、命中、事件:逻辑照搬
extension/src/port/UserInterface/*Module.cpp # net/player/chr/... 模块,调用已移植的 CPython* 单例
extension/src/platform/ui/ # 适配层:CGraphicImageInstance/CGraphicTextInstance
# 的绘制 -> Godot RenderingServer/CanvasItem
extension/third_party/cpython-2.7.18/ # 静态库
```
Godot 这边只剩一个宿主 Control:把输入转发给 `CPythonWindowManager`,每帧调用它的 Update/Render。
`root/*.py``uiscript/` 直接从 pack 读取,和 40250 相同。port-map 中 `Client/root` 的 3350 个函数
全部记为 `N_A`(原样运行,无需移植),python 层剩下约 1540 个 C++ 函数要移植。
## Android 验证(2026-09-22,已通过)
设备:一加 13PJZ110),Android API 36arm64-v8a。复现:`tools/py_embed_android/build-and-run.sh`
- **编译**CPython 2.7.18 官方源码,用 NDK 27.2 的 `aarch64-linux-android24-clang` 交叉 configure
`make libpython2.7.a` 一次通过。只需要一处改动:关掉 `HAVE_LANGINFO_H`,因为 bionic 从 API 26 才声明 `nl_langinfo`
另外 30 个 C 模块静态编进去,包括 `posix` `math` `time` `_struct` `operator` `itertools` `_collections`
`cStringIO` `cPickle` `_locale` `binascii` `datetime` `select` `fcntl` 等。全部编译通过,库大小 13 MB(含调试信息)。
- **链接**:测试程序 `tools/py_embed_android/main.c` 链接 `libpython2.7.a -lm -ldl`,没有任何未定义符号。
- **运行**`Py_NoSiteFlag`/`Py_IgnoreEnvironmentFlag`/`Py_FrozenFlag` 启动,标准库以不压缩的 zip(8 MB)
加入 `sys.path`,然后运行 `py_embed_spike.py`:**结果与 macOS 逐项相同**,66/74 个模块加载,失败的 8 个完全相同,
引导期调用的 33 个 C++ 函数也完全相同。
- **尚未验证**
- 这次是用 `adb shell` 运行的独立可执行文件,还没有在 APK 的 app 进程里跑过;
- 标准库是从文件系统读的,还没有走 PCK 或 `asset_io`
这两点属于集成工作,不涉及解释器本身能不能跑。
## 下一步
1. 把 CPython 2.7.18 放进 `extension/third_party/`,用我们自己的 CMake 编译(macOS / Android / iOS 共用一份源码清单和
`pyconfig.h`),静态链接进 `libmtgodot`
2. `pack` 模块走 `asset_io`,标准库的纯 Python 文件放进资源包,用 `system.py` 自带的导入钩子加载;
3. 在 APK 里跑同样的引导流程(C++ 桩模块),然后按同样方法验证 iOS;
4. 之后 python 层才开始移植:`ScriptLib/PythonLauncher.cpp``EterPythonLib` 窗口系统 → `*Module.cpp`