2012/08/22

C++ だと picojson ばかり使っていて、JSON使うからC++かなという言語の選び方をしていたりしたのですが、今日matsuuさんのブクマ経由で見つけた。

jsmn

The minimalistic portable framework for parsing JSON data format.

http://bitbucket.org/zserge/jsmn
特徴としては
  • C98コンパチブル
  • 動的なメモリアロケーションを行わない
  • 可能な限り最小のオーバーヘッド
  • JSONのパースは1パス
  • libc も含め依存物がない
  • MITライセンス下で配布され、プロプライエタリなプロジェクトでも使える
  • 単純で美しいデザイン
といったところ。C言語で使える JSON パーサは実は以前にも json-c を試した事はあったのですが、あれは .c の拡張子ファイルを C++ 指定でビルドしているだけというゴニョゴニョなあれなので使い物になりませんでした。
そんな中で見つけたこれは、実にシンプルで応用性がありそうなので記事にしたいと思います。ただし使用上の注意があるので使う人は要検討です。
まずこの 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, 0sizeof 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, 0sizeof 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, 0sizeof 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, 0sizeof 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, 0sizeof 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_PRIMITIVE1
tokens[6]JSMN_PRIMITIVEtrue
上記の様に、トークンは各識別毎に作られます。オブジェクトのキーおよび値もそれぞれのトークンとして格納されます。
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 == 0return 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, 0sizeof(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, 0sizeof(tweet) * count);

  for (i = 0; i < count; i++) {
    int k1, k2;
    k1 = tokens[off++].size;
    for (j = 0; j < k1 / 2; j++) {
      memset(buf, 0sizeof buf);
      strncpy(buf, js + tokens[off].start, tokens[off].end - tokens[off].start);
      off++;
      if (!strcmp(buf, "text")) {
        memset(buf, 0sizeof 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, 0sizeof buf);
          strncpy(buf, js + tokens[off].start, tokens[off].end - tokens[off].start);
          off++;
          if (!strcmp(buf, "screen_name")) {
            memset(buf, 0sizeof 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 リテラルがサポートされればもう少し使い道が出てくるかもしれません。
Posted at 01:53 | WriteBacks () | Edit
Edit this entry...

wikieditish message: Ready to edit this entry.






















A quick preview will be rendered here when you click "Preview" button.