在 Python 里用 ctypes 调用 C 库是非常方便的,但同时也是 内存管理的高风险区。这里的问题主要源自:
Python 自动管理内存(垃圾回收)
C 需要手动管理内存
两者交互时容易产生 悬空指针、内存泄漏或双重释放
我把核心问题梳理一下。
1. Python 对 ctypes 的对象管理
基本情况
import ctypes
lib = ctypes.CDLL("./mylib.so")
lib.make_buffer.restype = ctypes.POINTER(ctypes.c_char)
ptr = lib.make_buffer(100)
这里 Python 拿到的 ptr 是 一个指向 C 堆内存的指针。
关键:
Python 不知道这个内存的生命周期
ctypes 对这个指针本身有引用计数
但它 不会自动 free C 内存
错误示例:悬空指针
ptr = lib.make_buffer(100)
del ptr # Python 删除了 ctypes 对象
# C 内存仍然分配着,但 Python 没有引用
# 如果你再次访问,会出现悬空
2. C 内存分配与释放
C 库可能返回堆内存:
// mylib.c
#include
char* make_buffer(size_t n) {
return malloc(n);
}
void free_buffer(char* p) {
free(p);
}
Python 端必须保证:
ptr = lib.make_buffer(100)
# 使用完后手动释放
lib.free_buffer(ptr)
否则就是内存泄漏。
3. Python 对结构体的引用
如果你在 ctypes 中用结构体包装指针:
class MyStruct(ctypes.Structure):
_fields_ = [("buf", ctypes.POINTER(ctypes.c_char))]
s = MyStruct()
s.buf = lib.make_buffer(100)
注意:
s 被垃圾回收时不会调用 free,除非你手动调用
如果你直接把 s 传到 C 函数里,C 端可能修改 buf 内存
Python 如果继续使用 s.buf,但 C 已经 free,悬空访问
4. ctypes 字符串与 bytes 的问题
s = b"hello"
c_str = ctypes.create_string_buffer(s)
lib.process(c_str)
这里:
create_string_buffer 返回一个 ctypes 对象,Python 持有引用
当 c_str 被 GC 时,内存释放
如果 C 端保存指针到内部全局变量,Python 释放后,C 会悬空
解决办法:
保证 Python 对象的生命周期覆盖 C 使用周期
或者让 C 分配内存,并在 C 内部管理
5. 数组和缓冲区的陷阱
arr = (ctypes.c_int * 10)()
lib.process(arr)
这里 Python 仍然拥有数组内存,不需要 C 端 free。
但是如果你把 C 返回的指针赋值给 Python 数组:
arr_ptr = ctypes.cast(ptr, ctypes.POINTER(ctypes.c_int * 10))
注意:
Python 只是把指针包装成数组
内存仍在 C 堆
不能直接 del arr_ptr 释放 C 内存,需要调用 C 的 free
6. 使用 restype 和 argtypes 帮助安全
lib.make_buffer.restype = ctypes.POINTER(ctypes.c_char)
lib.make_buffer.argtypes = [ctypes.c_size_t]
lib.free_buffer.argtypes = [ctypes.POINTER(ctypes.c_char)]
好处:
Python 会做类型检查
避免传错类型导致悬空或崩溃
7. 避免重复释放
常见错误:
ptr = lib.make_buffer(100)
lib.free_buffer(ptr)
lib.free_buffer(ptr) # double free -> crash
解决方法:
Python 端最好封装成类:
class CBuffer:
def __init__(self, size):
self.ptr = lib.make_buffer(size)
self._freed = False
def free(self):
if not self._freed:
lib.free_buffer(self.ptr)
self._freed = True
def __del__(self):
self.free()
这样:
Python 对象销毁时自动释放
避免重复释放
8. 总结原则
谁分配,谁释放
C 分配的内存必须由 C 或封装类释放
Python 的 create_string_buffer 由 Python 管理
确保生命周期覆盖使用
Python 对象不要过早被 GC
C 指针不要在 Python 对象销毁后继续使用
避免手动操作裸指针
ctypes 尽量使用 Structure 或 create_string_buffer
封装好类管理生命周期
小心悬空与 double free
Python 不会自动 free C 堆内存
不要重复调用 free
明确类型
设置 argtypes 和 restype
避免传错类型导致未定义行为
如果你愿意,我可以
帮你画一张Python ↔ C 内存管理图,把 GC、C 堆、ctypes 对象、指针生命周期全都可视化,这样更直观理解什么时候会泄漏或悬空。