hagino3000's blog

平成アーカイブス (更新停止)

JavaScriptでRubyのmethod_missingを実装する

追記: Firefoxの実装で既に有る__noSuchMethod__に名前は合せた方が良い、というコメントを頂いたので名前を変えました。

何の役に立つか不明だけど書いてみた*1。Proxyでプロパティアクセスをフックして、存在しない場合は用意しておいた関数プロキシを返す。

上記の処理が書いてあるのはこの部分。

function enableMethodMissing(obj) {

  // 関数プロキシの作成
  var functionHandler = createBaseHandler({});
  functionHandler.get = function(receiver, name) {
    // プロパティアクセスの場合は何も返さない
    return function(){};
  }

  var calledProperty;
  var trapFn = Proxy.createFunction(functionHandler, function() {
    // 実行の場合は obj.methodMissing を呼び出す
    return obj.__noSuchMethod__(calledProperty, Array.prototype.slice.call(arguments));
  });

  // プロパティプロキシの作成
  var propertyAccessHandler = createBaseHandler(obj);
  propertyAccessHandler.get = function(receiver, name) {
    if (obj[name]) {
      return obj[name];
    } else {
      // 存在しないプロパティへのアクセスは関数プロキシを返す
      calledProperty = name;
      return trapFn;
    }
  }
  return Proxy.create(propertyAccessHandler);

  // (略)

method_missing用の関数をわざわざProxy.createFunctionしているのは、プロパティアクセスの時はundefinedを返す、関数呼び出しの時はmethod_missing用の関数実行と処理を分けるため。

使うとこの様になる。

// クラス定義
// コンストラクタの中でthisをごにょごにょする。
var Ninja = function(name) { 
  this.name = name; 
  return enableMethodMissing(this);
}

// 通常のメソッド定義
Ninja.prototype.getName = function() { 
  return this.name; 
}

// 存在しないメソッド呼び出しがされた時の関数
Ninja.prototype.__noSuchMethod__ = function(methodName, args) {
  console.log('method name:' + methodName);
  console.log(args);
};

var sasuke = new Ninja('Sasuke');
sasuke.getName(); // => Sasuke
sasuke.hoge(1,2,3); // => __noSuchMethod__('hoge', [1,2,3])
sasuke.hoge; // => undefined


Proxyが何に使えるか、というのはこの動画がわかりやすかった。

*1:Firefox4以降 or JavaScriptの実験機能を有効にしたChromeでのみ動作します。

ChromeでECMAScript 6のProxyを有効にする

ECMAScript 6(候補)のProxyはChromeのデフォルト状態では使えない、前の記事で紹介したStruct.jsはProxyを使っているため、Chromeでチェック機構を有効にするには設定をいじる必要があります。

手順

アドレスバーからchrome://flagsを開く

JavaScript の試験運用機能を有効にする の所で「有効にする」をクリック

chromeを再起動

動作確認

こんなコードでOK

>typeof(Proxy); // => object
>typeof(Proxy.create); // => function

ただ、これだけではいろいろと足りない物があるので、同時にECMAScript 6のメソッドを補完してくれる es6-shim.js を使うと良いです。

JavaScriptで厳格な型チェックを行なうライブラリを書いた

(追記) Version 0.3をリリースしました。

先日のjava-ja温泉で「JavaScriptは独りでヒャッハーするのには良いけど、複数人で開発しだすと途端にカオスになって苦労する」という話になった。それに対する解の一つはClosure Compilerが採用したアノテーションによる型の定義と静的チェックだろう。それとは別のアプローチで何かできないかなと考えていたら、ECMAScript 6にObjectのプロパティアクセスをフックできる仕組み(Proxy.create)があったのを思い出した。Proxy APIを使ってみたら型チェックが効くC言語の構造体みたいな物が作れたのでライブラリ化した。

これを使うと何が良いかというと

obj = {};
obj.hogeFuga = new Foo();

// do something

obj.hoge_fuga = null; // 不要になったので解放 (タイプミス)

といった、動かしてもすぐには気づきにくいミスに例外を投げてくれる。Object.seal でも良いけど、未定義プロパティのreadに対しては何もしてくれないのが不満だったので、それも例外が飛ぶようにした。

使い方は次の通り。Object.defineProperties に似せた。

// 構造体の定義
Struct.define('Position', {
  x: {type: 'number'},
  y: {type: 'number'}
});

Struct.define('Square', {
  name: {type: 'string', writable: false},
  pos: {type: 'struct:Position'},
  hidden: {type: 'boolean'}
});

// 作成
var sq = Struct.create('Square', {
  name: 'mySquare',
  hidden: true
});

// 通常のObjectと同様に扱える
sq.pos = Struct.create('Position', {x: 10, y: 20});
sq.hidden = false;
console.log(sq.name); // => mySquare
console.log(sq.pos.x); // => 10

// 以降の操作は全てチェックエラー
var name = sq.Name; // 未定義プロパティの読み取り
delete sq.Name; // 未定義プロパティのdelete
sq.visible = true; // プロパティの追加
sq.pos.x = "100"; // 型の不一致
sq.name = "hoge"; // 読み取り専用プロパティの変更

動作にはECMAScript 6 のProxyが必要なので、Firefox or 設定をいじったChromeじゃないと動かない。しかしProxyが無くても例外が飛ばないだけなので問題は無い、開発時にエラーが飛んでくれればそれで良いと考えているので。

(追記) ChromeでProxyを有効にする方法を書きました。

Node.jsのモジュールをC++で書く (ObjectとArrayを受け取るメソッド)

プリミティブな値を引数で受けとる場合よりもやや面倒。JavaScriptの仕様上どんな値や型でも受けとってしまうので、C++側のチェックコードがどんどん増えていく。

動作確認用のコードがこうだとする

var Test1 = require('./build/Release/Test1');

Test1.giveMeObject({
  option1: 'This is config option!!',
  option2: true
});

Test1.giveMeArray([1,2,3,4,5,6]);

モジュールのコードはこんな感じ

#include <node.h>
#include <v8.h>
#include <iostream>

using namespace v8;

Handle<Value> HandleObjectMethod(const Arguments& args) {
    HandleScope scope;

    // 第一引数の型がObjectかどうかをチェック
    if (!args[0]->IsObject()) {
        Local<String> msg = String::New("Argument must be Object type");
        ThrowException(Exception::TypeError(msg));
        return scope.Close(Undefined());        
    }

    // v8::Object型にキャスト
    Local<Object> obj = Local<Object>::Cast(args[0]);

    std::string strOption1 = "";
    bool bOption2 = "";
    {
        // Check Option1
        Local<Value> v = obj->Get(String::New("option1"));
        if (v.IsEmpty() || !v->IsString()) {
            Local<String> msg = String::New("Option1 is not specified or invalid type");
            ThrowException(Exception::TypeError(msg));
            return scope.Close(Undefined());                        
        } else {
            int length = v->ToString()->Length();
            char c[length];
            v->ToString()->WriteAscii(c);
            strOption1 += c;
        }
    }
    std::cout << "Option1 = " << strOption1 << std::endl;

    {
        // Check Option2
        Local<Value> v = obj->Get(String::New("option2"));
        if (v.IsEmpty() || !v->IsBoolean()) {
            Local<String> msg = String::New("Option2 is not specified or invalid type");
            ThrowException(Exception::TypeError(msg));
            return scope.Close(Undefined());                        
        } else {
            bOption2 = v->ToBoolean()->Value();
        }
    }
    std::cout << "Option2 = " << bOption2 << std::endl;

    return scope.Close(Boolean::New(true));
}

Handle<Value> HandleArrayMethod(const Arguments& args) {
    HandleScope scope;

    // 第一引数の型がArrayかどうかのチェック
    if (!args[0]->IsArray()) {
        Local<String> msg = String::New("Argument must be Array type");
        ThrowException(Exception::TypeError(msg));
        return scope.Close(Undefined());        
    }

    // v8::Arrayにキャスト
    Local<Array> arr = Local<Array>::Cast(args[0]);
    int length = arr->Length();
    std::cout << "Length:" << length << std::endl;

    for (int i = 0; i < length; i++) {
        // Arrayの各要素にアクセス
        Local<Value> v = arr->Get(i);

        // Something todo
    }

    return scope.Close(v8::Boolean::New(true));
}


void init(Handle<Object> target) {
    // メソッドのexport
    target->Set(String::NewSymbol("giveMeObject"), FunctionTemplate::New(HandleObjectMethod)->GetFunction());
    target->Set(String::NewSymbol("giveMeArray"), FunctionTemplate::New(HandleArrayMethod)->GetFunction());
}

NODE_MODULE(Test1, init)

実行結果

Option1 = This is config option!!
Option2 = 1
Length:6

CocoaでJSON-RPC

使った物

SBJson
http://stig.github.com/json-framework/
ASIHTTPRequest
http://allseeing-i.com/ASIHTTPRequest/

どちらもsocket.io-objcが依存しているため既に使っていた。ここでやりたいのは、リクエストボディはJSON文字列のみでPOST、レスポンスもJSON文字列のみという方式。

// 送信データの準備
NSMutableDictionary *dataDict = [[NSMutableDictionary alloc] init];
[dataDict setValue:@"hagino3000" forKey:@"name"];
[dataDict setValue:@"JavaScriipt" forKey:@"lang"];

// NSMutableDictionary -> JSON文字列 -> NSData に変換
NSString *dataStr = [dataDict JSONRepresentation];
NSLog(@"Request body: %@", dataStr);
NSData *postBody = [dataStr dataUsingEncoding:NSUTF8StringEncoding];

// HTTPリクエストの準備
NSURL *url = [NSURL URLWithString:@"http://dev.hagino3000.com/t"];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setRequestMethod:@"POST"];
[request setRequestHeaders:[NSMutableDictionary dictionaryWithObjectsAndKeys:
                            @"application/json", @"Content-Type"
                            , nil]];
