mixi engineer blog

*** 引っ越しました。最新の情報はこちら → https://medium.com/mixi-developers *** ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

Lua on Promenade

ドラクエのプレー時間がついに150時間を突破して妻の視線が痛いmikioです。今回は、かんたんCMS「Tokyo Promenade」にスクリプト言語Luaを組み込んでカスタマイズする方法について述べます。

なぜスクリプト言語処理系を組み込むのか

Tokyo Promenade(TP)はCで書かれていて軽量で高速に実行できるCMSです。PerlやRubyなどのスクリプト言語で書かれたCMSはそのソース自体を編集して改良するのが容易ですが、Cの場合は再コンパイル作業が必要だし下手に手を出すとメモリ破壊などの致命的なバグを入れてしまう可能性が比較的高いので、ソース自体を編集してカスタマイズを行うのは現実的ではありません。

そこで、TPではプレゼンテーション層の機能をできるだけテンプレート側に委譲させるとともに、さらに装飾の多くはCSSを編集するだけで変更できるように配慮しています。テンプレートにはJavaScriptを埋め込むこともできますから、入力パラメータに対して決定的な(つまり入力パラメータとTPのデータベースだけに依存してそれ以外の外部要因に依存しない)情報を表現するのであれば、そのカスタマイズ性は現状で十分だと言えます。

HTMLとCSSとJavaScriptをいじればほとんどのカスタマイズはできるのに、なぜサーバ側でのスクリプティングが必要なのでしょうか。主にJavaScriptの制限に起因する2つの理由があります。ひとつは、クライアント側のJavaScriptだけでは計算結果の保存をクッキーに頼ることになるので、サイズの大きいデータを保存したり、ユーザ間で共有したりするのが難しいということです。もう一つは、クライアント側のJavaScriptだけでは外部のサイト(クロスドメイン)のデータを通信で引っ張ってくることができないということです。

最も単純な例としてアクセスカウンタが挙げられます。これはページが閲覧される度に数値をカウントアップするだけの機能ですが、その値を全ユーザで共有する必要があるので、クライアント側の努力だけで実装するのは困難です。ページが閲覧される度に起動されるプロシージャをサーバ側に仕掛けることができればこの問題は簡単に解決します。

実用的な例だと、記事内にあるアンカーテキストのリンク先で書籍のISBNが書かれている場合に、その書籍のタイトルや表紙画像をAmazonから取得して表示することが考えられます。このように、リソース識別子に関連付けられたメタデータを外部サイトから取得して記事に埋め込んで表示したいというニーズは各種考えられますが、セキュリティ上の理由でクロスドメインの通信を禁じられているクライアント側の努力だけではそれに答えることはできません。

アクセシビリティ重要

Amazonの書籍情報を表示したいだけならば、Amazonが提供する埋め込み用のHTMLスニペットを記事に張り付ければいいと人は言うかもしれません。しかし、それはTPの思想からは外れます。TPの記事の素データはWiki記法で文書の論理構造を記述したものです。デフォルトでたまたまHTMLに変換して表示することができるようになっているだけで、HTMLは表現方法の一形態にすぎません。LaTeXやDocBookやroffを介してプレゼンテーションを行うUAへの対応を今後追加するかもしれませんし、そのような対応を容易にしておくことがTPのアクセシビリティのポリシーなのです。その観点に立つと、Wiki記法にHTMLを混ぜてしまったらHTMLとしてしか表現できないようになってしまうので、許容できません。

URIの普遍性という観点も忘れてはなりません。書籍紹介をする際にAmazonのURLを記事に埋め込むことがよくありますが、Wiki記法のレベルでそれをやるのは望ましくありません。その代わりに、ISBNなどの、より普遍性の高いものをリンク先のURIとして記述して、表示時にAmazonのURLに変換する方がアクセシビリティ(アベイラビリティ)が高いと言えます。仮にAmazonが潰れたり買収されたとしてもISBNは不変なので、表示スクリプトを改修してbk1や紀伊國屋にある同等の書籍紹介ページへ誘導することができるからです。

TPは記事の作者の思考を直列化するための機構です。したがって、言及の意図が「Amazonの書籍紹介ページ」に向けられているのであればAmazonのURLを使うべきですが、そうでなく「書籍の内容」に向けられているのであれば、ISBNなどを使うべきです。アクセシビリティを担保するためにまず第一に重視すべきはURIであり、alt属性やその他のメタデータはその補助にすぎないというのが私の考えです(もちろん、そのURIを解釈できない処理系のためにalt属性などを付随させるのは有用であり、その努力は尊敬されるべきものです)。

実際に組み込んでみる

さて、前置きはほどほどにして、インストール手順に進みましょう。TPにLua拡張を組み込むには、ビルド時に以下のような指定を行います。もちろん、その前提としてLuaをインストールしてあることが必要です。

