SpiderMonkeyを使ってPHPでサーバーサイドJavaScript

はじめまして。2009年に新卒で入社しました天野祐介です。amachang を期待された方はゴメンナサイ!

先日 SpiderMonkey を利用して PHP から JavaScript を実行する方法を調べる機会がありましたので、ご紹介します。

SpiderMonkey とは

SpiderMonkey は  C で実装された Mozilla の JavaScript エンジンです。 これを PHP から実行する拡張を利用すると、 PHP コード内で JavaScript が実行できます。

SpiderMonkey extension のインストール

こちらhttp://devzone.zend.com/article/4704に記載されている方法で CentOS にインストールしてみました。
PHP 5.3.0 以上が必要です。

$ wget http://ftp.mozilla.org/pub/mozilla.org/js/js-1.7.0.tar.gz
$ tar -xzvf js-1.70.tar-gz
$ cd js/src
$ make -f Makefile.ref
$ mkdir -p /usr/local/include/js/
$ cp *.{h,tbl} /usr/local/include/js/
$ cd Linux_All_DBG.OBJ/
$ cp *.h /usr/local/include/js/
$ cp js /usr/local/bin/
$ cp libjs.so /usr/local/lib/
$ /sbin/ldconfig
$ cd ~
$ svn export https://ookoo.org/svn/pecl-spidermonkey/ ./sm
$ cd sm
$ phpize
$ ./configure
$ make
$ make install
Installing shared extensions:     /usr/local/lib/php/extensions/no-debug-non-zts-20090626/

インストール先を確認して、 php.ini に以下の行を追加します。

extension = "/usr/local/lib/php/extensions/no-debug-non-zts-20090626/spidermonkey.so"

それでは動かしてみましょう!

Hello, World

<?php
$js = new JSContext();

// jsでPHPの関数を使えるようにする
$js->registerFunction('var_dump');

// jsコードの定義
$script = <<< END
    var hello = 'Hello, World!!';
    var_dump(hello);
END;

// jsコードを評価
$js->evaluateScript($script);
?>

実行結果

Hello, World!!

JavaScript から var_dump() を実行してコンソールに出力しています。

JSでPHPのクラス, 関数を使う

var_dump() 以外にも、 JavaScript から PHP のクラスや関数を使ってみます。

myclass.php

<?php
class hoge
{
    public $foo = array('abc', 'def');
    public static $bar = array('dog' => 'wanwan',
                               'cat' => 'nya-!');

    function pow($x)
    {
        return $x * $x;
    }

    static function hello($name)
    {
        return 'Hello, ' . $name . '!!';
    }
}
?>

call.php

<?php
require_once 'myclass.php';

$hoge = new hoge();
$js = new JSContext();

// PHPの関数, クラスを使えるようにする
$js->registerFunction('var_dump');
$js->registerClass('hoge', 'hoge');

// PHPの変数を使えるようにする
$js->assign('hogedog', hoge::$bar['dog']);

// define script
$script = <<< END
    var hoge = new hoge();
    var_dump(hogedog, hoge.foo, hoge.pow(5));
    var_dump(hoge.hello('amano'));
END;

$js->evaluateScript($script);
?>

実行結果

# php -f call.php
string(6) "wanwan"
object(stdClass)#4 (2) {
  ["0"]=>
  string(3) "abc"
  ["1"]=>
  string(3) "def"
}
int(25)
string(14) "Hello, amano!!"

配列もちゃんと出力できました。

JSで定義したオブジェクト, 関数を使う

次は、 JavaScript のオブジェクトを PHP から触ってみましょう。

jsobj.php

<?php
$js = new JSContext();

$js->registerFunction('var_dump');

// タイムゾーンを設定
date_default_timezone_set('Asia/Tokyo');
// 現在の時(24時間)を取得
$hour = date('H');

// assign
$js->assign('time', $hour);

// define script
$script1 = <<< END
    var menu = {morning:'ばなな', lunch:'sandwich', dinner:'steak'};
    var_dump(menu);
END;

$script2 = <<< END
    var menu = {morning:'ばなな', lunch:'sandwich', dinner:'steak'};
    var gohan = {
        okazu : function(time) {
            if (time > 5 && time < 10) {
                return menu['morning'];
            } else if (time > 10 && time < 14) {
                return menu['lunch'];
            } else {
                return menu['dinner'];
            }
        }
    };

    gohan.okazu(time);
END;

$js->evaluateScript($script1);
printf("今日のおかずは %s よ!\n", $js->evaluateScript($script2));
?>

実行結果

# php -f jsobj.php
object(stdClass)#2 (3) {
  ["morning"]=>
  string(9) "ばなな"
  ["lunch"]=>
  string(8) "sandwich"
  ["dinner"]=>
  string(5) "steak"
}
今日のおかずは steak よ!

evaluateScript() メソッドは最後に評価した値を返します。

外部 js ファイルを使う

以下の js ファイルを読み込んで実行してみます。

function.js

var F = {
    getYearMonth : function () {
        var date = new Date();
        return date.getFullYear() + '/' + (date.getMonth().valueOf() + 1);
    }
}

main.php

<?php
$js = new JSContext();

$library = file_get_contents('function.js');

$main = <<< END
    F.getYearMonth();
END;

$js->evaluateScript($library);
var_dump( $js->evaluateScript($main) );
?>

実行結果

string(6) "2010/2"

