3
4

More than 5 years have passed since last update.

JavaScriptバインディングの書き方 Level 2

Last updated at Posted at 2013-12-05

@kazuhoさんのJavaScriptバインディングの書き方に書かれてない、JavaScriptバインディング上級編です。Java/C++的な思想できちんと作られたクラスであれば問題ないのですが、JavaScriptのあふれんばかりのフリーダムを駆使したコードだと、ただnativeを付けたクラスを書くだけではうまく対応できなかったり、キャストしまくらないとダメだったりします。

返り値がオブジェクト型

esprimaというよくできたECMAScriptのパーサのライブラリのJSX版を作った時に遭遇した事象です。返り値が木構造のASTツリーなんですが、JSONというか、オブジェクトそのまま、という感じでした。もちろん、JSXでJSONは扱えますが、プロパティ名の短縮表記(json.member)は使えず、カッコで文字列でキーを指定した上で、asで型を明示しないとダメです。当然、コードも無駄に長く、読みにくくなります。

推奨する書き方は短く、危険だったりあまり使ってほしくない書き方は長く、というのは言語設計の中でよく行われる意思決定ですが、JSXではきちんと型を書くことでコードが綺麗になるという思想の元で作られています(要出典)。

この場合は、返り値で使われるJSONの型を定義し、nativeのメソッドからそれを返すようにすれば、result.member 形式で書けるようになるし、タイプミスも発見できるようになるので、ちょっと苦労してでもやる価値はあります。フリーダムに作られるJSONを分析して、「きっとこのタイプはこの属性しか持たないはずだ」という情報を元にクラスを定義していくのは、物によっては大変ですが・・・

書き方は、他のクラスと同じように書けばいいのですが、 native __fake__ を付けます。newはできないけど、コンパイル時に型チェックを行えるクラスというのが作れます。 final も付けるべきですかね?

esprima.jsxの一部
native __fake__ class EsprimaBlockToken
{
    var type : string;
    var body : EsprimaToken[];
}

native __fake__ class EsprimaProgramToken extends EsprimaBlockToken
{
    var comments : EsprimaCommentToken[];
    var tokens : EsprimaSimpleToken[];
}

native class esprima {
    static function parse(src : string) : EsprimaProgramToken;
    static function parse(src : string, option : Map.<boolean>) : EsprimaProgramToken;
    static function parse(src : string, option : EsprimaOption) : EsprimaProgramToken;
} = "require('esprima')";

一点課題もあって、メンバー名に使えないプロパティ名を使いたい時どうするの?という問題です。元々のJSONを使うJSで簡易表現ができないなどの弊害があるので、元のコードの素性が悪いといえばそれまでなんですが・・・esprima.jsxでも、演算子の種類(operatorというキーの値)が取れないのは既知の問題です。

JSXで使いたいインタフェースに合わせてJS側を改造

nativeのクラス宣言の後ろに = "require('module')"; と書くと、外部からロードするモジュールが扱えるようになる、というのは@kazuho先生のエントリーにもありますが、じつはここには何でもかけちゃいます。

Redisのnode.jsライブラリのJSXラッパーでは、既存のRedisClientクラスにメソッドを追加しています。node-redisは、Redisの2ワードコマンド(script loadとか)は、1ワード目がメソッド名、2ワード目を最初の引数に、というルールになっていますが、2ワード目によってメソッドのシグニチャがぜんぜん違うので、コンパイラの型チェックとの相性はとても良くないです。redis.jsxでは、2ワードメソッド(scriptloadとか)を個別にメソッド化することで、nativeラッパーを作成しやすくしています。

redis.jsxの簡略版
native class redis {
    static function createClient() : RedisClient;
} = '''
require('redis');

(function () {
    var RedisClient = require('redis').RedisClient;
    var twoWordsCommands = ['script exists', 'script kill', 'script flush', 'script load'];

    twoWordsCommands.forEach(function (command) {
        RedisClient.prototype[command.split(' ').join('')] = function () {
            // 新しい実装
        };
    });
})();
''';

native __fake__ class RedisClient {
    // 追加したメソッド
    function scriptexists(script : string, callback : (Error, int[]) -> void) : boolean;
    function scriptflush(callback : () -> void) : boolean;
    function scriptkill(callback : () -> void) : boolean;
    function scriptload(script : string, callback : (Error, string) -> void) : boolean;
    // 既存のメソッドも当然書けます。
    function get(key : string, callback : (Error, string) -> void) : boolean;
}

独立した関数として提供されているライブラリを、native構文の中でオブジェクトのメンバーとしてまとめて、「それぞれの関数はクラスのstatic関数である」という感じのインタフェースを与える、ということもできます (むしろそっちの方が多いかも)。

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4