ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

KBEngine 嵌入式 Python 整数对象 C API(PyLong)权威指南

KBEngine 嵌入式 Python 整数对象 C API(PyLong)权威指南
  • 游戏开发
  • 后端

【免费下载链接】kbengine

A MMOG engine of server.

项目地址:https://gitcode.com/gh_mirrors/kb/kbengine
点击查看免费下载

本指南以 KBEngine 仓库内嵌的 CPython 3.7.3 官方文档 kbe/src/lib/python/Doc/c-api/long.rst 为骨架,逐条讲解 Python C API 中整数对象(PyLong)的类型结构、创建(From 系列)与提取(As 系列)全部接口,并结合仓库源码(longobject.c、longobject.h)以及 KBEngine 在实体属性类型、内存流读写中的真实调用,说明如何在游戏服务端 C++ 与 Python 脚本之间安全地传递任意精度整数。

读完本文,你将掌握:如何判断一个PyObject*是否为整数、如何把 C 的long/long long/unsigned long long/Py_ssize_t/指针等转换成 Pythonint,如何把 Pythonint无损地取回 C 类型并正确处理溢出与错误,以及 KBEngine 的UINT64/INT64等实体属性在Python int ↔ C 类型 ↔ 网络/数据库流之间转换的底层实现方式。

一、背景:KBEngine 中为何关心 PyLong

KBEngine 是一款以 Python 为脚本语言的多人在线游戏服务端引擎,其 C++ 服务端进程(baseapp、cellapp、dbmgr 等)内嵌了完整的 CPython 解释器(当前仓库为 patchlevel.h 标识的 Python 3.7.3)。这意味着:

  • 开发者用 Python 编写的实体属性(如UINT32、INT64)最终都以 Pythonint对象的形式存在于解释器中;
  • 当这些属性需要写入网络流(baseapp 与 cellapp 通信)、数据库(MySQL/Redis)或与 C++ 数值运算时,就必须借助 PyLong 系列 API 在 C 类型与 Python 对象之间转换;
  • 服务端存在大量数字密集型逻辑(ID 分配、坐标、时间戳、统计计数),PyLong 的使用频率极高。

因此,PyLong C API 是连接 KBEngine 引擎层(C++)与玩法层(Python)的核心桥梁之一。

二、整数对象模型:PyLongObject 与 PyLong_Type

2.1 任意精度整数

在 CPython 3.x 中,所有整数都实现为任意精度的 "long" 整数对象,Python 层的int在 C 层就是PyLongObject:

  • PyLongObject:PyObject的子类型,代表一个 Python 整数对象。它由"符号 + 若干 30 位(或 15 位,视平台宏PyLong_SHIFT而定)数字位"组成,因此可以表达远超 C 内置整数范围的值。
  • PyLong_Type:PyTypeObject类型的实例,即 Python 层int类型对应的 C 类型对象。

在仓库头文件 longobject.h 中可以看到其声明:

typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */ PyAPI_DATA(PyTypeObject) PyLong_Type;

2.2 小整数缓存(-5 ~ 256)

官方文档特别提示了一个经典实现细节:PyLong_FromLong当前实现会为-5到256之间所有整数维护一个对象数组,当创建该范围内的整数时,实际返回的是既有对象的引用(引用计数 +1),而不是新建对象。因此从技术上说,可以通过某种方式改变1这个对象的值——但 Python 在该场景下的行为是未定义的,切勿尝试。

这一机制在仓库源码 longobject.c 中由两个宏定义:

#define NSMALLPOSINTS 257 /* 0 .. 256 */ #define NSMALLNEGINTS 5 /* -1 .. -5 */

配合预分配的静态数组small_ints[NSMALLNEGINTS + NSMALLPOSINTS](第 43 行)与get_small_int()(第 48-62 行)实现。PyLong_FromLong在进入分配逻辑前先执行CHECK_SMALL_INT(ival)(第 251 行)快速返回缓存对象。这一设计让高频出现的小整数(如循环计数、状态码、布尔值)零分配复用,显著减少内存与 GC 压力——对 KBEngine 这类每帧处理海量实体的服务端尤为重要。

三、类型检查:PyLong_Check 与 PyLong_CheckExact

  • int PyLong_Check(PyObject *p):当参数是PyLongObject或其子类型时返回真。
  • int PyLong_CheckExact(PyObject *p):仅当参数精确是PyLongObject(不是子类型)时返回真。

两者在头文件中的实现差异直观展示了"包含子类"与"排除子类"的区别:

#define PyLong_Check(op) PyType_FastSubclass(Py_TYPE(op), Py_TPFLAGS_LONG_SUBCLASS) #define PyLong_CheckExact(op) (Py_TYPE(op) == &PyLong_Type)

