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でチェック機構を有効にするには設定をいじる必要があります。
動作確認
こんなコードでOK
>typeof(Proxy); // => object >typeof(Proxy.create); // => function
ただ、これだけではいろいろと足りない物があるので、同時にECMAScript 6のメソッドを補完してくれる es6-shim.js を使うと良いです。
JavaScriptで厳格な型チェックを行なうライブラリを書いた
先日の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はソースディレクトリに配置して使うしか無いのかも。