MobiRuby/mruby-cocoaを読む

*はじめに [#ffe5d193]

[[mruby-cfuncを読んだ>MobiRuby/mruby-cfuncを読む]]ので次はmruby-cocoaを読みます。ちなみに、mruby-cocoaに含まれているコードだけではmruby-cocoaの機能を説明しきれないところについて、一部mobiruby-iosのコードも掲載しています。

*test/main.m [#p04ea175]

mruby-cfuncと同様にtest/main.mからスタートします。

#code(C){{
struct mrb_state_ud {
    struct cfunc_state cfunc_state;
    struct cocoa_state cocoa_state;
};

int main(int argc, char *argv[])
{
    mrb_state *mrb = mrb_open();
    mrb->ud = malloc(sizeof(struct mrb_state_ud));

    cfunc_state_offset = cfunc_offsetof(struct mrb_state_ud, cfunc_state);
    init_cfunc_module(mrb);

    cocoa_state_offset = cocoa_offsetof(struct mrb_state_ud, cocoa_state);
    init_cocoa_module(mrb);
    load_cocoa_bridgesupport(mrb, struct_table, const_table, enum_table);

    init_unittest(mrb);
    if (mrb->exc) {
        mrb_p(mrb, mrb_obj_value(mrb->exc));
    }

    init_cocoa_test(mrb);
    if (mrb->exc) {
        mrb_p(mrb, mrb_obj_value(mrb->exc));
    }
}
}}

mruby-cocoaはmruby-cfuncを使っているため、udにmruby-cfuncの情報を入れる必要があるようです。

*init_cocoa_module(src/init_cocoa.m) [#je08c216]

#code(C){{
void init_cocoa_module(mrb_state *mrb)
{
    if(cocoa_vm_count >= MAX_COCOA_MRB_STATE_COUNT - 1) {
        puts("Too much open vm"); // TODO
    }

    if(cocoa_mrb_states==NULL) {
        cocoa_mrb_states = malloc(sizeof(mrb_state *) * MAX_COCOA_MRB_STATE_COUNT);
        for(int i = 0; i < MAX_COCOA_MRB_STATE_COUNT; ++i) {
            cocoa_mrb_states[i] = NULL;
        }
    }
    cocoa_mrb_states[cocoa_vm_count++] = mrb;

    struct RClass *ns = mrb_define_module(mrb, "Cocoa");
    cocoa_state(mrb)->namespace = ns;

    init_objc_hook();
    init_cocoa_object(mrb, ns);
    init_cocoa_block(mrb, ns);
    init_cocoa_bridge_support(mrb);

    init_cocoa(mrb);
}
}}

まず初めに渡されたmrb_stateをグローバル変数に設定しています。mruby-cocoaでは複数のmrubyインスタンス(mrb_state)をサポートしているようです。スレッドごとにmrubyインスタンスを持てるようにしているのでしょうか。

その後、Objective-C、Cocoaとの接続の初期化が行われているようです。

