2

用C++实现一个轻量级Json库

 1 year ago
source link: https://xuranus.github.io/2022/11/27/%E7%94%A8C-%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E8%BD%BB%E9%87%8F%E7%BA%A7Json%E5%BA%93/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

用C++实现一个轻量级Json库

2022-11-272023-03-110 Comments 31k 28 分钟

项目地址https://github.com/XUranus/minijson

JSON是非常常用的结构对象序列化方式。相比于其他基于字节流的序列化方案(thrift,protobuff),JSON易于阅读和编辑;相较于同样基于文本的序列化方案XML,JSON在网络传输占用更小的空间。JSON很好平衡了序列化/反序列化的性能、可读性、空间占用的问题。

很多语言都在标准库中添加了JSON相关的API,但C/C++还没有。C++较为完善的开源JSON库有jsoncpp。这篇文件将介绍如何用1K行左右的代码实现一个精简C++的Json库。

在开始之前强烈建议读一下JSON官方描述文档,其中包含了JSON格式的详细定义,之后实现JSON解析的时候将会用得到。

一个JSON成员(JsonElement)可以是一个null、数字(number)、布尔值(bool)、字符串(string)这些基础类型;也可以是一个JSON数组(JsonArray)、JSON对象(JsonObject)这类复合类型。所以这6类类型可以看作是一个JsonElement



JsonElement
number
string
JsonObject
JsonArray


JsonElement应该提供以下能力:

  1. 记录自己对应的类型信息,并能转化成对应六种的类型之一
  2. 提供统一序列化接口,对每一种类型分别实现序列化

于是首先定义一个序列化接口,所有的JsonElement都应该实现这个接口。

class Serializable {
virtual std::string Serialize() const = 0;
};

可以定义一个JsonElement类,用枚举类型标记其是哪一种类型,并用联合体成员记存放其对应实际的值:

class JsonElement: public Serializable {
public:
enum class Type {
JSON_OBJECT,
JSON_ARRAY,
JSON_STRING,
JSON_NUMBER,
JSON_BOOL,
JSON_NULL
};

union Value {
JsonObject* objectValue;
JsonArray* arrayValue;
std::string* stringValue;
double numberValue;
bool boolValue;
};

std::string Serialize() const override;

private:
Type m_type = Type::JSON_NULL;
Value m_value {};
};

其中基础类型number类型用double表示,用bool表示布尔值,std::string表示字符串。null类型不需要存放值,只需要在m_type中标记类型即可。对于复合类型JSON数组和JSON对象用对应的指针类型来存放。

由于JsonElement需要提供对于各种类型的转换能力,需要定义从各种基础类型的构造方法,并提供转换成对应类型的AsXXX()ToXXX()方法。在操作JsonElement对象时需要用TypeName()IsXXX()判断JsonElement存放的的具体类型。API设计如下:

class JsonElement: public Serializable {
public:
JsonElement();
explicit JsonElement(JsonElement::Type type);
explicit JsonElement(bool value);
explicit JsonElement(double num);
explicit JsonElement(long num);
explicit JsonElement(const std::string &str);
explicit JsonElement(char const *str);
JsonElement(const JsonObject& object);
JsonElement(const JsonArray& array);

JsonElement(const JsonElement& ele);
explicit JsonElement(JsonElement&& ele);
JsonElement& operator = (const JsonElement& ele);
~JsonElement();

bool& AsBool();
double& AsNumber();
void* AsNull() const;
std::string& AsString();
JsonObject& AsJsonObject();
JsonArray& AsJsonArray();

bool ToBool() const;
double ToNumber() const;
void* ToNull() const;
std::string ToString() const;
JsonObject ToJsonObject() const;
JsonArray ToJsonArray() const;

bool IsNull() const;
bool IsBool() const;
bool IsNumber() const;
bool IsString() const;
bool IsJsonObject() const;
bool IsJsonArray() const;

std::string TypeName() const;
std::string Serialize() const override;
};

复合类型的JSON对象(JsonObject)和JSON数组(JsonArray)可以包含JsonElement。
JsonObject可以看作KV结构的字典,Key是字符串,Value是JsonElement类型;JsonArray可以看成JsonElement构成的数组。于是这里分别选用继承std::vector<JsonElement>std::map<std::string, JsonElement>来实现JsonArray和JsonObject:

class JsonObject: public std::map<std::string, JsonElement>, public Serializable {
public:
std::string Serialize() const override;
};

class JsonArray: public std::vector<JsonElement>, public Serializable {
public:
std::string Serialize() const override;
};

具体API的实现代码有400多行,都是比较简单的逻辑判断。这里就不详述了,详细见:Json.cpp

有了基本的API设计和实现,就已经可以开始实现序列化了。序列化较为简单,只需要实现JsonElement::Serialize()JsonArray::Serialize()JsonObject::Serialize()方法。

