Ctypes

日期2011-11-14(上次修改),2006-05-05(创建)

简介

ctypes 是 Python 2.3 及更高版本的高级外部函数接口包。它包含在 Python 2.5 的标准库中。

ctypes 允许调用从 DLL/共享库中公开的函数,并具有广泛的功能来在 Python 中创建、访问和操作简单和复杂的 C 数据类型——换句话说:用纯 Python 包装库。甚至可以在纯 Python 中实现 C 回调函数。

ctypes 还包含一个代码生成工具链,允许从 C 头文件自动创建库包装器。ctypes 在 Windows、Mac OS X、Linux、Solaris、FreeBSD、OpenBSD 和其他系统上运行。

确保您至少拥有 ctypes 1.0.1 或更高版本。

在 python 中调用或运行 C 代码的其他可能性包括:SWIGCythonWeavecffi 等。

Ctypes 入门

有关 ctypes 入门的详细信息,请参阅 ctypes 教程Python 的 ctypes 文档

假设您构建了一个名为 `foo.dll` 或 `libfoo.so` 的库,其中包含一个名为 `bar` 的函数,该函数接受指向双精度缓冲区的指针和一个 int 作为参数,并返回一个 int,以下代码应该可以帮助您入门。以下部分介绍了一些可能的构建脚本、C 代码和 Python 代码。

如果您想使用 distutils 构建您的 DLL/共享库,请查看 OOF2 中包含的 !SharedLibrary distutils 扩展。这可能应该在某个时候包含在 numpy.distutils 中。

Nmake Makefile (Windows)

在 Visual Studio 命令提示符中运行 nmake,使用以下文件进行构建。

无论用于编译 Python 的编译器版本如何,您都应该能够使用任何版本的 Visual Studio 编译器构建 DLL。请记住,您不应该跨不同的调试/发布和单线程/多线程运行时分配/释放内存,也不应该对来自不同运行时的 FILE* 进行操作。

CXX = cl.exe
LINK = link.exe

CPPFLAGS = -D_WIN32 -D_USRDLL -DFOO_DLL -DFOO_EXPORTS
CXXFLAGSALL = -nologo -EHsc -GS -W3 -Wp64 $(CPPFLAGS)
CXXFLAGSDBG = -MDd -Od -Z7 -RTCcsu
CXXFLAGSOPT = -MD -O2
#CXXFLAGS = $(CXXFLAGSALL) $(CXXFLAGSDBG)
CXXFLAGS = $(CXXFLAGSALL) $(CXXFLAGSOPT)

LINKFLAGSALL = /nologo /DLL
LINKFLAGSDBG = /DEBUG
LINKFLAGSOPT =
#LINKFLAGS = $(LINKFLAGSALL) $(LINKFLAGSDBG)
LINKFLAGS = $(LINKFLAGSALL) $(LINKFLAGSOPT)

all: foo.dll

foo.dll: foo.obj
    $(LINK) $(LINKFLAGS) foo.obj /OUT:foo.dll

svm.obj: svm.cpp svm.h
    $(CXX) $(CXXFLAGS) -c foo.cpp

clean:
    -erase /Q *.obj *.dll *.exp *.lib

SConstruct (GCC)

您可以使用以下文件与 SCons 构建共享库。

env = Environment()
env.Replace(CFLAGS=['-O2','-Wall','-ansi','-pedantic'])
env.SharedLibrary('foo', ['foo.cpp'])

foo.cpp

#include <stdio.h>

#ifdef FOO_DLL
#ifdef FOO_EXPORTS
#define FOO_API __declspec(dllexport)
#else
#define FOO_API __declspec(dllimport)
#endif /* FOO_EXPORTS */
#else
#define FOO_API extern /* XXX confirm this */
#endif /* FOO_DLL */

#ifdef __cplusplus
extern "C" {
#endif

extern FOO_API int bar(double* data, int len) {
   int i;
   printf("data = %p\n", (void*) data);
   for (i = 0; i < len; i++) {
      printf("data[%d] = %f\n", i, data[i]);
   }
   printf("len = %d\n", len);
   return len + 1;
}

#ifdef __cplusplus
}
#endif

在 Windows 上构建 foo 的 DLL 时,定义 `FOO_DLL` 和 `FOO_EXPORTS`(这是您在构建用于 ctypes 的 DLL 时想要做的)。在链接到 DLL 时,定义 `FOO_DLL`。在链接到包含 foo 的静态库时,或者在将 foo 包含在可执行文件中时,不要定义任何内容。

如果您不清楚这是什么,请阅读 C++ dlopen 迷你 HOWTO 的第 3 节。这允许您在许多 C++ 类之上编写具有 C 链接的函数包装器,以便您可以将它们与 ctypes 一起使用。或者,您可能更喜欢编写 C 代码。

foo.py

在 [ ]
import numpy as N
import ctypes as C
_foo = N.ctypeslib.load_library('libfoo', '.')
_foo.bar.restype = C.c_int
_foo.bar.argtypes = [C.POINTER(C.c_double), C.c_int]
def bar(x):
    return _foo.bar(x.ctypes.data_as(C.POINTER(C.c_double)), len(x))
x = N.random.randn(10)
n = bar(x)

NumPy 数组的 ctypes 属性

最近在 NumPy 数组中添加了一个 ctypes 属性

在 [ ]
In [18]: x = N.random.randn(2,3,4)

In [19]: x.ctypes.data
Out[19]: c_void_p(14394256)

In [21]: x.ctypes.data_as(ctypes.POINTER(c_double))

In [24]: x.ctypes.shape
Out[24]: <ctypes._endian.c_long_Array_3 object at 0x00DEF2B0>

In [25]: x.ctypes.shape[:3]
Out[25]: [2, 3, 4]

In [26]: x.ctypes.strides
Out[26]: <ctypes._endian.c_long_Array_3 object at 0x00DEF300>

In [27]: x.ctypes.strides[:3]
Out[27]: [96, 32, 8]

通常,C 函数可能会接受指向数组数据的指针,一个表示数组维数的整数(在此处传递 ndim 属性的值),以及两个指向形状和步长信息的 int 指针。

如果您的 C 函数假设连续存储,您可能希望使用一个 Python 函数对其进行包装,该函数对所有输入数组调用 !NumPy 的 `ascontiguousarray` 函数。

NumPy 的 ndpointer 与 ctypes argtypes

从 ctypes 0.9.9.9 开始,任何实现 from_param 方法的类都可以在函数的 argtypes 列表中使用。在 ctypes 调用 C 函数之前,它使用 argtypes 列表来检查每个参数。

使用 !NumPy 的 ndpointer 函数,可以构建一些非常有用的 argtypes 类,例如

在 [ ]
from numpy.ctypeslib import ndpointer
arg1 = ndpointer(dtype='<f4')
arg2 = ndpointer(ndim=2)
arg3 = ndpointer(shape=(10,10))
arg4 = ndpointer(flags='CONTIGUOUS,ALIGNED')
# or any combination of the above
arg5 = ndpointer(dtype='>i4', flags='CONTIGUOUS')
func.argtypes = [arg1,arg2,arg3,arg4,arg5]

现在,如果参数不满足要求,就会引发 !TypeError。这允许确保传递给 C 函数的数组是函数可以处理的形式。

另请参阅邮件列表线程 ctypes 和 ndpointer

通过回调进行动态分配

ctypes 支持 回调 的概念,允许 C 代码通过函数指针回调到 Python。这是可能的,因为 ctypes 在调用 C 函数之前会释放 Python 全局解释器锁 (GIL)。

我们可以使用此功能在需要缓冲区供 C 代码操作时分配 !NumPy 数组。这可以避免在某些情况下复制数据。您也不必担心在完成操作后释放 C 数据。通过将缓冲区分配为 !NumPy 数组,Python 垃圾收集器可以处理此问题。

Python 代码

在 [ ]
from ctypes import *
ALLOCATOR = CFUNCTYPE(c_long, c_int, POINTER(c_int))
# load your library as lib
lib.baz.restype = None
lib.baz.argtypes = [c_float, c_int, ALLOCATOR]

