jsmn特徴としては
The minimalistic portable framework for parsing JSON data format.
http://bitbucket.org/zserge/jsmn
- C98コンパチブル
- 動的なメモリアロケーションを行わない
- 可能な限り最小のオーバーヘッド
- JSONのパースは1パス
- libc も含め依存物がない
- MITライセンス下で配布され、プロプライエタリなプロジェクトでも使える
- 単純で美しいデザイン
そんな中で見つけたこれは、実にシンプルで応用性がありそうなので記事にしたいと思います。ただし使用上の注意があるので使う人は要検討です。
まずこの jsmn が提供する物はパーサのみ。しかもパース結果から得られるのは各トークンの位置と種別、データとして扱う際の開始終了位置のみです。えっそんなので使い物になるの?と思うかもしれませんが、実は用途によっては十分だったりもします。以下に例を示します。
#include <assert.h>
#include <string.h>
#include <jsmn.h>
#define countof(x) (sizeof(x)/sizeof(x[0]))
int
main() {
jsmn_parser p;
jsmntok_t tokens[10] = {0};
char buf[256];
const char* js = "{\"foo\": \"bar\", \"baz\": [1,true]}";
int r;
jsmn_init(&p);
r = jsmn_parse(&p, js, tokens, countof(tokens));
assert(r == JSMN_SUCCESS);
/* 全体の型はOBJECT */
assert(tokens[0].type == JSMN_OBJECT);
/* 中のトークン数は4 */
assert(tokens[0].size == 4);
/* 1つ目の型はSTRING */
assert(tokens[1].type == JSMN_STRING);
/* 1つ目の値は"foo" */
memset(buf, 0, sizeof buf);
strncpy(buf, js + tokens[1].start, tokens[1].end - tokens[1].start);
assert(!strcmp(buf, "foo"));
/* 2つ目の型はSTRING */
assert(tokens[2].type == JSMN_STRING);
/* 2つ目の値は"bar" */
memset(buf, 0, sizeof buf);
strncpy(buf, js + tokens[2].start, tokens[2].end - tokens[2].start);
assert(!strcmp(buf, "bar"));
/* 3つ目の型はSTRING */
assert(tokens[3].type == JSMN_STRING);
/* 3つ目の値は"baz" */
memset(buf, 0, sizeof buf);
strncpy(buf, js + tokens[3].start, tokens[3].end - tokens[3].start);
assert(!strcmp(buf, "baz"));
/* 4つ目の型はARRAY */
assert(tokens[4].type == JSMN_ARRAY);
/* 4つ目のARRAYのトークン数は2 */
assert(tokens[4].size == 2);
/* 5つ目の型はPRIMITIVE */
assert(tokens[5].type == JSMN_PRIMITIVE);
/* 5つ目の値は"1" */
memset(buf, 0, sizeof buf);
strncpy(buf, js + tokens[5].start, tokens[5].end - tokens[5].start);
assert(atoi(buf) == 1);
/* 6つ目の型はPRIMITIVE */
assert(tokens[6].type == JSMN_PRIMITIVE);
/* 6つ目の値は"true" */
memset(buf, 0, sizeof buf);
strncpy(buf, js + tokens[6].start, tokens[6].end - tokens[6].start);
assert(!strcmp(buf, "true"));
}
例を追って説明します。まず jsmn_parser を jsmn_init で初期化します。実際には構造体メンバに初期値を代入しているに過ぎません。初期化した jsmn_parser を jsmn_parse 引数に渡して JSON 文字列をパースします。この時、トークン数を渡す必要があります。このトークンは動的に確保されません。基本的に jsmn は内部でメモリを動的確保しません。これについての解決方法は後で説明します。
パースされた結果はトークンの配列となります。そしてこのトークンは以下の内容で構成されます。
- 型
- 値を示す JSON 文字列の開始位置
- 値を示す JSON 文字列の終了位置
- 値が配列の場合のアイテム個数
- 値がオブジェクトの場合のキーおよび値の個数
{"foo": "bar", "baz": [1,true]}
この JSON をパースした場合、トークンは
トークン | 型 | 値 |
---|---|---|
tokens[0] | JSMN_OBJECT | オブジェクト |
tokens[1] | JSMN_STRING | "foo" |
tokens[2] | JSMN_STRING | "bar" |
tokens[3] | JSMN_STRING | "baz" |
tokens[4] | JSMN_ARRAY | 配列 |
tokens[5] | JSMN_PRIMITIVE | 1 |
tokens[6] | JSMN_PRIMITIVE | true |
jsmn_parse はトークンの量が不足している場合、エラー JSMN_ERROR_NOMEM を返します。例えば、どれだけの量のトークンが JSON 文字列として与えられるか分からない場合、トークンのサイズを広げる必要があります。この場合、jsmn ではパーサを再初期化する事なしに、トークンを広げて再度 jsmn_parse を実行する事でパースを続行出来る様になっています。
ただしどれだけの量が不足していたかは分からないので、適度な増減を考慮する必要があります。
今日は試しに twitter のパブリックタイムラインをパースしてみました。
#include <assert.h>
#include <string.h>
#include <memory.h>
#include <curl/curl.h>
#include <jsmn.h>
#define countof(x) (sizeof(x)/sizeof(x[0]))
typedef struct {
char* data; // response data from server
size_t size; // response size of data
} MEMFILE;
MEMFILE*
memfopen() {
MEMFILE* mf = (MEMFILE*) malloc(sizeof(MEMFILE));
if (mf) {
mf->data = NULL;
mf->size = 0;
}
return mf;
}
void
memfclose(MEMFILE* mf) {
if (mf->data) free(mf->data);
free(mf);
}
size_t
memfwrite(char* ptr, size_t size, size_t nmemb, void* stream) {
MEMFILE* mf = (MEMFILE*) stream;
int block = size * nmemb;
if (!mf) return block; // through
if (!mf->data)
mf->data = (char*) malloc(block);
else
mf->data = (char*) realloc(mf->data, mf->size + block);
if (mf->data) {
memcpy(mf->data + mf->size, ptr, block);
mf->size += block;
}
return block;
}
char*
memfstrdup(MEMFILE* mf) {
char* buf;
if (mf->size == 0) return NULL;
buf = (char*) malloc(mf->size + 1);
memcpy(buf, mf->data, mf->size);
buf[mf->size] = 0;
return buf;
}
int
skip(jsmntok_t* tokens, int off) {
jsmntype_t t = tokens[off].type;
if (t == JSMN_ARRAY || t == JSMN_OBJECT) {
int n, l = tokens[off++].size;
for (n = 0; n < l; n++)
off = skip(tokens, off);
} else
off++;
return off;
}
typedef struct {
char* screen_name;
char* text;
} tweet;
int
main() {
jsmn_parser p;
jsmntok_t *tokens;
size_t len;
char buf[1024];
CURL* curl;
MEMFILE* mf = NULL;
char* js = NULL;
int i, j, k, count, off;
tweet* tweets = NULL;
mf = memfopen();
curl = curl_easy_init();
curl_easy_setopt(curl, CURLOPT_URL, "http://api.twitter.com/1/statuses/public_timeline.json");
curl_easy_setopt(curl, CURLOPT_WRITEDATA, mf);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, memfwrite);
curl_easy_perform(curl);
curl_easy_cleanup(curl);
js = memfstrdup(mf);
memfclose(mf);
jsmn_init(&p);
len = 5;
tokens = malloc(sizeof(jsmntok_t) * len);
if (tokens == NULL) {
perror("malloc");
goto leave;
}
memset(tokens, 0, sizeof(jsmntok_t) * len);
while (1) {
int r = jsmn_parse(&p, js, tokens, len);
if (r == JSMN_SUCCESS)
break;
assert(r == JSMN_ERROR_NOMEM);
len *= 2;
tokens = realloc(tokens, sizeof(jsmntok_t) * len);
if (tokens == NULL) {
perror("malloc");
goto leave;
}
}
off = 0;
count = tokens[off++].size;
tweets = malloc(sizeof(tweet) * count);
memset(tweets, 0, sizeof(tweet) * count);
for (i = 0; i < count; i++) {
int k1, k2;
k1 = tokens[off++].size;
for (j = 0; j < k1 / 2; j++) {
memset(buf, 0, sizeof buf);
strncpy(buf, js + tokens[off].start, tokens[off].end - tokens[off].start);
off++;
if (!strcmp(buf, "text")) {
memset(buf, 0, sizeof buf);
strncpy(buf, js + tokens[off].start, tokens[off].end - tokens[off].start);
tweets[i].text = strdup(buf);
off++;
} else if (!strcmp(buf, "user")) {
k2 = tokens[off].size;
off++;
for (k = 0; k < k2 / 2; k++) {
memset(buf, 0, sizeof buf);
strncpy(buf, js + tokens[off].start, tokens[off].end - tokens[off].start);
off++;
if (!strcmp(buf, "screen_name")) {
memset(buf, 0, sizeof buf);
strncpy(buf, js + tokens[off].start, tokens[off].end - tokens[off].start);
tweets[i].screen_name = strdup(buf);
off++;
} else
off = skip(tokens, off);
}
} else
off = skip(tokens, off);
}
}
for (i = 0; i < count; i++) {
printf("%s: %s\n", tweets[i].screen_name, tweets[i].text);
}
for (i = 0; i < count; i++) {
free(tweets[i].screen_name);
free(tweets[i].text);
}
free(tweets);
leave:
if (js) free(js);
if (tokens) free(tokens);
}
出力結果から分かる通り、実は jsmn はまだ \uXXXX
という文字列リテラルをパース出来ません。よって twitter のタイムライン上に流れるユニコード文字列は全てエスケープされたまま表示されます。ちょっと癖があるのでフラットな JSON であれば十分役立つのですが、twitter のタイムラインの様にオブジェクト構造がネストされた物になると扱う側が常にメモリを意識しなければならないので途端に難易度があがります。
例えばキーと値がベタに決まっていて、ネストも無いような JSON であれば少しは使い道はあるかもしれませんね。
\uXXXX
リテラルがサポートされればもう少し使い道が出てくるかもしれません。