[request setPostBody: [NSMutableData dataWithData:postBody]];

// 同期通信で送信
[request startSynchronous];

NSError *error = [request error];
if (error) {
    // Connection Error
}

// レスポンスの確認
NSString *response = [request responseString];
NSLog(@"Response: %@", response);

// JSON文字列をNSDictionaryに変換
SBJsonParser *parser = [[SBJsonParser alloc] init];
NSDictionary *result = [parser objectWithString:response error:&error];

if (error) {
    // Parse error
}

Node.jsのモジュールをC++で書く (引数と戻り値)

引き続きv8.hを読みながらNodeモジュールを書く練習をする。

モジュールのメソッド定義ですが、名前の通りv8::Argumentsが引数の表現で、[]オペレータでアクセスするとv8::Value型のそれぞれの引数が得られる。型チェックはIsNumberやらIsStringといったメソッドはあっても、型を直接返してくれる物は無いのでやや面倒。

試しに、こんなモジュールを作ってみる。

#include <node.h>
#include <v8.h>
#include <iostream>

using namespace v8;

// 引数の数と、それぞれの型をチェックするメソッド
Handle<Value> ArgumentTest(const Arguments& args) {
    HandleScope scope;
    for (int i = 0; i < args.Length(); i++) {
        if (args[i]->IsString()) {
            std::cout << "String!!" << std::endl;
        } else if (args[i]->IsFunction()) {
            std::cout << "Function!!" << std::endl;
        } else if (args[i]->IsObject()) {
            std::cout << "Object!!" << std::endl;
        } else {
            // Number or Boolean or Date ... etc
        }
    }
    return scope.Close(v8::Number::New(args.Length()));
}

