如何使用 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::stringconst char* 来表示字节数组,不幸的是,它们不能很好地移植到 python。

如果我的第二种解决方法对您来说没问题,我该如何避免内存泄漏?

如果将 return 值公开为 std::string 通常没问题,我怎样才能避免 UnicodeDecodeError

附加信息:

我建议您在 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());
}