如何使用 boost_python 将 C++ 序列化数据公开给 python
How to expose C++ serialized data to python using boost_python
我们决定将我们用 C++ 编写的 IPC(进程间通信)模块之一公开给 python(我知道,这不是最明智的想法)。我们使用可以序列化和反序列化的数据包 to/from std::string
(行为类似于 Protocol Buffers,只是效率不高),因此我们的 IPC class returns 并接受 std::string
还有。
将 class 暴露给 python 的问题是 std::string
c++ 类型被转换为 str
python 类型,如果 returned std::string
包含无法解码为 UTF-8
的字符(大多数情况下)我得到 UnicodeDecodeError
异常。
我设法找到了解决此问题的两个解决方法(甚至 "solutions"?),但我对其中任何一个都不是特别满意。
这是我的 C++ 代码,用于重现 UnicodeDecodeError
问题并尝试解决方案:
/*
* boost::python string problem
*/
#include <iostream>
#include <string>
#include <vector>
#include <boost/python.hpp>
#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
struct Packet {
std::string serialize() const {
char buff[sizeof(x_) + sizeof(y_)];
std::memcpy(buff, &x_, sizeof(x_));
std::memcpy(buff + sizeof(x_), &y_, sizeof(y_));
return std::string(buff, sizeof(buff));
}
bool deserialize(const std::string& buff) {
if (buff.size() != sizeof(x_) + sizeof(y_)) {
return false;
}
std::memcpy(&x_, buff.c_str(), sizeof(x_));
std::memcpy(&y_, buff.c_str() + sizeof(x_), sizeof(y_));
return true;
}
// whatever ...
int x_;
float y_;
};
class CommunicationPoint {
public:
std::string read() {
// in my production code I read that std::string from the other communication point of course
Packet p;
p.x_ = 999;
p.y_ = 1234.5678;
return p.serialize();
}
std::vector<uint8_t> readV2() {
Packet p;
p.x_ = 999;
p.y_ = 1234.5678;
std::string buff = p.serialize();
std::vector<uint8_t> result;
std::copy(buff.begin(), buff.end(), std::back_inserter(result));
return result;
}
boost::python::object readV3() {
Packet p;
p.x_ = 999;
p.y_ = 1234.5678;
std::string serialized = p.serialize();
char* buff = new char[serialized.size()]; // here valgrind detects leak
std::copy(serialized.begin(), serialized.end(), buff);
PyObject* py_buf = PyMemoryView_FromMemory(
buff, serialized.size(), PyBUF_READ);
auto retval = boost::python::object(boost::python::handle<>(py_buf));
//delete[] buff; // if I execute delete[] I get garbage in python
return retval;
}
};
BOOST_PYTHON_MODULE(UtfProblem) {
boost::python::class_<std::vector<uint8_t> >("UintVec")
.def(boost::python::vector_indexing_suite<std::vector<uint8_t> >());
boost::python::class_<CommunicationPoint>("CommunicationPoint")
.def("read", &CommunicationPoint::read)
.def("readV2", &CommunicationPoint::readV2)
.def("readV3", &CommunicationPoint::readV3);
}
它可以用g++ -g -fPIC -shared -o UtfProblem.so -lboost_python-py35 -I/usr/include/python3.5m/ UtfProblem.cpp
编译(在生产中我们当然使用CMake)。
这是一个简短的 python 脚本,可加载我的库并解码数字:
import UtfProblem
import struct
cp = UtfProblem.CommunicationPoint()
#cp.read() # exception
result = cp.readV2()
# result is UintVec type, so I need to convert it to bytes first
intVal = struct.unpack('i', bytes([x for x in result[0:4]]))
floatVal = struct.unpack('f', bytes([x for x in result[4:8]]))
print('intVal: {} floatVal: {}'.format(intVal, floatVal))
result = cp.readV3().tobytes()
intVal = struct.unpack('i', result[0:4])
floatVal = struct.unpack('f', result[4:8])
print('intVal: {} floatVal: {}'.format(intVal, floatVal))
在第一个解决方法中,而不是 returning std::string
I return std::vector<unit8_t>
。它工作正常,但我不喜欢这样的事实,它迫使我公开额外的人工 python 类型 UintVec
,它没有任何本机支持转换为 python bytes
.
第二个解决方法很好,因为它将我的序列化数据包公开为内存块,本机支持转换为字节,但它会泄漏内存。我使用 valgrind: valgrind --suppressions=../valgrind-python.supp --leak-check=yes -v --log-file=valgrindLog.valgrind python3 UtfProblem.py
验证了内存泄漏,除了 python 库中的大量无效读取(可能是误报)外,它向我显示
8 bytes in 1 blocks are definitely lost
在我为缓冲区分配内存时的行中。如果我在从函数 returning 之前删除内存,我将在 python.
中得到一些垃圾
问题:
如何才能将我的序列化数据适当地公开给 python?在 C++ 中,我们通常使用 std::string
或 const char*
来表示字节数组,不幸的是,它们不能很好地移植到 python。
如果我的第二种解决方法对您来说没问题,我该如何避免内存泄漏?
如果将 return 值公开为 std::string
通常没问题,我怎样才能避免 UnicodeDecodeError
?
附加信息:
- g++ (Debian 6.3.0-18+deb9u1) 6.3.0 20170516
- Python 3.5.3
- 提升 1.62
我建议您在 C++ 中定义自己的 return 类型 class 并使用 Boost Python 公开它。例如,您可以让它实现缓冲协议。然后您将拥有一个常规的 C++ 析构函数,它将在适当的时间被调用——您甚至可以在 class 中使用智能指针来管理分配内存的生命周期。
一旦你这样做了,下一个问题就是:为什么不让 returned 对象公开属性来访问字段,而不让调用者使用 struct.unpack()
?那么你的调用代码可以简单得多:
result = cp.readV5()
print('intVal: {} floatVal: {}'.format(result.x, result.y))
根据 AntiMatterDynamite 评论,返回 pythonic bytes
对象(使用 Python API)工作得很好:
PyObject* read() {
Packet p;
p.x_ = 999;
p.y_ = 1234.5678;
std::string buff = p.serialize();
return PyBytes_FromStringAndSize(buff.c_str(), buff.size());
}
我们决定将我们用 C++ 编写的 IPC(进程间通信)模块之一公开给 python(我知道,这不是最明智的想法)。我们使用可以序列化和反序列化的数据包 to/from std::string
(行为类似于 Protocol Buffers,只是效率不高),因此我们的 IPC class returns 并接受 std::string
还有。
将 class 暴露给 python 的问题是 std::string
c++ 类型被转换为 str
python 类型,如果 returned std::string
包含无法解码为 UTF-8
的字符(大多数情况下)我得到 UnicodeDecodeError
异常。
我设法找到了解决此问题的两个解决方法(甚至 "solutions"?),但我对其中任何一个都不是特别满意。
这是我的 C++ 代码,用于重现 UnicodeDecodeError
问题并尝试解决方案:
/*
* boost::python string problem
*/
#include <iostream>
#include <string>
#include <vector>
#include <boost/python.hpp>
#include <boost/python/suite/indexing/vector_indexing_suite.hpp>
struct Packet {
std::string serialize() const {
char buff[sizeof(x_) + sizeof(y_)];
std::memcpy(buff, &x_, sizeof(x_));
std::memcpy(buff + sizeof(x_), &y_, sizeof(y_));
return std::string(buff, sizeof(buff));
}
bool deserialize(const std::string& buff) {
if (buff.size() != sizeof(x_) + sizeof(y_)) {
return false;
}
std::memcpy(&x_, buff.c_str(), sizeof(x_));
std::memcpy(&y_, buff.c_str() + sizeof(x_), sizeof(y_));
return true;
}
// whatever ...
int x_;
float y_;
};
class CommunicationPoint {
public:
std::string read() {
// in my production code I read that std::string from the other communication point of course
Packet p;
p.x_ = 999;
p.y_ = 1234.5678;
return p.serialize();
}
std::vector<uint8_t> readV2() {
Packet p;
p.x_ = 999;
p.y_ = 1234.5678;
std::string buff = p.serialize();
std::vector<uint8_t> result;
std::copy(buff.begin(), buff.end(), std::back_inserter(result));
return result;
}
boost::python::object readV3() {
Packet p;
p.x_ = 999;
p.y_ = 1234.5678;
std::string serialized = p.serialize();
char* buff = new char[serialized.size()]; // here valgrind detects leak
std::copy(serialized.begin(), serialized.end(), buff);
PyObject* py_buf = PyMemoryView_FromMemory(
buff, serialized.size(), PyBUF_READ);
auto retval = boost::python::object(boost::python::handle<>(py_buf));
//delete[] buff; // if I execute delete[] I get garbage in python
return retval;
}
};
BOOST_PYTHON_MODULE(UtfProblem) {
boost::python::class_<std::vector<uint8_t> >("UintVec")
.def(boost::python::vector_indexing_suite<std::vector<uint8_t> >());
boost::python::class_<CommunicationPoint>("CommunicationPoint")
.def("read", &CommunicationPoint::read)
.def("readV2", &CommunicationPoint::readV2)
.def("readV3", &CommunicationPoint::readV3);
}
它可以用g++ -g -fPIC -shared -o UtfProblem.so -lboost_python-py35 -I/usr/include/python3.5m/ UtfProblem.cpp
编译(在生产中我们当然使用CMake)。
这是一个简短的 python 脚本,可加载我的库并解码数字:
import UtfProblem
import struct
cp = UtfProblem.CommunicationPoint()
#cp.read() # exception
result = cp.readV2()
# result is UintVec type, so I need to convert it to bytes first
intVal = struct.unpack('i', bytes([x for x in result[0:4]]))
floatVal = struct.unpack('f', bytes([x for x in result[4:8]]))
print('intVal: {} floatVal: {}'.format(intVal, floatVal))
result = cp.readV3().tobytes()
intVal = struct.unpack('i', result[0:4])
floatVal = struct.unpack('f', result[4:8])
print('intVal: {} floatVal: {}'.format(intVal, floatVal))
在第一个解决方法中,而不是 returning std::string
I return std::vector<unit8_t>
。它工作正常,但我不喜欢这样的事实,它迫使我公开额外的人工 python 类型 UintVec
,它没有任何本机支持转换为 python bytes
.
第二个解决方法很好,因为它将我的序列化数据包公开为内存块,本机支持转换为字节,但它会泄漏内存。我使用 valgrind: valgrind --suppressions=../valgrind-python.supp --leak-check=yes -v --log-file=valgrindLog.valgrind python3 UtfProblem.py
验证了内存泄漏,除了 python 库中的大量无效读取(可能是误报)外,它向我显示
8 bytes in 1 blocks are definitely lost
在我为缓冲区分配内存时的行中。如果我在从函数 returning 之前删除内存,我将在 python.
中得到一些垃圾问题:
如何才能将我的序列化数据适当地公开给 python?在 C++ 中,我们通常使用 std::string
或 const char*
来表示字节数组,不幸的是,它们不能很好地移植到 python。
如果我的第二种解决方法对您来说没问题,我该如何避免内存泄漏?
如果将 return 值公开为 std::string
通常没问题,我怎样才能避免 UnicodeDecodeError
?
附加信息:
- g++ (Debian 6.3.0-18+deb9u1) 6.3.0 20170516
- Python 3.5.3
- 提升 1.62
我建议您在 C++ 中定义自己的 return 类型 class 并使用 Boost Python 公开它。例如,您可以让它实现缓冲协议。然后您将拥有一个常规的 C++ 析构函数,它将在适当的时间被调用——您甚至可以在 class 中使用智能指针来管理分配内存的生命周期。
一旦你这样做了,下一个问题就是:为什么不让 returned 对象公开属性来访问字段,而不让调用者使用 struct.unpack()
?那么你的调用代码可以简单得多:
result = cp.readV5()
print('intVal: {} floatVal: {}'.format(result.x, result.y))
根据 AntiMatterDynamite 评论,返回 pythonic bytes
对象(使用 Python API)工作得很好:
PyObject* read() {
Packet p;
p.x_ = 999;
p.y_ = 1234.5678;
std::string buff = p.serialize();
return PyBytes_FromStringAndSize(buff.c_str(), buff.size());
}