Windows Phone 7 で Ruby スクリプトを動的ロードして実行!

MSDNマガジンにものすごいおもしろい記事が上がっていました。

いろいろな場所へ - Windows Phone 7 での IronRuby

Ruby .NET 実装である IronRubySilverlight 向けのDLLをプロジェクトに追加するとRubyスクリプトを実行することができるようです。なんと、RubyでWindows Phone 7のアプリがかけるとは!C#の次くらいにRubyが好きなので、これは何となく嬉しい!

もちろん、フルに使えるわけではなく、多少の問題もあります。

  • 完全にコンパイルされて動作するわけではなく、C#Ruby スクリプトを起動するコードを書かないといけない
  • System.Reflection.Emit が使えないため、高速化に使用している一部処理が使えないためパフォーマンスが落ちる。
  • Cライブラリに依存しているものは使えない。*1
    • gemなライブラリを使う場合はすべてアプリケーションのリソースとして含んでおく必要がある

制限はあるけど使えることは使えるのねーふむふむ、と思いながら読み進めるとサンプルコードにただならぬ記述を発見して驚愕しました。

// Read the IronRuby code
Assembly execAssembly = Assembly.GetExecutingAssembly();
Stream codeFile = execAssembly.GetManifestResourceStream("SampleWPApp.MainPage.rb");
string code = new StreamReader(codeFile).ReadToEnd();

// Execute the IronRuby code
engine.Execute(code);

ちょ、Marketplace経由のxapしか実行できないはずの環境で、文字列をその場でRubyスクリプトとして解釈して実行してやがる!!!

というわけでもしかして・・・と思ったので早速試してみました。

ダウンロードするのはIronRuby 1.1 (.NET 3.5 ZIP & Silverlight 3/Windows Phone 7)というパッケージ。用事があるのは展開されたパッケージのsilverlight\binディレクトリにあるDLL群。Silverlight用のものであるところがミソ。

Visual Studio で新規プロジェクトを作ります。

参照を追加します。・・・が、このときIronRuby.dllなどがType universeがどうのこうのというエラーが出て追加できません。

Type universe cannot resolve assembly: Microsoft.Dynamic, Version=2.0.5.1, Culture=neutral, PublicKeyToken=31bf3856ad364e35.

ちょっと調べてみるとGACに登録したりすると直るようなのですが、面倒そうなので一度ソリューション閉じてテキストエディタで直接編集。Referenceがいっぱいならんでるあたりをこんな感じで編集。

  <ItemGroup>
    <Reference Include="IronRuby">
      <HintPath>..\..\..\..\..\Desktop\ironruby-1.1-dotnet3.5\silverlight\bin\IronRuby.dll</HintPath>
    </Reference>
    <Reference Include="IronRuby.Libraries">
      <HintPath>..\..\..\..\..\Desktop\ironruby-1.1-dotnet3.5\silverlight\bin\IronRuby.Libraries.dll</HintPath>
    </Reference>
    <Reference Include="IronRuby.Libraries.Yaml">
      <HintPath>..\..\..\..\..\Desktop\ironruby-1.1-dotnet3.5\silverlight\bin\IronRuby.Libraries.Yaml.dll</HintPath>
    </Reference>
    <Reference Include="Microsoft.Dynamic">
      <HintPath>..\..\..\..\..\Desktop\ironruby-1.1-dotnet3.5\silverlight\bin\Microsoft.Dynamic.dll</HintPath>
    </Reference>
    <Reference Include="Microsoft.Phone" />
    <Reference Include="Microsoft.Phone.Interop" />
    <Reference Include="Microsoft.Scripting">
      <HintPath>..\..\..\..\..\Desktop\ironruby-1.1-dotnet3.5\silverlight\bin\Microsoft.Scripting.dll</HintPath>
    </Reference>
    <Reference Include="Microsoft.Scripting.Core">
      <HintPath>..\..\..\..\..\Desktop\ironruby-1.1-dotnet3.5\silverlight\bin\Microsoft.Scripting.Core.dll</HintPath>
    </Reference>
    <Reference Include="Microsoft.Scripting.Debugging">
      <HintPath>..\..\..\..\..\Desktop\ironruby-1.1-dotnet3.5\silverlight\bin\Microsoft.Scripting.Debugging.dll</HintPath>
    </Reference>
    <Reference Include="Microsoft.Scripting.Silverlight">
      <HintPath>..\..\..\..\..\Desktop\ironruby-1.1-dotnet3.5\silverlight\bin\Microsoft.Scripting.Silverlight.dll</HintPath>
    </Reference>
    <Reference Include="System.Windows" />
    <Reference Include="system" />
    <Reference Include="System.Core" />
    <Reference Include="System.Net" />
    <Reference Include="System.Xml" />

適当にRubyスクリプトでっち上げ、Webにアップロードする。今回は http://iseebi.half-done.net/wp7test.rbtxt におきました。*2

# Include namespaces for ease of use
include System::Windows::Media
include System::Windows::Controls

# テキストブロックを作る
textBlock = TextBlock.new
textBlock.text = "リモートからIronRubyのコード読み込んでWP7で実行しちゃったよ!"
textBlock.foreground = SolidColorBrush.new(Colors.Green)
textBlock.font_size = 48
textBlock.text_wrapping = System::Windows::TextWrapping.Wrap

# Add the text block to the page
Phone.find_name("ContentPanel").children.add(textBlock)

で、C#側ではボタンをおいて、そのイベントハンドラにこんな感じで書きました。*3

private void button1_Click(object sender, RoutedEventArgs e)
{
    WebClient wc = new WebClient();
    wc.DownloadStringCompleted += delegate(object sender2, DownloadStringCompletedEventArgs e2)
    {
        Dispatcher.BeginInvoke(delegate
        {
            // IronRuby のスクリプトエンジンを生成
            ScriptEngine engine = Ruby.CreateEngine();

            // IronRuby のコンテキストに必要なアセンブリをロードする
            engine.Runtime.LoadAssembly(typeof(Color).Assembly);

            // IronRuby 側に変数をセットする
            engine.Runtime.Globals.SetVariable("Phone", this);

            // WebClientの戻りをセット
            engine.Execute(e2.Result);
        });
    };

    wc.DownloadStringAsync(new Uri("http://iseebi.half-done.net/wp7test.rbtxt"));
}

結果、このボタンを押すとこの通り!なんてこったい!


スクレイピングやってデータを表示するアプリなんかは、元のサイトのデータ構造が変わると審査待ちですぐに修正をアップできないという問題がありますが、この方法でパーサをRubyで書いておけばスクリプトを動的更新できたりしそう。

ただ、セキュリティうんぬんはいいのかよ・・・って感じになってしまいますね。

*1:元記事にgemがCライブラリ依存だから使えないとか書いてあったけどちゃいますよね?公式のソースみた感じではRubyしかないようでしたし・・・。たとえPure RubyでもIsolatedStorageとの絡みで難しそう。

*2:find_nameで探してるのが、元記事と違いContentPanelになっていますが、これはWPDTのRTMのデフォルトがこうなっているからです。

*3:DownloadStringCompleteなど、Web系の非同期メソッドでコールバックメソッドを読んでいるのは別スレッドなので、メインスレッドに同期するためにDispatcher.BeginInvokeを使います。Windows FormsのControl.Invokeみたいなものです。