@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
も付けるべきですかね?
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ラッパーを作成しやすくしています。
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関数である」という感じのインタフェースを与える、ということもできます (むしろそっちの方が多いかも)。