如何获取 ctypes 指针数据的格式字符串

How to get format-string for data of a ctypes-pointer

给定一个 ctypes 指针,例如 double**:

import ctypes
data=(ctypes.POINTER(ctypes.c_double)*4)()   #  results in [NULL, NULL, NULL, NULL]

是否可以得到一个描述data内存布局的format string

现在,我创建一个内存视图来获取这些信息,感觉有点傻:

view=memoryview(data)
print(view.format)   # prints: &<d

是否有更直接且开销更少的方法?也许通过使用 C-API?


可以用有意义的值填充 data,如果这有任何帮助的话:

import ctypes
data=(ctypes.POINTER(ctypes.c_double)*2)(
             (ctypes.c_double*2)(1.0,2.0), 
             (ctypes.c_double*1)(3.0))  

#  results in [ 
#               ptr0 -> [1,2],
#               ptr1 -> [3]
#             ]   
print(data[1][0])  #  prints 3.0      

似乎没有什么比 memoryview(data).format 从根本上更好的了。但是,可以通过使用 C-API.

稍微加快速度

格式字符串(它扩展了 struct format-string-syntax 描述的 in PEP3118) is calculated recursively and is stored in the format-member of the StgDictObject-object, which can be found in the tp_dict-字段 ctypes-arrays/pointers:

typedef struct {
    PyDictObject dict;          /* first part identical to PyDictObject */
    ...
    /* pep3118 fields, pointers neeed PyMem_Free */
    char *format;
    int ndim;
    Py_ssize_t *shape;
    ...
} StgDictObject;

format 字段仅在递归计算期间和 a buffer is exported 时访问 - 这就是 memoryview 获取此信息的方式:

static int PyCData_NewGetBuffer(PyObject *myself, Py_buffer *view, int flags)
{
    ...
    /* use default format character if not set */
    view->format = dict->format ? dict->format : "B";
    ...
    return 0;
}

现在我们可以使用 C-API 填充缓冲区(无需创建实际的 memoryview),此处在 Python:

中实现
%%cython

from cpython cimport buffer

def get_format_via_buffer(obj):
    cdef buffer.Py_buffer view
    buffer.PyObject_GetBuffer(obj, &view, buffer.PyBUF_FORMAT|buffer.PyBUF_ANY_CONTIGUOUS)
    cdef bytes format = view.format
    buffer.PyBuffer_Release(&view)
    return format

这个版本比通过 memoryview:

快 3 倍左右
import ctypes
c=(ctypes.c_int*3)()

%timeit get_format_via_buffer(c)   #  295 ns ± 10.3 
%timeit memoryview(c).format       #  936 ns ± 7.43 ns 

在我的机器上,调用 def 函数大约需要 160 ns 的开销,创建字节对象大约需要 50 ms。


即使由于不可避免的开销而进一步优化它没有多大意义,但至少在理论上仍然对如何加速它感兴趣。

如果真的想削减填写 Py_buffer-struct 的成本,那么没有干净的方法:ctypes-module 不是 Python-C-[= 的一部分56=](它不在include-directory中),所以前进的方法是重复the solution Cython uses with the array.array, i.e. hardcoding the memory layout of the object (which makes this solution brittle because the memory-layout of StgDictObject可以不同步)。

这里使用 Cython,没有错误检查:

%%cython -a  
from cpython cimport PyObject

# emulate memory-layout (i.e. copy definitions from ctypes.h)
cdef extern from *:
    """
    #include <Python.h>

    typedef struct _ffi_type
    {
      size_t size;
      unsigned short mem[2];
      struct _ffi_type **elements;
    } ffi_type;

    typedef struct {
        PyDictObject dict;          /* first part identical to PyDictObject */

        Py_ssize_t size[3];            /* number of bytes,alignment requirements,number of fields */
        ffi_type ffi_type_pointer;
        PyObject *proto;            /* Only for Pointer/ArrayObject */
        void *setfunc[3];          

        /* Following fields only used by PyCFuncPtrType_Type instances */
        PyObject *argtypes[4];       
        int flags;                  /* calling convention and such */

        /* pep3118 fields, pointers neeed PyMem_Free */
        char *format;
        int ndim;

    } StgDictObject;
    """

    ctypedef struct StgDictObject:
        char *format


def get_format_via_hack(obj):
    cdef PyObject *p =<PyObject *>obj
    cdef StgDictObject *dict = <StgDictObject *>(p.ob_type.tp_dict)
    return dict.format

而且速度很快:

%timeit get_format_via_hack(c) # 243 ns ± 14.5 ns