什麼是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;
}