// 第一引数をそのままreturnするメソッド
Handle<Value> ReturnTest(const Arguments& args) {
    HandleScope scope;
    return scope.Close(args[0]);    
}

// メソッドのexport定義
void init(Handle<Object> target) {
    target->Set(String::NewSymbol("argumentTest"), FunctionTemplate::New(ArgumentTest)->GetFunction());
    target->Set(String::NewSymbol("returnTest"), FunctionTemplate::New(ReturnTest)->GetFunction());
}

NODE_MODULE(Test1, init)

呼び出し側のコード

var Test1 = require('./build/Release/Test1');

console.log("Check arguments...");
var result = Test1.argumentTest("str", function(){}, {hoge:100});
console.log(result + ' arguments passed');

console.log("");
console.log("Check return value type");
console.log(typeof(Test1.returnTest("str")));
console.log(typeof(Test1.returnTest(100)));
console.log(typeof(Test1.returnTest(new Date())));
console.log(typeof(Test1.returnTest(function(){})));

実行結果

Check arguments...
String!!
Function!!
Object!!
3 arguments passed

Check return value type
string
number
object
function

クラスメソッドしか持たないモジュールはこの書き方がわかれば作れそう。あと、コードを書く時はIDEの支援が欲しいので、Xcodeでソース開いてv8とnodeのコードをヘッダ検索パスに追加した状態で作業した方が楽できますね。

