- 游戏开发
- 后端
【免费下载链接】kbengine
A MMOG engine of server.
KBEngine 是一个使用 C++ 编写核心、Python 编写游戏逻辑的分布式游戏服务器引擎,其在kbe/src/lib/python下内置了一份定制化的 CPython 3.7.3 解释器(版本号见 patchlevel.h),引擎的 C++ 代码与 Python 脚本层正是通过这份 C API 交互。本指南以该解释器自带的官方参考文档 List Objects(list.rst) 为骨架,完整梳理PyList_*类型检查、创建、读写、修改、切片、排序、转元组及内存池的全部接口,并结合 listobject.c 的底层实现与引擎各模块的真实调用代码,讲透引用计数语义、宏与函数差异和常见陷阱,帮助你写出能在 KBEngine 扩展模块中安全、正确地操作 Python list 的 C/C++ 代码。
一、背景:为什么引擎 C++ 需要直接操作 Python list
KBEngine 的服务器进程(baseapp、cellapp、dbmgr 等)在 C++ 层完成高并发网络、AOI、导航等核心逻辑,而实体属性、AI、玩法规则用 Python 脚本编写。双方交换批量数据时,Python list 是最常用的容器:实体属性批量下发、方法参数收集、序列化与反序列化(pickle)、EntityDef 装饰器参数解析等场景都会产生 list。
引擎源码中到处可见PyList_*的调用,例如:
- sequence.cpp 中
Sequence::seq_concat、seq_repeat、seq_slice用PyList_New+PyList_SET_ITEM构造拼接、重复、切片结果; - vector2.cpp 的
ScriptVector2::__py_pyList用PyList_New(VECTOR_SIZE)+PyList_SetItem把向量转成 Python list; - fixedarray.cpp 用
PyList_New(0)生成空初始化数据,并在__py_reduce_ex__中用PyList_New(len)+PyList_SET_ITEM实现实体定长数组的序列化; - py_entitydef.cpp 用
PyList_Size+PyList_GetItem遍历inspect.getfullargspec返回的 list,校验 EntityDef 装饰器方法参数。
理解这些调用的语义,是读懂乃至扩展 KBEngine 底层模块的基础。
二、类型对象与类型检查
2.1 PyListObject 与 PyList_Type
PyListObjectPyListObject是PyObject的一个子类型,代表一个 Python list 对象。其内部结构(见 listobject.c 中的定义)由ob_item(PyObject **元素数组)、Py_SIZE(当前逻辑长度)与allocated(已分配容量)构成,扩容逻辑由list_resize负责——这正是 Python list 采用“动态数组 + 预分配容量”策略、且append均摊 O(1) 的原因。
PyTypeObject PyList_TypePyList_Type是PyTypeObject类型的实例,代表 Python 的 list 类型本身,与 Python 层的list是同一个对象(PyList_Type的初始化见 listobject.c)。任何模块要注册、判断或实例化 list 类型都要用到它。
2.2 PyList_Check 与 PyList_CheckExact
int PyList_Check(PyObject *p) int PyList_CheckExact(PyObject *p)PyList_Check(p):若p是 list 对象、或 list 类型的子类型实例,返回真(内部等价于PyObject_TypeCheck(p, &PyList_Type))。PyList_CheckExact(p):仅当p是 list 对象且不是子类型实例时返回真(内部直接比较Py_TYPE(p) == &PyList_Type)。
两者返回 0 表示“不是”,不做异常设置。选择建议:若后续要按PyListObject的内部布局直接访问ob_item(例如PyList_GET_ITEM),应使用PyList_CheckExact,因为子类型可能重载了序列行为;若只是泛型序列处理,用PyList_Check更宽容。
三、创建与填充:PyList_New 的“NULL 元素”陷阱
PyObject* PyList_New(Py_ssize_t len)成功时返回一个长度为len的新 list,失败返回NULL。其实现(listobject.c)会先尝试从空闲链表(free list)复用对象,否则PyObject_GC_New新分配;当len > 0时用PyMem_Calloc申请元素数组(自动清零),并设置Py_SIZE(op) = size、op->allocated = size。
文档中有一条必须牢记的 note:
若
len大于 0,返回 list 的所有元素槽位均被置为NULL。因此在把所有槽位用PyList_SetItem填上真实对象之前,不能对它调用PySequence_SetItem之类的抽象 API,也不能把它暴露给 Python 代码。
也就是说,PyList_New(5)之后若立刻被 Python 侧访问或打印,会因槽位是 NULL 而崩溃或触发断言。正确模式是“创建后立即用PyList_SetItem/PyList_SET_ITEM逐个填充”,KBEngine 的 vector2.cpp 就是标准示范:
PyObject* pyList = PyList_New(VECTOR_SIZE); // 先建空槽位列表 for (int i = 0; i < VECTOR_SIZE; ++i) PyList_SetItem(pyList, i, PyFloat_FromDouble(v[i])); // 逐个填充 return pyList;PyList_New的两个相关实现细节:
len < 0会调用PyErr_BadInternalCall()并返回NULL,属于编程错误;len <= 0时ob_item置为NULL,得到空列表。
四、读取列表:长度与元素
Py_ssize_t PyList_Size(PyObject *list) // 等价于 len(list) Py_ssize_t PyList_GET_SIZE(PyObject *list) // 宏,无错误检查PyList_Size返回 list 的长度,等价于 Python 层的len(list);实现在 listobject.c,先用PyList_Check校验,失败时调用PyErr_BadInternalCall()并返回-1。PyList_GET_SIZE是其宏形式,跳过类型检查直接返回Py_SIZE(list),仅在确认参数确实是PyListObject时使用。
PyObject* PyList_GetItem(PyObject *list, Py_ssize_t index) PyObject* PyList_GET_ITEM(PyObject *list, Py_ssize_t i)PyList_GetItem返回index位置的元素。只支持正数索引,不支持从尾部负数索引;越界时返回NULL并设置IndexError异常(实现见 listobject.c,异常对象使用缓存的"list index out of range"字符串,避免重复创建)。- 返回的是借用引用(borrowed reference):调用方不能
Py_DECREF它,同时要注意在持有期间对象可能被其他代码修改而失效。 PyList_GET_ITEM是宏形式,无错误检查,仅限确定索引有效时使用。
KBEngine 在 py_entitydef.cpp 中组合使用这两个接口遍历方法参数名列表:
Py_ssize_t argsSize = PyList_Size(pyGetMethodArgsResult); // 取长度 for (Py_ssize_t i = 1; i < argsSize; ++i) { PyObject* pyItem = PyList_GetItem(pyGetMethodArgsResult, i); // 取元素(借用引用) const char* ccattr = PyUnicode_AsUTF8AndSize(pyItem, NULL); defContext.argsvecs.push_back(ccattr); }五、写入与修改:引用窃取语义是关键
int PyList_SetItem(PyObject *list, Py_ssize_t index, PyObject *item) // 0 成功,-1 失败 void PyList_SET_ITEM(PyObject *list, Py_ssize_t i, PyObject *o) // 宏,无错误检查PyList_SetItem把index位置的元素设为item,实现见 listobject.c:越界时返回-1并设置IndexError(同时会Py_XDECREF(newitem),所以传入的引用在失败时也会被释放,调用方不应再使用);成功时通过Py_XSETREF(*p, newitem)完成“新引用 + 释放旧元素”。
文档对二者给出两条关键 note:
PyList_SetItem:该函数“窃取”(steals)对
item的一个引用,并释放被替换位置原有元素的一个引用。
PyList_SET_ITEM:同样窃取
o的引用,但与PyList_SetItem不同,它不会释放被替换位置元素的引用——若该位置原本已有对象,其引用会被泄漏。因此这个宏通常只用于填充全新列表(原本无内容)。
含义:
- 调用
PyList_SetItem/PyList_SET_ITEM之后,调用方不再拥有item的引用(要么已转移给 list,要么在失败时已被释放),一般不再对其Py_DECREF; - 填充新建列表时常用
PyList_SET_ITEM(零成本、不检查),但要保证槽位原本为空;对已有内容的位置改写用PyList_SetItem才能正确释放旧值。
int PyList_Insert(PyObject *list, Py_ssize_t index, PyObject *item) // 0 成功,-1 失败 int PyList_Append(PyObject *list, PyObject *item) // 0 成功,-1 失败PyList_Insert等价于list.insert(index, item),把item插入到index之前。底层ins1(listobject.c)先做list_resize(self, n+1)扩容,再处理负索引归一化、元素后移、Py_INCREF(v)后写入。注意index允许为负(与 Python 语义一致),且越界会被夹到 [0, n] 区间;长度达到PY_SSIZE_T_MAX时设置OverflowError。PyList_Append等价于list.append(item),底层app1(listobject.c)同样先扩容再Py_INCREF(v)后写入尾部。二者失败时返回-1并设置异常。
与PyList_SetItem的窃取语义不同,PyList_Insert/PyList_Append不窃取item的引用,调用方仍拥有自己的那份引用,需自行Py_DECREF(若不再使用)。
六、切片操作:GetSlice / SetSlice
PyObject* PyList_GetSlice(PyObject *list, Py_ssize_t low, Py_ssize_t high) int PyList_SetSlice(PyObject *list, Py_ssize_t low, Py_ssize_t high, PyObject *itemlist)PyList_GetSlice返回[low, high)区间元素构成的新 list,等价于list[low:high];失败返回NULL并设置异常。底层list_slice(listobject.c)先PyList_New(len)再对每个元素Py_INCREF复制,即浅拷贝。不支持负索引(与 Python 切片不同),且未做边界钳制,调用前应自行确保0 <= low <= high <= 长度。PyList_SetSlice把[low, high)区间替换为itemlist的内容,等价于list[low:high] = itemlist。itemlist可以为NULL,表示赋空列表,即删除该切片(del list[low:high])。成功返回 0,失败返回 -1,同样不支持负索引。底层list_ass_slice(listobject.c)负责处理长度差带来的扩容/缩容与元素引用释放。
切片操作的边界规范可总结为:
| 操作 | Python 等价 | 负索引 | 越界行为 |
|---|---|---|---|
PyList_GetSlice | list[low:high] | 不支持 | 未钳制,需调用方保证合法 |
PyList_SetSlice | list[low:high] = itemlist | 不支持 | 未钳制,需调用方保证合法 |
itemlist == NULL | del list[low:high] | — | 同上 |
七、排序、反转与转元组
int PyList_Sort(PyObject *list) // 0 成功,-1 失败,等价于 list.sort() int PyList_Reverse(PyObject *list) // 0 成功,-1 失败,等价于 list.reverse() PyObject* PyList_AsTuple(PyObject *list) // 等价于 tuple(list)PyList_Sort原地排序,实现(listobject.c)先校验类型再委托list_sort_impl,即 Python 层list.sort()的同一实现(Timsort),支持key与reverse参数路径;参数不是 list 时设置BadInternalCall并返回-1。PyList_Reverse原地反转,长度大于 1 时调用reverse_slice交换元素(listobject.c),元素引用计数不变。PyList_AsTuple返回包含list全部内容的新 tuple(浅拷贝:新 tuple 对每个元素Py_INCREF,见 listobject.c)。若 list 后续会频繁修改而调用方需要稳定快照,用它比保留 list 更安全。
八、内存优化:free list 与 PyList_ClearFreeList
int PyList_ClearFreeList() // 清空空闲链表,返回释放的元素总数(3.3 版本新增)CPython 为 list 维护了一个空闲对象池(free list):list 对象被销毁时,若池未满且是精确 list 类型(PyList_CheckExact),会把对象挂入池中而不是直接归还内存(list_dealloc,见 listobject.c);下次PyList_New时优先复用池中对象,显著减少高频创建/销毁小列表时的 malloc/free 开销。
池的容量与结构(listobject.c):
#ifndef PyList_MAXFREELIST #define PyList_MAXFREELIST 80 #endif static PyListObject *free_list[PyList_MAXFREELIST]; static int numfree = 0;PyList_ClearFreeList遍历池并PyObject_GC_Del释放全部缓存对象,返回被释放数量。因为该 API 自 Python 3.3 引入(文档标注versionadded:: 3.3),本仓库内置的 3.7.3 解释器完全可用。它主要供嵌入式宿主(如 KBEngine)在需要释放解释器内部缓存、统计内存或配合gc模块时调用;KBEngine 在批量创建脚本对象的高频路径上正是受益于这一池化机制。
九、引用计数与安全使用速查
综合文档与 listobject.c 的实现,把最容易出错的地方汇总如下:
| 场景 | 正确做法 | 依据 |
|---|---|---|
PyList_New(len)后 | 必须用PyList_SetItem/PyList_SET_ITEM填满所有槽位,才能暴露给 Python | 文档 note:槽位初始为NULL |
PyList_SetItem/PyList_SET_ITEM | 窃取item引用,调用方此后不再拥有 | 文档 note |
PyList_SET_ITEM改写旧槽 | 会泄漏被替换元素的引用,仅限填充新列表 | 文档 note |
PyList_Insert/PyList_Append | 不窃取引用,调用方需自行释放自己的那份 | 文档与ins1/app1实现(Py_INCREF(v)) |
PyList_GetItem/PyList_GET_ITEM | 返回借用引用,不可Py_DECREF | 文档语义 |
| 索引访问 | 只用正索引;越界会得到IndexError(GetItem/SetItem) | 文档与 listobject.c |
| 切片 | GetSlice/SetSlice不支持负索引,边界需自行保证 | 文档 note |
| 类型判断 | 需要精确 list 用PyList_CheckExact,宽容判断用PyList_Check | 文档语义 |
| 宏 vs 函数 | 宏(GET_SIZE/GET_ITEM/SET_ITEM)不做错误检查,仅限类型与索引已确认 | 文档语义 |
十、KBEngine 源码中的典型调用模式
引擎内部对PyList_*的调用可分为三类典型模式,值得作为扩展开发模板:
模式一:新建 + 填充(配合窃取引用)。vector2.cpp 的向量转 list;fixedarray.cpp 的__py_reduce_ex__序列化定长数组,先用PyList_New(len)建空槽,再对每个元素Py_INCREF后PyList_SET_ITEM填入——这正是文档要求的标准填充流程。
模式二:序列运算的中间结果。sequence.cpp 中:
seq_concat(PyList_New(seqSize1 + seqSize2))先复制自身元素,再用PySequence_GetItem取对方元素直接PyList_SET_ITEM填入(该函数返回新引用,被窃取后无需再释放);seq_repeat对n <= 0直接返回PyList_New(0),并通过PyList_GET_ITEM+PyList_SET_ITEM复制已填内容完成循环展开;seq_slice用PyList_New(length)+PyList_SET_ITEM构造切片结果。
模式三:读取外部返回值。py_entitydef.cpp 用PyList_Size+PyList_GetItem遍历 Pythoninspect模块返回的参数名 list,属于只读借用场景。
十一、接口总览速查表
以下表格完整覆盖本仓库官方文档 list.rst 所列全部接口:
| 接口 | 签名 | 返回值 | Python 等价 / 说明 |
|---|---|---|---|
PyListObject | — | — | list 对象的 C 结构类型,PyObject子类型 |
PyList_Type | PyTypeObject | — | list 类型对象,即 Python 层list |
PyList_Check | int (PyObject *p) | 真/假 | p是 list 或其子类型实例 |
PyList_CheckExact | int (PyObject *p) | 真/假 | p是 list 且非子类型实例 |
PyList_New | PyObject* (Py_ssize_t len) | list 或NULL | 新列表,len>0时槽位为NULL |
PyList_Size | Py_ssize_t (PyObject *list) | 长度或-1 | len(list) |
PyList_GET_SIZE | Py_ssize_t (PyObject *list) | 长度 | 宏,无错误检查 |
PyList_GetItem | PyObject* (PyObject *list, Py_ssize_t index) | 元素或NULL | 仅正索引,越界置IndexError |
PyList_GET_ITEM | PyObject* (PyObject *list, Py_ssize_t i) | 元素 | 宏,无错误检查 |
PyList_SetItem | int (PyObject *list, Py_ssize_t index, PyObject *item) | 0 或-1 | 窃取引用,释放被替换旧引用 |
PyList_SET_ITEM | void (PyObject *list, Py_ssize_t i, PyObject *o) | — | 宏,窃取引用但会泄漏被替换引用 |
PyList_Insert | int (PyObject *list, Py_ssize_t index, PyObject *item) | 0 或-1 | list.insert(index, item) |
PyList_Append | int (PyObject *list, PyObject *item) | 0 或-1 | list.append(item) |
PyList_GetSlice | PyObject* (PyObject *list, Py_ssize_t low, Py_ssize_t high) | 新 list 或NULL | list[low:high],不支持负索引 |
PyList_SetSlice | int (PyObject *list, Py_ssize_t low, Py_ssize_t high, PyObject *itemlist) | 0 或-1 | list[low:high] = itemlist;NULL表示删除切片 |
PyList_Sort | int (PyObject *list) | 0 或-1 | list.sort() |
PyList_Reverse | int (PyObject *list) | 0 或-1 | list.reverse() |
PyList_AsTuple | PyObject* (PyObject *list) | 新 tuple 或NULL | tuple(list),浅拷贝 |
PyList_ClearFreeList | int () | 释放数量 | 清空 list 空闲池(3.3+) |
结语
PyList_*接口看似简单,但“槽位初始为 NULL”“引用窃取”“借用引用”“宏不做检查”这四类语义决定了它比普通 C 数组更容易踩坑。本文以 KBEngine 内置 Python 3.7.3 的官方文档 list.rst 为主线、以 listobject.c 的实现为证据,逐项覆盖了类型检查、创建填充、读写、插入追加、切片、排序反转、转元组与内存池机制,并给出了引擎自身在 pyscript、entitydef 模块中的真实用法。后续无论是为 KBEngine 编写新的 C++ 扩展模块,还是阅读引擎底层代码,都可以把本文的速查表作为直接参考。
- 游戏开发
- 后端
【免费下载链接】kbengine
A MMOG engine of server.
相关推荐
KBEngine 中的 Python C API 抽象对象层(Abstract Objects Layer)完全指南:七大协议详解与引擎源码实践
KBEngine 中的 Python C API 抽象对象层(Abstract Objects Layer)完全指南:七大协议详解与引擎源码实践 导读 本文以
游戏开发后端KBEngine 内嵌 Python 3.7 的 Codec 注册机制:C API 函数族深度解析
KBEngine 内嵌 Python 3.7 的 Codec 注册机制:C API 函数族深度解析 本篇技术指南以 KBEngine 仓库内嵌 Python 3
游戏开发后端Python C API 字符串转换与格式化:PyOS_* 系列函数全解析(KBEngine 内置 Python 3.7 源码视角)
Python C API 字符串转换与格式化:PyOS_ 系列函数全解析(KBEngine 内置 Python 3.7 源码视角) 导读 本文围绕 Python
游戏开发后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考