在Python中通过ctypes模块定义的结构体,经常需要跨越Python与C库之间的边界传递数据。当结构体内部包含指针字段,例如指向另一个结构体数组或者字符串缓冲区的指针时,普通的赋值操作并不能产生真正独立的副本。理解ctypes对象的内存模型,是正确处理指针字段深度复制的前提。

ctypes结构体内存模型与指针字段本质
ctypes中定义的结构体继承自ctypes.Structure,其字段通过_fields_元组和C语言一样被布局在连续的内存块中。如果某个字段声明为POINTER类型,例如LP_c_int,那么在结构体占用内存里存放的仅仅是一个地址值,而不是地址所指向的数据本身。这意味着结构体对象A和结构体对象B即使拥有各自独立的结构体内存,只要它们的指针字段保存了相同的地址,就会共享同一份外部数据。
当我们写出b = a这样的语句时,在ctypes里只是让变量b引用了同一个Structure实例,任何通过b的修改都会直接反映到a上,这甚至谈不上复制。若使用copy.copy或者copy.deepcopy,对于指针字段,标准库的浅拷贝会复制指针地址,深拷贝在面对ctypes的指针对象时往往无法自动追踪并复制其指向的C层内存,结果仍然是两个结构体指向同一块通过create_string_buffer或cast得到的内存区域。下面的代码展示了这种共享现象:
import ctypes
class Point(ctypes.Structure):
_fields_ = [("x", ctypes.c_int), ("y", ctypes.c_int)]
class Line(ctypes.Structure):
_fields_ = [("start", ctypes.POINTER(Point))]
p = Point(1, 2)
l1 = Line()
l1.start = ctypes.pointer(p)
l2 = ctypes.Structure.__new__(Line)
# 仅复制结构体自身内存,指针字段存的是同一个地址
ctypes.memmove(ctypes.addressof(l2), ctypes.addressof(l1), ctypes.sizeof(Line))
print(l1.start.contents.x) # 1
l2.start.contents.x = 99
print(l1.start.contents.x) # 99,原结构体的数据被改动了
从上面的例子可以看出,单纯使用memmove复制结构体自身的内存,虽然让l2拥有了独立的结构体头部,但指针字段依然指向原来的Point对象。如果原Point被释放或者超出作用域被Python垃圾回收,l2访问start.contents就会引发非法内存访问。因此深度复制必须同时处理指针目标数据的分配与写入。
基于内存拷贝与手动重建的深度复制方案
实现真正安全的深度复制,核心思路是分两步:第一步复制结构体本身所在的连续内存,保证非指针字段的值被完整保留;第二步遍历所有指针类型字段,为目标数据新申请一块C层内存,把原指针指向的内容拷贝过去,并将新地址写进副本的指针字段。这种方式既利用了memmove的高效,又通过人工干预切断了数据共享。
我们可以编写一个通用的深度复制函数,利用_fields_反射结构体的字段定义。对于普通值类型字段,memmove已经处理;对于POINTER类型,则通过pointer_type._type_获取基类型大小,使用create_string_buffer或者cast配合新pointer来分配。下面给出一个处理单层指针的示例:
import ctypes
def deep_copy_struct(obj):
cls = type(obj)
new_obj = cls.__new__(cls)
ctypes.memmove(ctypes.addressof(new_obj), ctypes.addressof(obj), ctypes.sizeof(cls))
for name, ctype in cls._fields_:
if hasattr(ctype, "contents") or getattr(ctype, "__name__", "").startswith("LP_"):
old_ptr = getattr(obj, name)
if not old_ptr:
continue
base_type = ctype._type_
size = ctypes.sizeof(base_type)
buf = ctypes.create_string_buffer(size)
ctypes.memmove(buf, ctypes.addressof(old_ptr.contents), size)
new_ptr = ctypes.cast(buf, ctype)
setattr(new_obj, name, new_ptr)
return new_obj
这个函数在遇到指针字段时,为目标数据分配了独立的create_string_buffer,再把原内容拷入,最后用cast转成对应指针类型赋给新结构体。这样做之后,修改new_obj.start.contents不会影响原对象。不过要注意,如果指针指向的是数组或者嵌套结构体,上述代码只复制了第一个元素,需要额外根据长度信息循环拷贝,否则依然会出现部分共享。
嵌套指针与数组指针字段的完整处理策略
实际项目中,ctypes结构体常包含指向数组的指针,例如LP_c_int配合一个长度字段表示缓冲区。此时深度复制不仅要复制指针目标的首元素,还要按照真实长度复制整段连续内存。我们可以在结构体中约定长度字段名,或者在复制函数外传入每个指针字段对应的元素个数,从而精确分配和拷贝。
假设有一个音频帧结构体,其中data字段是指向c_short数组的指针,len字段记录采样数。深度复制时,应先按len * sizeof(c_short)申请缓冲区,再用memmove整段复制,而不是只处理一个c_short。示例代码如下:
import ctypes
class AudioFrame(ctypes.Structure):
_fields_ = [
("len", ctypes.c_int),
("data", ctypes.POINTER(ctypes.c_short))
]
def deep_copy_audio(frame):
new_frame = AudioFrame.__new__(AudioFrame)
ctypes.memmove(ctypes.addressof(new_frame), ctypes.addressof(frame), ctypes.sizeof(AudioFrame))
if frame.data:
n = frame.len
elem_size = ctypes.sizeof(ctypes.c_short)
buf = ctypes.create_string_buffer(n * elem_size)
ctypes.memmove(buf, ctypes.addressof(frame.data.contents), n * elem_size)
new_frame.data = ctypes.cast(buf, ctypes.POINTER(ctypes.c_short))
return new_frame
对于指向结构体的指针数组,策略类似:通过循环对每个元素调用前面提到的单结构体深度复制,再把得到的新结构体地址依次填入新分配的指针数组。若忽略循环,只复制首个结构体,后续元素仍关联原内存,会在原对象释放后造成悬空指针。此外,如果原结构体使用了byref方式传递给C函数,复制前应确保其指向的内存归Python侧所有,否则create_string_buffer新建的副本在C回调中可能无法被正确识别。
总结来说,ctypes结构体的深度复制绝不能依赖语言内置的拷贝工具。只有认清指针字段在结构体中仅保存地址这一事实,结合memmove复制头部、针对每个指针手动分配并拷贝目标内存,才能构造出完全独立、生命周期安全的副本。在涉及多层嵌套或数组时,必须把长度信息纳入复制逻辑,才能真正做到完整指南中所说的妥善处理。