続きます
Node.jsのモジュールをC++で書く (ObjectとArrayを受け取るメソッド)
http://d.hatena.ne.jp/hagino_3000/20120320/1332228809

Node.jsのモジュールをC++で書く (Hello World編)

C++弱者ながらC++でNodeモジュールを作る事にしたのでいろいろと。

まずは公式サイトの解説を参考にしてみる。

Hello World

最初にHello Worldのコードが載っているが

using namespace v8;

とあるので早速この行を消してみるとこうなる。

#include <node.h>
#include <v8.h>

v8::Handle<v8::Value> Method(const v8::Arguments& args) {
    v8::HandleScope scope;
    return scope.Close(v8::String::New("world"));
}

void init(v8::Handle<v8::Object> target) {
    target->Set(v8::String::NewSymbol("hello"),
                v8::FunctionTemplate::New(Method)->GetFunction());
}


NODE_MODULE(hello, init)

v8::Stringやv8::ObjectというクラスがJSのそれぞれの型を表現しており、v8.hで定義されている模様。v8.hのコードを眺めるとJavaScriptの型が全て定義されているのがわかる。

NODE_MODULEマクロがどこで定義されているかを探すと node.h で定義されている。

#define NODE_MODULE(modname, regfunc)                                 \
  extern "C" {                                                        \
    NODE_MODULE_EXPORT node::node_module_struct modname ## _module =  \
    {                                                                 \
      NODE_STANDARD_MODULE_STUFF,                                     \
      regfunc,                                                        \
      NODE_STRINGIFY(modname)                                         \
    };                                                                \
  }

モジュールエクスポート用の関数とモジュールの名前を渡す物らしい。なので上記Hello Worldのコードは、Stringの"world"を返すhelloというメソッドを一つ持つhelloモジュールを作っている。

ビルド

次にモジュールをビルドするためのwscriptのコードが載っている。ビルドするにはwscriptを作ってnode-wafコマンドを使う。

node-wafはpythonのビルドツールのhttp://docs.waf.googlecode.com/git/apidocs_16/index.htmlそのものと書いてある。rubyにおけるrakeコマンドとRakefileみたいな物なのかな。例として載っている

node-waf configure build

configureタスクの後にbuildタスクを実行する。

wscriptの1行目

srcdir = '.'

を適当に変えてソースディレクトリを指すようにしたら、何故かソースが読み込まれない。WAFのドキュメントにもsrcdirは載っていないので、wscriptはソースディレクトリに配置して使うしか無いのかも。

続きます Node.jsのモジュールをC++で書く (引数と戻り値)