如何使用boost.python将预填充的“unsigned char *”缓冲区传递给C++方法?

4
我有一个C++类,其中一个成员函数需要一个unsigned char*缓冲区和一个unsigned int长度作为参数并对它们进行操作。我已经使用Boost::Python包装了这个类,并希望从Python脚本中向该类传递预先填充的缓冲区。Python端的缓冲区是使用struct.pack创建的。我无法弄清如何使参数类型匹配,并不断收到Boost.Python.ArgumentError错误。

include/Example.h

#ifndef EXAMPLECLASS_H_
#define EXAMPLECLASS_H_

#include <cstdio>

class ExampleClass
{
public:
    ExampleClass() {}
    virtual ~ExampleClass() {}

    void printBuffer(unsigned char* buffer, unsigned int length)
    {
        for (unsigned int i = 0; i < length; ++i)
        {
            printf("%c", buffer[i]);
        }

        printf("\n");
    }
};

#endif

src/example.cpp

#include "Example.h"

int main(int argc, char** argv)
{
    unsigned char buf[4];
    buf[0] = 0x41;
    buf[1] = 0x42;
    buf[2] = 0x43;
    buf[3] = 0x44;

    ExampleClass e;
    e.printBuffer(buf, 4);

    return 0;
}

src/Example_py.cpp

#include <boost/python.hpp>
#include "Example.h"

using namespace boost::python;

BOOST_PYTHON_MODULE(example_py)
{
    class_<ExampleClass>("ExampleClass")
    .def("printBuffer", &ExampleClass::printBuffer)
    ;
}

scripts/example.py

#!/usr/bin/env python

import example_py
import struct
import ctypes

buf = struct.pack('BBBB', 0x41, 0x42, 0x43, 0x44)

print 'python:'
print buf

e = example_py.ExampleClass()

print 'c++:'
print e.printBuffer(ctypes.cast(ctypes.c_char_p(buf), ctypes.POINTER(ctypes.c_ubyte)), len(buf))

CMakeLists.txt(不完整)

include_directories(
    include
    ${Boost_INCLUDE_DIRS}
    ${PYTHON_INCLUDE_DIRS}
)

add_library(example_py
    src/Example_py.cpp
)
target_link_libraries(example_py ${Boost_LIBRARIES} ${PYTHON_LIBRARIES})
set_target_properties(example_py PROPERTIES PREFIX "")

add_executable(example src/example.cpp)
target_link_libraries(example example_py)

输出

$ ./example
ABCD

$ ./scripts/example.py
python: ABCD
c++:
Traceback (most recent call last):
  File "/home/dustingooding/example/scripts/example.py", line 13, in <module>
    print 'c++:', e.printBuffer(ctypes.cast(ctypes.c_char_p(buf), ctypes.POINTER(ctypes.c_ubyte)), len(buf))
Boost.Python.ArgumentError: Python argument types in
    ExampleClass.printBuffer(ExampleClass, LP_c_ubyte, int)
did not match C++ signature:
    printBuffer(ExampleClass {lvalue}, unsigned char*, unsigned int)

我尝试了多种方法(直接传递'buf',将'buf'作为ctypes.c_char_p传递,创建一个ctypes.ubyte数组并用'buf'的内容填充它再传递),但似乎都不起作用。
我不明白为什么'LP_c_ubyte'和'unsigned char*'不匹配。
编辑
这是一个Github项目,具有现成的代码库。随意使用。我已经添加了@Tanner的修复。https://github.com/dustingooding/boost_python_ucharp_example
2个回答

6

考虑将Pythonic辅助函数作为ExampleClass.printBuffer方法暴露给Python,它代表了c-ish的ExampleClass::printBuffer成员函数。例如,这将允许Python用户调用:

import example
import struct

buf = struct.pack('BBBB', 0x41, 0x42, 0x43, 0x44)
e.printBuffer(buf)

与其要求用户执行正确的ctypes转换和缩小,不如使用struct.pack()方法在Python2中返回一个str对象,在Python3中返回一个bytes对象,因此辅助C++函数需要使用从strbytes中构建连续内存块的元素。


boost::python::stl_input_iterator可以提供一种方便的方式来构建C++容器(例如std::vector<char>)从Python对象(例如strbytes)。唯一的奇怪之处是stl_input_iterator期望Python类型支持可迭代协议,而str不这样做。但是,内置的Python方法iter()可用于创建可迭代对象。

/// @brief Auxiliary function used to allow a Python iterable object with char
///        elements to be passed to ExampleClass.printBuffer().
void example_class_print_buffer_wrap(
  ExampleClass& self,
  boost::python::object py_buffer)
{
  namespace python = boost::python;
  // `str` objects do not implement the iterator protcol (__iter__),
  // but do implement the sequence protocol (__getitem__).  Use the
  // `iter()` builtin to create an iterator for the buffer.
  // >>> __builtins__.iter(py_buffer)
  python::object locals(python::borrowed(PyEval_GetLocals()));
  python::object py_iter = locals["__builtins__"].attr("iter");
  python::stl_input_iterator<char> begin(
     py_iter(py_buffer)), end;

  // Copy the py_buffer into a local buffer with known continguous memory.
  std::vector<char> buffer(begin, end);

  // Cast and delegate to the printBuffer member function.
  self.printBuffer(
    reinterpret_cast<unsigned char*>(&buffer[0]),
    buffer.size());
}