这不是定义分配器最漂亮的方式(我也不确定 c_long 是否是正确的返回类型),但 ctypes 中有一些错误似乎使这成为目前唯一的方法。最终,我们希望像这样编写分配器(但它目前还不能工作)

在 [ ]
from numpy.ctypeslib import ndpointer
ALLOCATOR = CFUNCTYPE(ndpointer('f4'), c_int, POINTER(c_int))

以下内容似乎也会导致问题

在 [ ]
ALLOCATOR = CFUNCTYPE(POINTER(c_float), c_int, POINTER(c_int))
ALLOCATOR = CFUNCTYPE(c_void_p, c_int, POINTER(c_int))
ALLOCATOR = CFUNCTYPE(None, c_int, POINTER(c_int), POINTER(c_void_p))

可能的故障包括引发 !SystemError 异常、解释器崩溃或解释器挂起。查看这些邮件列表线程以了解更多详细信息:* 在回调中分配时指针到指针未更改 * 回调返回 POINTER(c_float) 时挂起 * 回调函数和 as_parameter 与 NumPy ndpointer 结合使用时的错误

现在举个例子。示例的 C 代码

#ifndef CSPKREC_H
#define CSPKREC_H
#ifdef FOO_DLL
#ifdef FOO_EXPORTS
#define FOO_API __declspec(dllexport)
#else
#define FOO_API __declspec(dllimport)
#endif
#else
#define FOO_API
#endif
#endif
#include <stdio.h>
#ifdef __cplusplus
extern "C" {
#endif

typedef void*(*allocator_t)(int, int*);

extern FOO_API void foo(allocator_t allocator) {
   int dim = 2;
   int shape[] = {2, 3};
   float* data = NULL;
   int i, j;
   printf("foo calling allocator\n");
   data = (float*) allocator(dim, shape);
   printf("allocator returned in foo\n");
   printf("data = 0x%p\n", data);
   for (i = 0; i < shape[0]; i++) {
      for (j = 0; j < shape[1]; j++) {
         *data++ = (i + 1) * (j + 1);
      }
   }
}

#ifdef __cplusplus
}
#endif

如果您不熟悉 C 或 C++ 中的函数指针,请查看 函数指针教程。以及 Python 代码

在 [ ]
from ctypes import *
import numpy as N

allocated_arrays = []
def allocate(dim, shape):
    print 'allocate called'
    x = N.zeros(shape[:dim], 'f4')
    allocated_arrays.append(x)
    ptr = x.ctypes.data_as(c_void_p).value
    print hex(ptr)
    print 'allocate returning'
    return ptr

lib = cdll['callback.dll']
lib.foo.restype = None
ALLOCATOR = CFUNCTYPE(c_long, c_int, POINTER(c_int))
lib.foo.argtypes = [ALLOCATOR]

print 'calling foo'
lib.foo(ALLOCATOR(allocate))
print 'foo returned'

print allocated_arrays[0]

allocate 函数创建一个新的 !NumPy 数组并将其放入列表中,以便我们在回调函数返回后保留对它的引用。预期输出

calling foo
foo calling allocator
allocate called
0xaf5778
allocate returning
allocator returned in foo
data = 0x00AF5778
foo returned
[[ 1.  2.  3.]
 [ 2.  4.  6.]]

这是另一个用于管理此类事物的 Allocator 类的想法。除了维度和形状之外,此分配器函数还接受一个字符,指示要分配的数组类型。您可以从 ndarrayobject.h 头文件中的 `NPY_TYPECHAR` 枚举中获取这些类型代码。

在 [ ]
from ctypes import *
import numpy as N

class Allocator:
    CFUNCTYPE = CFUNCTYPE(c_long, c_int, POINTER(c_int), c_char)

    def __init__(self):
        self.allocated_arrays = []

    def __call__(self, dims, shape, dtype):
        x = N.empty(shape[:dims], N.dtype(dtype))
        self.allocated_arrays.append(x)
        return x.ctypes.data_as(c_void_p).value

    def getcfunc(self):
        return self.CFUNCTYPE(self)
    cfunc = property(getcfunc)

在 Python 中像这样使用它

