首页 > 解决方案 > 具有重复字段的嵌套 protobuf 结构导致调试断言失败

问题描述

有关最小示例,请参阅更新 2。

我正在尝试使用 protobuf 和 TCP/IP 将数据从一个进程发送到另一个进程。为此,我创建了以下 proto 文件:

syntax = "proto3";
option cc_enable_arenas = false;

message TCPMessage {
    enum Type {
        SETUP = 0;
        DATA = 1;
        START = 2;
        STOP = 3;
    }
    Type messageType = 1;
    oneof message {
        SetupMessage setupMessage = 2;
        DataMessage dataMessage = 3;
        StartMessage startMessage = 4;
        StopMessage stopMessage = 5;
    }
    uint64 timestamp = 6;
}

message StartMessage{
    bool diagnosticMode = 1;
}

message StopMessage{

}

message SetupMessage {
    repeated string entities = 1;
    repeated string objects = 2;
    repeated string commands = 3;
    repeated VariableDescription commandDescriptions = 4;
    repeated ProtoVariable initialState = 5;
}

message DataMessage {
    repeated ProtoVariable variables = 1;
    uint64 timeSpan = 2;
}


message VariableDescription {

    enum DataType {
        DOUBLE = 0;
        // FLOAT = 1;
        // INT32 = 2;
        INT64 = 3;
        // UINT32 = 4;
        // UINT64 = 5;
        // Reserved if ever needed
        // SINT32 = 6;
        // SINT64 = 7;
        // FIXED32 = 8;
        // FIXED64 = 9;
        // SFIXED32 = 10;
        // SFIXED64 = 11;
        BOOL = 12;
        STRING = 13;
        BYTES = 14;
    }

    string entity = 1;
    string name = 2;
    DataType dataType = 3;
    repeated uint64 dimensions = 4;
}

message ProtoVariable
{
    VariableDescription metaData = 1;
    bytes data = 2;
}

如您所见,我正在使用具有重复字段的嵌套消息结构将某些变量的信息从一个进程发送到另一个进程。在 python 端(接收端)我对代码没有问题。所有信息都按预期收到,一切正常。但是,在 C++ 方面,我遇到了由删除 TCPMessage 对象引起的调试断言问题。

在消息字段中发送填充了 StartMessage 的 TCPMessage 时,我没有任何问题,一切都按预期工作,但是,在消息字段中发送 DataMessage 时,我遇到了所描述的问题。

首先是代码,我如何创建 StartMessage:

start_msg.set_messagetype(TCPMessage_Type_START);
StartMessage* tmp = new StartMessage();
start_msg.set_allocated_startmessage(tmp);
sendMessage(std::make_unique<TCPMessage>(start_msg))

正如我所说,这行得通。在线阅读有关该mutable_foo()方法的信息后,我创建了以下用于创建 DataMessage 的代码:

std::unique_ptr<TCPMessage> msg = std::make_unique<TCPMessage>();
msg->set_messagetype(TCPMessage::DATA);
DataMessage* data_msg = msg->mutable_datamessage();

auto var = data_msg->add_variables();
VariableDescription* meta_data = var->mutable_metadata();

meta_data->set_entity(entity);
meta_data->set_name(cmd_identifier);
meta_data->set_datatype(stored_meta_data.getType());
for (uint64_t i = 0; i < stored_meta_data.getDimensions().size(); i++) {
  meta_data->add_dimensions(stored_meta_data.getDimensions()[i]);
}

double val = 12345.6789;
char const* d = reinterpret_cast<char const*>(&val);

std::string* data_str = var->mutable_data();
for (int i = 0; i < 8; ++i) {
  data_str->operator+=(d[i]);
}
sendMessage(std::move(msg));

我知道有一些奇怪的代码(data_str->operator+=(d[i])例如参见),这与我试图以任何可能的方式使其工作有关。

最后是sendMessage(std::unique_ptr<TCPMessage> msg)方法的代码:

int TCPConnection::sendMessage(std::unique_ptr<TCPMessage>& msg)
{
  // bool a = msg->messagetype() == TCPMessage::DATA; // This was used for debugging (see further down)
  std::string out = "";
  msg->SerializeToString(&out);
  std::string message_len = "";
  for (int i = 0; i < 8; ++i) {
    message_len += char((int)(((uint64_t)out.size() >> (i * 8)) & 0xFF));
  }
  std::string out_buffer = "";
  size_t i = 0;
  for (; i < 8; ++i) {
    out_buffer += message_len[i];
  }
  size_t j = 0;
  for (; j < out.size(); ++j) {
    out_buffer += out[j];
  }
  int i_send_result = ::send(tcp_client_socket_, &out_buffer[0], out.size() + 8, 0);
  if (i_send_result == SOCKET_ERROR) {
    std::cout << "send failed with error: " << WSAGetLastError() << std::endl;
    closesocket(tcp_client_socket_);
    WSACleanup();
    return i_send_result;
  }
//  if (a) {
//    int o = 1;
//    msg->~TCPMessage(); // Here I was figuring out, that the debug assertion happens in ~DataMessage()
                          // in the RepeatedPtrField<Element>::~RepeatedPtrField() destructor
//  }

  return i_send_result;
}

如果有人能指出我在内存分配或其他任何方面出错的地方,我会非常高兴!我尝试使用该set_allocated_datamessage()功能或set_data()​​功能或或多或少所有我能想到的......

其他可能感兴趣的事情:

在 VisualStudio 2019 中以调试模式运行时触发的断点指向 VisualStudio 的 MSVC 文件夹内的文件 delete_scalar.cpp 中的以下代码(如果有帮助):

_CRT_SECURITYCRITICAL_ATTRIBUTE
void __CRTDECL operator delete(void* const block) noexcept
{
    #ifdef _DEBUG
    _free_dbg(block, _UNKNOWN_BLOCK); //<-- Breakpoint
    #else
    free(block);
    #endif
}

这是调用堆栈:调用栈

更新:

我设法发现,这显然是由于stringMessage和. 为了找出问题所在,我将生成 TCP 消息的代码更改为以下内容:VariableDescriptionnameentity

TCPMessage* msg = google::protobuf::Arena::CreateMessage<TCPMessage>(proto_arena_);
msg->set_messagetype(TCPMessage::DATA);

DataMessage* data_msg = google::protobuf::Arena::CreateMessage<DataMessage>(proto_arena_);

auto v = data_msg->add_variables();
auto md = v->mutable_metadata();
md->set_datatype(VariableDescription_DataType_DOUBLE);
auto dim = md->mutable_dimensions();
dim->Add(1);
string test = "test";
md->set_allocated_name(&test); // I tried this
// md->set_name("test")        // And this
// auto n = md->mutable_name();// And those two lines
// n->assign("test");

msg->set_allocated_datamessage(data_msg);

auto msg_string = msg->SerializeAsString(); // If I remove this it runs through?!
proto_arena_->Reset();

所有这些设置名称的尝试都不起作用。但是,如果我使用

auto n = md->mutable_name();
n->push_back('a');

有用!但是,如果我遍历字符串并逐个 push_back 每个字符,它就不起作用了......

更新 2:

我只是在我能想到的最简单的问题上进行了尝试,但我仍然遇到同样的问题。我更改了 proto 文件中的一些内容,所以这里有一个完整的示例:

主.cpp:

#include <iostream>
#include "tcp_data_message.pb.h"

int main()
{

  auto msg_t = std::make_unique<TCPMessage>();
  msg_t->set_messagetype(TCPMessage_Type_DATA);
  DataMessage* data_msg = msg_t->mutable_datamessage();

  auto v = data_msg->add_variables();
  auto md = v->mutable_metadata();
  md->set_datatype(VariableDescription_DataType_DOUBLE);
  auto dim = md->mutable_dimensions();
  dim->Add(1);
  md->set_entityid(1);
  md->set_id(2345678);
}

原型文件:

syntax = "proto3";
option cc_enable_arenas = true;
option optimize_for = LITE_RUNTIME;

message TCPMessage {
    enum Type {
        SETUP = 0;
        DATA = 1;
        START = 2;
        STOP = 3;
        // DATATEST = 4;
    }
    Type messageType = 1;
    oneof message {
        SetupMessage setupMessage = 2;
        DataMessage dataMessage = 3;
        StartMessage startMessage = 4;
        StopMessage stopMessage = 5;
        // DataMessageTest dataMessagetest = 7;
    }
    uint64 timestamp = 6;
}

