通过 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这样的误解,请考虑以下几点:
- 这不是 XY 问题,因为假定的 Y 问题已经使用 C++ headers/standard 库和语法(上面的链接)以 correct/canonical 方式解决。这个问题的目的是纯粹的好奇心。以我熟悉的语法解决问题将帮助我理解 pybind11 数据类型和功能的基本性质。请不要试图找到Y问题并解决它。
- 我完全知道 pybind11 是一个 C++ 库,无论如何都必须使用 C++ 编译器编译代码。
- 如果您能在评论中咨询我对我的问题的必要编辑,而不是自己动手,我将不胜感激。我知道您想提供帮助,但我已尝试尽可能准确地提出我的问题以避免混淆。
- 如果您能尽可能避免更改我的 C/C++ 和 python 代码的 none 注释部分,我将不胜感激。
- 我知道使用 "C/C++" 这个词是错误的。我使用该术语来指代以 C 语法编写并使用 C headers 的 C++ 代码。抱歉,我不知道更好的调用方式。
- 正如
example.cpp
文件的注释部分所示,可以使用 pybind11 数据类型然后将它们转换为 C。但我怀疑纯 C 解决方案也可能。例如,参见 this attempt。
下面我重新格式化了我使用 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 调用等效于 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.
好的,如果我们到了这里,那么我们就有了 argc
和 argv
,所以现在我们可以使用它们了:
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。
关注 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
示例
#example.py
import example
print(example.run(["Lorem", "ipsum", "dolor", "sit", "amet"]))
为避免像this这样的误解,请考虑以下几点:
- 这不是 XY 问题,因为假定的 Y 问题已经使用 C++ headers/standard 库和语法(上面的链接)以 correct/canonical 方式解决。这个问题的目的是纯粹的好奇心。以我熟悉的语法解决问题将帮助我理解 pybind11 数据类型和功能的基本性质。请不要试图找到Y问题并解决它。
- 我完全知道 pybind11 是一个 C++ 库,无论如何都必须使用 C++ 编译器编译代码。
- 如果您能在评论中咨询我对我的问题的必要编辑,而不是自己动手,我将不胜感激。我知道您想提供帮助,但我已尝试尽可能准确地提出我的问题以避免混淆。
- 如果您能尽可能避免更改我的 C/C++ 和 python 代码的 none 注释部分,我将不胜感激。
- 我知道使用 "C/C++" 这个词是错误的。我使用该术语来指代以 C 语法编写并使用 C headers 的 C++ 代码。抱歉,我不知道更好的调用方式。
- 正如
example.cpp
文件的注释部分所示,可以使用 pybind11 数据类型然后将它们转换为 C。但我怀疑纯 C 解决方案也可能。例如,参见 this attempt。
下面我重新格式化了我使用 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 调用等效于 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.
好的,如果我们到了这里,那么我们就有了 argc
和 argv
,所以现在我们可以使用它们了:
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。