如何使用Cython将Python列表传递给C函数

5
我正在使用树莓派与连接到GPIO的自定义硬件进行交互。控制软件是用Python编写的,而自定义硬件的接口则是用C编写的,因为它是一个更快的C实现。现在我需要从Python中调用我的C函数,并最近学习了如何用Cython包装C。我已经使一切正常工作,除了将Python列表传递给C函数。
我的自定义硬件需要发送1到32个字节,因此使用了数组。
我阅读过的Cython教程和其他参考资料要么非常简单,不包括如何将列表传递给C,要么使用我没有使用的numpy,要么使用非常复杂的代码示例,缺乏足够的文档使我无法正确理解它。
现在我所拥有的是:
test.c
#include <stdio.h>
#include "test.h"
void pop(void) {
    a[0] = 0x55;
    a[1] = 0x66;
    a[2] = 0x77;
    a[3] = '\0';
}
void putAll(int n, char c[]) {
    memcpy(a, c, n);
}
char *getAll(void) {
    return &a[0];
}

test.h

char a[4];

void putAll(int n, char[]);
char *getAll(void);

pytest.pyx

cimport defns

# Populate C array with values
def pypop():
    defns.pop()

# Pass python list to C
def pyPutAll(int n, char[:] pyc):
    cdef char* c = pyc
    defns.putAll(n, c)

# Get array from C
def pyGetAll():
    cdef char* c = defns.getAll()
    cdef bytes pyc = c
    print pyc

defns.pxd

cdef extern from "test.h":
    char a[4]
    void pop()
    void putAll(int n, char c[])
    char *getAll()

使用在 cython.org 上的教程,我的 getAll() 和 pop() 函数能够正常工作,但当我包含 putAll() 函数(从链接中找到的 process_byte_data 示例代码中获取,关于 Unicode 和传递字符串 > 从 Python 代码接受字符串),我遇到了以下错误:
python setup.py build_ext -i

Error compiling Cython file:
------------------------------------------------------------
...

def pyputAll(int n, char[:] pyc):
                        ^
------------------------------------------------------------

pytest.pyx:13:25: Expected an identifier or literal

现在,我有一个解决方法 - 将最多32个字节组合成int并作为long int传递,然后在C中拆分它 - 但它非常丑陋。
另外,我不需要Cython来获得任何性能提升,除了使用C实现的库与Python实现的库相比,用于与我的自定义硬件进行接口。
3个回答

2
你可以使用Python的bytearray来配合Cython使用,我认为这比使用ctypes更加简洁易懂:
test.py
larr = bytearray([4, 1, 2])
pyPutAll(3, larr)

这与您的原始putAll C函数兼容:
测试.c
...
void putAll(int n, char c[]) {
    memcpy(a, c, n);
}
...

pytest.pyx

# Pass python bytearray to C
def pyPutAll(int n, char[:] pyc):
    defns.putAll(n, &pyc[0])

如果您需要传递一个列表,您需要在pyx函数内将其转换为向量,并传递其引用:

pytest.pyx

def pyPutAllList(int n, list pyc):
    cdef vector[char] vec = pyc
    defns.putAll(n, &vec[0])

1
我设法让这个工作起来。以下是我现在的代码,供任何需要的人使用。

pytest.pyx:

...
def pyPutAll(int n, c):
    cdef int *ptr
    ptr = <int *>malloc(n*cython.sizeof(int))
    if ptr is NULL:
            raise MemoryError()
    for i in xrange(n):
            ptr[i] = c[i]
    defns.putAll(n, ptr)
    free(ptr)
...

test.c:

void putAll(int n, int c[])
{
    char d[n];
    int i;
    for (i=0;i<n;i++) {
            d[i] = c[i];
    }
    memcpy(addr, d, n);
}

这段代码不够优化,因为它在Python/Cython代码中使用了int类型,然后在C函数中将其转换为char类型。pytest.pyc中的pyPutAll()函数接受一个普通的Python列表。然后它创建一个C指针并分配内存。遍历列表时,每个值都被放入C数组中,最后将指针传递给C函数。
这样可以完成工作,但我相信其他人可能会提供更有效率的解决方案。
这个答案是由原作者Hengy在CC BY-SA 3.0下发布的,作为对问题“如何使用Cython将Python列表传递给C函数”的回答编辑

-1

ctypes 更适合你所尝试的操作。

例如:(test.py)

from ctypes import create_string_buffer, c_char_p, c_int, CDLL

libtest = CDLL("./libtest.so")

_putAll = libtest.putAll
_putAll.restype = None
_putAll.argtypes = [c_int, c_char_p]

def putAll(values):
    """Expects a bytes object, bytearray, or a list of ints."""
    char_buffer = create_string_buffer(bytes(values))
    _putAll(len(char_buffer), char_buffer)

getAll = libtest.getAll
getAll.restype = c_char_p
getAll.argtypes = None

使用方法:

import test
test.putAll(b"hello world")
assert test.getAll() == b"hello world"
test.putAll(bytearray(b"another string"))
test.putAll([1, 2, 3, 255])

以上适用于Python 3。如果您正在运行Python 2,则可以将bytes替换为str,但该函数的灵活性较差。另外,请注意create_string_buffer会创建一个C字符串(在字符串末尾添加额外的NUL字符)。
编译共享库需要执行以下操作:
gcc -fPIC -g -c -Wall test.c
gcc -shared -Wl,-soname,libtest.so.1 -o libtest.so test.o

4
Q: "我怎样才能更换汽车火花塞?" A: "最好骑自行车代替开车。" - Jonathon Reinhart
1
我不同意。问题类似于“如何使用电锤钉钉子”。回答向操作者展示了如何将字符串传递给 C 库,并仅使用标准库。Cython 是强大的工具,但对于操作者要实现的目标来说过于复杂。 - Dunes
3
但这并没有回答问题提问者的问题。我通过谷歌来到这里是因为我在使用Cython,而这个答案对我来说毫无用处。对我来说,使用ctypes完全不可能。提问者可能已经接受了这个答案,但这甚至不是他最终使用的解决方案。他应该自己编写并接受他自己的答案。 - Jonathon Reinhart

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接