【问题标题】:Memory management and removal of leaks, in C and Python with ctypes, using the example of strings使用字符串示例在 C 和 Python 中使用 ctypes 进行内存管理和消除泄漏
【发布时间】:2021-11-23 21:12:30
【问题描述】:

我正在尝试通过 C 库修复基于 ctypes 的 Python 绑定中的泄漏。在使用 ctypes 时,我对字符串和其他指针的内存管理行为感到困惑。

据我所知,基本上我需要解决 3 个案例:

A) 在 C 中创建内存并将 c_char_p 作为函数的结果返回给 Python。

B) 在 Python 中创建一个字符串,并将其作为 C 函数的 char const* 参数传递,而不是在 C 中释放它:这是我唯一自信地解决的问题:我可以做 @ 987654323@ 转换成c_char_p,Python Alloc/GC 处理分配和释放。

C) 在 Python 中创建内存,并将其存储在 C 中(例如作为结构中的 char* 字段),以便稍后释放(使用 C 库中的函数)。

我的问题:

一)

  • c_char_p 是否因此创建了第二个指针?如果是这样,C分配的指针会发生什么?
  • 我们如何释放从 C 中返回的指针?
  • c_void_pc_char_p 被 GC 处理为 restype 的行为是否存在差异?

C)

  • 将 Python 中分配的内存指针的所有权“授予”给 C 的正确方法是什么? .encode() 显然创建了一个经过 GC 处理的 Python 对象。
  • 是由create_string_buffer 创建的字符串,也是Python-GC'ed ?

