PyPy环境下CFFI调用时bytes对象缓冲区重分配导致指针失效问题
问题成因与解答
1. 关于CFFI直接获取bytes底层缓冲区指针的假设是否成立
这个假设仅在CPython解释器下成立,在你使用的PyPy解释器下不成立:
- CPython采用引用计数为主、非移动分代GC为辅的内存管理机制,只要Python侧持有bytes对象的引用,其底层缓冲区的内存地址就不会发生变化,CFFI传递bytes到
char*类型参数时确实直接提取内部缓冲区指针,不会产生临时拷贝。 - PyPy采用可移动的分代GC机制,会定期进行内存紧凑整理,移动堆上存活对象的位置以减少内存碎片。bytes对象的底层缓冲区存储在GC托管的堆上,GC移动对象时只会自动更新Python侧的引用地址,不会同步修改C层持有的裸指针,因此调用返回后保留的指针会直接失效。你观测到同一个bytes对象两次调用传入的指针地址不同,就是两次调用的间隔触发了GC内存整理,bytes缓冲区被移动到了新的内存地址。
2. Python是否允许静默重分配bytes底层缓冲区
- CPython不会进行这类操作,只要bytes对象未被回收,底层缓冲区地址固定。
- PyPy的移动GC设计本身就允许静默移动包括bytes在内的所有堆内对象,该过程不需要感知外部C代码是否持有其指针,PyPy默认约定FFI调用传递的堆内对象指针仅在本次调用执行期间有效,调用返回后继续持有属于未定义行为。
3. 长期持有C层指针的正确实现方式
如果需要原生代码长期持有数据指针,需要使用CFFI分配的非移动堆内存,而非Python原生的bytes对象,示例如下:
from cffi import FFI ffi = FFI() lib = ffi.dlopen(libname) ffi.cdef(""" void foo(char*); """) x = b'abc123' # 用ffi.new分配非GC移动的内存,持有cdata_obj引用即可保证地址不变 cdata_obj = ffi.new("char[]", x) lib.foo(cdata_obj)
只要Python侧一直持有cdata_obj的引用,其对应的内存地址就不会发生变化,原生代码保留的指针可以长期安全使用。
内容的提问来源于stack exchange,提问作者DeducibleSteak
相关产品推荐
相关产品推荐

