- 游戏开发
- 后端
【免费下载链接】kbengine
A MMOG engine of server.
本篇基于 KBEngine 仓库内置的 CPython 官方移植指南 Porting Extension Modules to Python 3(作者 Benjamin Peterson),系统讲解 C/C++ 扩展模块从 Python 2 迁移到 Python 3 的全部 C-API 变化:条件编译、str/unicode 与 int/long 类型统一、模块初始化体系(PEP 3121)、CObject 到 Capsule 的替换。读完后你可以理解 KBEngine 的pyscriptC++ 脚本绑定层为何能稳定运行在 Python 3 之上,并具备自行编写双版本兼容扩展模块的能力。
1. 仓库背景:KBEngine 为什么内置了整套 CPython 源码
KBEngine 的服务端是一个 C++ 引擎,却允许开发者用 Python 编写游戏逻辑(服务器脚本)。这要求 C++ 层通过 CPython 的 C-API 与解释器交互,而这份 cporting.rst 正是 KBEngine 随源码一起内置的 CPython 移植指南——仓库在 kbe/src/lib/python 下携带了完整的 CPython 发行树(Include/、Python/、Objects/、Modules/等),供编译时链接 Python 支持库。
从 patchlevel.h 可以直接确认内置版本:
#define PY_MAJOR_VERSION 3 #define PY_MINOR_VERSION 7 #define PY_MICRO_VERSION 3 #define PY_VERSION "3.7.3"即 KBEngine 绑定的是Python 3.7.3 的 C-API。这意味着 kbe/src/lib/pyscript 目录下的整个 C++ 脚本绑定层(script.cpp、py_memorystream.cpp、pickler.cpp、pyurl.cpp等)必须完全使用 Python 3 的 C-API 编写。例如解释器初始化位于 script.cpp 的Py_Initialize(),GIL 锁封装在 pythread_lock.h 中使用PyGILState_Ensure()。下面按照官方文档的脉络,逐项讲解移植要点,并结合 KBEngine 源码给出印证。
2. 条件编译:用 PY_MAJOR_VERSION 隔离 Python 2/3 代码
最直接的兼容手段是检查PY_MAJOR_VERSION宏是否大于等于 3,把只属于某一版本的代码放进条件块中:
#if PY_MAJOR_VERSION >= 3 #define IS_PY3K #endif对于在 Python 2/3 中名称不同(或其中一版不存在)的 API 函数,可以在条件块内做别名映射。PY_MAJOR_VERSION等常量定义于 patchlevel.h,同文件还给出了PY_VERSION_HEX(形如0x030703F0的四字节十六进制整数),适合用#if PY_VERSION_HEX >= ...做更精细的版本判断——后面的 capsulethunk.h 就是靠它区分 Python 2.6/2.7 与 3.0/3.1+ 的。
3. 对象 API 变化:类型合并与分离
官方文档的定性描述是:Python 3 把一些功能相似的类型合并了(str/unicode、int/long),同时把另一些类型干净地分开了(文本与二进制)。
3.1 str / unicode 统一
Python 3 的str等价于 Python 2 的unicode,两者对应的 C 函数都叫PyUnicode_*;旧的 8 位字符串类型变成了bytes,对应PyBytes_*。Python 2.6+ 提供了兼容头文件 bytesobject.h,把PyBytes_*名称映射到PyString_*。官方推荐的最佳实践是:文本数据用PyUnicode,二进制数据用PyBytes,且要记住 Python 3 中PyBytes与PyUnicode不可互相混用(Python 2 中PyString与PyUnicode却可以)。
文档给出的标准示例如下(文本版 + 二进制版):
#include "stdlib.h" #include "Python.h" #include "bytesobject.h" /* text example */ static PyObject * say_hello(PyObject *self, PyObject *args) { PyObject *name, *result; if (!PyArg_ParseTuple(args, "U:say_hello", &name)) return NULL; result = PyUnicode_FromFormat("Hello, %S!", name); return result; } /* just a forward */ static char * do_encode(PyObject *); /* bytes example */ static PyObject * encode_object(PyObject *self, PyObject *args) { char *encoded; PyObject *result, *myobj; if (!PyArg_ParseTuple(args, "O:encode_object", &myobj)) return NULL; encoded = do_encode(myobj); if (encoded == NULL) return NULL; result = PyBytes_FromString(encoded); free(encoded); return result; }注意两个细节:PyArg_ParseTuple的格式符"U"专门接收 Unicode 字符串(Python 2 对应"s"或"z");PyUnicode_FromFormat的%S转换符把任意PyObject*安全地嵌入 Unicode 结果中,免去手工拼接和编码转换。
KBEngine 的pyscript层正是按这个规范实现的。以 HTTP 请求解析 pyurl.cpp 为例,URL、请求体等文本字段全部走PyUnicode_FromString;而对请求头与 POST 数据则先做类型分派,文本用PyUnicode_AsUTF8AndSize取出,二进制用PyBytes_AsStringAndSize取出(pyurl.cpp):
if (!PyUnicode_Check(key) || !PyUnicode_Check(value)) ... map_headers[PyUnicode_AsUTF8AndSize(key, NULL)] = PyUnicode_AsUTF8AndSize(value, NULL); ... else if (PyBytes_Check(pyobj)) { if (PyBytes_AsStringAndSize(pyobj, &postData, &postDataLength) < 0)同样,序列化层 pickler.cpp 用PyBytes_AsString+PyBytes_Size读取序列化结果(序列化产物是二进制,用 bytes 是正确的选择)。可以确认:pyscript目录内没有任何PyString_*遗留调用,说明该层已完全按 Python 3 的 API 组织。
3.2 int / long 统一
Python 3 只有一个整数类型int,但它对应的是 Python 2 的long——Python 2 的int类型被移除了。在 C-API 层面,PyInt_*系列函数被PyLong_*等价物取代。
KBEngine 源码中可以印证这一迁移已经彻底完成:py_memorystream.cpp 把脚本值读入MemoryStream时,按 8/16/32/64 位宽度分别调用PyLong_AsLong/PyLong_AsLongLong;py_gc.cpp 的 GC 调试开关、py_compression.cpp 的压缩类型参数也一律使用PyLong_AsLong。全仓库pyscript/helper/common范围内检索不到任何PyInt_AsLong之类的旧式调用。
4. 模块初始化与模块状态(PEP 3121)
Python 3 重写了扩展模块初始化体系(即 PEP 3121)。核心原则:模块状态不应存在全局变量中,而应存放在解释器专属的(interpreter specific)结构体里,以支持多解释器场景。要同时兼容 Python 2 和 3 比较繁琐,文档给出了完整的双版本示例:
#include "Python.h" struct module_state { PyObject *error; }; #if PY_MAJOR_VERSION >= 3 #define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) #else #define GETSTATE(m) (&_state) static struct module_state _state; #endif static PyObject * error_out(PyObject *m) { struct module_state *st = GETSTATE(m); PyErr_SetString(st->error, "something bad happened"); return NULL; } static PyMethodDef myextension_methods[] = { {"error_out", (PyCFunction)error_out, METH_NOARGS, NULL}, {NULL, NULL} }; #if PY_MAJOR_VERSION >= 3 static int myextension_traverse(PyObject *m, visitproc visit, void *arg) { Py_VISIT(GETSTATE(m)->error); return 0; } static int myextension_clear(PyObject *m) { Py_CLEAR(GETSTATE(m)->error); return 0; } static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, "myextension", NULL, sizeof(struct module_state), myextension_methods, NULL, myextension_traverse, myextension_clear, NULL }; #define INITERROR return NULL PyMODINIT_FUNC PyInit_myextension(void) #else #define INITERROR return void initmyextension(void) #endif { #if PY_MAJOR_VERSION >= 3 PyObject *module = PyModule_Create(&moduledef); #else PyObject *module = Py_InitModule("myextension", myextension_methods); #endif if (module == NULL) INITERROR; struct module_state *st = GETSTATE(module); st->error = PyErr_NewException("myextension.Error", NULL, NULL); if (st->error == NULL) { Py_DECREF(module); INITERROR; } #if PY_MAJOR_VERSION >= 3 return module; #endif }示例中值得注意的几个点:
PyModuleDef的m_size字段传入sizeof(struct module_state),运行时PyModule_GetState()依据它返回每个解释器实例独立的 state 指针;Python 2 侧则退化为static struct module_state _state全局变量。m_traverse/m_clear:模块持有的引用(本例是error异常类型)必须接入 GC 的遍历与清理回调,避免循环引用或引用计数残留。这是 PEP 3121 相对旧体系新增的显式要求。- 入口函数命名:Python 3 的入口是
PyInit_myextension并返回模块对象;Python 2 是initmyextension且无返回值——条件编译宏INITERROR同时处理了两个版本的错误返回语义(return NULLvsreturn)。
KBEngine 的脚本子系统没有走"双版本兼容"路线,而是直接面向 Python 3.7 编写:解释器由 script.cpp 中Py_Initialize()启动并用Py_IsInitialized()校验;各子模块(如 math.cpp 创建的math模块,其__doc__即PyUnicode_FromString("This module is created by KBEngine!"))通过PyModule_GetDict挂入解释器命名空间。这与文档中"Python 3 模块体系"的结论一致。
5. CObject 被 Capsule 取代
Capsule对象在 Python 3.1 与 2.7 中引入,用以取代CObject。CObject 的 API 存在根本性缺陷:无法区分不同的 CObject,混用不匹配的 CObject 会直接让解释器崩溃,且部分 API 依赖了 C 语言中的未定义行为(文档指引参阅 Python issue tracker 的 5630 号问题了解设计动机)。
版本影响面(以文档为准):
CObject在 3.1 / 2.7 中被弃用,在 Python 3.2 中被彻底移除;- 只支持 2.7 或 3.1+ 的项目可以直接切换到 Capsule;
- 需要同时支持 3.0 或早于 2.7 的版本时,必须同时兼容 CObject 与 Capsule(文档同时指出 Python 3.0 已不再支持,不推荐生产使用)。
为后者场景,Python 官方提供了 capsulethunk.h(本仓库即收录了该文件,共 134 行)。用法是:代码只面向 Capsule API 编写,在Python.h之后 include 此头文件,它会自动在有 Capsule 的版本上原样使用 Capsule,在缺失 Capsule 的版本上用 CObject 模拟。从源码看,其核心是一组条件编译宏,例如(capsulethunk.h):
#define PyCapsule_New(pointer, name, destructor) \ (PyCObject_FromVoidPtr(pointer, destructor)) #define PyCapsule_GetPointer(capsule, name) \ (PyCObject_AsVoidPtr(capsule))由于 CObject 没有地方存放 capsule 的 "name",模拟版 Capsule 与真实 Capsule 存在四点行为差异(文档原文逐条列出):
PyCapsule_New传入的 name 参数被忽略;PyCapsule_IsValid与PyCapsule_GetPointer的 name 参数被忽略,且不做名称校验;PyCapsule_GetName恒返回NULL(见 capsulethunk.h);PyCapsule_SetName恒抛出异常并返回失败——因为没有地方存 name,文档作者认为"响亮的失败"优于"静默的失败"(capsulethunk.h 中通过PyErr_SetString(PyExc_NotImplementedError, ...)实现)。
对 KBEngine 的实际意义:由于内置解释器是 3.7.3,pyscript层若需跨 C++ 与 C 扩展共享不透明指针,应直接使用原生 Capsule API,无需 thunk 层。
6. 其他选择:Cython
文档最后指出:如果是新写扩展模块,可以考虑 Cython——它把一种 Python 风格的语言翻译成 C,生成的扩展模块同时兼容 Python 3 与 Python 2。对维护现有 C-API 代码的项目(如 KBEngine 的pyscript层)而言,Cython 更多是新增绑定时的可选方案;对存量 C 代码的迁移,本文前述的条件编译 + API 别名 + 模块状态迁移路径仍是主线。
7. 移植要点小结
把 cporting.rst 的核心内容整理成可核对的迁移清单:
| 主题 | Python 2 | Python 3 | 迁移要点 |
|---|---|---|---|
| 版本判断 | — | — | #if PY_MAJOR_VERSION >= 3(patchlevel.h),细粒度用PY_VERSION_HEX |
| 文本 | unicode/PyUnicode_* | str/PyUnicode_* | 文本一律PyUnicode;PyArg_ParseTuple用"U"格式符 |
| 二进制 | str/PyString_* | bytes/PyBytes_* | 二进制一律PyBytes;2.6+ 可用bytesobject.h做名称映射 |
| 整数 | int+long/PyInt_* | 单一int/PyLong_* | PyInt_*全部替换为PyLong_* |
| 模块初始化 | initxxx(void)+ 全局状态 | PyInit_xxx(void)+PyModuleDef+PyModule_GetState | 状态移入 interpreter-specific 结构;补m_traverse/m_clear |
| 不透明指针 | CObject(3.2 移除) | Capsule(3.1/2.7 引入) | 面向 Capsule 编写;跨 3.0 旧版本时用 capsulethunk.h |
| 替代方案 | — | — | 新模块可评估 Cython |
适用前提再强调一次:本文所有结论以 KBEngine 仓库内置的 CPython 3.7.3 源码树(kbe/src/lib/python)及其文档为准,其中PY_VERSION、Capsule 可用性等事实均已由 patchlevel.h 与 capsulethunk.h 的实际内容确认;若读者在自己的工程中链接的是其他 Python 版本,应重新核对patchlevel.h后再套用上述清单。
- 游戏开发
- 后端
【免费下载链接】kbengine
A MMOG engine of server.
相关推荐
Hugo 图像处理:images.Opacity 透明度滤镜的用法与实现原理
Hugo 图像处理:images.Opacity 透明度滤镜的用法与实现原理 images.Opacity 是 Hugo 内置的图像处理模板函数,用于创建一个改
游戏开发后端CPython 扩展与嵌入 FAQ:用 C 编写 Python 内置模块、调用与调试实践指南
CPython 扩展与嵌入 FAQ:用 C 编写 Python 内置模块、调用与调试实践指南 本文基于 CPython 仓库的 扩展/嵌入 FAQ https:
编程语言语言运行时解释器标准库CPython标准库:内置模块与C扩展的实现原理
CPython标准库:内置模块与C扩展的实现原理 CPython标准库的模块组织架构体现了精心设计的层次化结构,其加载机制融合了编译时初始化与运行时动态查找的巧
编程语言语言运行时解释器标准库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考