$ ./configure --enable-lua
$ make
$ sudo make install

そして、テンプレートファイル promenade.tmpl の冒頭にある設定に以下の行を加えます(最新バージョンのデフォルトテンプレートではその行が最初からあります)。

[% CONF scrext "hogehoge.lua" \%]

こうすると、TPの起動時に hogehoge.lua というファイルが読み込まれて、それに記述された関数が各種のトリガに応じて呼び出されるようになります。トリガと関数の仕様は以下のようになっています。該当の関数が実装されていない場合は単に無視されます。

  • _begin() : DBを読み込む前に呼ばれます。引数なし。戻り値はテンプレートのbeginmsgという変数に格納されます。
  • _end() : DBを閉じた後に呼ばれます。引数なし。戻り値はテンプレートのendmsgという変数に格納されます。
  • _procart(wiki) : 各記事の表示データを作る直前に、そのWiki記法データを表示用に加工するために用います。引数は元のwiki記法文字列で、戻り値は加工後のwiki記法文字列です。
  • _procpage(html) : ページ全体のHTMLを表示する直前に、HTMLを表示用に加工するために用います。引数は元のHTML文字列で、戻り値は加工後のHTML文字列です。

また、以下の機能が組み込まれています。

  • _params : CGIスクリプトの呼び出しパラメータがテーブルとして保存されているグローバル変数。
  • _user : ユーザの認証情報(nameとpassとinfo)がテーブルとして保存されているグローバル変数。
  • _strstr(src, pat, repl) : 文字列の単純な検索と置換を行う組み込み関数。srcは対象文字列で、patはマッチングパターンで、replは置換文字列。replが省略された場合は一致の検査のみを行う。
  • _regex(src, pat, repl) : 正規表現による検索と置換を行う組み込み関数。srcは対象文字列で、patはマッチングパターンで、replは置換文字列。replが省略された場合は一致の検査のみを行う。

Luaの標準ライブラリにも文字列検索や文字列置換の機能はあるので_strstrと_regexは不要とも言えますが、標準のものはびっくりするほど使いにくいので、_strstrと_regexは重宝することになると思います。ちなみにそれらはTCのLuaバインディングにもおまけで組み込まれています。

サンプル

ネタでいくつか実装してみましょう。ログインユーザに挨拶文を出すためのスクリプトは以下のようになります。

function _begin()
   local name = "World!!"
   if _user then
      name = _user.name
   end
   return "Hello, " .. name .. "!!"
end

各記事をラムちゃん風にするためのスクリプトは以下のようになります。

function _procart(wiki)
   wiki = _strstr(wiki, "ります。", "るっちゃ。")
   wiki = _strstr(wiki, "です。", "っちゃ。")
   wiki = _strstr(wiki, "だ。", "だっちゃ。")
   return wiki
end

不適切な語を含む記事のタイトルにラベルをつけるスクリプトは以下のようになります。

function _procart(wiki)
   wiki = _strstr(wiki, "Tokyo", "Osaka")
   if _regex(wiki, "*sex") or _regex(wiki, "*fuck") then
      wiki = _regex(wiki, "^#!", "#! [sexual] ")
   end
   return wiki
end

簡易的なアクセスカウンタは以下のようになります(排他制御をしていないので本気利用はできませんが...)。上記3例はJavaScriptでもできますが、この例はLuaならではと言えるでしょう。

tmpfile = "/tmp/promscrcount.txt"

function _begin()
   local file = io.open(tmpfile, "r+")
   local count = 0
   if file then
      count = file:read()
      file:seek("set", 0)
   else
      file = io.open(tmpfile, "w")
   end
   if not count then
      count = 0
   end
   count = count + 1
   file:write(string.format("%d\n\n", count))
   file:close()
   return "The access count is " .. count .. "."
end

まとめ

Tokyo PromenadeにLuaを組み込む方法とその思想的背景について説明しました。クライアント側で完結するカスタマイズはJavaScriptでやった方が簡単だしサーバの負荷も軽くなるのでよいのですが、それがサーバ側の能力が必要な場合に組み込みのLuaで補うというのは合理的な選択だと思います。そして、言及するリソースの種類に応じたプレゼンテーションを人間が書き分けるのではなく、記事にはURIのみを記述して表示時にスクリプトが適切なプレゼンテーションを生成するということの意義についても伝われば幸いです。

TPそれ自体をスクリプト言語で書けばいいじゃんとか、サーバ側のスクリプト言語もJavaScriptでいいじゃんとか、いろいろご意見があると思います。CとLuaの組み合わせが絶対最強だと主張するつもりはありません。でも、自分で使ってみた限りでは、そこそこイケてるんじゃないかと思っています。