【问题讨论】:

    标签: python c memory-management memory-leaks ctypes


    【解决方案1】:

    使用ctypes.c_char_p 实现的关键是ctypes 对此返回类型进行了特殊处理,它将返回的以null 结尾的字节字符串复制到Python bytes 对象中并返回它。由于实际收到的返回类型是bytes 对象,而不是指针,因此无法访问原始指针。请注意,c_wchar_p 也会发生同样的事情,它被转换为 str 对象。

    对于A),解决方案是使用ctypes.POINTER(ctypes.c_char_p),不会发生转换,但如果需要可以手动完成。返回值可以传递回一个释放内存的 C 函数。

    对于B),您的想法是正确的。只需将bytes 对象传递给采用const char* 的C 函数,Python 将管理内存。如果 C 函数需要对象在函数调用之后仍然有效(可能用于稍后的回调),请务必保留对该对象的引用,直到不再需要它为止。

    对于C),您可以使用任何一种技术。如果 C 将在 C 函数中管理内存分配,请不要使用 c_char_p 获取它,并在 C 函数中释放它;否则,请使用create_string_buffer() 供 Python 管理和保留 C 代码需要的引用。

    所有技术的示例:

    test.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    #define API __declspec(dllexport)
    
    API char* funca() {
        char* p = malloc(6);
        strcpy_s(p, 6, "hello");
        return p;
    }
    
    API void funca_free(void* p) {
        *(char*)p = 'x'; // overwrite just to prove accessing a freed buffer
        free(p);         // that hasn't been re-used yet.
    }
    
    API void funcb(const char* p) {
        printf("B: %s\n", p);
    }
    
    static char* store = NULL;
    
    API void funcc(char* p) {
        store = p;
    }
    
    API char* funcc_get() {
        return store;
    }
    

    test.py

    import ctypes as ct
    
    dll = ct.CDLL('./test')
    dll.funca.argtypes = ()
    dll.funca.restype = ct.POINTER(ct.c_char) # NOT c_char_p, char* is lost
    dll.funca_free.argtypes = ct.c_void_p,
    dll.funca_free.restype = None
    dll.funcb.argtypes = ct.c_char_p,
    dll.funcb.restype = None
    dll.funcc.argtypes = ct.c_char_p,
    dll.funcc.restype = None
    dll.funcc_get.argtypes = ()
    dll.funcc_get.restype = ct.POINTER(ct.c_char) # NOT c_char_p
    
    def funca_wrap():
        p = dll.funca() # C-managed
        s = ct.cast(p, ct.c_char_p).value # Make a copy as a Python byte string
        dll.funca_free(p) # Free by C as needed
        return s
    
    print('A:', funca_wrap()) # Use a wrapper to capture string and free buffer
    
    dll.funcb(b'input') # Python-managed and freed
    
    s = ct.create_string_buffer(b'python') # Python-managed
    dll.funcc(s)
    p = dll.funcc_get()
    print('C(py-alloc):', ct.cast(p,ct.c_char_p).value)
    del s # Python-freed
    print('C(py-freed):', ct.cast(p,ct.c_char_p).value) # access freed memory (could crash)
    
    s = dll.funca() # C-managed (NOT c_char_p)
    dll.funcc(s)
    p = dll.funcc_get()
    print('C(C-alloc):', ct.cast(p,ct.c_char_p).value)
    dll.funca_free(s) # C-freed
    print('C(C-freed):', ct.cast(p,ct.c_char_p).value) # access freed memory (could crash)
    

    输出:

    A: b'hello'
    B: input
    C(py-alloc): b'python'
    C(py-freed): b'\x88\xc9?\xf1\x8a\x01'      # note freed memory reused in my case
    C(C-alloc): b'hello'
    C(C-freed): b'xello'                       # freed memory wasn't reused yet
    

    【讨论】:

    • 非常有帮助的答案,谢谢!不过我确实有一个问题:鉴于c_char_p 的特殊性,如果我想通过引用返回char*(我的意思是我的C 函数签名int my_func(char* *address_of_result); 并用char* str = NULL; status = my_func(&amp;str); 调用),应该如何那我继续你的计划吗?像POINTER(POINTER(c_char)),或者更确切地说是POINTER(c_char_p)
    • @TristanDuquesne 后者有效。由于您必须创建s = c_char_p() 并通过引用(例如,byref(s))将其传递给函数,因此当函数返回时,您将通过s 获得实际更新的c_char_p 对象,稍后可以将其释放如果需要的话。 s.value 将返回 char* 数据作为 bytes 对象。
    • 我收集到的是 c_char_p 不应该用于来自 C 的任何内容,除了字符串文字,因为它通常会丢失内存。无论如何都很高兴知道。
    • @TristanDuquesne 如果该值是以空字符结尾的字符串,并且您不需要为 C 管理指针,请使用它;否则,不要。
    【解决方案2】:

    这是改编自我在 Python discord 服务器上与回答问题的人的交流:

    • c_char_p 是否因此创建了第二个指针?如果是这样,C分配的指针会发生什么?

    是的,是的。 c_char_p 是一个位于堆上的 PyObject* 指针,它在其中存储了原始的 C char* 指针。

    • 我们如何释放从 C 中返回的指针?

    使用ctypesc_char_p 传递给free()

    • c_void_pc_char_p 被 GC 处理为 restype 的行为是否存在差异?

    不,在这两种情况下,当 c_void_pc_char_p 被垃圾回收时,它什么都不做,即它不会释放指向的原始 C 对象。

    • 将 Python 中分配的内存指针的所有权“授予”C 的正确方法是什么?

    您通常不会这样做 - Python 创建的内存通常由 Python 释放,C 创建的内存通常由 C 释放。如果您需要将某些内容从一个域传递到另一个域,最好复制它.可以通过使用 Python C API 使 C 端保持对 Python 对象的 Python 引用,但这可能不是最好的主意。

    • 是由create_string_buffer创建的字符串,也是Python-GC'ed ?

    在这种情况下,当 Python 对象被 GC 时,它也会释放 C 内存。 c_char_p 知道它是否拥有它指向的内存,并且当它被 GC 时,如果它拥有它,它会释放 C 内存。

    ========

    由于我(我 = 提问者)的目标是为 Python 最终用户提供尽可能少(不)对 ctypes 类型的引用,鉴于上述答案,我将执行以下操作:

    • 当我在函数 char* do_c_func() 中 malloc 在 C 中的 char* 时,我将它作为 Python 中的 c_char_p restype 返回。在我的 Python 中,我有一个调用 do_c_func() 的包装函数 do_py_func(),接收称为 my_c_char_pc_char_p,并通过某种 s = str(my_c_char_p) 将其内容存储在 Python 字符串中。然后我调用ctypes.free(my_c_char_p):这会释放分配的char*,函数结束时会递减最后一个引用计数器,这最终会垃圾收集my_c_char_p

    • 我创建了一个转换函数python_str_to_c_str(py_str),它接受一个普通的python字符串,并返回stdc.strdup(py_str.encode('utf-8'))(带有stdc.strdup.restype = c_char_p),从而为C提供一个新的char*(Python会识别它是在C)。我在需要将字符串从 Python 传递到 C 的任何地方都使用此转换函数。这使得my_string.encode('utf-8')) 创建的缓冲区和stdc.strdup 返回的c_char_p 都由 Python 进行垃圾收集,但char* 本身不是。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-05-14
      • 2011-08-20
      • 1970-01-01
      • 2011-05-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多