在 [ ]
lib.func.argtypes = [..., Allocator.CFUNCTYPE]
def func():
    alloc = Allocator()
    lib.func(..., alloc.cfunc)
    return tuple(alloc.allocated_arrays[:3])

相应的 C 代码

typedef void*(*allocator_t)(int, int*, char);

void func(..., allocator_t allocator) {
   /* ... */
   int dims[] = {2, 3, 4};
   double* data = (double*) allocator(3, dims, 'd');
   /* allocate more arrays here */
}

上面介绍的所有分配器都不是线程安全的。如果您有多个 Python 线程调用调用回调的 C 代码,您将不得不采取更智能的方法。

更多有用的代码片段

假设您有一个像下面这样的 C 函数,它操作指针到指针的数据结构。

void foo(float** data, int len) {
    float** x = data;
    for (int i = 0; i < len; i++, x++) {
        /* do something with *x */
    }
}

您可以使用以下代码从现有的二维 !NumPy 数组创建必要的结构

在 [ ]
x = N.array([[10,20,30], [40,50,60], [80,90,100]], 'f4')
f4ptr = POINTER(c_float)
data = (f4ptr*len(x))(*[row.ctypes.data_as(f4ptr) for row in x])

`f4ptr*len(x)` 创建一个 ctypes 数组类型,它的大小足以包含指向数组每一行的指针。

异构类型示例

以下是在使用异构 dtype(记录数组)时的简单示例。

但是,请注意,NumPy recarrays 和 C 中的相应结构 **可能不** 相容。

此外,结构在不同平台上没有标准化...换句话说,**“注意填充问题!”**

sample.c

#include <stdio.h>

typedef struct Weather_t {
    int timestamp;
    char desc[12];
} Weather;

void print_weather(Weather* w, int nelems)
{
    int i;
    for (i=0;i<nelems;++i) {
        printf("timestamp: %d\ndescription: %s\n\n", w[i].timestamp, w[i].desc);
    }
}

SConstruct

env = Environment()
env.Replace(CFLAGS=['-O2','-Wall','-ansi','-pedantic'])
env.SharedLibrary('sample', ['sample.c'])

sample.py

在 [ ]
import numpy as N
import ctypes as C

dat = [[1126877361,'sunny'], [1126877371,'rain'], [1126877385,'damn nasty'], [1126877387,'sunny']]

dat_dtype = N.dtype([('timestamp','i4'),('desc','|S12')])
arr = N.rec.fromrecords(dat,dtype=dat_dtype)

_sample = N.ctypeslib.load_library('libsample','.')
_sample.print_weather.restype = None
_sample.print_weather.argtypes = [N.ctypeslib.ndpointer(dat_dtype, flags='aligned, contiguous'), C.c_int]


def print_weather(x):
    _sample.print_weather(x, x.size)



if __name__=='__main__':
    print_weather(arr)

斐波那契示例(使用 NumPy 数组、C 和 Scons)

以下测试通过,并在 Windows(使用 MinGW)和 GNU/Linux 32 位操作系统(上次测试于 2009 年 8 月 13 日)上运行。将所有三个文件复制到同一个目录。

C 代码(它递归地计算斐波那契数)

/*
    Filename: fibonacci.c
    To be used with fibonacci.py, as an imported library. Use Scons to compile,
    simply type 'scons' in the same directory as this file (see www.scons.org).
*/

/* Function prototypes */
int fib(int a);
void fibseries(int *a, int elements, int *series);
void fibmatrix(int *a, int rows, int columns, int *matrix);

int fib(int a)
{
    if (a <= 0) /*  Error -- wrong input will return -1. */
        return -1;
    else if (a==1)
        return 0;
    else if ((a==2)||(a==3))
        return 1;
    else
        return fib(a - 2) + fib(a - 1);
}

void fibseries(int *a, int elements, int *series)
{
    int i;
    for (i=0; i < elements; i++)
    {
    series[i] = fib(a[i]);
    }
}

void fibmatrix(int *a, int rows, int columns, int *matrix)
{
    int i, j;
    for (i=0; i<rows; i++)
        for (j=0; j<columns; j++)
        {
            matrix[i * columns + j] = fib(a[i * columns + j]);
        }
}