message StartMessage{
    bool diagnosticMode = 1;
}

message StopMessage{

}

message SetupMessage {
    map<string, int32> entities = 1;
    map<string, int32> objects = 2;
    map<string, int32> commands = 3;
    repeated CommandDescription commandDescriptions = 4;
}

message CommandDescription {
    VariableDescription description = 1;
    string name = 2;
}

message DataMessage {
    repeated ProtoVariable variables = 1;
    uint64 timeSpan = 2;
}

message VariableDescription {

    enum DataType {
        DOUBLE = 0;
        // FLOAT = 1;
        // INT32 = 2;
        INT64 = 3;
        // UINT32 = 4;
        // UINT64 = 5;
        // Reserved if ever needed
        // SINT32 = 6;
        // SINT64 = 7;
        // FIXED32 = 8;
        // FIXED64 = 9;
        // SFIXED32 = 10;
        // SFIXED64 = 11;
        BOOL = 12;
        STRING = 13;
        BYTES = 14;
    }

    int32 entityID = 1;
    int32 ID = 2;
    DataType dataType = 3;
    repeated uint64 dimensions = 4;
}

message ProtoVariable
{
    VariableDescription metaData = 1;
    bytes data = 2;
}

message VariableDescriptionOld {

    enum DataType {
        DOUBLE = 0;
        // FLOAT = 1;
        // INT32 = 2;
        INT64 = 3;
        // UINT32 = 4;
        // UINT64 = 5;
        // Reserved if ever needed
        // SINT32 = 6;
        // SINT64 = 7;
        // FIXED32 = 8;
        // FIXED64 = 9;
        // SFIXED32 = 10;
        // SFIXED64 = 11;
        BOOL = 12;
        STRING = 13;
        BYTES = 14;
    }

    string entity = 1;
    string name = 2;
    DataType dataType = 3;
    repeated uint64 dimensions = 4;
}

message ProtoVariableOld
{
    VariableDescriptionOld metaData = 1;
    bytes data = 2;
}

我仍然得到同样的错误......

但是,如果我创建一个 SetupMessage 它可以工作:

#include <iostream>
#include "tcp_data_message.pb.h"

int main()
{

  auto msg_t = std::make_unique<TCPMessage>();
  msg_t->set_messagetype(TCPMessage_Type_SETUP);
  auto setup_msg = msg_t->mutable_setupmessage();
  auto entities = setup_msg->mutable_entities();
  (*entities)["test"] = 123;


  /*
  DataMessage* data_msg = msg_t->mutable_datamessage();

  auto v = data_msg->add_variables();
  auto md = v->mutable_metadata();
  md->set_datatype(VariableDescription_DataType_DOUBLE);
  auto dim = md->mutable_dimensions();
  dim->Add(1);
  md->set_entityid(1);
  md->set_id(2345678);
  */
}

更新 3:

显然这是 libprotobuf-lite.dll 和 libprotobuf-lited.dll 的问题,我没有为我的代码使用调试 dll。我现在设法让我的最小示例运行,但是我遇到了另一个问题。将消息从字符串解析到消息时,出现读取访问冲突错误。但是,在我的最小示例中,它可以工作...

最小的例子:


#include <iostream>
#include "tcp_data_message.pb.h"