创建了辅助函数后,只需要将其公开为 ExampleClass.printBuffer 方法即可:
BOOST_PYTHON_MODULE(example)
{
  namespace python = boost::python;
  python::class_<ExampleClass>("ExampleClass")
    .def("printBuffer", &example_class_print_buffer_wrap)
    ;
}

这里有一个完整的示例演示这种方法:

#include <cstdio>
#include <vector>
#include <boost/python.hpp>
#include <boost/python/stl_iterator.hpp>

// Mocks...
/// @brief Legacy class that cannot be changed.
class ExampleClass
{
public:
  void printBuffer(unsigned char* buffer, unsigned int length)
  {
    for (unsigned int i = 0; i < length; ++i)
    {
      printf("%c", buffer[i]);
    }

    printf("\n");
  }
};

/// @brief Auxiliary function used to allow a Python iterable object with char
///        elements to be passed to ExampleClass.printBuffer().
void example_class_print_buffer_wrap(
  ExampleClass& self,
  boost::python::object py_buffer)
{
  namespace python = boost::python;
  // `str` objects do not implement the iterator protcol (__iter__),
  // but do implement the sequence protocol (__getitem__).  Use the
  // `iter()` builtin to create an iterator for the buffer.
  // >>> __builtins__.iter(py_buffer)
  python::object locals(python::borrowed(PyEval_GetLocals()));
  python::object py_iter = locals["__builtins__"].attr("iter");
  python::stl_input_iterator<char> begin(
     py_iter(py_buffer)), end;

  // Copy the py_buffer into a local buffer with known continguous memory.
  std::vector<char> buffer(begin, end);

  // Cast and delegate to the printBuffer member function.
  self.printBuffer(
    reinterpret_cast<unsigned char*>(&buffer[0]),
    buffer.size());
}

BOOST_PYTHON_MODULE(example)
{
  namespace python = boost::python;
  python::class_<ExampleClass>("ExampleClass")
    .def("printBuffer", &example_class_print_buffer_wrap)
    ;
}

交互式使用:

>>> import example
>>> import struct
>>> buf = struct.pack('BBBB', 0x41, 0x42, 0x43, 0x44)
>>> print 'python:', buf
python: ABCD
>>> e = example.ExampleClass()
>>> e.printBuffer(buf)
ABCD

ctypes 方法对我来说看起来像是一个 XY 问题,因此我选择不在这个答案中详细讨论它。 - Tanner Sansbury
这是一个很好的例子。谢谢你的建议。我原本希望不需要辅助函数,但显然它有效。让我们再等一会儿,看看是否有人能提供直接调用的解决方案。如果没有,我会选择采纳你的答案。 - Dustin
1
例如,LP_c_ubyte 不是 unsigned char*,而是 ctypes 知道如何在调度通过 ctypes 获得的函数时将其转换为 unsigned char* 的类型。Boost.Python 不知道如何将 LP_c_ubyte 类型转换为 unsigned char* - Tanner Sansbury
这非常有道理。我实际上找到了另一个使用字符串类型直接包装而无需设置一些基础设施的Pythonic包装器的示例。明天我会发布一个带有详细信息的问题编辑。 - Dustin
1
@Dustin 当你更倾向于通用性而非速度时,请使用“stl_input_iterator”; 当你更倾向于速度而非通用性时,请使用“PyX_AsString ()”函数。该问题涉及一个“unsigned char”缓冲区,并提到了“struct.pack()”,它在Python2和Python3之间改变返回类型,因此我选择了一种通用解决方案(它也适用于“array”模块)。如果您始终使用Python2“struct.pack()”并且从不修改缓冲区,则还可以直接重新解释字符串:演示 - Tanner Sansbury
显示剩余2条评论

1

Python文档基本数据类型章节中列出了以下内容:

class ctypes.c_char_p

表示C语言中指向以零结尾的字符串的char *数据类型。对于可能还指向二进制数据的一般字符指针,必须使用POINTER(c_char)。构造函数接受整数地址或字符串。

这表明您应该使用c_char_p类型。如果使用POINTER()函数,则应使用LP_c_char_p

该类型

LP_c_ubyte   /* corresponds to */  unsigned char;

you should probably use

LP_c_char_p    /* which corresponds to */    char *;
更新: 我已经纠正了上面的类型。另外:我不是Python专家,所以可能有错误。还有this answer

如果 ctypes.POINTER(ctypes.c_ubyte) 给出了 LP_c_ubyte,那么 LP_c_ubyte_p 是什么?并且没有 ctypes.c_ubyte_p 可以提供给 ctypes.POINTER - Dustin
你说得没错,不过正确的是ctypes.c_char_p。等一下,我在更新我的答案。 - user23573
将第一个参数简单更改为 ctypes.c_char_p(buf) 会给出类似的错误信息:ExampleClass.printBuffer(ExampleClass, c_char_p, c_uint)。我认为这个失败是因为无法将 char* 转换为 unsigned char* - Dustin
c_char_p是一种基本数据类型,但它与unsigned char*不匹配,只能与char*匹配。如果我想使用c_char_p,我需要更改函数声明以使用char*,但我并不确定这是一个好主意(在我的情况或一般情况下)。 - Dustin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接