js_of_ocamlの導入とenchant.jsの実行

入院のおかげ(せい?)で、予定より遥かに早く貯金が減ってしまったので、転職活動を始めました。ちょうど12月はこういう活動も活発にやられるらしく、忙しい師走になりそうです。別に師じゃないんだけどなぁ。

それは置いておいて、結構前から、話だけは聞いていて興味を持っていたenchant.jsですが、ぶっちゃけ私はJavaScriptについては、基本はとりあえずわかってるつもりですが、現状がどんな風になっているか、とかはまったくわかりません。HTML5とかなんかごった煮にしか見えないし・・・、という程度です。
それでも現実はHTML5 + JavaScript(またはジェネレータになる言語)という流れは、業務システムの内部側でもない限り、いずれは避けられない流れでしょう、ということで、おとなしくenchant.jsを勉強するついでに、JavaScriptも学びます。今ならわざわざJavaScriptを学ばなくても、HaxeとかTypeScriptを学んだ方がいいような気がするのはとりあえず置いておきます。

・・・なんですが、いかんせんJavaScriptについてネガティブな感情しかわいてきません。なんかないかなー、と思った時に、OCamlからJavaScriptの生成ができるjs_of_ocamlを思いだしました。
というわけで、長い前置きになりましたが、js_of_ocamlの導入と、そこからenchant.jsを動作させた手順を記録として残しておきます。失敗とかエラーとかの記録もありますので、大分長いですがご了承願います。
なお、js_of_ocamlの導入とか動作については、↓の記事を盛大に参考にさせていただきました。
ウェブブラウザで関数型プログラミング! js_of_ocaml

js_of_ocamlの導入

まずはjs_of_ocamlの導入です。今回導入するバージョンは1.2です。インストールに必要なライブラリは、参考記事にもありますが、

  • Findlib
  • Lwt (Version 2.3.0 以降)

私の環境ではLwtが入っていなかったため、Lwtも導入します。OASISにあるようですので、そちらから入れてもいいとは思いますが、とりあえず手動で入れます。Lwtのバージョンは最新の2.4.2で、必要なライブラリは以下です。

ついでにreactも入ってなかったので入れます。入れなくてもビルドは成功するので必要なのかどうかのかよくわかりませんが、ここはreadmeに従います。

cd react-0.9.4
ocaml setup.ml -configure
ocaml setup.ml -build
sudo ocaml setup.ml -install

cd ../lwt-2.4.2
ocaml setup.ml -configure
ocaml setup.ml -build
sudo ocaml setup.ml -install
sudo ocaml setup.ml -uninstall # アンインストールする場合

次にjs_of_ocamlを入れます。Makefile.confを編集することでインストール先とかを変える形式です。デフォルトではインストール先は/usr/local/binになっています。あくまで実行バイナリのインストール先で、ライブラリとかはすべてfindlibの管轄下に置かれます。

cd js_of_ocaml-1.2
make
sudo make install

これで導入できました。動くかどうかは、js_of_ocamlのexampleディレクトリでmakeしてから、index.htmlにアクセスすれば確認できます。いつも使っているブラウザで動くかと思いますが、w3mに入れてもJavaScriptは動きません。
Gentoo amd64上のfirefox(17.0)ではすべて動作しましたが、chromium (23.0.1271.91)では一部しか動作しませんでした。この辺はなんでなのかは特に調べていません。

ここで、参考記事にある通りに、examlpleディレクトリのソースをコピーして、自前でビルドしてみます。

cd js_of_ocaml-1.2
cp -r example/cubes ~/develop/ocaml/
cd ~/develop/ocaml/cubes
rm *cmx *js *cmi index.html *byte
ocamlfind ocamlc -syntax camlp4o -package lwt,js_of_ocaml.syntax -g -c cubes.ml
ocamlfind ocamlc -package lwt,js_of_ocaml -linkpkg -o cubes.byte cubes.cmo
js_of_ocaml cubes.byte
Missing primitives:
  caml_ml_output_char
  caml_sys_exit

なんか出ました。このMissing primitives:〜とかいうエラーについては、examplesをコンパイルする際にも出ていましたので、特に問題は無いと思われます。ここでちゃんと開ければ問題無いです。

enchant.jsの導入

とりあえず導入自体は上手くいったようですので、続いてenchant.jsを導入してみます。
enchant.jsは公式サイトから入手できます。
http://enchantjs.com/
ダウンロードしたら、zip形式で圧縮されているので、unzipします。使うだけなら、enchant.jsだけがあれば動くようですので、とりあえずreadmeに書いてあったサンプルを実行してみます。

