Skip to content

Python 内嵌机制 ​

CPython 3.12.2 以动态库形式嵌入:python312.dll 与 Program.exe 同目录, 标准库和扩展模块在同级的 python312/ 目录里。运行时必须有这两样东西。

链接方式见 Python 运行时与扩展。

初始化流程 ​

PythonRuntime::startUp()(application/Python/PythonRuntime.cpp):

  1. 设 Py_NoSiteFlag = 1、Py_IgnoreEnvironmentFlag = 1 —— 不导入 site.py,也不让外部 PYTHONPATH 干扰

  2. SetPythonHome() —— 用 GetModuleFileNameA 取 exe 路径,把 PYTHONHOME 设为 <exe 目录>\python312(非 Windows 平台回退为 .)

    标准库就在这个 prefix 下的 python312\Lib\:CPython 的 getpath 在 Windows 上 用 Lib\os.py 作 landmark 来找标准库,所以整个目录随便搬到哪都能跑。

  3. Py_Initialize()

  4. PyEval_SaveThread() 释放 GIL(让工作线程能自己获取)

  5. PyGILState_Ensure() → initModules() —— 注册本工程自己的 19 个模块

  6. 把 sys.stdout / sys.stderr 重配置为 UTF-8 + 行缓冲:

    python
    for _s in (sys.stdout, sys.stderr):
        _s.reconfigure(encoding='utf-8', line_buffering=True, errors='replace')

    这一步不能省:终端界面会把 stdout 接管成管道,管道下 Python 默认块缓冲并用 ANSI 代码页编码,插件的 print() 会攒够 4~8KB 才吐出来,中文还会乱码。

  7. 按模式设置 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,而是读打包文件:

  1. 打开 vanilla.mcp 里的 redirect.mcs
  2. 头部 4 字节与常量 1966019809 异或
  3. zlib 解压
  4. marshal 反序列化成 code object,执行成 redirect 模块
  5. import init

模块注册(19 个) ​

PythonRuntime::initModules() 逐个调用 PyInit_*() 创建模块,再由 RegisterModule() 塞进 sys.modules:

cpp
static void RegisterModule(PyObject* m, const char* name)
{
    if (m) {
        PyDict_SetItemString(PyImport_GetModuleDict(), name, m);
        Py_DECREF(m);
    }
}
模块init 符号创建方式
api_errorsPyInit_api_errorsC API
nbtPyInit_nbtpybind11
packetsPyInit_packetspybind11
game_statePyInit_game_statepybind11
client_instancePyInit_client_instancepybind11
easy_utilsPyInit_easy_utilspybind11
_clientPyInit__clientC API
tan_lobby_game_clicpp_wrapperregister_tan_lobby_game_module()pybind11 create_extension_module
_websocketPyInit__websocketC API
aesPyInit_aesC API
_chachaPyInit__chachaC API
_raknetPyInit__raknetC API
utilityPyInit_utilityC API
settingPyInit_settingC API
mod_logPyInit_mod_logC API
enginePyInit_engineC API
rotorPyInit_rotorC API
fopPyInit_fopC API
pktPyInit_pktC 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.dllHTTPS / TLS
_hashlib.pydhashlib 的 OpenSSL 实现
_sqlite3.pyd + sqlite3.dllSQLite
unicodedata.pyd _decimal.pyd _ctypes.pyd _elementtree.pyd pyexpat.pyd 等常见插件依赖
zlib1.dllzlib 模块

和静态嵌入的关键差别

如果当初把 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)。

下一步 ​

第三方库请参阅各自目录下的许可证文件