经常有网友问xpack(原来的x2struct)的实现原理,这里用一个文章大概讲解一下。以下分析均基于Ubuntu 16.04环境进行。写的比较简略,有疑惑的可以在评论区留言,我慢慢补全。
xpack主要包含两部分:
先说结论。
可以写一段简单的代码辅助分析(保存为test.cpp):
#include "xpack/xpack.h"
struct TestStruct {
int uid;
std::string name;
std::string email;
XPACK(A(uid, "id"), O(name, email));
};
然后执行命令(需要安装astyle):
g++ -E test.cpp | astyle | grep -A 100 TestStruct
可以看到展开的代码如下(经过一些调整)
struct TestStruct {
int uid;
std::string name;
std::string email;
public:
// 用于表征这个结构体添加了XPACK宏
static bool const __x_pack_value = true;
// 生成decode函数
// __X_PACK_DOC 是decoder,命名不是很好
// __X_PACK_ME 是当前这个结构体,不用this的原因是为了XPACK_OUT
template<class __X_PACK_DOC, class __X_PACK_ME>
void __x_pack_decode(__X_PACK_DOC& __x_pack_obj, __X_PACK_ME &__x_pack_self, const xpack::Extend *__x_pack_extp) {
(void)__x_pack_extp;
// 每个字母包含会生成一个代码块。核心就是调用__x_pack_obj.decode函数
// 传入的是变量的名字(别名特殊处理),变量本身,和extend控制参数
// 这里对应的是A(uid, "id")
{
int __x_pack_flag = 0 | 0 ;
{
static xpack::Alias __x_pack_alias("uid", "id");
xpack::Extend __x_pack_ext(__x_pack_flag, &__x_pack_alias);
const char *__new_name = __x_pack_alias.Name(__x_pack_obj.Type());
__x_pack_obj.decode(__new_name, __x_pack_self.uid, &__x_pack_ext);
}
}
// 这里对应的是O(name, email)
{
int __x_pack_flag = 0 | 0 ;
xpack::Extend __x_pack_ext(__x_pack_flag, __null);
__x_pack_obj.decode("name", __x_pack_self.name, &__x_pack_ext);
__x_pack_obj.decode("email", __x_pack_self.email, &__x_pack_ext);
}
}
// 生成encode函数
template <class __X_PACK_DOC, class __X_PACK_ME>
void __x_pack_encode(__X_PACK_DOC& __x_pack_obj, const __X_PACK_ME &__x_pack_self, const xpack::Extend *__x_pack_extp) const {
(void)__x_pack_extp;
{
int __x_pack_flag = 0 | 0 ;
{
static xpack::Alias __x_pack_alias("uid", "id");
xpack::Extend __x_pack_ext(__x_pack_flag, &__x_pack_alias);
const char *__new_name = __x_pack_alias.Name(__x_pack_obj.Type());
__x_pack_obj.encode(__new_name, __x_pack_self.uid, &__x_pack_ext);
}
}
{
int __x_pack_flag = 0 | 0 ;
xpack::Extend __x_pack_ext(__x_pack_flag, __null);
__x_pack_obj.encode("name", __x_pack_self.name, &__x_pack_ext);
__x_pack_obj.encode("email", __x_pack_self.email, &__x_pack_ext);
}
};
};
从代码可以看出,XPACK核心就是给结构体添加了两个模板函数:
大概的展开过程可以看xpack.h的注释。
这里面主要用到的是获取宏参数个数的一个小技巧,核心的宏是这里。我们可以把这个宏简化一下:
#define X_PACK_COUNT(LEVEL, ACTION, _2,_1,N,...) LEVEL##N
可以理解为X_PACK_COUNT这个宏的展开结果就是第一个参数(LEVEL)和第五个参数(N)的拼接。可以看看调用这个宏的地方:
#define X_PACK_N(LEVEL, ACTION, ...) X_PACK_COUNT(LEVEL, ACTION, __VA_ARGS__, _2, _1)
也就是把X_PACK_N后面的不定参数在占位符之前展开了。如果X_PACK_N带一个参数a,那么展开的效果就是X_PACK_COUNT(LEVEL, ACTION, a, _2, _1)那么X_PACK_COUNT宏的第五个参数就是_1,那么LEVEL##N就是LEVEL_1。如果带两个参数,那么就是X_PACK_COUNT(LEVEL, ACTION, a, b, _2, _1),那么结果就是LEVEL_2,从而实现了根据宏参数个数展开成不同的宏的效果。
XPACK宏并不是直接包含变量,而是需要通过一个字母来包含这些变量。原因有:
这个带来的问题就是展开的过程变得复杂了,需要展开两次。然后因为宏展开的过程是禁止“递归”的,也就是展开形成的结果不允许是之前的宏。这也是为何定义了两份类似的宏(比如X_PACK_N和X_PACK_N2)的原因,如果中途复用X_PACK_N这个宏,预处理器会停止展开,因为它认为循环了。
通过XPACK宏,给结构体添加了encode和decode函数,函数里面会遍历所有的变量,每个变量都会调用模板传递的obj的encoe/decode函数。这个时候就可以通过编写不同的encoder/decoder来实现不同的功能了。下面以decoder为例说明。
decoder要实现一个名为decode的模板函数,核心问题是如何针对各种不同的类型编写不同的decode函数,这里需要大量用到C++的SFINAE特性。xpack目前支持json/xml/bson三种编码,这里通过xdecoder.h把一些公共类型包装了进去,各个类型的decoder只需要写基础类型和一些特定接口即可。但是xdecoder.h这种并不一定适合自定义decoder,需要根据自己的需求来。
XPACK宏给结构体/类添加了__x_pack_decode和__x_pack_decode两个函数,两个函数都是遍历了结构体的变量,可以自己写encoder或者decoder(选哪个看需求)来实现一些功能。比如之前有网友提过一个需求:根据变量名给目的结构体的变量赋值。比如SetFields(a, b, "uid,name")就是b.uid = a.uid; b.name = a.name;这里用decoder更合适一些,因为要改变目标结构体的值。代码如下:
#include <iostream>
#include <string>
#include "xpack/json.h"
using namespace std;
// assign value by name
class Assign {
public:
// 构造函数,把源、目的结构体地址,需要设置的变量名存进来
Assign(const std::vector<std::string>&vs, const unsigned char *src, unsigned char *dst) {
for (size_t i=0; i<vs.size(); ++i) {
fields.insert(vs[i]);
}
this->src = src;
this->dst = dst;
}
public:
// 实现一个decode,通过地址偏移获得变量指针,然后赋值
template<class T>
bool decode(const char *key, T&val, const xpack::Extend *ext) {
// 如果变量在变量名列表里面
if (fields.end() != fields.find(key)) {
// 通过val获取偏移,通过偏移获得源的指针
unsigned int offset = (unsigned int)((unsigned char*)&val - dst);
val = *((T*)(src+offset));
}
return true;
}
private:
std::set<std::string> fields;
const unsigned char *src;
unsigned char *dst;
};
template <class T>
void SetFields(const T&src, T&dst, const std::string&fields) {
std::vector<std::string> vs;
xpack::Util::split(vs, fields, ',');
if (vs.size() == 0) {
return;
}
Assign obj(vs, (unsigned char*)(&src), (unsigned char*)(&dst));
dst.__x_pack_decode(obj, dst, NULL);
}
// 以下是测试代码
struct Data {
int a;
int b;
std::string c;
XPACK(O(a,b,c));
};
int main(int argc, char *argv[]) {
(void)argc;
(void)argv;
Data d1;
Data d2;
d1.a = 5;
d1.b = 10;
d1.c = "hello";
d2.b = 9;
SetFields<Data>(d1, d2, "a,c");
cout<<d2.a<<','<<d2.c<<endl;
return 0;
}