【问题标题】:passing a list of strings from python to C through pybind11通过pybind11将字符串列表从python传递到C
【发布时间】:2020-05-20 20:29:37
【问题描述】:

在this post 之后,我想知道如何通过 Pybind11 将字符串列表从 Python 传递到 C(即,使用 C 标头和语法,而不是 C++)。我完全知道 Pybind11 是一个 C++ 库,代码必须由 C++ 编译器编译。但是,我很难理解 C++ 实现,例如 here 和 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");
}

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

#example.py
import example

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

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

  • 这不是一个 XY 问题,因为假定的 Y 问题已经使用 C++ 头文件/标准库和语法(上面的链接)以正确/规范的方式解决了。这个问题的目的是纯粹的好奇。用我熟悉的语法解决问题将帮助我理解 pybind11 数据类型和功能的基本性质。请不要试图找到 Y 问题并解决它。
  • 我完全知道 pybind11 是一个 C++ 库,无论如何都必须使用 C++ 编译器编译代码。
  • 如果您能在 cmets 中就我的问题所需的编辑问题向我咨询,而不是自己动手,我将不胜感激。我知道您想提供帮助,但我已尝试尽可能准确地提出我的问题以避免混淆。
  • 如果您尽可能避免更改我的 C/C++ 和 python 代码中没有注释的部分,我将不胜感激。
  • 我知道使用术语“C/C++”是错误的。我使用该术语来指代以 C 语法编写并使用 C 标头的 C++ 代码。很抱歉,我不知道更好的称呼方式。
  • 正如example.cpp 文件的注释部分所示,使用pybind11 数据类型然后将它们转换为C 是可以的。但我怀疑纯C 解决方案也可能是可能的。例如,请参阅this attempt。

【问题讨论】:

  • the code must be compiled with a C++ - 所以编写一个简短的包装器,将std::vector&lt;std::vector&lt;char&gt;&gt; 转换为char **
  • @KamilCuk 这实际上是大多数现有解决方案所做的。请考虑我只想使用 C 语法、头文件和标准库。
  • @KamilCuk 可以使用 py::list 这样的 pybind11 数据类型,然后将它们转换为纯 C。
  • 将 C++ 数据类型转换为 C 代码可以理解的结构是不可能的。您必须了解您的特定 C++ 编译器和链接器如何在内存中组织对象,即便如此,这也并非易事。
  • 我还要补充一点,这些格式会随着不同的编译器和链接器设置以及版本的不同而变化。

标签: python c pybind11


【解决方案1】:

下面我重新格式化了我使用 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();

代码将只接受实现序列协议的容器,因此可以循环。这同时涵盖了两个最重要的PyTuple 和PyList(尽管比直接检查这些类型要慢一点,但这将使代码更紧凑)。为了完全通用,此代码还应检查迭代器协议(例如,检查生成器并可能拒绝 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 调用等效于 Python 的“[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.

好的,如果我们到达这里,那么我们有一个argc 和argv,所以现在我们可以使用它们了:

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

最后,清理分配的内存。

    free(argv);

注意事项:

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

请问是否还有什么不清楚的地方。

编辑:如果你真的只想拥有一个 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)。

【讨论】:

  • 非常感谢。你太棒了。介意我重新格式化你的帖子,这样对我来说更具可读性吗?不幸的是,它会应用我的编辑而不需要您的同行评审。但您可以恢复或重新编辑您不喜欢的任何部分。
  • 我重新格式化了帖子以提高可读性。希望你不要介意。
  • “item”的定义在原始代码中。既可用于PySequence_GetItem,也可用于稍后需要的Py_DECREF(在您使用的PyList_GetItem 中不需要,因为这会返回借用的引用)。您链接的代码“按原样”工作,所以我想我正在查看比您的评论更新的版本。不过,我真的建议保留错误处理。使用long* 不起作用 b/c 您首先需要退出 pybind11,它似乎没有适合您的转换器。
  • 重新编辑并做了一些关于使用 Python list 的注释并添加了 py::list 示例代码。
  • 当然;如果这堵文字墙太长,请打开一个新的。如果里面有“pybind11”,我很快就会遇到它。
猜你喜欢
  • 1970-01-01
  • 2021-12-11
  • 2012-10-06
  • 1970-01-01
  • 2020-05-28
  • 2010-11-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多