Python 代码

在 [ ]
"""
Filename: fibonacci.py
Demonstrates the use of ctypes with three functions:

    (1) fib(a)
    (2) fibseries(b)
    (3) fibmatrix(c)
"""

import numpy as nm
import ctypes as ct

# Load the library as _libfibonacci.
# Why the underscore (_) in front of _libfibonacci below?
# To mimimise namespace pollution -- see PEP 8 (www.python.org).
_libfibonacci = nm.ctypeslib.load_library('libfibonacci', '.')

_libfibonacci.fib.argtypes = [ct.c_int] #  Declare arg type, same below.
_libfibonacci.fib.restype  =  ct.c_int  #  Declare result type, same below.

_libfibonacci.fibseries.argtypes = [nm.ctypeslib.ndpointer(dtype = nm.int),\
                                     ct.c_int,\
                                     nm.ctypeslib.ndpointer(dtype = nm.int)]
_libfibonacci.fibseries.restype  = ct.c_void_p

_libfibonacci.fibmatrix.argtypes = [nm.ctypeslib.ndpointer(dtype = nm.int),\
                                     ct.c_int, ct.c_int,\
                                    nm.ctypeslib.ndpointer(dtype = nm.int)]
_libfibonacci.fibmatrix.restype  = ct.c_void_p

def fib(a):
    """Compute the n'th Fibonacci number.

    ARGUMENT(S):
        An integer.

    RESULT(S):
        The n'th Fibonacci number.

    EXAMPLE(S):
    >>> fib(8)
    13
    >>> fib(23)
    17711
    >>> fib(0)
    -1
    """
    return _libfibonacci.fib(int(a))

def fibseries(b):
    """Compute an array containing the n'th Fibonacci number of each entry.

    ARGUMENT(S):
        A list or NumPy array (dim = 1) of integers.

    RESULT(S):
        NumPy array containing the n'th Fibonacci number of each entry.

    EXAMPLE(S):
    >>> fibseries([1,2,3,4,5,6,7,8])
    array([ 0,  1,  1,  2,  3,  5,  8, 13])
    >>> fibseries(range(1,12))
    array([ 0,  1,  1,  2,  3,  5,  8, 13, 21, 34, 55])
    """
    b = nm.asarray(b, dtype=nm.intc)
    result = nm.empty(len(b), dtype=nm.intc)
    _libfibonacci.fibseries(b, len(b), result)
    return result

def fibmatrix(c):
    """Compute a matrix containing the n'th Fibonacci number of each entry.

    ARGUMENT(S):
        A nested list or NumPy array (dim = 2) of integers.

    RESULT(S):
        NumPy array containing the n'th Fibonacci number of each entry.

    EXAMPLE(S):
    >>> from numpy import array
    >>> fibmatrix([[3,4],[5,6]])
    array([[1, 2],
           [3, 5]])
    >>> fibmatrix(array([[1,2,3],[4,5,6],[7,8,9]]))
    array([[ 0,  1,  1],
           [ 2,  3,  5],
           [ 8, 13, 21]])
    """
    tmp = nm.asarray(c)
    rows, cols = tmp.shape
    c = tmp.astype(nm.intc)
    result = nm.empty(c.shape, dtype=nm.intc)
    _libfibonacci.fibmatrix(c, rows, cols, result)
    return result

以下是 SConstruct 文件内容(文件名:SConstruct)

env = Environment()
env.Replace(CFLAGS=['-O2', '-Wall', '-ansi', '-pedantic'])
env.SharedLibrary('libfibonacci', ['fibonacci.c'])

在 Python 解释器(或您使用的任何工具)中,执行

在 [ ]
>>> import fibonacci as fb
>>> fb.fib(8)
13
>>> fb.fibseries([5,13,2,6]
array([  3, 144,   1,   5])

相关邮件列表主题

ctypes-users 邮件列表中的一些有用主题

Thomas Heller 的回答特别有见地。

文档

章节作者:AlbertStrasheim、GaelVaroquaux、StefanVanDerWalt、TravisOliphant、DavidLinke、Unknown[41]、AndrewStraw、Unknown[42]、WilliamHunter、mauro