思いのほか簡単に動きました!ユーティリティ関数を外部 js の中に詰め込んでおけば、 PHP と JavaScript で共有できそうですね。

PHP に値を渡した時の挙動

JavaScript から PHP にオブジェクトを渡した時の型の変化をもう少し詳しく見てみましょう。

Array

<?php
$js = new JSContext();
$js->registerFunction('var_dump');

$script = <<< END    var array_func = function() {
        var array = new Array(5);
        array[0] = 1;
        array[4] = 5;
        var_dump(array.length);
        return array;
    }
    array_func();
END;

$result = get_object_vars($js->evaluateScript($script));
var_dump( $result );
var_dump( count($result) );
?>

実行結果

int(5)
array(2) {
  [0]=>
  int(1)
  [4]=>
  int(5)
}
int(2)

返り値の array は StdClass に包まれていたので、 get_object_vars() で取り出しています。

JavaScript は array[1]〜array[3] に "undefined" が入り長さは 5 ですが、PHP では長さ 2 となります。
PHP で存在しないインデックス(array[1]〜array[3])にアクセスすると、 NULL が返ります。

Function

<?php
$js = new JSContext();

$script = <<< END
    var func = function() {
        var x = function(n){return n;}
        return x;
    }
    func();
END;

$result = $js->evaluateScript($script);
var_dump($result);
?>

実行結果

object(stdClass)#2 (1) {
  ["prototype"]=>
  object(stdClass)#3 (0) {
  }
}

Function は空のオブジェクトになりました。

実数

Math オブジェクトや Number オブジェクトに含まれる実数値を PHP に渡して出力してみました。
コードは割愛します。

Math.PI

js  -> 3.141592653589793
php -> 3.1415926535898

Number.MAX_VALUE

js  -> 1.7976931348623157e+308
php -> 1.7976931348623e+308

Number.MIN_VALUE

js  -> 5e-324
php -> 4.9406564584125e-324

浮動小数点数は異なる精度で出力されてしまいました。 PHP に実数を渡す場合は、 BC Math 関数http://www.php.net/manual/ja/ref.bc.phpなどを使う必要がありそうです。
ちなみに、JavaScriptの未定義値 "undefined" をPHPに渡したら NULL になりました。

連想配列

<?php
$js = new JSContext();
$js->registerFunction('var_dump');

$script = <<< END
    var func = function() {
    var hash = {num:1, str:'aiueo', array:[1,2], func:function(x){return x}};
    return hash;
    }
    func();
END;

$result = $js->evaluateScript($script);
var_dump( get_object_vars($result) );
?>

実行結果

array(4) {
  ["num"]=>
  int(1)
  ["str"]=>
  string(5) "aiueo"
  ["array"]=>
  object(stdClass)#3 (2) {
    ["0"]=>
    int(1)
    ["1"]=>
    int(2)
  }
  ["func"]=>
  object(stdClass)#4 (1) {
    ["prototype"]=>
    object(stdClass)#5 (0) {
    }
  }
}

PHP でも連想配列として扱うことができました。 関数オブジェクトはやはり空のオブジェクトになってしまいますが。。。

PHP に値を渡したときの挙動まとめ

  • JavaScriptが 数値, 文字列, 論理値, null, undefined, NaN 以外の値を返す場合、PHPには StdClass オブジェクトとして渡される。
  • 配列や連想配列は get_object_vars() で取り出せる
  • 関数オブジェクトは PHP に渡せない

エラー処理

JavaScript コード内でのエラーの扱いを見てみます。

<?php
$js = new JSContext();
$js->registerFunction('var_dump');
$script = <<< END
    try {
        hoge();  // 未定義関数
    }
    catch(e) {
        var_dump(e);
    }
END;

$js->evaluateScript($script);
?>

実行結果

object(stdClass)#2 (4) {
  ["message"]=>
  string(19) "hoge is not defined"
  ["fileName"]=>
  string(0) ""
  ["lineNumber"]=>
  int(1)
  ["stack"]=>
  string(4) "@:1
"
}
セグメンテーション違反です

var_dump() との折り合いが悪いのかセグメンテーション違反が発生してしまいましたが、エラーオブジェクトのプロパティは正しく参照できます。
e.linenumber は 0 から始まり、エラーの発生した行が格納されています。

e.linenumber でエラー行が分かるのは便利ですが、 ひとつの PHP コード内で 複数回 evalueateScript() を実行したときに、どの evaluateScript() での行番号か特定できないという問題があります。
この問題は後述のパッチを適用することで回避可能です。パッチを適用すると、 evaluateScript() の第2引数に e.fileName に出力される名前を設定できるようになります。

まとめ

いかがでしたでしょうか。PHP と JavaScript でお互いの関数やオブジェクトを繰り返し実行すると結構バギーな動きをするので安全とは言い難いですが、 共通の処理を JavaScript のコードにまとめられたり JavaScript からサーバーサイドのオブジェクトが操作できるのは魅力的だと思います。JavaScript の活躍する場所はクライアントサイドだけじゃないということで、何かのご参考になれば幸いです。

パッチ情報

こちらでラボの星野さんが作成されたパッチを公開しています。
http://developer.cybozu.co.jp/oss/2010/01/spidermonkey-ph.html
windows build のサポート,evaluateScript() の引数の追加,報告されたバグの改修の3点です。

参考にさせていただいたページ