Python 内嵌机制
CPython 3.12.2 以动态库形式嵌入:python312.dll 与 Program.exe 同目录, 标准库和扩展模块在同级的 python312/ 目录里。运行时必须有这两样东西。
链接方式见 Python 运行时与扩展。
初始化流程
PythonRuntime::startUp()(application/Python/PythonRuntime.cpp):
设
Py_NoSiteFlag = 1、Py_IgnoreEnvironmentFlag = 1—— 不导入site.py,也不让外部PYTHONPATH干扰SetPythonHome()—— 用GetModuleFileNameA取 exe 路径,把PYTHONHOME设为<exe 目录>\python312(非 Windows 平台回退为.)标准库就在这个 prefix 下的
python312\Lib\:CPython 的getpath在 Windows 上 用Lib\os.py作 landmark 来找标准库,所以整个目录随便搬到哪都能跑。Py_Initialize()PyEval_SaveThread()释放 GIL(让工作线程能自己获取)PyGILState_Ensure()→initModules()—— 注册本工程自己的 19 个模块把
sys.stdout/sys.stderr重配置为 UTF-8 + 行缓冲:pythonfor _s in (sys.stdout, sys.stderr): _s.reconfigure(encoding='utf-8', line_buffering=True, errors='replace')这一步不能省:终端界面会把 stdout 接管成管道,管道下 Python 默认块缓冲并用 ANSI 代码页编码,插件的
print()会攒够 4~8KB 才吐出来,中文还会乱码。按模式设置
sys.path,最后import init
sys.path 的两种模式
startUp() 里有两条分支:
| 模式 | 触发 | sys.path 处理 |
|---|---|---|
| source 模式(默认) | ConfigLoader::use_mcp == false | 不清空(清空会丢掉标准库路径),只追加 ./source、插件目录、./python312/Lib/site-packages |
| MCP 模式 | use_mcp == true | 先 PyList_SetSlice 整个清空,再追加 vanilla.mcp、vanilla.mcp/minecraft/、framework/、lib/、lobby/、mod/、sunshine/ 与插件目录 |
两条分支不对称,加路径时要注意
source 模式特意不清空 —— 代码里留了注释说明清空会破坏所有标准库 import。 MCP 模式则会清空后重建,路径集合完全不同。往 sys.path 里加东西前先确认自己处在哪个模式。
插件目录由 --PluginDir 决定(默认 ./scripts);VQ 在 MCP 模式下会传一个 %TEMP% 下的随机临时目录。同一个路径还会通过环境变量 VECTOR_PLUGIN_DIR 暴露给 Python(init.py 读它)。
MCP 模式的加载细节
MCP 模式不从目录读 .py,而是读打包文件:
- 打开
vanilla.mcp里的redirect.mcs - 头部 4 字节与常量
1966019809异或 - zlib 解压
marshal反序列化成 code object,执行成redirect模块import init
模块注册(19 个)
PythonRuntime::initModules() 逐个调用 PyInit_*() 创建模块,再由 RegisterModule() 塞进 sys.modules:
static void RegisterModule(PyObject* m, const char* name)
{
if (m) {
PyDict_SetItemString(PyImport_GetModuleDict(), name, m);
Py_DECREF(m);
}
}| 模块 | init 符号 | 创建方式 |
|---|---|---|
api_errors | PyInit_api_errors | C API |
nbt | PyInit_nbt | pybind11 |
packets | PyInit_packets | pybind11 |
game_state | PyInit_game_state | pybind11 |
client_instance | PyInit_client_instance | pybind11 |
easy_utils | PyInit_easy_utils | pybind11 |
_client | PyInit__client | C API |
tan_lobby_game_clicpp_wrapper | register_tan_lobby_game_module() | pybind11 create_extension_module |
_websocket | PyInit__websocket | C API |
aes | PyInit_aes | C API |
_chacha | PyInit__chacha | C API |
_raknet | PyInit__raknet | C API |
utility | PyInit_utility | C API |
setting | PyInit_setting | C API |
mod_log | PyInit_mod_log | C API |
engine | PyInit_engine | C API |
rotor | PyInit_rotor | C API |
fop | PyInit_fop | C API |
pkt | PyInit_pkt | C API |
这些模块编译进 exe,所以插件里 import engine 随时可用,不需要 .pyd 文件。
两个已知的编译期坑
fopmodule.cpp/rotormodule.c通过.h里#include "xxx.cpp"编译进多个 TU, init 函数必须static(否则链接重复定义)。- pybind11 2.13 的
create_extension_module会对传入指针做 placement new, 传nullptr会写地址 0 崩溃;TanGame.cpp传的是new PyModuleDef()。
标准库扩展模块走 python312/DLLs/
CPython 3.12 把 _socket / select / _queue 等一批 stdlib C 模块做成了独立 .pyd。 当前是动态嵌入,这些文件就用官方的,放在 python312\DLLs\ 里即可:
| 文件 | 用途 |
|---|---|
_socket.pyd select.pyd | 网络与 IO 复用,框架硬依赖 |
_ssl.pyd + libssl-3.dll + libcrypto-3.dll | HTTPS / TLS |
_hashlib.pyd | hashlib 的 OpenSSL 实现 |
_sqlite3.pyd + sqlite3.dll | SQLite |
unicodedata.pyd _decimal.pyd _ctypes.pyd _elementtree.pyd pyexpat.pyd 等 | 常见插件依赖 |
zlib1.dll | zlib 模块 |
和静态嵌入的关键差别
如果当初把 CPython 核心静态编进 exe,这批 .pyd 就加载不了 —— 必须把它们的 C 源码 一起编进 exe 并注册成 builtin 模块。当前实现不需要这些:.pyd 直接用官方的, 升级 Python 版本也不用改工程。
字符串编码边界
| 方向 | 规则 |
|---|---|
| C++ → Python 文本 | PyTextFromUtf8(UTF-8 数据)/ PyTextFromGbk(GBK 配置,走 Python 的 gbk codec) |
| C++ → Python 二进制 | PyBytes_FromStringAndSize |
Python → C++(s / s#) | Py3 下给出 UTF-8 |
Python → C++(y#) | 给出 bytes |
集中辅助在 application/Python/Py3Compat.h(header-only,不依赖 windows.h)。