KBEngine 应用实例:在实体属性类型系统 datatype.cpp 中,UInt64Type::isSameType()首先调用PyLong_Check(pyValue)确认脚本传入的默认值/属性值确实是整数对象,否则抛出类型错误:

if (!PyLong_Check(pyValue)) { OUT_TYPE_ERROR("UINT64"); return false; }

随后才调用PyLong_AsUnsignedLongLong(pyValue)等转换接口并检查PyErr_Occurred(),形成"先检查类型、再尝试转换、最后核对异常"的稳健范式。

四、创建整数对象:PyLong_From* 系列

4.1 从有符号整数创建

PyObject* PyLong_FromLong(long v); PyObject* PyLong_FromLongLong(long long v); PyObject* PyLong_FromSsize_t(Py_ssize_t v);
  • 返回新的PyLongObject;失败返回NULL。
  • PyLong_FromLong是最高频的入口:先命中-5~256小整数缓存,再按PyLong_SHIFT位数逐段构造对象(longobject.c)。
  • PyLong_FromLongLong处理 64 位 C 整数;PyLong_FromSsize_t处理平台相关的Py_ssize_t(通常为 64 位),适合表示长度、索引、缓冲区大小。

4.2 从无符号整数创建

PyObject* PyLong_FromUnsignedLong(unsigned long v); PyObject* PyLong_FromUnsignedLongLong(unsigned long long v); PyObject* PyLong_FromSize_t(size_t v);

用于把 C 的无符号类型转成 Pythonint,覆盖数据库自增 ID、字节长度、内存地址等非负数值场景。KBEngine 的py_memorystream.cpp在向 Python 层返回流读写位置时大量使用:

PyTuple_SET_ITEM(args1, 0, PyLong_FromUnsignedLong(pPyMemoryStream->stream().rpos())); PyTuple_SET_ITEM(args1, 1, PyLong_FromUnsignedLong(pPyMemoryStream->stream().wpos()));

4.3 从浮点数创建

PyObject* PyLong_FromDouble(double v);

取v的整数部分构造PyLongObject,失败返回NULL。注意它不进行四舍五入,行为等价于 Python 层的int(float_value)。

4.4 从字符串创建

PyObject* PyLong_FromString(const char *str, char **pend, int base);
  • 根据基数base解释str中的数字串构造整数。
  • 若pend非NULL,*pend会指向str中紧跟数字表示之后的第一个字符(可用于字符串解析器逐段消费输入)。
  • 若base == 0,按 Python 的 整数字面量规则 解释:即支持0x/0o/0b前缀自动判定进制;此时若一个非零十进制数带有前导零,会抛出ValueError(Python 3 禁止0123这种写法)。
  • 若base != 0,则必须在 2~36(含)之间。
  • 忽略前导空格,以及进制前缀之后、数字之间的单个下划线(Python 3.6+ 的字面量下划线分隔符)。
  • 若没有任何数字,抛出ValueError。

4.5 从 Unicode 字符串创建

PyObject* PyLong_FromUnicode(Py_UNICODE *u, Py_ssize_t length, int base); PyObject* PyLong_FromUnicodeObject(PyObject *u, int base);
  • 两者均先将 Unicode 串用PyUnicode_EncodeDecimal编码为字节串,再交给PyLong_FromString完成解析。
  • PyLong_FromUnicode属于旧的Py_UNICODEAPI,自 3.3 起标记弃用、计划 4.0 移除,请迁移到PyLong_FromUnicodeObject(3.3 新增)。

4.6 从指针创建

PyObject* PyLong_FromVoidPtr(void *p);

把 C 指针p的值(即地址数值)转成 Python 整数,供脚本以整数形式安全持有句柄;配套的取回接口是PyLong_AsVoidPtr。KBEngine 中各类资源句柄、原生对象指针需要透传给 Python 层时即可采用此通道。

五、提取 C 值:PyLong_As* 系列

以下接口把 Pythonint对象转换回 C 类型。共同要点:

  1. 若obj不是PyLongObject实例,会先尝试调用其__int__方法(若存在)转换成PyLongObject后再提取;
  2. 值超出目标 C 类型范围时抛出OverflowError;
  3. 出错时返回哨兵值(如-1、-1.0、NULL),必须用PyErr_Occurred()区分"合法值"与"错误",因为-1本身可能是合法数值。

5.1 有符号提取

函数返回类型溢出异常备注
PyLong_AsLonglongOverflowError(相对LONG_MAX/LONG_MIN)错误时返回-1
PyLong_AsLongAndOverflowlong通过*overflow报告见下文
PyLong_AsLongLonglong longOverflowError3.2 起提供
PyLong_AsLongLongAndOverflowlong long通过*overflow报告见下文
PyLong_AsSsize_tPy_ssize_tOverflowError(相对PY_SSIZE_T_MAX)要求精确为PyLongObject实例