int main()
{
  std::string in = "18 97 10 12 10 4 98 97 108 108 16 -1 -1 -1 -1 7 18 14 10 10 112 111 115 105 116 105 111 110 95 121 16 1 18 14 10 10 118 101 108 111 99 105 116 121 95 121 16 2 26 18 10 14 115 101 116 95 118 101 108 111 99 105 116 121 95 121 16 3 34 29 10 11 8 -1 -1 -1 -1 7 16 3 34 1 1 18 14 115 101 116 95 118 101 108 111 99 105 116 121 95 121 0";
//^this is the chars I'm trying to parse
  std::string s = "";
  size_t pos = 0;
  std::string delimiter = " ";
  while ((pos = in.find(delimiter)) != std::string::npos) {
    std::string token = in.substr(0, pos);
    char c = static_cast<char>(std::stoi(token));
    s += c;
    in.erase(0, pos + delimiter.length());
  }
  std::cout << s << std::endl;
  {
    auto msg_t = std::make_unique<TCPMessage>();
    if (msg_t->ParseFromString(s)) {
      std::cout << "Wuhu!" << std::endl;
    }
    msg_t->set_messagetype(TCPMessage_Type_SETUP);
    auto setup_msg = msg_t->mutable_setupmessage();
    auto entities = setup_msg->mutable_entities();
    (*entities)["test"] = 123;

    DataMessage* data_msg = msg_t->mutable_datamessage();

    auto v = data_msg->add_variables();
    auto md = v->mutable_metadata();
    md->set_datatype(VariableDescription_DataType_DOUBLE);
    auto dim = md->mutable_dimensions();
    dim->Add(1);
    md->set_entityid(1);
    md->set_id(2345678);
  }
  std::cout << "Test" << std::endl;
}

如果我复制粘贴整个字符串并创建它等等,我的“正确”代码中会出现读取访问冲突。我相信这一定是dll或类似的问题。

“真实”代码:

//...

std::string in = "18 97 10 12 10 4 98 97 108 108 16 -1 -1 -1 -1 7 18 14 10 10 112 111 115 105 116 105 111 110 95 121 16 1 18 14 10 10 118 101 108 111 99 105 116 121 95 121 16 2 26 18 10 14 115 101 116 95 118 101 108 111 99 105 116 121 95 121 16 3 34 29 10 11 8 -1 -1 -1 -1 7 16 3 34 1 1 18 14 115 101 116 95 118 101 108 111 99 105 116 121 95 121 0";
  std::string s = "";
  size_t pos = 0;
  std::string delimiter = " ";
  while ((pos = in.find(delimiter)) != std::string::npos) {
    std::string token = in.substr(0, pos);
    char c = static_cast<char>(std::stoi(token));
    s += c;
    in.erase(0, pos + delimiter.length());
  }
  //std::cout << "Received Message: " << buf_string << std::endl;
  TCPMessage* msg = new TCPMessage();
  if (!msg->ParseFromString(s)) {
    std::cout << "ERROR: Parsing Message from String failed" << std::endl;
    return NULL;
  }

当我通过 VisualStudio 调试代码时,我可以发现,在ParseFromString(std::string data)数据对象中已经显示“无法读取内存”。抛出的异常是:

在 program.exe 中的 0x01133B87 (v​​cruntime140d.dll) 处引发异常:0xC0000005:访问冲突读取位置 0xCCCCCCCC。

它向我展示了 memcpy.asm 的这一部分: 在此处输入图像描述

任何我可能出错的想法。

这是调试器在调用“msg->ParseFromString(s)”之前向我显示的内容:
在此处输入图像描述

这是它在ParseFromString(ConstStringParam data)方法中向我展示的内容:
在此处输入图像描述

标签: c++windowsprotocol-buffersdestructor

解决方案


我终于弄清楚了问题所在,所以对于遇到类似问题的每个人:

1对于第一个描述的堆问题,请确保您使用正确的库进行适当的配置(调试/发布)。我使用 libprotobuf.lib/dll 而不是 libprotobuf d .lib/dll。

2对于传递给ParseFromString()方法的字符串的问题:我_ITERATOR_DEBUG_LEVEL=0在程序的预处理器定义中进行了设置。这与 vcpkg 生成的 dll 不兼容。为了使其兼容,您必须将其包含_ITERATOR_DEBUG_LEVEL=0到 protobuflib 的编译中。您可以通过添加来做到这一点

set(VCPKG_C_FLAGS_DEBUG "/D_ITERATOR_DEBUG_LEVEL=0")
set(VCPKG_CXX_FLAGS_DEBUG "/D_ITERATOR_DEBUG_LEVEL=0")

到用于在 vcpkg 中编译 proto 库的 cmake 文件。例如C:\Program Files\vcpkg\triplets\x86-windows.cmake,如果调用vcpkg install protobuf protobuf:x86-windows(最好创建一个新的 cmake 文件并在安装命令中的冒号后调用它)。然后在您的项目中使用新创建的 lib/dll 文件。


推荐阅读