<script src='./enchant.js'></script>
<script>
    enchant();
    window.onload = function(){
        var game = new Game(320, 320);

        var label = new Label('Hello, enchant.js!');
        game.rootScene.addChild(label);

        game.start();
    }
</script>

htmlとしては正しくないんじゃまいかと思ったりしましたが、とりあえずこれsample.htmlとして、enchant.jsと同じディレクトリに置いてを実行してみると、"Hello, enchant.js!"と書かれた画像?がぽつんと出てきます。どうやらちゃんと動いているようです。

js_of_ocamlでenchant.jsを動かす

とりあえず、↑のシンプルすぎるやつを一応まともなHTMLにしておきます。

<html>
  <head>
    <script src='./enchant.js'></script>
    <script>
        enchant();
        window.onload = function(){
            var game = new Game(320, 320);

            var label = new Label('Hello, enchant.js!');
            game.rootScene.addChild(label);

            game.start();
        }
    </script>
  </head>
  <body>
  </body>
</html>

これが動くことを確認したら、二つめのscriptタグを、sample.jsを読み込むようにします。ui.enchant.jsも読み込んでいるのは、次節のsample.jsで必須だからです。これに気付くまでに結構かかりました・・・。
また、ui.enchant.jsは、enchant.js/images/ ディレクトリのpad.png, apad.png, icon0.png, font0.pngを使いますので、同じディレクトリにコピーしておきます。

<html>
  <head>
    <script src='./enchant.js'></script>
    <script src='./plugins/ui.enchant.js'></script>
    <script src='./sample.js'></script>
  </head>
  <body>
  </body>
</html>
js_of_ocamlJavaScriptを書く

js_of_ocamlでsample.jsを記述していきます。ここで、sample.jsの中身として、enchant.jsにあるサンプルを利用させてもらいます。
http://enchantjs.com/ja/sample.html
とりあえず記述してみます。参考記事に従って、Unsafe.eval_stringで単純に文字列をJavaScriptとして評価します。↓をsample.mlとして保存し、js_of_ocamlJavaScriptに変換します。

open Js
let _ = Unsafe.eval_string "
enchant();

