0%

记录 C++ protobuf 动态消息一次踩坑

尽量 C++ 并不直接支持反射的功能,但是 protobuf 自行实现了一套反射的功能,用于动态生成消息。protobuf 提供了两种方式用于生成动态消息,一种是调用一个 message 实例的 New()接口,另外一种则是通过导入 proto 文件,利用工厂类实现生成。这两种方式生成的消息类型都是 google::protobuf::Message*,即 protobuf 消息的基类。

遇到的问题

由于通过反射的方式对 google::protobuf::DynamicMessageFactory生成的消息进行赋值较为麻烦,因此想通过 google::protobuf::down_cast<T>()方法对生成的消息进行向下转换。使用一下代码段进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Input input;
// 使用 New 接口生成消息
auto newedMessage = input.New();

// 封装了 DynamicMessageGenerator,生成消息
Message * generatedMessage = generator.paramToMessage("Hello", "World", "this_is_str", 1);
std::cout << "Generated Content:\n" <<generatedMessage->DebugString() << std::endl;

// 将 generatedMessage 赋值给 newedMessage
newedMessage->ParseFromString(generatedMessage->SerializeAsString());

auto downNewed = google::protobuf::internal::DownCast< Input *>(newedMessage);

std::cout << "downNewed Content:\n"<< downNewed->DebugString() << std::endl;

auto downGenerated = google::protobuf::internal::DownCast< Input *>(generatedMessage);

std::cout << "downGenerated Content:\n"<< downGenerated->DebugString() << std::endl;

但是这样使用会导致使用动态工厂生成的消息抛出类型检查错误,错误信息为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 输出信息
Generated Content:
strVal: "this_is_str"
intVal: 1

downNewed Content:
strVal: "this_is_str"
intVal: 1

Assertion failed: (f == nullptr || dynamic_cast<To>(f) != nullptr), function down_cast, file casts.h, line 94.


// 对应 protobuf 代码段
template<typename To, typename From> // use like this: down_cast<T*>(foo);
inline To down_cast(From* f) { // so we only accept pointers
// Ensures that To is a sub-type of From *. This test is here only
// for compile-time type checking, and has no overhead in an
// optimized build at run-time, as it will be optimized away
// completely.
if (false) {
implicit_cast<From*, To>(0);
}

#if !defined(NDEBUG) && PROTOBUF_RTTI
assert(f == nullptr || dynamic_cast<To>(f) != nullptr); // RTTI: debug mode only!
#endif
return static_cast<To>(f);
}

根据代码中的提醒信息,将 CMake 的构建方式设置为 Release 模式, assert 代码被跳过,测试程序可以正常运行。

1
2
3
4
5
6
7
8
9
10
11
Generated Content:
strVal: "this_is_str"
intVal: 1

downNewed Content:
strVal: "this_is_str"
intVal: 1

downGenerated Content:
strVal: "this_is_str"
intVal: 1

这里的一行 assert 代码在 Debug 模式时确实会比较麻烦,算是一个小坑吧。

动态工厂反射简单封装

由于动态工厂的接口直接用起来非常麻烦,需要使用多个类才能完成反射的赋值,因此将这几个类进行封装,并最终通过一行代码进行动态消息生成。这里主要是通过 C++中的类型检查和动态参数来完成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
using namespace google::protobuf;
using namespace google::protobuf::compiler;

class DynamicGenerator {
public:

/// 构造函数,主要负责从硬盘中读取 proto 文件并导入内存
/// \param disk_path 读取路径
/// \param proto_name proto 文件名
DynamicGenerator(const std::string &disk_path,const std::string &proto_name) :
m_factory(new DynamicMessageFactory), source(new DiskSourceTree) {
source->MapPath("", disk_path);
importer.reset(new Importer(&*source, {}));
//runtime compile foo.proto
importer->Import(proto_name);
pool = importer->pool();

}

/// 获得反射赋值后的 Message
/// \tparam Types 模板参数,允许每一个 param 类型不同
/// \param type Message 类型描述符
/// \param params Message 参数,需要按照定义顺序输入
/// \return 反射赋值后的 Message 指针
template<typename ... Types>
Message *paramToMessage(const Descriptor *type, Types ... params) {

const Message *tempo = m_factory->GetPrototype(type);
Message *message = tempo->New();

const Reflection *reflection = message->GetReflection();

if (sizeof...(params) > 0)
addValues(input, reflection, message, params...);

field_id = 1;

return message;
}


private:
const DescriptorPool *pool;
std::unique_ptr<DynamicMessageFactory> m_factory;
std::unique_ptr<DiskSourceTree> source;
std::unique_ptr<Importer> importer;

int field_id = 1;

// 赋值实现,利用 Reflection 设置指定 field 的值
template<typename Type>
void
addValues(const Descriptor *input, const Reflection *reflection, Message *message,
const Type &param) {

auto field = input->FindFieldByNumber(field_id);

if (field == nullptr) {
std::cout << "Invalid field_id.\n";
}

if constexpr(std::is_same_v<Type, std::string> || std::is_same_v<Type, const char *>) {
reflection->SetString(message, field, param);
} else if constexpr (std::is_same_v<Type, int>) {
reflection->SetInt32(message, field, param);
} else if constexpr(std::is_same_v<Type, float>) {
reflection->SetFloat(message, field, param);
} else if constexpr(std::is_same_v<Type, double>) {
reflection->SetDouble(message, field, param);
} else {
std::cout << "Unknown Type: " << typeid(param).name() << "\n";
}

field_id++;

}

// 接口,依次递归调用每一个 params
template<typename Type, typename ...Types>
void addValues(const Descriptor *input, const Reflection *reflection, Message *message,
const Type &param, const Types &... params) {

addValues(input, reflection, message, param);

addValues(input, reflection, message,params...);

}
};