通过 pybind11 将字符串列表从 python 传递到 C

passing a list of strings from python to C through pybind11

关注 this post, I want to know how I can pass a list of strings from Python to C (i.e., using C headers and syntax, not C++), through Pybind11. I'm completely aware of the fact that Pybind11 is a C++ library and codes must be compiled by a C++ compiler anyway. However, it is difficult for me to understand the C++ implementations, for example here and here

Here 我试图通过指针传递一个 python 字符串列表,表示为整数,然后在 C 中通过 long* 接收它们,但它没有用。

C/C++ 代码应该是这样的:

// example.cpp
#include <stdio.h>
#include <stdlib.h>

#include <pybind11/pybind11.h>

int run(/*<pure C or pybind11 datatypes> args*/){

    // if pybind11 data types are used convert them to pure C :
    // int argc = length of args
    // char* argv[] =  array of pointers to the strings in args, possible malloc

    for (int i = 0; i < argc; ++i) {
        printf("%s\n", argv[i]);
    } 

    // possible free

    return 0;
}

PYBIND11_MODULE(example, m) {

    m.def("run", &run, "runs the example");
}

还提供了一个简单的 CMakeLists.txt 示例 。 Python 代码可以是这样的:

#example.py
import example

print(example.run(["Lorem", "ipsum", "dolor", "sit", "amet"]))

为避免像this这样的误解,请考虑以下几点:

下面我重新格式化了我使用 C++ 结构的 previous example code,只使用 C 和 pybind11 结构。

#include <pybind11/pybind11.h>
#include <stdio.h>

#if PY_VERSION_HEX < 0x03000000
#define MyPyText_AsString PyString_AsString
#else
#define MyPyText_AsString PyUnicode_AsUTF8
#endif

namespace py = pybind11;

int run(py::object pyargv11) {
int argc = 0;
char** argv = NULL;

PyObject* pyargv = pyargv11.ptr();
if (PySequence_Check(pyargv)) {
    Py_ssize_t sz = PySequence_Size(pyargv);
    argc = (int)sz;

    argv = (char**)malloc(sz * sizeof(char*));
    for (Py_ssize_t i = 0; i < sz; ++i) {
        PyObject* item = PySequence_GetItem(pyargv, i);
        argv[i] = (char*)MyPyText_AsString(item);
        Py_DECREF(item);
        if (!argv[i] || PyErr_Occurred()) {
            free(argv);
            argv = nullptr;
            break;
        }
    }
}

if (!argv) {
    //fprintf(stderr,  "argument is not a sequence of strings\n");
    //return;

    if (!PyErr_Occurred())
        PyErr_SetString(PyExc_TypeError, "could not convert input to argv");
    throw py::error_already_set();
}

for (int i = 0; i < argc; ++i)
    fprintf(stderr, "%s\n", argv[i]);

free(argv);

return 0;
}

PYBIND11_MODULE(example, m) {
m.def("run", &run, "runs the example");
}

下面我会大量评论它来​​解释我在做什么以及为什么。

在 Python2 中,字符串对象是基于 char* 的,在 Python3 中,它们是基于 Unicode 的。因此下面的宏 MyPyText_AsString 改变了基于 Python 版本的行为,因为我们需要达到 C 风格 "char*".

#if PY_VERSION_HEX < 0x03000000
#define MyPyText_AsString PyString_AsString
#else
#define MyPyText_AsString PyUnicode_AsUTF8
#endif

pyargv11 py::object 是 Python C-API 句柄对象上的精简句柄;因为下面的代码使用了 Python C-API,直接处理底层的 PyObject* 更容易。