window.onload = function() {
enchant();

window.onload = function() {
    var game = new Game(320, 320);
    game.fps = 24;
    game.preload('chara1.png');
    // The images used in the game should be preloaded

    game.onload = function() {
        var bear = new Sprite(32, 32);
        bear.x = 8;
        bear.y = 8;
        bear.image = game.assets['chara1.png'];

        bear.addEventListener('enterframe', function() {
            // check input (from key or pad) on every frame
            if (game.input.right) {
                bear.x += 2;
            }
            if (game.input.left) {
                bear.x -= 2;
            }

            if (game.input.up) {
                bear.y -= 2;
            }
            if (game.input.down) {
                bear.y += 2;
            }
        });

        // add bear to rootScene (default scene)
        game.rootScene.addChild(bear);

        // display d-pad
        var pad = new Pad();
        pad.x = 0;
        pad.y = 224;
        game.rootScene.addChild(pad);
        game.rootScene.backgroundColor = '#ffffff';
    };
    game.start();
};
"

中で使われているchara1.pngは、enchant.jsのzipファイルを解凍した中にあるimages/chara1.pngを使わせてもらいます。chara1.pngを、↑のsample.mlと同じディレクトリに格納しておきます。そして、ブラウザでsample.htmlを開いてみます。
すると、熊の絵と画面の下の方に方向キーがあるはずです。この熊の絵は方向キーで動かせますし、画面上のパッドをマウスでクリックしても動きます。

関数に分けてみる

JavaScript中で、addEventListenerに渡されている無名関数を分離してみます。

let moveBear = Unsafe.eval_string "
    // check input (from key or pad) on every frame
    if (game.input.right) {
        bear.x += 2;
    }
    if (game.input.left) {
        bear.x -= 2;
    }

    if (game.input.up) {
        bear.y -= 2;
    }
    if (game.input.down) {
        bear.y += 2;
    }
"

let _ = Unsafe.eval_string "
enchant();

window.onload = function() {
    var game = new Game(320, 320);
    game.fps = 24;
    game.preload('chara1.png');
    // The images used in the game should be preloaded

    game.onload = function() {
        var bear = new Sprite(32, 32);
        bear.x = 8;
        bear.y = 8;
        bear.image = game.assets['chara1.png'];
";
  Unsafe.meth_call (Unsafe.variable "bear") "addEventListener" [|Unsafe.inject moveBear|];
  Unsafe.eval_string "
        bear.addEventListener('enterframe', );

        // add bear to rootScene (default scene)
        game.rootScene.addChild(bear);

        // display d-pad
        var pad = new Pad();
        pad.x = 0;
        pad.y = 224;
        game.rootScene.addChild(pad);
        game.rootScene.backgroundColor = '#ffffff';
    };
    game.start();
};
"

これはコンパイル自体は通りますが、動きません。まぁ、二つのeval_stringが別々の関数となっているとすると、上手くいかないのは当然みたいなものですね。こいつを動くように頑張ってみます。

まずは、game.onloadに入れられていたものを再度切り出します。

let game_onload = Unsafe.eval_string "
         var bear = new Sprite(32, 32);
        bear.x = 8;
        bear.y = 8;
        bear.image = game.assets['chara1.png'];

        bear.addEventListener('enterframe', function() {
            // check input (from key or pad) on every frame
            if (game.input.right) {
                bear.x += 2;
            }
            if (game.input.left) {
                bear.x -= 2;
            }

            if (game.input.up) {
                bear.y -= 2;
            }
            if (game.input.down) {
                bear.y += 2;
            }
        });

        // add bear to rootScene (default scene)
        game.rootScene.addChild(bear);
        game.rootScene.backgroundColor = '#ffffff';
 "

ここで問題になるのは、この中で当たり前のように使用されているgameと、Spriteでしょうか。後が大変になるので、Padはとりあえず削りました。ここでは、とりあえず変数bearに入れられているSpriteクラスのインターフェースを作ってみます。実際作ってみると↓のようになります。

class type sprite = object
  method x : js_int t prop
  method y : js_int t prop
  method image : 'a t prop
  method addEventListener: js_string t -> (unit -> unit) -> unit meth
  method _Sprite: (js_int t -> js_int t) constr readonly_prop
end

この宣言に対する細かい内容は、参考記事の方を参照していただければと思います。上の関数では、x, y, imageが変更可能なプロパティ、addEventListenerがメソッド、Spriteというコンストラクタを持つので、それをそのままインターフェースにしただけです。
さて、これでgame_onloadの中身を変更できそうです。定義したspriteを使うと次のようになります。

let game_onload =
  let bear = jsnew (sprite##_Sprite) (Js.int 32, Js.int 32) in
  bear##x <- 8;
  bear##y <- 8;
  bear##image <- (* game.assets['chara1.png'] *);
  bear##addEventListener (Js.string "enterframe", move_bear);

まだ続いていますが、それ以降はさらに追記しないとならないので・・・。さて、これだけではまだまだ動作しませんので、さらにインターフェースを追加していきますが、試行錯誤を書くと埒が開かなくなるので、やったことを箇条書きにして、その後に結果のソースを貼っていきます。
この後は、

  • Gameクラスのclass typeを作る
  • inputのclass typeを作る
  • rootSceneのclass typeを作る
  • enchantの関数は、グローバルな空間にぶちまけられているけれど、window.enchantにも束縛されているので、そっちをトップレベルに束縛しておく。enchantのclass typeも作っておく

で、結果はこんなかんじになります。実際に動作することを確認しているソースです。

open Js

class type _sprite = object
  method x : int prop
  method y : int prop
  method image : 'a t prop
  method addEventListener: js_string t -> (unit -> unit) -> unit meth
end

class type i = object
  method right: bool t readonly_prop
  method left: bool t readonly_prop
  method up: bool t readonly_prop
  method down: bool t readonly_prop
end

class type s = object
  method addChild:'a t -> unit meth
  method backgroundColor: js_string t prop
end

class type g = object
  method start: unit meth
  method fps: int prop
  method input: i t readonly_prop
  method rootScene: s t readonly_prop
  method preload: js_string t -> unit meth
  method onload: ('a, unit -> unit) meth_callback prop
end

class type e = object
  method _Sprite: (int -> int -> _sprite t) constr readonly_prop
  method _Game: (int -> int -> g t) constr readonly_prop
end

let enchant: e t = Unsafe.variable "enchant"

let move_bear game bear () =
  if game##input##right then bear##x <- bear##x + 2;

  if game##input##left then bear##x <- bear##x - 2;

  if game##input##up then bear##y <- bear##y - 2;

  if game##input##down then bear##y <- bear##y + 2;
;;

let game_onload game _ =
  let bear = jsnew (enchant##_Sprite) (32, 32) in
  bear##x <- 8;
  bear##y <- 8;
  let assets = Unsafe.get game (Js.string "assets") in
  bear##image <- Unsafe.get assets (Js.string "chara1.png");

  bear##addEventListener (Js.string "enterframe", move_bear game bear);

  game##rootScene##addChild (bear);
  game##rootScene##backgroundColor <- Js.string "#ffffff"

let onload _ =
  let game = jsnew (enchant##_Game) (320, 320) in
  game##fps <- 24;
  game##preload (Js.string "chara1.png");
  game##onload <- wrap_meth_callback game_onload;
  game##start ();
  _false

let _ =
  ignore (Unsafe.eval_string "enchant();");
  Dom_html.window##onload <- Dom_html.handler onload

参考記事になかった情報としては、「コールバックはどう登録するか」ということと、「ハッシュはどうやって取得するのか」という部分になります。これがまた結構苦労できたので、追加で解説を入れさせて頂きます

コールバックの書き方

JavaScriptも関数がファーストクラスオブジェクトなので、コールバックとして入れたりなんだかんだする機会が多いですね。イベントドリブンだと特にそんな感じがします。
js_of_ocamlでは、

type ('a, 'b -> 'c) meth_callback
val wrap_meth_callback: ('a -> 'b -> 'c) -> ('a, 'b -> 'c) meth_callback

という形で、コールバックを表す型が用意されています。このコールバックの型は、「'aをthisオブジェクトとして、'a -> 'b -> 'cというシグネチャの関数をコールバックする」というものです。
つまり、'b -> 'cで表されている部分は、thisの分を除いたシグネチャになっている、というものです。色々試したところ、wrap_meth_callbackは、そのプロパティが定義されているオブジェクトをthisとして渡すようなコールバックオブジェクトを想定しているものになります。

js_of_ocamlでハッシュからデータを取得する

開発版では、{:〜:}という拡張構文でハッシュリテラルが扱えるらしいですが、それはそうとしてハッシュをどうやって取得するのか、というものが、APIを眺めていてもよくわかりませんでした。
私の微かなJavaScriptの記憶では、ハッシュリテラルはつまるところオブジェクトのプロパティと同義だ、という記憶があります。ですが、今回のは動的に追加されるプロパティであり、js_of_ocamlで扱えるかどうかはよくわかりませんが、多分できないと思います。
ここで、Unsafe.getが活躍します。Unsafe.getは、あるオブジェクトのあるプロパティを取得するための関数です。↑のソースでの該当部分を抽出してみます。

let assets = Unsafe.get game (Js.string "assets") in
bear##image <- Unsafe.get assets (Js.string "chara1.png");

今回問題になったのはgame.assetsですが、まずgameオブジェクトからassetsを取得するためにUnsafe.getを使い、そのassetsからchara1.pngのプロパティ(実際には、ハッシュで登録されたもの)を取得する、という形になります。ハッシュを扱うもっと洗練された方法が用意されているような気もするのですが、ひとまずこれで動きます。

終わりに

本当はこれをベースにして、enchant.jsのサンプルを動かしていきたかったのですが、これを動作させられるような形にもっていくだけですげー時間がかかりました。ただ、最終的な形を見ると、確かにわりかしすっきりとした形になっているような気がします。まぁ、このサンプルはほとんどオブジェクトの呼び出しだけで終わっているようなものなので、本当にJavaScriptをjs_of_ocamlの形にしただけではありますが。

サンプルだけとはいえ、enchant.jsを初めて触ってみましたが、基本的な部分はシンプルにまとまっていると思いました。それほど難解なものでもないので、なんか作ってみたい、という場合にさっくり作ってみる、ということができそうな感じがします。それをjs_of_ocamlでやるのは、なかなか苦労するような気がしますが。

SDLOpenGLバインディングも作っていますが、フレームワークバインディング作りは(個人的に)大変です。物好きな方が作ってくれれば、OCamlがもっと盛り上がるかもしれません。・・・素直にHaxeとかで書いた方が楽な気がするのですけど。
とはいえ、OCamlの型安全性を甘受しつつJavaScriptを構築できるのは、私のような動的型言語があんまり合わない人間にはかなりビビッとくるものです。Haxeとかが最近盛り上がっているのも、そういう部分があるんでしょうねー。