*AndOverflow变体的语义(以PyLong_AsLongAndOverflow(PyObject *obj, int *overflow)为例):

  • 若obj的值大于LONG_MAX或小于LONG_MIN,将*overflow置为1或-1并返回-1;
  • 否则*overflow置0;
  • 若发生其他异常(如__int__失败),*overflow置0并照常返回-1。

PyLong_AsLongLongAndOverflow与之等价,只是比较对象换成PY_LLONG_MAX/PY_LLONG_MIN。该变体的价值在于:调用方既能通过*overflow精确得知"是否因超范围而失败",又能继续用PyErr_Occurred()排查其他错误。

5.2 无符号提取

函数返回类型溢出异常备注
PyLong_AsUnsignedLongunsigned longOverflowError(相对ULONG_MAX)要求精确为PyLongObject;错误返回(unsigned long)-1
PyLong_AsSize_tsize_tOverflowError(相对SIZE_MAX)要求精确为PyLongObject
PyLong_AsUnsignedLongLongunsigned long longOverflowError自 3.1 起负值抛OverflowError而非TypeError
PyLong_AsUnsignedLongMaskunsigned long无溢出时按ULONG_MAX + 1取模(掩码语义)
PyLong_AsUnsignedLongLongMaskunsigned long long无溢出时按PY_ULLONG_MAX + 1取模(掩码语义)

掩码(Mask)变体的关键差异:普通 As 接口在值过大时抛OverflowError;而 Mask 系列采用截断取模语义,把值折叠进目标位宽(类似 C 的隐式窄化),不抛异常。这在需要"低 N 位"位运算、哈希计算、随机数生成等场景非常实用。

KBEngine 应用实例:UInt64Type::addToStream()采用"逐级降档"策略把脚本整数写入网络流——先尝试PyLong_AsUnsignedLongLong,失败则清错后尝试PyLong_AsUnsignedLong,再失败则尝试PyLong_AsLong,每次转换后用PyErr_Occurred()判断并PyErr_Clear()清理异常,最终得到uint64 v后通过(*mstream) << v;写入内存流:

uint64 v = static_cast<uint64>(PyLong_AsUnsignedLongLong(pyValue)); if (PyErr_Occurred()) { PyErr_Clear(); v = (uint64)PyLong_AsUnsignedLong(pyValue); if (PyErr_Occurred()) { PyErr_Clear(); v = (uint64)PyLong_AsLong(pyValue); // ... 最终失败则报 TypeError 并置 0 } } (*mstream) << v;

对应地,UInt64Type::createFromStream()从流读出uint64 val后用PyLong_FromUnsignedLongLong(val)重建 Python 整数供脚本使用。这种"流读出 → C 类型 →PyLong_From*→ Python 对象 →PyLong_As*→ C 类型 → 流写入"的往返,正是 KBEngine 实体属性在进程间与数据库间传输的通用模式。

5.3 浮点与指针提取

  • double PyLong_AsDouble(PyObject *pylong):转成 Cdouble;超出double范围抛OverflowError;出错返回-1.0,需用PyErr_Occurred()判别。
  • void* PyLong_AsVoidPtr(PyObject *pylong):把整数转回 C 指针;仅对由PyLong_FromVoidPtr产生的值保证可用的指针;转换失败抛OverflowError;出错返回NULL。

六、错误处理范式:-1 返回值的歧义

官方文档反复强调的"第一守则":大多数PyLong_As*API 出错时返回(返回类型)-1,而-1本身完全可能是合法数值(例如PyLong_AsLong(PyLong_FromLong(-1)))。因此绝不能只用返回值判断成败,必须配合:

if (PyErr_Occurred()) { /* 处理异常:打印、清理或转换 */ PyErr_Clear(); }

KBEngine 源码正是这一范式的教科书级应用——datatype.cpp与py_memorystream.cpp中几乎每个PyLong_As*调用之后都紧跟PyErr_Occurred()检查,并在降档重试前用PyErr_Clear()清除挂起异常,避免异常状态污染后续调用。

七、KBEngine 中的综合实战:PyLong 贯穿属性类型系统与内存流

7.1 实体属性类型(entitydef/datatype.cpp)

KBEngine 的实体属性定义支持INT8/INT16/INT32/INT64/UINT8/UINT16/UINT32/UINT64等整型。以datatype.cpp为证,每个类型都实现四件事:

  1. isSameType(PyObject*):用PyLong_Check初筛 +PyLong_As*试转换 +PyErr_Occurred()终判;
  2. parseDefaultStr(std::string):把.def文件中的默认值字符串解析为数值,用PyLong_FromUnsignedLongLong等构造 Python 对象;
  3. addToStream(MemoryStream*, PyObject*):PyLong_As*取 C 值后写入MemoryStream(序列化到网络包/存档);
  4. createFromStream(MemoryStream*):从流读 C 值后用PyLong_From*还原 Python 对象。

INT64与UINT64的处理逻辑几乎相同,仅把PyLong_AsUnsignedLongLong/PyLong_FromUnsignedLongLong换成PyLong_AsLongLong/PyLong_FromLongLong,并对负值做范围约束。

7.2 Python 内存流(pyscript/py_memorystream.cpp)

KBEngine 将 C++MemoryStream封装给 Python 层使用(脚本可自定义网络协议包)。在 py_memorystream.cpp 中可以看到一整套完整的整数读写映射:

  • 写入方向:PyLong_AsUnsignedLong/PyLong_AsLong/PyLong_AsUnsignedLongLong/PyLong_AsLongLong按目标宽度取出 C 值;
  • 读取方向:PyLong_FromUnsignedLong/PyLong_FromLong/PyLong_FromUnsignedLongLong/PyLong_FromLongLong把流中读出的 C 值封装回 Python 对象(如第 425-467 行的按类型分发逻辑)。

这再次印证:任何要在 C++ 与 Python 之间传递整数的系统调用,都绕不开本节介绍的 From/As API 及其错误处理约定。

7.3 其他典型调用点

仓库中PyLong_*还广泛出现在 script.cpp、py_compression.cpp、py_gc.cpp、pyurl.cpp 以及服务端 entity.cpp、proxy.cpp、pybots.cpp 等文件中,承担参数解析、ID 传递、实体属性收发等职责。

八、实践要点与常见陷阱

  1. 不要忘记PyErr_Occurred():返回值-1/-1.0/NULL均可能是合法结果,只有异常状态能区分。
  2. 小整数缓存是共享引用:-5~256范围内的PyLong_FromLong返回既有对象(引用计数 +1),切勿通过修改其内容"改变 1 的值",行为未定义。
  3. 无符号转换与负值:PyLong_AsUnsignedLongLong对负值自 Python 3.1 起抛OverflowError;需要位模式语义时改用 Mask 系列(按2^位宽取模)。
  4. *AndOverflow与普通接口的选择:需要区分"超范围"与"其他错误"时用PyLong_AsLongAndOverflow/PyLong_AsLongLongAndOverflow;仅需简单转换时用普通接口。
  5. PyLong_AsSsize_t/PyLong_AsUnsignedLong/PyLong_AsSize_t要求精确类型:文档明确这些接口要求参数必须是PyLongObject实例(不自动调用__int__),传入其他类型需先自行转换。
  6. 字符串解析注意进制定位:base=0时0123这类前导零十进制会抛ValueError;下划线分隔符与进制前缀之后、数字之间的单个下划线会被忽略。
  7. 内存所有权:所有PyLong_From*都返回新引用(小整数除外),用完需Py_DECREF或交给Py_BEGIN_ALLOW_THREADS等所有权管理;KBEngine 中使用S_RELEASE(pyval)宏统一释放。
  8. 跨平台位宽差异:long在 Windows 为 32 位、Linux 为 64 位;Py_ssize_t、size_t随平台变化。需要固定 64 位语义时优先使用long long/unsigned long long系接口。

九、总结

PyLong C API 是 CPython 整数对象的标准接口集:PyLong_Check/PyLong_CheckExact负责类型判定,PyLong_From*负责从各类 C 值、字符串、指针构造整数,PyLong_As*负责把整数提取回各类 C 值并妥善处理溢出与错误。在 KBEngine 中,这套 API 是实体属性类型系统(datatype.cpp)、Python 内存流(py_memorystream.cpp)以及服务端与脚本交互的基石。理解其"任意精度 + 小整数缓存 + 哨兵错误值 +PyErr_Occurred()判定"的设计哲学,即可在自研扩展模块或引擎二次开发中写出健壮、零内存泄漏的桥接代码。

如需查阅全部接口的权威定义,可直接阅读仓库内嵌文档 kbe/src/lib/python/Doc/c-api/long.rst,并结合 longobject.h(声明)与 longobject.c(实现)交叉验证。

  • 游戏开发
  • 后端

【免费下载链接】kbengine

A MMOG engine of server.

项目地址:https://gitcode.com/gh_mirrors/kb/kbengine
点击查看免费下载
上一篇:如何使用Larastan确保Eloquent模型关系的类型安全:完整指南
下一篇:yuzu Switch 模拟器快速上手指南:免费 5 分钟跑通 NSP/XCI 首次出画面

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进