Cython字节转C char*

6 投票
3 回答
7958 浏览
提问于 2025-04-16 08:30

我正在尝试写一个Cython扩展,用来包装mcrypt库,这样我就可以在Python 3中使用它了。不过,我遇到了一个问题,就是在使用mcrypt的某个API时程序崩溃了。

出问题的代码是:

def _real_encrypt(self, source):
    src_len = len(source)
    cdef char* ciphertext = source
    cmc.mcrypt_generic(self._mcStream, <void *>ciphertext, src_len)
    retval = source[:src_len]
    return retval

根据我对Cython文档的理解,第三行的赋值应该是把缓冲区的内容(在Python 3中是一个对象)复制到C语言的字符串指针里。我本以为这也意味着会分配内存,但当我做了这个修改后:

def _real_encrypt(self, source):
    src_len = len(source)
    cdef char* ciphertext = <char *>malloc(src_len)
    ciphertext = source
    cmc.mcrypt_generic(self._mcStream, <void *>ciphertext, src_len)
    retval = source[:src_len]
    return retval

程序还是崩溃了,出现了段错误。崩溃发生在mcrypt_generic内部,但当我用普通的C代码时,它能正常工作,所以我觉得我对Cython和C数据的配合还有些不太明白。

谢谢大家的帮助!

补充说明: 这个问题其实是我自己的错误。我在熬夜太久后做这个(这是不是我们大家都经历过的事情?)结果错过了一些简单的东西。现在我有了可以工作的代码:

def _real_encrypt(self, source):
    src_len = len(source)
    cdef char *ciphertext = <char *>malloc(src_len)
    cmc.strncpy(ciphertext, source, src_len)
    cmc.mcrypt_generic_init(self._mcStream, <void *>self._key,
                            len(self._key), NULL)
    cmc.mcrypt_generic(self._mcStream, <void *>ciphertext,
                       src_len)

    retval = ciphertext[:src_len]
    cmc.mcrypt_generic_deinit(self._mcStream)
    return retval

这可能不是世界上最有效率的代码,因为它在加密时做了一次复制,然后在返回值时又做了一次复制。我不太确定是否可以避免这种情况,因为我不确定是否能把新分配的缓冲区直接作为字节串返回给Python。不过现在我有了一个可以工作的函数,我打算实现一个逐块处理的方法,这样就可以提供一个块的可迭代对象进行加密或解密,而且可以在不把整个源文件和目标文件都放在内存中的情况下进行——这样就能加密或解密超大的文件,而不必担心在任何时候内存中要保持三份副本...

谢谢大家的帮助!

3 个回答

2

我使用的方法是(在Python 2.x中)在函数的定义中声明字符串类型的参数,这样Cython代码就会自动处理所有的转换和类型检查。

def _real_encrypt(self,char* src):
    ...
3

我想给你的代码提几点建议,希望能帮到你。Python的C API里有一些函数,正好可以满足你的需求,并且能确保一切都符合Python的做法。它能够处理嵌入的NULL值,完全没有问题。

与其直接调用 malloc,不如把这段代码:

cdef char *ciphertext = <char *>malloc(src_len)

改成:

cdef str retval = PyString_FromStringAndSize(PyString_AsString(source), <Py_ssize_t>src_len)
cdef char *ciphertext = PyString_AsString(retval)

上面的代码会创建一个全新的Python字符串对象,并用 source 的内容来初始化。第二行代码让 ciphertext 指向 retval 的内部字符缓冲区,而不是复制内容。这样,任何对 ciphertext 的修改都会影响到 retval。因为 retval 是一个全新的Python字符串,所以可以在从 _real_encrypt 返回之前,用C代码对它进行修改。

想了解更多细节,可以查看Python C/API文档中关于这些函数的说明,这里这里

这样做的好处是省去了一个复制的步骤。整个代码大概会是这样的:

cdef extern from "Python.h":
    object PyString_FromStringAndSize(char *, Py_ssize_t)
    char *PyString_AsString(object)

def _real_encrypt(self, source):
    src_len = len(source)
    cdef str retval = PyString_FromStringAndSize(PyString_AsString(source), <Py_ssize_t>src_len)
    cdef char *ciphertext = PyString_AsString(retval)
    cmc.mcrypt_generic_init(self._mcStream, <void *>self._key,
                            len(self._key), NULL)
    cmc.mcrypt_generic(self._mcStream, <void *>ciphertext,
                       src_len)
    # since the above initialized ciphertext, the retval str is also correctly initialized, too.
    cmc.mcrypt_generic_deinit(self._mcStream)
    return retval
4

第一个是把 char* 指向了 Python 字符串。第二个是分配了一块内存,但之后又把指针指向了 Python 字符串,结果就忽略了刚刚分配的那块内存。你应该是想在 Cython 中调用 C 语言的 strcpy 函数,但我不太清楚具体的细节。

撰写回答