**init_objc_hook(src/cocoa_obj_hook.m) [#yf05aeaa]

見た感じ、Objective-C的にオブジェクトが削除されるときにmruby的にもオブジェクトが削除されるための処理を行っているようなのですが、Objective-Cに詳しくないのでパスします。

**init_cocoa_object(src/cocoa_object.m) [#wf68fcb9]

Cocoa::Objectの定義とメソッドの登録を行っています。各メソッドがどのように動くかは後で見ることにします。

**init_cocoa_block(src/cocoa_block.m) [#yfeb145e]

Cocoa::Blockの定義を行っています。

**init_cocoa_brigdge_support [#d52eebe7]

Cocoa::StructとCocoa::Constを定義し、const_missingをオーバーライドしています。Constの方でmethod_missingも登録しているのは小文字で始まる変数への対応と思われます。

**init_cocoa(src/mrb/cocoa.rb) [#y8489beb]

Cocoa::ObjectなどについてRubyで書いた方が書きやすい、という処理が追加で定義されています。どのように動くかについては以下で見ていきます。

ちなみに、init_cocoa()で何をやっているかはsrc/mrb/cocoa.rbを見ればよいというからくりは[[mruby-cfuncを読む>MobiRuby/mruby-cfuncを読む]]の方をご参照ください。

*init_cocoa_test(test/mrb/cocoa_test.rb) [#jbac7246]

mruby-cocoaの初期化が終わったので例によってテストコードを通してmruby-cocoaの使い方を見ていきます。

**Cocoaクラスのロード [#j3c20859]

初めの疑問は

 Cocoa::NSString

と書いたときにどう動いているかです。これまでにCocoa::NSStringの定義はどこにもありませんでした。ていうか、すべてのCocoaクラスに対してRubyのラッパークラスを定義するなんて非現実的すぎるでしょう:-P

答えは以下の部分です。

src/mrb/cocoa.rb
 module Cocoa
     def self.const_missing(name)
         if ::Cocoa::Object.exists_cocoa_class?(name)
             return ::Cocoa::Object.load_cocoa_class(name)
         end
         raise "uninitialized constant #{name}" # ToDo: NameError
     end
 end

src/mrb/cocoa_object.m
#code(C){{
mrb_value
cocoa_class_load_cocoa_class(mrb_state *mrb, mrb_value klass)
{
    mrb_value class_name_mrb;
    mrb_get_args(mrb, "o", &class_name_mrb);
    const char *class_name = mrb_sym2name(mrb, mrb_symbol(class_name_mrb));
    
    if(!NSClassFromString([NSString stringWithCString:class_name encoding:NSUTF8StringEncoding])) {
       mrb_raise(mrb, E_NAME_ERROR, "Can't load %s class from Cocoa", class_name);
    }
    
    struct RClass *object_class = cocoa_state(mrb)->object_class;
    return mrb_obj_value(mrb_define_class_under(mrb, cocoa_state(mrb)->namespace, class_name, object_class));
}
}}

どう動くかというと、

-1回目のCocoa::NSString参照
-Cocoa::NSStringなどないのでCocoa::const_missingが呼び出される
-NSClassFromString()を使ってCocoaクラスをロード
-Ruby的にCocoa::NSStringを定義する
-2回目以降はCocoa::NSStringが定義されているのでそれが利用される

という動きになります。

**Cocoaメソッドの呼び出し [#ue6943bc]

次はメソッドの呼び出しです。test/mrb/cocoa_test.rbの先頭では以下のようなメソッド呼び出しが見られます。

 Cocoa::NSString._stringWithUTF8String("string")

見落としそうですが、'_'が付いてます。この_stringWithUTF8Stringが呼び出されると以下のmethod_missingが反応します。

 class Cocoa::Object
     def method_missing(name, *args, &block)
         if '_' == name.to_s[0, 1]
             return self.class.call_cocoa_method(:self, self, name.to_s[1..-1], *args, &block)
         else
             raise "Unknow method #{name}"
         end
     end

というわけでCocoaメソッドを呼ぶときは'_'を本来のメソッド名の頭に付けて呼ぶという規約のようです。

その後、Rubyで書かれたcall_cocoa_methodで呼び出すメソッド名を作成し、Cで書かれたcocoa_object_objc_msgSend()でObjective-CランタイムとFFIを用いてメソッドの呼び出しを行っています。

メソッド名を作成するということについて少し細くしておきます。cocoa_test.rbだとありませんがmobiruby-iosにあるhello.rbにあるような

 alert = Cocoa::MyAlertView._alloc._initWithTitle "Hello",
 :message, "I am MobiRuby",
 :delegate, nil,
 :cancelButtonTitle, "I know!",
 :otherButtonTitles, "What's?", nil

という呼び出しに対して"initWithTitle:message:delegate:cancelButtonTitle:otherButtonTitles:"というメソッド名を作成するということです。

**プロパティの参照 [#q25f01a2]

mruby-cocoaでプロパティを参照する場合は以下のように書きます。

 result = @test1[:prop1]

個人的には、

 @test1.prop1

と書きたいところですが、そうするとRubyのメソッドなのかObjective-Cのプロパティなのかわからないので可読性を考えるとこちらの方がいいですね。

それはともかく、プロパティ参照が行われると、

+src/mrb/cocoa.rbのCocoa::Object#[]
+src/mrb/cocoa.rbのCocoa::Object#objc_property ← getter, setter名生成
+src/cocoa_object.mのcocoa_object_objc_msgSend()

と処理が流れます。

**RubyでのCocoaメソッドの定義 [#m0703251]

mruby-cocoaでObjective-Cのメソッドを定義するにはdefではなく、defineを利用します。複数引数のサンプルということでmobiruby-iosのhello.rbで定義されているメソッドを取り上げます。

 define C::Void, :alertView, C::Pointer, :clickedButtonAtIndex, C::SInt32 do |me, index|
     if index.to_i == 1
         app = Cocoa::UIApplication._sharedApplication
         url = Cocoa::NSURL._URLWithString("http://mobiruby.org")
         app._openURL url
     end
 end

というわけでdefineの引数は

+戻り値
+メソッド名
+第1引数の型
+第2引数の名前
+第3引数の型
+...

と記述すればいいようです。また、メソッドの処理はブロックで記述し、引数はブロック変数で受け取るようです。

Cocoa::Object#defineの中身に入っていくと、

+ブロックで渡されたメソッド本体をRubyのメソッドとして定義
+定義したRubyメソッドを呼び出すmruby-cfuncのクロージャを定義
+Objective-Cにメソッドを追加

ということを行っています。

で終わろうと思ったのですが気になる点があるので続けます。気になる点は以下、

 class Cocoa::Object
     def self.define(rettype_rb, *args, &block)
         ...
         closure = CFunc::Closure.new(rettype_rb, [CFunc::Pointer, CFunc::Pointer] + types) do |*a|
             self.new(a[0]).send(internal_method_name, *a[2, a.length-2])
         end

a[0]って何が渡されるの?というのが気になる点ですが、self.newの実装である((なお、defineはクラスメソッドですのでこの場合のselfというのはCocoa::Objectクラスオブジェクトです))cocoa_object_class_new()を見るとidなようです。a[1]はメソッド名?

**RubyでのCocoaブロックの定義 [#ddd8ae60]

ブロックを定義するには以下のように書けばいいようです。

 block = Cocoa::Block.new(CFunc::Int, [CFunc::Int]) { |i|
   i.value + 1
 }

Cocoa::Block.newの引数は、

+第1引数:戻り値の型
+第2引数:ブロック引数の配列

と指定するようです。

**BridgeSupport [#d21bb00e]

BridgeSupport(Cocoaで定義されている定数等の参照)は以下のように記述します。

 Cocoa::Const::kCFAbsoluteTimeIntervalSince1904

参照のためには下準備が必要です。test/main.mに書いてあるBridgeSupportの初期化部分を掲載します。

#code(C){{
struct BridgeSupportStructTable struct_table[] = {
    {.name = "CGPoint", .definition = "x:f:y:f"},
    ...
    {.name = NULL, .definition = NULL}
};

struct BridgeSupportConstTable const_table[] = {
    {.name = "kCFAbsoluteTimeIntervalSince1904", .type = "d", .value = &kCFAbsoluteTimeIntervalSince1904},
    ...
    {.name = NULL, .type=NULL, .value = NULL}
};

struct BridgeSupportEnumTable enum_table[] = {
    {.name="enum1", .value=1},
    {.name = NULL, .value = NULL}
};

int main(int argc, char *argv[])
{
    mrb_state *mrb = mrb_open();
    mrb->ud = malloc(sizeof(struct mrb_state_ud));

    cfunc_state_offset = cfunc_offsetof(struct mrb_state_ud, cfunc_state);
    init_cfunc_module(mrb);

    cocoa_state_offset = cocoa_offsetof(struct mrb_state_ud, cocoa_state);
    init_cocoa_module(mrb);
    load_cocoa_bridgesupport(mrb, struct_table, const_table, enum_table);
}}

といった感じに利用したい構造体、定数の情報を記述し、load_cocoa_bridgesupport()を呼び出す必要があります。実際の参照時はconst_missingが発生し((正確には先頭が小文字なのでmethod_missing))参照処理が行われるようです。

*おわりに [#of725705]

今回はmruby-cocoaを読んできました。鍵となるのはconst_missingとmethod_missingを用いた動的なクラスロード、メソッド呼び出しのようです。

また、オブジェクト管理についてRubyでの管理とObjective-Cでの管理についてかなりめんどうなことをやっているように見られます。が、Objective-Cは使ったことがないので突っ込んだ解説ができません。理解が進む、つっこみなどをいただくなどしたら随時情報を追記していくようにしたいと思います。

トップ   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS