MSDNマガジンにものすごいおもしろい記事が上がっていました。
いろいろな場所へ - Windows Phone 7 での IronRuby
Ruby の .NET 実装である IronRuby の Silverlight 向けの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みたいなものです。