在 Cython 中使用 char** 调用方法

Calling methods using char** with Cython

我正在尝试让一些 Cython 绑定在外部 C 代码使用类型 char** 的参数的地方工作,正如通常在 main 方法中看到的那样。

不幸的是,我之前的所有尝试都失败了,而且我找不到任何关于如何实现这一目标的资源。我能找到的现有解决方案通常是指数字数组或需要重写原始代码。

如何调用使用 char** 参数的方法,最好不必修改我正在连接的底层 C 代码的调用语义?


例子

# File setup.py
from setuptools import setup
from Cython.Build import cythonize

setup(
    ext_modules = cythonize("my_test.pyx", language_level=3)
)
# File my_test.pyx
from binding cimport add as _add, main as _main


def add(a, b):
    return _add(a, b)


def main(argc, argv):
    cdef char[:, ::1] argv_array = [b'fixed', b'values'] + [x.encode() for x in argv]
    return _main(argc + 2, &argv_array[0][0])
# File binding.pxd
cdef extern from "module1.c":
    int add(int a, int b)
    int main(int argc, char** argv)
// File module1.c
#include <stdio.h>

static int add(int a, int b) {
    return a + b;
}


int main(int argc, char** argv) {
    printf("Result: %d\n", add(40, 2));
    for (int i = 0; i < argc; i++) {
        printf("%s\n", argv[i]);
    }
    return 0;
}

错误信息

(venv) user@host ~/path/to/directory $ python setup.py build_ext --inplace
Compiling my_test.pyx because it changed.
[1/1] Cythonizing my_test.pyx

Error compiling Cython file:
------------------------------------------------------------
...
    return _add(a, b)


def main(argc, argv):
    cdef char[:, ::1] argv_array = [x.encode() for x in argv]
    return _main(argc, &argv_array[0][0])
                      ^
------------------------------------------------------------

my_test.pyx:12:23: Cannot assign type 'char *' to 'char **'
Traceback (most recent call last):
  File "setup.py", line 5, in <module>
    ext_modules = cythonize("my_test.pyx", language_level=3)
  File "/home/user/path/to/directory/venv/lib/python3.8/site-packages/Cython/Build/Dependencies.py", line 1127, in cythonize
    cythonize_one(*args)
  File "/home/user/path/to/directory/venv/lib/python3.8/site-packages/Cython/Build/Dependencies.py", line 1250, in cythonize_one
    raise CompileError(None, pyx_file)
Cython.Compiler.Errors.CompileError: my_test.pyx

声明 ctypedef char* cchar_tp 并将其用作 cdef cchar_tp[:, ::1] argv_array 将产生另一条错误消息:

Invalid base type for memoryview slice: cchar_tp

您面临的问题是 2D memoryview/array 不是指向指针的指针(因为这通常是存储数组的糟糕方式)。相反,它是一个单一的一维数组和一些定义维度长度的大小。请注意,char**(表示字符串“列表”)与二维数组并不完全相同,因为通常字符串的长度不同。

因此您必须创建一个单独的指针数组,每个指针都可以指向更大的数组。 this question, which I originally marked as a duplicate, and still think is probably a duplicate 中对此进行了讨论。那里的方法应该仍然有效。

您可以使用 Python 字节对象采取一种快捷方式 - 它们可以直接分配给 const char*。该指针将指向 Python-owned 内存,因此 bytes 对象的寿命必须比 C 指针长。在这种情况下,我通过将它们安全地存储在列表中来确保这一点。

from libc.stdlib cimport malloc, free

cdef extern from *:
    """
    int m(int n, const char**) {
        return 1;
    }
    """
    int m(int n, const char**)

def call_m():
    cdef const char** to_pass
    args = [b"arg1", b"arg2"]
    to_pass = <const char**>malloc(sizeof(const char*)*len(args))
    try:
        for n, a in enumerate(args):
            to_pass[n] = a  # use auto-conversion from Python bytes to char*
        m(len(args), to_pass)
    finally:
        free(to_pass)