void closed_func_wrap(py::object pyargv11) {
    int argc = 0;            // the length that we'll pass
    char** argv = NULL;      // array of pointers to the strings

    // convert input list to C/C++ argc/argv :

    PyObject* pyargv = pyargv11.ptr();

该代码将只接受实现序列协议的容器,因此可以循环。这同时涵盖了两个最重要的 PyTuplePyList(虽然比直接检查这些类型慢一点,但这将使代码更紧凑)。为了完全通用,此代码还应检查迭代器协议(例如,对于生成器并可能拒绝 str 对象,但两者都不太可能。

    if (PySequence_Check(pyargv)) {

好的,我们有一个序列;现在得到它的大小。 (此步骤是您需要使用 Python 迭代器协议的范围的原因,因为它们的大小通常是未知的(尽管您可以请求提示)。)

        Py_ssize_t sz = PySequence_Size(pyargv);

一部分,大小搞定,存入变量,可以传递给其他函数。

        argc = (int)sz;

现在将指针数组分配给 char*(技术上 const char*,但这并不重要,因为我们将丢弃它)。

        argv = (char**)malloc(sz * sizeof(char*));

接下来,遍历序列以检索各个元素。

        for (Py_ssize_t i = 0; i < sz; ++i) {

这从序列中获取单个元素。 GetItem 调用等效于 Pythons“[i]”,或 getitem 调用。

            PyObject* item = PySequence_GetItem(pyargv, i);

在 Python2 中,字符串对象是基于 char* 的,在 Python3 中,它们是基于 unicode 的。因此,下面的宏 "MyPyText_AsString" 会根据 Python 版本更改行为,因为我们需要使用 C 风格 "char*".

这里从const char*char*的转换原则上是安全的,但是argv[i]的内容不能被其他函数修改。 main()argv 参数也是如此,所以我假设是这种情况。

请注意,未复制 C 字符串。原因是在 Py2 中,您只需访问底层数据,而在 Py3 中,转换后的字符串作为 Unicode 对象的数据成员保存,Python 将进行内存管理。在这两种情况下,我们都保证它们的生命周期至少与输入 Python 对象(pyargv11)的生命周期一样长,因此至少在这个函数调用期间是这样。如果其他函数决定保留指针,则需要副本。

            argv[i] = (char*)MyPyText_AsString(item);

PySequence_GetItem 的结果是一个新的引用,所以现在我们已经完成了,放弃它:

            Py_DECREF(item);

输入数组可能不仅仅包含 Python 个 str 对象。在这种情况下,转换将失败,我们需要检查这种情况,否则 "closed_function" 可能会出现段错误。

            if (!argv[i] || PyErr_Occurred()) {

清理之前分配的内存。

                free(argv);

将 argv 设置为 NULL 以便稍后检查是否成功:

                argv = nullptr;

放弃循环:

                break;

如果给定的对象不是一个序列,或者如果序列的一个元素不是一个字符串,那么我们就没有 argv 所以我们放弃:

    if (!argv) {

下面有点懒,但如果你只想看C代码,可能会更好理解。

        fprintf(stderr,  "argument is not a sequence of strings\n");
        return;

您真正应该做的是检查是否已设置错误(例如 b/c 转换问题),如果没有设置错误。然后通知 pybind11。这将在调用方端为您提供干净的 Python 异常。事情是这样的:

        if (!PyErr_Occurred())
            PyErr_SetString(PyExc_TypeError, "could not convert input to argv");
        throw py::error_already_set();       // by pybind11 convention.

好的,如果我们到了这里,那么我们就有了 argcargv,所以现在我们可以使用它们了:

    for (int i = 0; i < argc; ++i)
        fprintf(stderr, "%s\n", argv[i]);

最后,清理分配的内存。

    free(argv);

备注:

  • 我仍然提倡至少使用 std::unique_ptr,因为如果抛出 C++ 异常(来自任何输入对象的自定义转换器),这会让生活变得更加轻松。
  • 我原本希望能够在 #include <pybind11/stl.h> 之后用一行 std::vector<char*> pv{pyargv.cast<std::vector<char*>>()}; 替换所有代码,但我发现那行不通(即使它可以编译) .使用 std::vector<std::string> 也没有(也编译,但在 运行 时也失败)。

有什么不明白的就问。

编辑:如果你真的只想拥有一个 PyListObject,只需调用 PyList_Check(pyargv11.ptr()),如果为真,则转换结果:PyListObject* pylist = (PyListObject*)pyargv11.ptr()。现在,如果您想使用 py::list,您还可以使用以下代码:

#include <pybind11/pybind11.h>
#include <stdio.h>

#if PY_VERSION_HEX < 0x03000000
#define MyPyText_AsString PyString_AsString
#else
#define MyPyText_AsString PyUnicode_AsUTF8
#endif

namespace py = pybind11;

int run(py::list inlist) {
    int argc = (int)inlist.size();
    char** argv = (char**)malloc(argc * sizeof(char*));

    for (int i = 0; i < argc; ++i)
        argv[i] = (char*)MyPyText_AsString(inlist[i].ptr());

    for (int i = 0; i < argc; ++i)
        fprintf(stderr, "%s\n", argv[i]);

    free(argv);

    return 0;
}

PYBIND11_MODULE(example, m) {
    m.def("run", &run, "runs the example");
}

这段代码只是更短 b/c 它的功能较少:它只接受列表并且在错误处理方面也更笨拙(例如,如果在整数列表中传递它会泄漏,因为 pybind11 抛出一个例外;要解决这个问题,请像在第一个示例代码中那样使用 unique_ptr,以便在出现异常时释放 argv。