什么是JSON生成器?
JSON生成器负责把树形数据结构转化为JSON文本,这个过程又称为字符串化(stringify)。
头文件
生成器的API:
char* lept_stringify(const lept_value* v, size_t* length);
在实现JSON解析的时候,我们加入了一个动态堆栈,用于存储临时的解析结果。而在JSON生成器中,也要存储生成的结果,所以最简单的再利用该数据结构,作为输出缓冲区。
因为我们已经写过JSON解析器,所以生成器的写法就照着解析器写就可以。
#ifndef LEPT_PARSE_STRINGIFY_INIT_SIZE
#define LEPT_PARSE_STRINGIFY_INIT_SIZE 256
#endif
//int lept_stringify(const lept_value* v, char** json, size_t* length){
// lept_context c;
// int ret;
// assert(v != NULL);
// assert(json != NULL);
// c.stack = (char*)malloc(c.size = LEPT_PARSE_STRINGIFY_INIT_SIZE);
// c.top = 0;
// if (LEPT_PARSE_OK != (ret = lept_stringify_value(&c, v))){
// free(c.stack);
// *json = NULL;
// return ret;
// }
// if (length)
// *length = c.top;
// PUTC(&c, '\0');
// *json = c.stack;
// return LEPT_PARSE_OK;
//}
char* lept_stringify(const lept_value* v, size_t* length)
{
lept_context c;
assert(v != NULL);
c.stack = (char*)malloc(c.size = LEPT_PARSE_STRINGIFY_INIT_SIZE);
c.top = 0;
lept_stringify_value(&c, v);
if (length)
*length = c.top;
PUTC(&c, '\0');
return c.stack;
}
测试代码
测试代码编写的思路:将一个JSON进行解析生成字符串,然后利用这个字符串在生成JSON2,最后逐字符对比两个JSON是否相同,这种测试称为“往返测试”。
#define TEST_ROUNDTRIP(json)\
do {\
lept_value v; \
char* json2; \
size_t length; \
lept_init(&v); \
EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, json)); \
json2 = lept_stringify(&v, &length); \
EXPECT_EQ_STRING(json, json2, length); \
lept_free(&v); \
free(json2); \
} while (0)
static void test_stringify_number() {
TEST_ROUNDTRIP("0");
TEST_ROUNDTRIP("-0");
TEST_ROUNDTRIP("1");
TEST_ROUNDTRIP("-1");
TEST_ROUNDTRIP("1.5");
TEST_ROUNDTRIP("-1.5");
TEST_ROUNDTRIP("3.25");
TEST_ROUNDTRIP("1e+20");
TEST_ROUNDTRIP("1.234e+20");
TEST_ROUNDTRIP("1.234e-20");
TEST_ROUNDTRIP("1.0000000000000002"); /* the smallest number > 1 */
TEST_ROUNDTRIP("4.9406564584124654e-324"); /* minimum denormal */
TEST_ROUNDTRIP("-4.9406564584124654e-324");
TEST_ROUNDTRIP("2.2250738585072009e-308"); /* Max subnormal double */
TEST_ROUNDTRIP("-2.2250738585072009e-308");
TEST_ROUNDTRIP("2.2250738585072014e-308"); /* Min normal positive double */
TEST_ROUNDTRIP("-2.2250738585072014e-308");
TEST_ROUNDTRIP("1.7976931348623157e+308"); /* Max double */
TEST_ROUNDTRIP("-1.7976931348623157e+308");
}
static void test_stringify_string() {
TEST_ROUNDTRIP("\"\"");
TEST_ROUNDTRIP("\"Hello\"");
TEST_ROUNDTRIP("\"Hello\\nWorld\"");
TEST_ROUNDTRIP("\"\\\" \\\\ / \\b \\f \\n \\r \\t\"");
TEST_ROUNDTRIP("\"Hello\\u0000World\"");
}
static void test_stringify_array() {
TEST_ROUNDTRIP("[]");
TEST_ROUNDTRIP("[null,false,true,123,\"abc\",[1,2,3]]");
}
static void test_stringify_object() {
TEST_ROUNDTRIP("{}");
TEST_ROUNDTRIP("{\"n\":null,\"f\":false,\"t\":true,\"i\":123,\"s\":\"abc\",\"a\":[1,2,3],\"o\":{\"1\":1,\"2\":2,\"3\":3}}");
}
static void test_stringify() {
TEST_ROUNDTRIP("null");
TEST_ROUNDTRIP("false");
TEST_ROUNDTRIP("true");
test_stringify_number();
test_stringify_string();
test_stringify_array();
test_stringify_object();
}
函数实现
首先看看lept_parse_value函数的实现。
//解析的代码
static int lept_parse_value(lept_context* c, lept_value* v) {
switch (*c->json) {
case 't': return lept_parse_literal(c, v, "true", LEPT_TRUE);
case 'f': return lept_parse_literal(c, v, "false", LEPT_FALSE);
case 'n': return lept_parse_literal(c, v, "null", LEPT_NULL);
default: return lept_parse_number(c, v);//对于number的情况,可以这样处理。
case '\0': return LEPT_PARSE_EXPECT_VALUE;
}
}
//仿写生成的函数
#define PUTS(c, s, len) memcpy(lept_context_push(c, len), s, len)
static int lept_stringify_value(lept_context* c, const lept_value* v) {
size_t i;
int ret;
switch (v->type) {
case LEPT_NULL: PUTS(c, "null", 4); break;
case LEPT_FALSE: PUTS(c, "false", 5); break;
case LEPT_TRUE: PUTS(c, "true", 4); break;
/* ... */
}
return LEPT_STRINGIFY_OK;
}
生成数字
case LEPT_NUMBER:
{
char buffer[32];
int length = sprintf(buffer, "%.17g", v->u.n);
PUTS(c, buffer, length);
}
break;
但这样需要在 PUTS() 中做一次 memcpy(),实际上我们可以避免这次复制,只需要生成的时候直接写进 c 里的推栈,然后再按实际长度调查 c->top:
case LEPT_NUMBER:
{
char* buffer = lept_context_push(c, 32);
int length = sprintf(buffer, "%.17g", v->u.n);
c->top -= 32 - length;
}
break;
简写成:
case LEPT_NUMBER:
c->top -= 32 - sprintf(lept_context_push(c, 32), "%.17g", v->u.n);
break;
生成字符串
static void lept_stringify_string(lept_context* c, const char* s, size_t len) {
size_t i;
assert(s != NULL);
PUTC(c, '"');
for (i = 0; i < len; i++) {
unsigned char ch = (unsigned char)s[i];
switch (ch) {
case '\"': PUTS(c, "\\\"", 2); break;
case '\\': PUTS(c, "\\\\", 2); break;
case '\b': PUTS(c, "\\b", 2); break;
case '\f': PUTS(c, "\\f", 2); break;
case '\n': PUTS(c, "\\n", 2); break;
case '\r': PUTS(c, "\\r", 2); break;
case '\t': PUTS(c, "\\t", 2); break;
default:
if (ch < 0x20) {
char buffer[7];
sprintf(buffer, "\\u%04X", ch);
PUTS(c, buffer, 6);
}
else
PUTC(c, s[i]);
}
}
PUTC(c, '"');}
static void lept_stringify_value(lept_context* c, const lept_value* v) {
switch (v->type) {
/* ... */
case LEPT_STRING: lept_stringify_string(c, v->u.s.s, v->u.s.len); break;
}
}
优化 lept_stringify_string()
上面的 lept_stringify_string() 实现中,每次输出一个字符/字符串,都要调用 lept_context_push()。如果我们使用一些性能剖测工具,也可能会发现这个函数消耗较多 CPU。
static void* lept_context_push(lept_context* c, size_t size) {
void* ret;
assert(size > 0);
if (c->top + size >= c->size) { // (1)
if (c->size == 0)
c->size = LEPT_PARSE_STACK_INIT_SIZE;
while (c->top + size >= c->size)
c->size += c->size >> 1; /* c->size * 1.5 */
c->stack = (char*)realloc(c->stack, c->size);
}
ret = c->stack + c->top; // (2)
c->top += size; // (3)
return ret; // (4)
}
中间最花费时间的,应该会是 (1),需要计算而且作分支检查。即使使用 内联函数去减少函数调用的开销,这个分支也无法避免。所以,一个优化的点子是,预先分配足够的内存,每次加入字符就不用做这个检查了。但多大的内存才足够呢?我们可以看到,每个字符可生成最长的形式是 \u00XX,占 6 个字符,再加上前后两个双引号,也就是共 len * 6 + 2 个输出字符。那么,使用 char* p = lept_context_push() 作一次分配后,便可以用 *p++ = c 去输出字符了。最后,再按实际输出量调整堆栈指针。另一个小优化点,是自行编写十六进位输出,避免了 printf() 内解析格式的开销。
static void lept_stringify_string(lept_context* c, const char* s, size_t len) {
static const char hex_digits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
size_t i, size;
char* head, *p;
assert(s != NULL);
p = head = lept_context_push(c, size = len * 6 + 2); /* "\u00xx..." */
*p++ = '"';
for (i = 0; i < len; i++) {
unsigned char ch = (unsigned char)s[i];
switch (ch) {
case '\"': *p++ = '\\'; *p++ = '\"'; break;
case '\\': *p++ = '\\'; *p++ = '\\'; break;
case '\b': *p++ = '\\'; *p++ = 'b'; break;
case '\f': *p++ = '\\'; *p++ = 'f'; break;
case '\n': *p++ = '\\'; *p++ = 'n'; break;
case '\r': *p++ = '\\'; *p++ = 'r'; break;
case '\t': *p++ = '\\'; *p++ = 't'; break;
default:
if (ch < 0x20) {
*p++ = '\\'; *p++ = 'u'; *p++ = '0'; *p++ = '0';
*p++ = hex_digits[ch >> 4];
*p++ = hex_digits[ch & 15];
}
else
*p++ = s[i];
}
}
*p++ = '"';
c->top -= size - (p - head);
}
要注意的是,很多优化都是有代价的。第一个优化采取空间换时间的策略,对于只含一个字符串的 JSON,很可能会分配多 6 倍内存;但对于正常含多个值的 JSON,多分配的内存可在之后的值所利用,不会造成太多浪费。而第二个优化的缺点,就是有稍增加了一点程序体积。也许有人会问,为什么 hex_digits 不用字符串字面量 “0123456789ABCDEF”?其实是可以的,但这会多浪费 1 个字节(实际因数据对齐可能会浪费 4 个或更多)。
生成数组和对象
生成数组也是非常简单,只要输出 [ 和 ],中间对逐个子值递归调用 lept_stringify_value()。只要注意在第一个元素后才加入 ,。而对象也仅是多了一个键和 :。
static void lept_stringify_value(lept_context* c, const lept_value* v) {
size_t i;
switch (v->type) {
/* ... */
case LEPT_ARRAY:
PUTC(c, '[');
for (i = 0; i < v->u.a.size; i++) {
if (i > 0)
PUTC(c, ',');
lept_stringify_value(c, &v->u.a.e[i]);
}
PUTC(c, ']');
break;
case LEPT_OBJECT:
PUTC(c, '{');
for (i = 0; i < v->u.o.size; i++) {
if (i > 0)
PUTC(c, ',');
lept_stringify_string(c, v->u.o.m[i].k, v->u.o.m[i].klen);
PUTC(c, ':');
lept_stringify_value(c, &v->u.o.m[i].v);
}
PUTC(c, '}');
break;
/* ... */
}}
最终代码
#ifndef LEPT_PARSE_STRINGIFY_INIT_SIZE
#define LEPT_PARSE_STRINGIFY_INIT_SIZE 256
#endif
#define PUTS(c, s, len) memcpy(lept_context_push(c,len),s,len)
static void lept_stringify_string(lept_context* c, const char* s, size_t len){
static const char hex_digits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'A', 'B', 'C', 'D', 'E', 'F' };
size_t i, size;
char* head, *p;
assert(s != NULL);
p = head = lept_context_push(c, size = len * 6 + 2);//每个字符可生成最长的形式是\u00XX,占 6 个字符,再加上前后两个双引号,也就是共 len * 6 + 2 个输出字符
*p++ = '"';
//PUTC(c, '"'); //PUTC 函数每次都会调用lept_context_push函数,这个函数的if(c->top + size >= c->size) 这一句开销比较大,需要计算而且作分支检查。即使使用 C99 的inline 关键字(或使用宏)去减少函数调用的开销,这个分支也无法避免; 写成*p++比较好
for (i = 0; i < len; i++){
unsigned char ch = (unsigned char)s[i];
switch (ch)
{
/*case '\"': PUTC(c, "\\\"", 2); break;
case '\\': PUTC(c, "\\\\", 2); break;
case '\b': PUTC(c, "\\b", 2); break;
case '\f': PUTC(c, "\\f", 2); break;
case '\n': PUTC(c, "\\n", 2); break;
case '\r': PUTC(c, "\\r", 2); break;
case '\t': PUTC(c, "\\t", 2); break;*/
case '\"': *p++ = '\\'; *p++ = '\"'; break;
case '\\': *p++ = '\\'; *p++ = '\\'; break;
case '\b': *p++ = '\\'; *p++ = 'b'; break;
case '\f': *p++ = '\\'; *p++ = 'f'; break;
case '\n': *p++ = '\\'; *p++ = 'n'; break;
case '\r': *p++ = '\\'; *p++ = 'r'; break;
case '\t': *p++ = '\\'; *p++ = 't'; break;
default:
if (ch < 0x20){
//char buffer[7];
//sprintf(buffer, "\\u%04X", ch);//其他少于 0x20 的字符需要转义为 \u00xx 形式。
//PUTS(c, buffer, 0);
*p++ = '\\'; *p++ = 'u'; *p++ = '0'; *p++ = '0';
*p++ = hex_digits[ch >> 4];
*p++ = hex_digits[ch & 15];
}
else
//PUTC(c, s[i]);
*p++ = s[i];
}
}
//PUTC(c, '"');
*p++ = '"';
c->top -= size - (p - head);
}
static int lept_stringify_value(lept_context* c, const lept_value* v){
size_t i;
int ret;
switch (v->type)
{
case LEPT_NULL: PUTS(c, "null", 4); break;
case LEPT_FALSE: PUTS(c, "false", 5); break;
case LEPT_TRUE: PUTS(c, "true", 4); break;
case LEPT_NUMBER:
//{
// char* buffer = lept_context_push(c, 32);
// int length = sprintf(buffer, "%.17g", v->u.n);//sprintf把浮点数转换为文本
// c->top -= 32 - length;
//}
c->top -= 32 - sprintf(lept_context_push(c, 32), "%.17g", v->u.n);
break;
case LEPT_STRING: lept_stringify_string(c, v->u.s.s, v->u.s.len); break;
case LEPT_ARRAY:
PUTC(c, '[');
for (i = 0; i < v->u.a.size; i++){
if (i > 0)
PUTC(c, ',');
lept_stringify_value(c, &v->u.a.e[i]);
}
PUTC(c, ']');
break;
case LEPT_OBJECT:
PUTC(c, '{');
for (i = 0; i < v->u.o.size; i++){
if (i>0)
PUTC(c, ',');
lept_stringify_string(c, v->u.o.m[i].k, v->u.o.m[i].klen);
PUTC(c, ':');
lept_stringify_value(c, &v->u.o.m[i].v);
}
PUTC(c, '}');
break;
default:
assert(0 && "invalid type");
}
}
char* lept_stringify(const lept_value* v, size_t* length)
{
lept_context c;
assert(v != NULL);
c.stack = (char*)malloc(c.size = LEPT_PARSE_STRINGIFY_INIT_SIZE);
c.top = 0;
lept_stringify_value(&c, v);
if (length)
*length = c.top;
PUTC(&c, '\0');
return c.stack;
}