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 by



2012/08/15


MojoliciousとCPANモジュールで作る「Nopaste」チュートリアル - ゆーすけべー日記
http://yusukebe.com/archives/20120627/181253.html
yusukebe さんが Mojolicious だったので僕は Amon2::Lite でやってみた。
おんなじ調子でやってもしょうが無いので、ちょっとキャラを変えてやってみる。

まず amon2-setup で外枠を作りやがれ。
Nopaste くらいならフレーバーは Lite で十分だ。お前の作る物なんか Lite で十分だって事だ。ヒャッハーーー!
$ amon2-setup --flavor=Lite nopaste
-- Running flavor: Lite --
[Flavor::Lite] writing app.psgi
[Flavor::Lite] writing Makefile.PL
[Flavor::Lite] writing t/Util.pm
[Flavor::Lite] writing t/01_root.t
[Flavor::Lite] writing xt/03_pod.t

$ cd nopaste
ディレクトリ構造はこんな感じだ。見たところでお前なんかに分かるのか?フハハハハハ!
|   app.psgi
|   Makefile.PL
|   
+---t
|       01_root.t
|       Util.pm
|       
`---xt
        03_pod.t
        
ちっちぇなー。お前みたいだぜ!ちょっと中身見せてみろよ!!
use strict;
use warnings;
use utf8;
use File::Spec;
use File::Basename;
use lib File::Spec->catdir(dirname(__FILE__), 'extlib''lib''perl5');
use lib File::Spec->catdir(dirname(__FILE__), 'lib');
use Plack::Builder;
use Amon2::Lite;

# put your configuration here
sub config {
    +{
    }
}

get '/' => sub {
    my $c = shift;
    return $c->render('index.tt');
};

# for your security
__PACKAGE__->add_trigger(
    AFTER_DISPATCH => sub {
        my ( $c$res ) = @_;
        $res->header'X-Content-Type-Options' => 'nosniff' );
        $res->header'X-Frame-Options' => 'DENY' );
    },
);

# load plugins
__PACKAGE__->load_plugins(
    'Web::CSRFDefender',
);

builder {
    enable 'Plack::Middleware::Static',
        path => qr{^(?:/static/|/robot\.txt$|/favicon.ico$)},
        root => File::Spec->catdir(dirname(__FILE__));
    enable 'Plack::Middleware::ReverseProxy';
    enable 'Plack::Middleware::Session';

    __PACKAGE__->to_app();
};

__DATA__

@@ index.tt
<!doctype html>
<html>
<head>
    <met charst="utf-8">
    <title>nopaste</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
    nopaste
</body>
</html>
意外としっかりしてやがるじゃねぇか!ヒャッハーーーー!
タイトルと本文を入力させて、DBに格納しつつエントリを表示する様な物に仕立て上げてやるぜぇぇぇ!
テメエら行くぞ!ブロロロロロ・・・

項目が決まったら、さっさと入力画面を作りやがれ!

ちゃんと見ろよ! __DATA__ 部に index.tt ってあるだろ!直せ、直すんだよ!
<!doctype html>
<html>
<head>
    <met charst="utf-8">
    <title>nopaste</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
<style type="text/css">
#title { width400px}
#content { width400pxheight400px}
</style>
</head>
<body>
  <form action="/" method="post">
  <label for="title">Title:</label><br />
  <input id="title" name="title" type="text" value="[% title|escape %]" /><br />
  <label for="content">Content:</label><br />
  <textarea id="content" name="content">[% content|escape %]</textarea><br />
  <input type="submit">
  </form>
</body>
</html>
ちょっと待ってろ見てやっからよぉ!
$ plackup ブラウザ Chrome かぁ?お前なんかには高機能すぎるだろ!
http://localhost:5000 を開いて今のうちにデザインでも弄っとくんだな!ヒーーーヒィーーー!
nopaste1
何ボケっとしてんだよ!POST されたらDBに入れるだろ!
config だよ!app.psgi の上見ろよ上!
__PACKAGE__->load_plugins('DBI');

# put your configuration here
sub config {
    +{
        'DBI' => [
            "dbi:SQLite:dbname=nopaste.db",
            '',
            '',
            +{
                sqlite_unicode => 1,
            }
        ],
    }
}
Amon2 ではこの load_plugins というおまじないだけで $c というコンテキストに dbh というハンドラが出来上がります。
オイッ!お前 SQL 書けるんだってな!ちょっと書いてみろよ!
create table nopaste (
  id integer not null primary key,
  title string,
  content string
);
書けるじゃねぇか。最初っから言えよ!
$ sqlit3 nopaste.db < nopaste.sql さてと!お前の脳みそに title と content をブチ込んでやるぜ!ヒャーーーーッハーーー!
post '/' => sub {
    my $c = shift;
    my $title = $c->req->param('title');
    my $content = $c->req->param('content');
    unless ($title && $content) {
        $c->redierct('/');
        return;
    }
    $c->dbh->do(q{INSERT INTO nopaste (title, content) VALUES (?, ?)}, {},
        $title$content);
    my $id = $c->dbh->last_insert_id(undefundefundefundef);
    return $c->redirect("/paste/" . $id);
};
タイトルが空だったら問答無用で / にリダイレクトしてやるぜ!ヒャーーーッハーー!
入力データを nopaste テーブルにインサートし、last_insert_id から得た ID へリダイレクトします。
本当は Data::GUID 等を使って ID 自身が皆に分からない様にすべきですが、ここでは割愛します。
お前にちょっと見せ場作ってやるぜ!nopaste のエントリページでな!
index.tt の下に entry.tt を追加します。
@@ entry.tt
<!doctype html>
<html>
<head>
    <met charst="utf-8">
    <title>nopaste - [% title|html %]</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link media="all" rel="stylesheet" href="http://google-code-prettify.googlecode.com/svn/trunk/src/prettify.css" type="text/css" />
    <script src="http://google-code-prettify.googlecode.com/svn/trunk/src/prettify.js"></script>
</head>
<body onload="prettyPrint()">
  <h3 id="title">[% title|html %]</h3>
  <pre id="content" class="prettyprint"><code>[% content|html %]</code></pre><br />
  <a href="/">back</a>
</body>
</html>
title と content を html フィルタで表示します。シンタックスハイライトは google code pretty を使います。

お前に最後の仕事をくれてやるぜ。ちょっとDBからエントリ持ってきな! get '/paste/{id}' => sub {
    my ($c$args) = @_;
    my $id = $args->{id};
    unless ($id) {
        $c->redierct('/');
        return;
    }
    my ($title$content) = $c->dbh->selectrow_array(q{
        SELECT title, content FROM nopaste WHERE id = ? LIMIT 1
    }, {}, $id);
    return $c->render('entry.tt', {
        title => $title,
        content => $content,
    });
};
やりゃぁ出切るじゃねぇか!ハァ!?簡単だっただぁ?黙ってろ、クソが!

nopaste2

ねっ簡単でしょ?
Posted at by



2012/08/12


KoRoNさんから贈り物届いた。
spartan-vim1

spartan-vim2
TシャツとスパルタンVim 2.0を頂きました。ありがとうございます!
予想だにしなかった内容にガクブルです。
Posted at by