JsonElement::Serialize()中实现五种基础类型的序列化,null和布尔值的truefalse直接返回对应的字面量即可,数字类型可以用std::to_string直接获得字面量,然后删除末尾冗余的浮点0(详见Json.cppDoubleToString()。字符串序列化需要前后带上引号,需要注意特殊字符的转义。JsonObjectJsonArray两种复合类型在对应自身的Serialize()中分别实现:

std::string JsonElement::Serialize() const
{
switch (m_type) {
case JsonElement::Type::JSON_NULL: {
return "null";
}
case JsonElement::Type::JSON_BOOL: {
return m_value.boolValue ? "true" : "false";
}
case JsonElement::Type::JSON_NUMBER: {
return DoubleToString(m_value.numberValue);
}
case JsonElement::Type::JSON_STRING: {
return std::string("\"") + EscapeString(*m_value.stringValue) + "\"";
}
case JsonElement::Type::JSON_OBJECT: {
return JsonObject(*m_value.objectValue).Serialize();
}
case JsonElement::Type::JSON_ARRAY: {
return JsonArray(*m_value.arrayValue).Serialize();
}
}
Panic("unknown type to serialize: %s", TypeName().c_str());
return "";
}

字符串转义需要考虑",\,/,\f,\b,\n,\r,\t这类字符,此处提供的实现如下:

std::string util::EscapeString(const std::string& str)
{
std::string res;
for (const char ch: str) {
switch (ch) {
case '"':
case '\\':
case '/':
res.push_back('\\');
res.push_back(ch);
break;
case '\f': {
res.push_back('\\');
res.push_back('f');
break;
}
case '\b': {
res.push_back('\\');
res.push_back('b');
break;
}
case '\r': {
res.push_back('\\');
res.push_back('r');
break;
}
case '\n': {
res.push_back('\\');
res.push_back('n');
break;
}
case '\t': {
res.push_back('\\');
res.push_back('t');
break;
}
default: {
res.push_back(ch);
break;
}
}
}
return res;
}

JsonObject序列化是一个遍历所有的KV对,递归调用字符串类型KEY和JsonElement类型的JsonElement::Serialize的过程:

std::string JsonObject::Serialize() const
{
std::string res = "{";
bool moreThanOneItem = false;
for (const auto& kv: *this) {
moreThanOneItem = true;
res += std::string("\"") + EscapeString(kv.first) + "\":" + kv.second.Serialize() + ",";
}
if (moreThanOneItem) {
res.pop_back();
}
res += "}";
return res;
}

JsonArray的序列化则是迭代调用所有数组成员类型JsonElement::Serialize的过程:

std::string JsonArray::Serialize() const
{
std::string res = "[";
bool moreThanOne = false;
for (const auto& v: *this) {
moreThanOne = true;
res += v.Serialize() + ",";
}
if (moreThanOne) {
res.pop_back();
}
res += "]";
return res;
}

写个Demo验证序列化成果:

TEST(SerializationTest, JsonBasicSerializationTest) {
JsonObject object {};
object["name"] = JsonElement("xuranus");
object["age"] = JsonElement(300.0);
object["isDeveloper"] = JsonElement(true);
object["skills"] = JsonElement(null);
EXPECT_EQ(object.Serialize(), R"({"age":300,"name":"xuranus", "isDeveloper":true, "skills":null})");
}

TOKEN

反序列化需要实现一个JSON字符串扫描和解析的模块,传入一个JSON字符串转为一个JsonElement。为了降低实现的复杂度,这里将解析的模块分成两块:

  • JsonScanner:逐字节扫描输入的字符串,依次识别一个个有意义的JSON语法TOKEN,并逐个返回
  • JsonParser:基于JsonScanner返回的TOKEN流,依据JSON的格式定义解析完整的JSON成员

JSON的TOKEN是一个个较为简单的语法单位,这里定义如下的TOKEN:

  • WHITESPACE : 空白符 (' ', '\n', '\r', '\t')
  • NUMBER : 数字,例如3.141145141919.810
  • STRING : 字符串,例如"xuranus"
  • LITERAL_TRUE : true字面量
  • LITERAL_FALSE : false字面量
  • LITERAL_NULL : null字面量
  • COMMA : 逗号,
  • COLON : 冒号:
  • ARRAY_BEGIN : JSON数组开始符[
  • ARRAY_END : JSON数组结束符]
  • OBJECT_BEGIN : JSON对象开始符{
  • OBJECT_END : JSON对象结束符}
  • EOF_TOKEN : 标记串末尾EOF

举个例子:

{
"name" : "xuranus",
"age" : 114514,
"skills": ["Java", "CPP", "JS"]
}

如果忽略所有的空白符,该JSON字符产长这样:

{"name":"xuranus","ID":114514,"skills":["Java","CPP","JS"]}

按照上述Token定义,我们对该字符串进行Token解析,可以获得Token流:

Token Token 字符串 数字
{ OBJECT_BEGIN
"name" STRING name
: COLON
"xuranus" STRING xuranus
, COMMA
"ID" STRING ID
: COLON
114514 NUMBER 114514
, COMMA
"skills" STRING skills
: COLON
[ ARRAY_BEGIN
"Java" STRING Java
, COMMA
"CPP" STRING CPP
, COMMA
"JS" STRING JS
] ARRAY_END
} OBJECT_END

从Token流可以进一步解析出JSON的AST:



JsonObject
OBJECT_BEGIN
PAIR_1
COMMA
PAIR_2
COMMA
PAIR_3
STRING
COLON
STRING
STRING
COLON
NUMBER
STRING
COLON
JsonArray
ARRAY_BEGIN
STRING
COMMA
STRING
COMMA
STRING
ARRAY_END


明确了原理,接下来就来分别看看JsonScannerJsonParser的实现。

JsonScanner

JsonScanner将在内部存放一个带解析的JSON字符串std::string m_str并维护一个指向其当前读取位置的的下标索引std::size_t m_posJsonScanner将提供一个返回Token的Next()方法供外部调用者调用,返回下一个解析出的Token。

在上述的13种Token种,数字Token(NUMBER)和字符串Token(STRING)解析起来比较复杂,我们将其解析逻辑放入单独的两个解析方法:

double GetNumberValue();
std::string GetStringValue();

他们解析的类型依然由Next()方法返回,但是他们解析出的值需要单独存档在类成员中,可以用std::string m_tmpStrValuedouble m_tmpNumberValue来暂存解析出的值,并由以上两个方法返回。其余的11种Token解析基本拥有固定的字面量(例如LITERAL_TRUE就是trueARRAY_BEGIN就是[,拿到了Token就是拿到了字面量),他们解析起来也较为容易,解析逻辑将在Next()中实现。

基于以上思路,就可以给出JsonScanner的定义:

class JsonScanner {
public:
enum class Token {
WHITESPACE, // ' ', '\n', '\r', '\t'
NUMBER,
STRING,
LITERAL_TRUE, // true
LITERAL_FALSE, // false
LITERAL_NULL, // null
COMMA, // ,
COLON, // :
ARRAY_BEGIN, // [
ARRAY_END, // ]
OBJECT_BEGIN, // {
OBJECT_END, // }
EOF_TOKEN // mark the end of the json string
};

public:
JsonScanner(const std::string &str);
void Reset();
Token Next();
double GetNumberValue();
std::string GetStringValue();
inline void RollBack() { m_pos = m_prevPos; }
inline size_t Position() { return m_pos; }

private:
void ScanNextString();
void ScanNextNumber();

private:
std::string m_str;
std::size_t m_pos = 0;
std::size_t m_prevPos = 0;

std::string m_tmpStrValue {};
double m_tmpNumberValue {0};
};

其中Reset()RollBack()Position()用于设置当前索引下标的跳转以辅助解析流程,之后再看他们的实现。

void JsonScanner::Reset() { m_pos = 0; m_prevPos = 0; }

首先看Next()方法,这是Token解析最核心的部分。Next()的任务是返回下一个Token,在解析下一个Token之前一般需要先跳过空白符,空白符(' ''\n''\r''\t')的存在一般只是用于缩进为了方便阅读JSON,而对JSON的解析毫无意义。空白符可以存在任何两个Token之间,所以要先跳过空白符。这里提供跳过空白符的实现:

class JsonScanner {
//...

inline bool IsWhiltespaceToken(char ch)
{
return (ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t');
}

inline bool SkipWhitespaceToken()
{
while (m_pos < m_str.size() && IsWhiltespaceToken(m_str[m_pos])) {
m_pos++;
}
return m_pos < m_str.size();
}
};

其中SkipWhitespaceToken将会在解析到字符串末尾时返回false。在每次指向Next()都应该检测解析是否结束,如果结束则返回EOF_TOKEN。当拿到下一个非空白符的字符时,就可以开始解析Token了:如果第一个字符是-或者数字,则下一个Token有可能是一个数字,此时则调用ScanNextNumber进入数字解析模式;如果当前字符是",则下一个Token可能是一个字符串;如果当前字符串是tfn,则下一个Token可能LITERAL_TRUELITERAL_FALSELITERAL_NULL,他们的Token对应的字面量固定,用ScanLiteral()检测。ScanLiteral()用于检测之后的字符串是否匹配给定的字面量,失败则用Panic抛异常,实现如下:

inline void Panic(const char* str, ...)
{
char message[100];
va_list args;
va_start(args, str);
vsprintf(message, str, args);
va_end(args);
throw std::logic_error(message);
}

class JsonScanner {
// ...
inline void ScanLiteral(const std::string& literal, int offset)
{
if (m_str.compare(m_pos, offset, literal) == 0) {
m_pos += offset;
} else {
Panic("unknown literal token at position = %lu, do you mean: %s ?", m_pos, literal.c_str());
}
}
};

最后,ARRAY_BEGINOBJECT_BEGIN等剩余的Token长度为1,直接用当前字符就能判断并返回。终于,基于以上逻辑,给出Next()的实现:

// return a non space token
JsonScanner::Token JsonScanner::Next()
{
m_prevPos = m_pos;
if (m_str.length() <= m_pos || !SkipWhitespaceToken()) {
return Token::EOF_TOKEN;
}

char curChar = m_str[m_pos];
if (IsDigit(curChar) || curChar == '-') {
ScanNextNumber();
return Token::NUMBER;
}
switch (curChar) {
case '\"':
ScanNextString();
return Token::STRING;
case 't':
ScanLiteral("true", 4);
return Token::LITERAL_TRUE;
case 'f':
ScanLiteral("false", 5);
return Token::LITERAL_FALSE;
case 'n':
ScanLiteral("null", 4);
return Token::LITERAL_NULL;
case '[':
m_pos ++;
return Token::ARRAY_BEGIN;
case ']':
m_pos ++;
return Token::ARRAY_END;
case '{':
m_pos ++;
return Token::OBJECT_BEGIN;
case '}':
m_pos ++;
return Token::OBJECT_END;
case ',':
m_pos ++;
return Token::COMMA;
case ':':
m_pos ++;
return Token::COLON;
}
Panic("Invalid token at position %lu", m_pos);
return Token::LITERAL_NULL;
}

接着来看Next()中用于识别下一个字符串的ScanNextString()方法。ScanNextString()当前位置开始扫描字符串,如果成果就将结果存在m_tmpStrValue中。字符串以"开始、以"结束,由于在调用该方法之前Next()方法总已经扫到了字符串开始的",接下来扫描字符串也就是要找到下一个",取其中的值作为字符串值。

在遍历的过程中需要注意特殊字符的转义。由于JSON序列化后的字符串中特殊字符已经被转义成了占用2字节的字面量,例如'\n''\r',或者也可能包含占用5个字符unicode编码字面量,例如\u1289。在反序列化的过程中需要检测\字符,将其视为转义字符。当遇到转义符时,将额外读取1个或4个字符。在解析到下一个"时,字符串读取完毕,此时该字符字串是含有转义字符字面量的串,需要再次进行反转义获得原始字符串。

ScanNextString()和字符串反转义实现如下:

void JsonScanner::ScanNextString()
{
size_t beginPos = m_pos;
m_pos ++; // skip left "
while (m_pos < m_str.size() && m_str[m_pos] != '\"') {
char curChar = m_str[m_pos ++];
if (curChar == '\\') {
// " quotation mark
// \ reverse soildus
// / sodilus
// b backspace
// f formfeed
// n linefeed
// r carriage return
// t horizontal tab
// u (4 hex digits)
if (m_pos >= m_str.size()) {
Panic("missing token, position: %lu", m_pos);
return;
} else {
char escapeChar = m_str[m_pos];
if (escapeChar == '\"' || escapeChar == 'r' || escapeChar == 'f' || escapeChar == 'n' ||
escapeChar == 't' || escapeChar == 'b' || escapeChar == '\\' || escapeChar == '/') {
m_pos ++;
} else if (escapeChar == 'u') {
m_pos += 4;
}
}
}
}
if (m_pos >= m_str.size()) {
Panic("missing end of string, position: %lu", beginPos);
}
m_pos ++; // skip right "
std::string rawStr = m_str.substr(beginPos + 1, m_pos - beginPos - 2);
m_tmpStrValue = util::UnescapeString(rawStr);
}

std::string util::UnescapeString(const std::string& str)
{
std::string res;
for (size_t i = 0; i < str.size(); ++i) {
char curChar = str[i];
if (curChar == '\\' && i + 1 < str.size()) {
char escapeChar = str[++i];
switch (escapeChar) {
case '"':
res.push_back('"');
break;
case '\\':
res.push_back('\\');
break;
case '/':
res.push_back('/');
break;
case 'f': {
res.push_back('\f');
break;
}
case 'b': {
res.push_back('\b');
break;
}
case 'r': {
res.push_back('\r');
break;
}
case 'n': {
res.push_back('\n');
break;
}
case 't': {
res.push_back('\t');
break;
}
Panic("invalid escaped char \\%c", escapeChar);
}
} else {
res.push_back(curChar);
}
}
return res;
}

std::string JsonScanner::GetStringValue() { return m_tmpStrValue; }

除了字符串解析,另一个复杂Token的解析就是数字了。JSON中的数字可能是整数,也可能是浮点数;可能带符号,也可能不带符号,浮点数还可能是科学计数法表示。数字解析相关实现如下:

class JsonScanner {
// ...
inline bool IsDigit(char ch)
{
return '0' <= ch && ch <= '9';
}
};

void JsonScanner::ScanNextNumber()
{
size_t beginPos = m_pos;
// example: "-114.51E-4"
m_pos ++; // skip + or - or first digit
while (m_pos < m_str.size() && IsDigit(m_str[m_pos])) {
m_pos ++;
}
if (m_pos + 1 < m_str.size() && m_str[m_pos] == '.' && IsDigit(m_str[m_pos + 1])) {
m_pos ++; // skip .
while(m_pos < m_str.size() && IsDigit(m_str[m_pos])) {
m_pos ++;
}
}
if (m_pos + 1 < m_str.size() && (m_str[m_pos] == 'E' || m_str[m_pos] == 'e')) {
m_pos ++;
if (m_str[m_pos] == '-' || m_str[m_pos] == '+') {
m_pos ++;
}
// parse number
while (m_pos < m_str.size() && IsDigit(m_str[m_pos])) {
m_pos ++;
}
}

std::string numberStr = m_str.substr(beginPos, m_pos - beginPos);
try {
m_tmpNumberValue = std::atof(numberStr.c_str());
} catch (std::exception &e) {
Panic("invalid number %lf, pos: %lu", m_tmpNumberValue, beginPos);
}
}

double JsonScanner::GetNumberValue() { return m_tmpNumberValue; }

此时,JsonScanner的全部代码就实现完了,我们已经获得了一个对JSON字符串实现词法分析的引擎。接下来我们只需要在此基础上基于Token流解析完整的JSON结构。

JsonParser

由于对JSON的解析依赖于JsonScanner提供的Token解析,JsonParser内部持有一个JsonScanner的instance。

仿照之前JsonScanner的设计方式,将解析流程分成简单结构的解析方法和复杂结构的解析方法,JsonParser可以将数字、布尔值、字符串等基本结构放在一个ParseNext()方法中,而复杂点的复合结构JsonArrayJsonObject则由独立的ParseJsonArray()ParseJsonObject负责解析。

给出JsonParser定义如下:

class JsonParser {
private:
JsonScanner m_scanner;

public:
JsonParser(const std::string str);
JsonElement Parse();
private:
JsonElement ParseNext();
JsonObject ParseJsonObject();
JsonArray ParseJsonArray();
};

正如JsonScanner从前往后解析TOKEN,JsonParser从前往后解析JsonElementJsonParser::ParseNextJsonParser的核心逻辑,他返回下一个解析出的JsonElement。类似JsonScanner根据当前字符推断下一个可能的Token,JsonParser根据当前Token推断下一个可能的结构,所以ParseNext()的实现十分简单:

JsonElement JsonParser::ParseNext()
{
JsonScanner::Token token = m_scanner.Next();
switch (token) {
case JsonScanner::Token::OBJECT_BEGIN: {
return ParseJsonObject();
}
case JsonScanner::Token::ARRAY_BEGIN: {
return ParseJsonArray();
}
case JsonScanner::Token::STRING: {
return JsonElement(m_scanner.GetStringValue());
}
case JsonScanner::Token::NUMBER: {
return JsonElement(m_scanner.GetNumberValue());
}
case JsonScanner::Token::LITERAL_TRUE: {
return JsonElement(true);
}
case JsonScanner::Token::LITERAL_FALSE: {
return JsonElement(false);
}
case JsonScanner::Token::LITERAL_NULL: {
return JsonElement();
}
}
Panic("scanner return unexpected token: %d", token);
return JsonElement();
}

其中字符串、布尔、数字、null这四种基础类型可以直接用JsonScanner提供的TOKEN解析能力直接拿到。而JsonArrayJsonObject的解析较为复杂,则放到单独的ParseJsonObject()ParseJsonArray()方法中实现。

ParseJsonObject()本质上就是解析STRINGCOLON,然后再递归调用ParseNext()解析value对象,直到遇到OBJECT_END退出:

JsonObject JsonParser::ParseJsonObject()
{
JsonObject object {};
JsonScanner::Token token = m_scanner.Next();
if (token == JsonScanner::Token::OBJECT_END) {
return object;
}
m_scanner.RollBack();

while (true) {
size_t pos = m_scanner.Position();
token = m_scanner.Next();
if (token != JsonScanner::Token::STRING) {
Panic("expect a string as key for json object, position: %lu", pos);
}
std::string key = m_scanner.GetStringValue();

pos = m_scanner.Position();
token = m_scanner.Next();
if (token != JsonScanner::Token::COLON) {
Panic("expect ':' in json object, position: %lu", pos);
}
JsonElement ele = ParseNext();
object[key] = ele;

pos = m_scanner.Position();
token = m_scanner.Next();
if (token == JsonScanner::Token::OBJECT_END) {
break;
}
if (token != JsonScanner::Token::COMMA) {
Panic("expect ',' in json object, position: %lu", pos);
}
}
return object;
}

解析数组的原理同理,先解析ARRAY_BEGIN,然后依次递归调用ParseNext()解析下一个JsonElement对象,知道遇到ARRAY_END完成解析。

JsonArray JsonParser::ParseJsonArray()
{
JsonArray array {};
JsonScanner::Token token = m_scanner.Next();
if (token == JsonScanner::Token::ARRAY_END) {
return array;
}
m_scanner.RollBack();

while (true) {
array.push_back(ParseNext());
size_t pos = m_scanner.Position();
token = m_scanner.Next();
if (token == JsonScanner::Token::ARRAY_END) {
break;
}
if (token != JsonScanner::Token::COMMA) {
Panic("expect ',' in array, pos: %lu", pos);
}
}
return array;
}

由于进入ParseJsonArray()ParseJsonObject()之前JsonScanner的字符串索引已经指向了[{,进入对应方法后需要RollBack()回滚索引。

到此位置,反序列化也完全实现了。接下来写个用例验证下:

TEST(SerializationTest, JsonParserBasicTest) {
std::string str = R"(
{
"name" : "xuranus",
"age" : 300,
"skills" : ["C++", "Java", "Python"]
}
)";
JsonElement element = JsonParser(str).Parse();
JsonObject object = element.AsJsonObject();
EXPECT_EQ(object["name"].AsString(), "xuranus");
EXPECT_EQ(object["age"].AsNumber(), 300);
EXPECT_EQ(object["skills"].AsJsonArray()[0].AsString(), "C++");
EXPECT_EQ(object["skills"].AsJsonArray()[1].AsString(), "Java");
EXPECT_EQ(object["skills"].AsJsonArray()[2].AsString(), "Python");
}

此时,虽然一个完整的JSON序列化和反序列化工具已经实现,但是用起来还是比较麻烦。在Java、GO中都有注解实现的ORM框架,能实现类/结构体的自动序列化/反序列化,我们尝试在C++中也实现这一功能。

在下面的样例中,我们期望实现两个函数:

  • std::string util::Serialize(T&)将类型为T的结构体序列化成JSON字符串
  • void util::Deserialize(const std::string&, T&)将JSON字符串反序列化成对应的结构体
    struct Book {
    std::string m_name;
    int m_id;
    float m_price;
    bool m_soldOut;
    std::vector<std::string> m_tags;
    };

    TEST(SerializationTest, BasicStructSerialization) {
    Book book1 {};
    book1.m_name = "C++ Primer";
    book1.m_id = 114514;
    book1.m_price = 114.5;
    book1.m_soldOut = true;
    book1.m_tags = {"C++", "Programming", "Language"};

    // serialization here
    std::string jsonStr = util::Serialize(book1);

    Book book2 {};
    // deserialization here
    util::Deserialize(jsonStr, book2);
    EXPECT_EQ(book1.m_name, book2.m_name);
    EXPECT_EQ(book1.m_id, book2.m_id);
    EXPECT_EQ(book1.m_price, book2.m_price);
    EXPECT_EQ(book1.m_soldOut, book2.m_soldOut);
    EXPECT_EQ(book1.m_tags, book2.m_tags);
    }
    为了实现对每一种类型的结构体都能序列化,我们必须对其中成员序列化映射规则做出定义。为了实现这点,我们不得不在结构体中侵入式的定义一个成员函数_XURANUS_JSON_CPP_SERIALIZE_METHOD_
    struct Book {
    std::string m_name;
    int m_id;
    float m_price;
    bool m_soldOut;
    std::vector<std::string> m_tags;

    void _XURANUS_JSON_CPP_SERIALIZE_METHOD_(xuranus::minijson::JsonObject& object, bool toJson) {
    if (toJson) {
    util::SerializeTo(object, "name", m_name);
    util::SerializeTo(object, "id", m_id);
    util::SerializeTo(object, "price", m_price);
    util::SerializeTo(object, "soldOut", m_soldOut);
    util::SerializeTo(object, "tags", m_tags);
    } else {
    util::DeserializeFrom(object, "name", m_name);
    util::DeserializeFrom(object, "id", m_id);
    util::DeserializeFrom(object, "price", m_price);
    util::DeserializeFrom(object, "soldOut", m_soldOut);
    util::DeserializeFrom(object, "tags", m_tags);
    }
    }
    };
    我们可以用宏定义简化这部分的表示:
    #define SERIALIZE_SECTION_BEGIN                                                                  \
    public: \
    using __XURANUS_JSON_SERIALIZATION_MAGIC__ = void; \
    public: \
    void _XURANUS_JSON_CPP_SERIALIZE_METHOD_(xuranus::minijson::JsonObject& object, bool toJson) \
    { \

    #define SERIALIZE_SECTION_END \
    }; \

    #define SERIALIZE_FIELD(KEY_NAME, ATTR_NAME) \
    do { \
    if (toJson) { \
    util::SerializeTo(object, #KEY_NAME, ATTR_NAME); \
    } else { \
    xuranus::minijson::util::DeserializeFrom(object, #KEY_NAME, ATTR_NAME); \
    } \
    } while (0) \
    定义宏之后的结构体声明就可以写成如下形式,由一组SERIALIZE_SECTION_BEGINSERIALIZE_SECTION_END以及一系列SERIALIZE_FIELD声明成员的序列化/反序列化名称映射关系。:
    struct Book
    {
    std::string m_name;
    int m_id;
    float m_price;
    bool m_soldOut;
    std::vector<std::string> m_tags;

    SERIALIZE_SECTION_BEGIN
    SERIALIZE_FIELD(name, m_name);
    SERIALIZE_FIELD(id, m_id);
    SERIALIZE_FIELD(price, m_price);
    SERIALIZE_FIELD(soldOut, m_soldOut);
    SERIALIZE_FIELD(tags, m_tags);
    SERIALIZE_SECTION_END
    };
    当类定义了上述的成员方法后,就拥有了序列化和反序列化的能力。此时就可以实现供外部调用的接口了:
    template<typename T>
    auto Serialize(T& value) -> decltype(typename T::__XURANUS_JSON_SERIALIZATION_MAGIC__(), std::string())
    {
    JsonObject object{};
    value._XURANUS_JSON_CPP_SERIALIZE_METHOD_(object, true);
    return object.Serialize();
    }

    template<typename T>
    auto Deserialize(const std::string& jsonStr, T& value) -> decltype(typename T::__XURANUS_JSON_SERIALIZATION_MAGIC__())
    {
    JsonParser parser(jsonStr);
    JsonElement ele = parser.Parse();
    JsonObject object = ele.AsJsonObject();
    value._XURANUS_JSON_CPP_SERIALIZE_METHOD_(object, false);
    }

    这里用到了Trailing Return Type,__XURANUS_JSON_SERIALIZATION_MAGIC__在之前的成员宏中已经定义为void,本身并无意义,只是用于标记该类实现了成员序列化方法,用于给序列化/反序列化工具函数在编译期进行检测。如果结构体没定义相关的宏,则模板函数特化时会失败。

于是接下来的任务就只是实现上述_XURANUS_JSON_CPP_SERIALIZE_METHOD_方法中用到的两个辅助函数:

  • std::string util::SerializeTo(JsonObject&, const std::string&, T&)
  • void util::DeserializeFrom(JsonObject&, const std::string&, T&)

首先来看std::string util::SerializeTo(JsonObject&, const std::string&, T&)。它的用途是把参数3的值以参数2作为key转化成JsonElement存入参数1的JsonObject。而void util::DeserializeFrom(JsonObject&, const std::string&, T&)则相反,它的作用是从参数1的JsonObject中取出参数2的key对应的JsonElement,转化为T类型的值存入参数3。这两个模板函数可以实现为:

template<typename T>
void SerializeTo(JsonObject& object, const std::string& key, const T& field)
{
JsonElement ele{};
CastToJsonElement<T>(ele, field);
object[key] = ele;
}

template<typename T>
void DeserializeFrom(const JsonObject& object, const std::string& key, T& field)
{
JsonElement ele = object.find(key)->second;
CastFromJsonElement<T>(ele, field);
}

其中CastToJsonElement(JsonElement & ele, const T & value)CastFromJsonElement(const JsonElement & ele, T & value)是定义了某一JsonElementT类型直线转化关系的模板函数。后续我们需要针对不同的类型逐一实现他们。

如果T类型本身是一个已经定义了__XURANUS_JSON_SERIALIZATION_MAGIC__的结构,我们可以递归调用其中的_XURANUS_JSON_CPP_SERIALIZE_METHOD_方法来实现类型映射:

// cast between struct and JsonElement
template<typename T>
auto CastFromJsonElement(const JsonElement& ele, T& value) -> decltype(typename T::__XURANUS_JSON_SERIALIZATION_MAGIC__()) {
JsonObject object = ele.ToJsonObject();
value._XURANUS_JSON_CPP_SERIALIZE_METHOD_(object, false);
return;
};

template<typename T>
auto CastToJsonElement(JsonElement& ele, const T& value) -> decltype(typename T::__XURANUS_JSON_SERIALIZATION_MAGIC__()) {
JsonObject object{};
T* valueRef = reinterpret_cast<T*>((void*)&value);
valueRef->_XURANUS_JSON_CPP_SERIALIZE_METHOD_(object, true);
ele = JsonElement(object);
return;
};

我们将这复合一类型的模板作为主模板,利用SFINAE来进一步定义更多基础类型的映射模板:

字符串类型:

// cast between std::string and JsonElement
template<typename T, typename std::enable_if<std::is_same<T, std::string>::value>::type* = nullptr>
void CastFromJsonElement(const JsonElement& ele, T& value) {
value = ele.ToString();
return;
}

template<typename T, typename std::enable_if<std::is_same<T, std::string>::value>::type* = nullptr>
void CastToJsonElement(JsonElement& ele, const T& value) {
ele = JsonElement(value);
return;
}

数字类型:

// cast between numeric and JsonElement
template<typename T, typename std::enable_if<(std::is_integral<T>::value || std::is_floating_point<T>::value)
&& !std::is_same<T, bool>::value>::type* = nullptr>
void CastFromJsonElement(const JsonElement& ele, T& value) {
value = static_cast<T>(ele.ToNumber());
return;
}

template<typename T, typename std::enable_if<std::is_integral<T>::value && !std::is_same<T, bool>::value>::type* = nullptr>
void CastToJsonElement(JsonElement& ele, const T& value) {
ele = JsonElement(static_cast<long>(value));
return;
}

template<typename T, typename std::enable_if<std::is_floating_point<T>::value>::type* = nullptr>
void CastToJsonElement(JsonElement& ele, const T& value) {
ele = JsonElement(static_cast<double>(value));
return;
}

布尔类型:

// cast between bool and JsonElement
template<typename T, typename std::enable_if<std::is_same<T, bool>::value>::type* = nullptr>
void CastFromJsonElement(const JsonElement& ele, T& value) {
value = ele.ToBool();
return;
}

template<typename T, typename std::enable_if<std::is_same<T, bool>::value>::type* = nullptr>
void CastToJsonElement(JsonElement& ele, const T& value) {
ele = JsonElement(value);
return;
}

std::vectorstd::list

// cast between std::vector or std::list and JsonElement
template<typename T, typename std::enable_if<
std::is_same<T, std::vector<typename T::value_type>>::value ||
std::is_same<T, std::list<typename T::value_type>>::value
>::type* = nullptr>
void CastFromJsonElement(const JsonElement & ele, T & value) {
JsonArray array = ele.ToJsonArray();
value.clear();
for (const JsonElement& eleItem : array) {
typename T::value_type t;
CastFromJsonElement<typename T::value_type>(eleItem, t);
value.push_back(t);
}
return;
}

template<typename T, typename std::enable_if<
std::is_same<T, std::vector<typename T::value_type>>::value ||
std::is_same<T, std::list<typename T::value_type>>::value
>::type* = nullptr>
void CastToJsonElement(JsonElement & ele, const T & value) {
JsonArray array;
for (const typename T::value_type& item : value) {
JsonElement itemElement;
CastToJsonElement<typename T::value_type>(itemElement, item);
array.push_back(itemElement);
}
ele = JsonElement(array);
return;
}

std::mapstd::unordered_map

// cast between std::map and JsonElement
template<typename T, typename std::enable_if<
std::is_same<std::string, typename T::key_type>::value && (
std::is_same<T, std::map<std::string, typename T::mapped_type>>::value ||
std::is_same<T, std::unordered_map<std::string, typename T::mapped_type>>::value
)>::type* = nullptr>
void CastFromJsonElement(const JsonElement& ele, T& value) {
JsonObject object = ele.ToJsonObject();
value.clear();
for (const std::pair<std::string, JsonElement>& p : object) {
typename T::mapped_type v;
CastFromJsonElement<typename T::mapped_type>(p.second, v);
value[p.first] = v;
}
return;
}

template<typename T, typename std::enable_if<
std::is_same<std::string, typename T::key_type>::value && (
std::is_same<T, std::map<std::string, typename T::mapped_type>>::value ||
std::is_same<T, std::unordered_map<std::string, typename T::mapped_type>>::value
)>::type* = nullptr>
void CastToJsonElement(JsonElement& ele, const T& value) {
JsonObject object;
for (const std::pair<std::string, typename T::mapped_type>& p : value) {
JsonElement valueElement;
CastToJsonElement<typename T::mapped_type>(valueElement, p.second);
object[p.first] = valueElement;
}
ele = JsonElement(object);
return;
}

到此位置,该JSON库的所有功能都已实现了,来跑一个复杂点的用例:

struct Book
{
std::string m_name;
int m_id;
float m_price;
bool m_soldOut;
std::vector<std::string> m_tags;

SERIALIZE_SECTION_BEGIN
SERIALIZE_FIELD(name, m_name);
SERIALIZE_FIELD(id, m_id);
SERIALIZE_FIELD(price, m_price);
SERIALIZE_FIELD(soldOut, m_soldOut);
SERIALIZE_FIELD(tags, m_tags);
SERIALIZE_SECTION_END
};

struct Author
{
std::string m_name;
std::list<Book> m_books;

SERIALIZE_SECTION_BEGIN
SERIALIZE_FIELD(name, m_name);
SERIALIZE_FIELD(books, m_books);
SERIALIZE_SECTION_END
};

bool operator==(const Book &book1, const Book &book2)
{
return (book1.m_name == book2.m_name &&
book1.m_id == book2.m_id &&
book1.m_price == book2.m_price &&
book1.m_soldOut == book2.m_soldOut &&
book1.m_tags == book2.m_tags);
}

TEST(SerializationTest, NestedStructSerialization)
{
Book book1{};
book1.m_name = "C++ Primer";
book1.m_id = 114514;
book1.m_price = 114.5;
book1.m_soldOut = true;
book1.m_tags = {"C++", "Programming", "Language"};

Book book2{};
book2.m_name = "Essential C++";
book2.m_id = 1919810;
book2.m_price = 19.19;
book2.m_soldOut = false;
book2.m_tags = {"Programming", "Computer Science"};

Author author1{};
author1.m_name = "Stanley B. LippmanBarbara E. Moo JoséeLaJoie";
author1.m_books = {book1, book2};

Author author2{};
std::string jsonStr = util::Serialize(author1);
util::Deserialize(jsonStr, author2);

EXPECT_EQ(author2.m_name, author1.m_name);
EXPECT_EQ(author2.m_books, author1.m_books);

JsonElement ele = JsonParser(jsonStr).Parse();
EXPECT_TRUE(ele.IsJsonObject());
JsonObject authorObject = ele.AsJsonObject();
EXPECT_TRUE(authorObject["name"].IsString());
EXPECT_TRUE(authorObject["books"].IsJsonArray());
JsonArray booksArray = authorObject["books"].AsJsonArray();
EXPECT_EQ(booksArray[0].AsJsonObject()["name"].AsString(), "C++ Primer");
}

完整项目代码见:https://github.com/XUranus/minijson


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK