Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

この文書は、The Architecture of Open Source Applications Volume II: Structure, Scale, and a Few More Fearless Hacksに収録されているThe Dynamic Language Runtime and the Iron Languagesの日本語訳です。原文と同様、日本語訳もcc-by unported 3.0によって公開されます。

動的言語ランタイム(DLR)とIron言語

Jeff Hardy (原文) / Atsushi Eno (日本語訳)

Iron言語は、IronPythonをはじめとして、"Iron"を名前に含む、各種言語実装の非公式な集合体です。これらの言語には、少なくともひとつ共通していることがあります。これらは、共通言語ランタイム(CLR)を対象とする動的言語であり、動的言語ランタイム(DLR)の上に構築されています。CLRは、むしろ.NET Frameworkとして知られているでしょう。("CLR"はより汎用的な用語です。.NET FrameworkはMicrosoftの実装であり、この他にオープンソースの実装であるMonoがあります。) DLRは、CLR上で動的言語を高度にサポートするための、CLR用ライブラリの集合体です。IronPythonとIronRubyは、何十ものクローズドソースあるいはオープンソースのプロジェクトで使用されており、いずれもアクティブに開発されています。DLRは、オープンソースプロジェクトとして始められましたが、これは.NET FrameworkとMonoの一部となっています。

アーキテクチャとしては、IronPython、IronRuby、DLRは、単純でもあり、悪魔的なまでに複雑でもあります。高水準なところでは、その設計は他の多くの言語実装に似ており、パーサ、コンパイラー、コードジェネレーターから成っています。しかし、少し近寄ってみると、その面白い詳細部分が顔を出してきます。コールサイト(call sites)、バインダー、適用的コンパイル(adaptive compilation)、その他各種技術によって、静的言語用に設計されたプラットフォーム上でも、動的言語が静的言語にほぼ匹敵するパフォーマンスを出せるようになっています。

8.1. 歴史

Iron言語の歴史は2003年に始まります。 Jim Huguninは、この時すでに、Java仮想マシン(JVM)用に、Jythonと呼ばれるPythonの実装を書いていました。一方その頃、.NET Frameworkの共通言語ランタイム(CLR)は、Pythonのような動的言語を実装するのには適していない、と考えられてきました(誰によって、というと定かではありませんが)。既にJVM用にPythonを実装していたJimは、なぜMicrosoftが.NETをJavaよりずっと劣るものに作り上げられるのか、不思議に思っていました。2006年9月のブログの投稿で、彼は次のように書いています:

私は、MicrosoftがどうやってCLRをJVMよりも動的言語の用途に劣るプラットフォームとして作り上げることが出来たのか、理解したかった。私の計画は、CLR上で動作するPython実装のプロトタイプを数週間かけて構築し、それを用いて「CLRが動的言語に全然向かないプラットフォームである理由」とすっぱりと題した記事を書く予定だった。このプロトタイプに着手してほどなく、私の計画は変更となった。Pythonは、CLR上で非常に優れた動作を見せたのだった。多くのケースで、Cベースの実装よりも著しく高速だった。標準のpystoneベンチマークにおいて、CLR上のIronPythonは、Cベースの実装より1.7倍くらい高速だった。

(名前の"Iron"という部分は、Jimの当時の会社名である、Want of a Nail Softwareの名前からの言葉遊びで付けられています。)

[訳注: これは "for want of a nail" という「一本の釘が足りないことで蹄鉄がダメになり、蹄鉄がダメになったことで馬が走らなくなり、馬が走らなくなったことで伝令が動けなくなり、伝令が機能しなかったことで戦に敗れ、戦に敗れたことで国が滅びた。一本の蹄鉄の釘が原因だったのだ」という詩に由来する。]

ほどなく、Jimは、.NETをより動的言語に適したプラットフォームとするべく、Microsoftに雇われることになりました。Jim(そしてその他の多くの人々)は、もとのIronPythonのコードから、言語中立の要素を抜き出して、DLRとしました。DLRは、.NET用の動的言語を実装するための共通のコアを提供するべく設計され、.NET 4の主要な新機能となりました。

(2007年4月に)DLRがアナウンスされた時、Microsoftは、DLR上で動作する新しいバージョンのIronPython(IronPython 2.0)と同時に、DLRの多言語の適用可能性を示すべくIronRubyをDLRに基づいて開発することもアナウンスしました。(2010年10月に、MicrosoftはIronPythonとIronRubyの開発を停止し、これらは独立したオープンソースのプロジェクトとなりました。) DLRを用いた動的言語との統合は、C#およびVisual Basicにとっても主要な部分となり、新しいキーワード( dynamic )が、DLRで実装された言語や任意の動的データソースの呼び出しを、簡単に出来るようにしました。CLRはすでに静的言語を実装する良いプラットフォームでしたが、DLRがこれらの言語を一級市民として押し上げたのです。

Microsoft外部でも、IronSchemeIronJSといった、DLRを使用した言語実装があります。さらに、MicrosoftのPowershellバージョン3では、その独自の動的オブジェクトシステムの代わりにDLRを使用します。

8.2. 動的言語ランタイムの原則

CLRは、静的言語を前提に設計されています。型に関する情報は、ランタイムの奥深くにまで焼き付けられており、そのキーとなるひとつの前提として、型は変化しないということが挙げられます。変数はその型を変更することはなく、また、型はプログラムの実行中にフィールドやメンバーを追加されたり削除されたりすることはありません。これはC#やJavaのような言語においては問題ありませんが、動的言語は、定義上、これらの規則に従いません。また、CLRは、静的な型の共通オブジェクトシステムを提供しており、これによって、いかなる.NET言語も他の.NET言語によって書かれたオブジェクトを、特別な作業を要することなく呼び出すことができます。

DLRが無い場合、すべての動的言語が、独自のオブジェクトモデルを提供しなければならなかったでしょう。各種の動的言語が、他の動的言語で書かれたオブジェクトを呼び出すことはかなわず、C#からIronPythonやIronRubyを等しく扱うこともできなかったはずです。すなわち、DLRの中核は、動的オブジェクトを実装しつつ、バインダーを用いて言語独自の振る舞いをカスタマイズすることを可能にする、標準化された方法なのです。これには、動的な命令を可能な限り高速化できるようにするコールサイトキャッシュと呼ばれるメカニズムや、コードをデータとして簡単に操作できるようにする式ツリーを構築するためのクラス群も含まれます。

CLRは、動的言語を便利にするための機能もいくつか提供しています。高度なガベージコレクター、.NETのコンパイラーが生成する共通中間言語(IL)バイトコードを実行時にマシンコードに変換するジャストインタイム(JIT)コンパイラー、そして最後に、実行時にコードを生成され静的メソッド呼び出しよりもわずかに多いオーバーヘッドのみで実行できる動的メソッド(または軽量コード生成)があります。(JVMは、Java 7で invokedynamic という類似の機能を得ました。)

このDLRの設計の成果として、IronPythonやIronRubyのような言語は、共通の動的オブジェクトモデルを使用することによって、互いの(そして他の任意のDLR言語の)オブジェクトを呼び出せるようになりました。このオブジェクトモデルのサポートは、C# 4で( dynamic キーワードによって)、またVisual Basic 10で(VBの既存の「遅延バインディング」の手法に加えて)追加され、同様にこれらのオブジェクトに対する動的な呼び出しを実行できるようになりました。こうしてDLRは、動的言語を.NETにおける一級市民としたのです。

面白いことに、DLRは完全に.NET 2.0上でもビルドして実行できるライブラリの集合体として実装されました。これを実装するために、いかなるCLRの変更も必要ではなかったというわけです。

8.3. 言語実装の詳細

どの言語実装にも、ふたつの基本的なステージがあります。パース(フロントエンド)とコード生成(バックエンド)です。DLRにおいては、それぞれの言語が独自のフロントエンドである言語パーサーと文法ツリー生成を実装します。DLRは式ツリーを受け取ってCLRが利用できるような中間言語(IL)を生成する共通のバックエンドを提供します。CLRはこのILを、プロセッサが実行するマシンコードを生成するジャストインタイム(JIT)コンパイラーに渡します。実行時に定義される(および eval を用いて実行される)コードも同様に処理されますが、全てはファイルの読み込み時ではなく eval のコールサイトで行われます。

言語のフロントエンドの主要な部分を実装する方法はいくつかあり、IronPythonとIronRubyが非常に似ているにもかかわらず(実のところこれらは並行して開発されていたわけです)、これらはいくつかの主要な部分を異にしています。IronPythonもIronRubyもきわめて標準的なパーサーの設計を用いており、テキストをトークンに分割するトークナイザー(あるいは lexer )と、トークンをプログラムを表す抽象構文ツリー(AST)に変換するパーサーを利用しています。しかしながら、これらの言語はそれらの実装を完全に別々にしています。

8.4. パース

IronPythonのトークナイザーは IronPython.Compiler.Tokenizer クラスにあり、パーサーは IronPython.Compiler.Parser クラスにあります。トークナイザーはPythonのキーワード・演算子・名前を認識して対応するトークンを生成する手書きのステートマシンです。それぞれのトークンには、任意の追加情報(定数値や名前など)と、そのトークンのソース中の位置が、デバッグの補助情報として含まれます。そして、パーサーは、このトークンの集合を受け取って、Python文法に基づいて解読し、正しいPython生成規則に従っているかを判断します。

IronPythonのパーサーはLL(1)の再帰下降構文パーサー(recursive decent parser)です。このパーサーは入力トークンを見て、そのトークンが許容されるか判断する関数を呼び出し、許容されない場合はエラーを返します。再帰下降構文パーサーは、相互に再帰的な関数の集合から構築されます。これらの関数は究極的にはひとつのステートマシンを実装することになり、それぞれの新しいトークンがひとつの状態遷移をトリガーします。トークナイザーと同様に、IronPythonのパーサーは手書きで作られています。

一方、IronRubyは、Gardens Point Parser Generator(GPPG)が生成したトークナイザーおよびパーサーをもっています。パーサーは Parser.y ファイル( Languages/Ruby/Ruby/Compiler/Parser/Parser.y )に記述されています。これは、文法を記述する規則に基づいて、IronRubyの文法を高レベルで記述した yacc フォーマットのファイルです。GPPGは Parser.y を受け取って、実際のパーサー関数およびテーブルを作成します。この出力はテーブルに基づくLALR(1)のパーサーとなります。生成されたテーブルは整数の長い配列群で、それぞれの整数が状態を表します。このテーブルは、現在の状態と現在のトークンから、次にどの状態に遷移するかを決定します。IronPythonの再帰下降パーサーは非常に読みやすいものですが、IronRubyの生成されたパーサーはそうではありません。遷移テーブルは膨大なもので(540種類の状態と45,000の遷移)、これを手作業で修正するのはほぼ不可能です。

結局のところ、これはエンジニアリングのトレードオフです。IronPythonのパーサーは手作業で修正するのは簡単ですが、言語の構造を曖昧にしてしまう程度には複雑です。一方、IronRubyのパーサーは、 Parser.y ファイルで言語の構造を理解するのは簡単ですが、サードパーティのツールに依存してカスタムの(とはいえほぼ既知ですが)ドメイン固有言語(DSL)を使用し、それ特有のバグや独自性に悩まされることになります。この場合、IronPythonチームは外部ツール依存性に踏み込みたくはなく、一方でIronRubyチームはそれを気に病まなかったということです。

とはいえ、いかなるフェーズの解析においても、ステートマシンが重要であることは明らかです。いかなる解析タスクにおいても、それがどれだけ簡単なものであっても、ステートマシンが常に正しい解なのです。

どちらの言語についても、パーサーの出力結果は抽象構文ツリー(AST)です。これはプログラムの構造を高レベルで記述するもので、それぞれのノードは言語の生成規則である、文または式に対応します。これらのツリーは、実行時に操作でき、時としてコンパイル前にプログラムの最適化が施されます。しかしながら、言語のASTはその言語に密接に結びついています。DLRは、いかなる言語独自の生成規則も含まない、汎用的なもののみを含むツリーを、操作する必要があります。

8.5.式ツリー

式ツリーもまた、実行時に操作できるプログラムの表現ですが、より低レベルな、言語独立の形式です。.NETでは、ノードの型は System.Linq.Expressions ネームスペースにあり、全てのノードの型は抽象型 Expression クラスから派生します。(このネームスペースには歴史的な背景があります。式ツリーは、もともとは.NET 3.5でLINQこと言語統合クエリを実装するために追加されたもので、DLRの式ツリーはこれを拡張したのです。) これらの式ツリーは、実のところ、単なる式以上のものをカバーしており、 if 文や try ブロックや、ループなどのノードの型もあります。言語によっては(たとえばRubyでは)、これらは式であって、文ではありません。

これらは、ひとつのプログラミング言語で必要とされるであろう、ほぼすべての機能をカバーするノード群です。しかしながら、これらは得てして低レベルに定義されています。 ForExpressionWhileExpression などといったものの代わりに、 LoopExpression がひとつだけ存在しており、 これは GotoExpression と組み合わせることによって、どの種類のループも記述することができます。言語をより高レベルで記述するために、言語では、その独自のノードを、 Expression から派生して、Reduce() メソッドをオーバーライドして別の式ツリーを返すようにすることで、定義できます。IronPythonでは、その解析ツリーもまたDLR式ツリーですが、これはDLRが通常は解さないようなカスタムノードを数多く含んでいます(たとえば ForStatement など)。これらのカスタムノードは、DLRが理解できるようなかたちの式ツリー( LoopExpressionGotoExpression の組み合わせなど)に解消(reduce)することができます。カスタム式ノードは、別のカスタム式ノード群に解消でき、この解消処理は、DLRに内在するノードのみが残るように、再帰的に行われます。IronPythonとIronRubyの大きな違いの一つは、IronPythonではASTもまた式ツリーであるのに比べ、IronRubyではそうなっていないということです。IronRubyでは、ASTは次のステージに進む前に式ツリーに変換されます。ASTが式ツリーでもあることが実際に有用であるか否かは、議論の余地があり、IronRubyはそのような実装を行わなかったということになります。

各ノードの型は、自身を解消する方法を知っており、そして一方向にのみ解消できます。ツリーの外側のコードによる変換処理、たとえば定数折り畳み(constant folding)最適化や、Python生成系のIronPython実装、においては、 ExpressionVisitor のサブクラスが使用されます。 ExpressionVisitor には Visit() メソッドがあり、これは Expression クラスの Accept() メソッドを呼び出し、その Expression のサブクラスは Accept() をオーバーライドして、VisitBinary() など、 ExpressionVisitor の個別の Visit() のメソッドを呼び出します。これは、訪問(visit)できる限られた型のノード群と、それに対する無限数の操作から成る、教科書的なGammaらのVisitorパターンの実装です。式ビジターは、ノードを訪問して、通常は再帰的にその子ノード群を訪問し、さらにその子ノード群、さらにその子…と、ツリーを下降的に辿ります。しかし、式ツリーは不変(immutable)なので、ExpressionVisitor は、自らが訪問しているツリーに手を加えることはできません。もしこの式ビジターがノードを修正する必要がある場合は(子を削除する場合など)、その古いノードを置き換える新しいノードと、その(子の)親を同様に生成してやる必要があります。

いったん式ツリーが生成され、解消され、訪問されると、これは最終的に実行される必要があります。 式ツリーは直接ILコードにコンパイルすることができますが、IronPythonとIronRubyでは、これらをまずインタープリターに渡します。直接的なILへのコンパイルはコストがかかるもので、ほんの数回しか実行されないかもしれないコードにはもったいないのです。

8.6. インタープリットとコンパイル

.NETのように、JITコンパイラを使うことの問題点のひとつは、起動時に、ILバイトコードをプロセッサが実行できるマシンコードに変換するのに時間がかかるということです。JITコンパイルでは、インタープリタに比べると実行速度は非常に速いですが、やろうとしていることによっては、初期コストがひどく高いということがあります。たとえば、Webアプリケーションなど長時間生存するサーバープロセスでは、起動時間はほぼ無関係で、リクエストごとの時間こそがクリティカルであり、同一のコードを反復的に実行する傾向があるので、JITの利益を享受します。一方で、めったに実行することはないが短時間で実行されるプログラム、たとえばMercurialのコマンドラインクライアントなどは、短い起動時間の方が重要であり、僅かなコードを一度だけ実行することが多く、JITされたコードが速いなどという事実は、より長い起動時間がかかるという事実に勝るものではないからです。

.NETは、ILコードを直接実行することは出来ません。常にマシンコードにJITコンパイルされ、これには時間がかかります。特に、プログラム起動時間は、多くのコードがJITコンパイルされなければならない.NET Frameworkの弱点の一つです。静的な.NETのプログラムについては、このJITペナルティを回避する方法がありますが(ネイティブイメージ生成、NGEN)、これは動的プログラムには機能しません。IronRubyとIronPythonでは、常に直接ILにコンパイルする代わりに、JITコンパイルされたコードほど高速ではないけど起動にかかる時間が著しく少ない、自前のインタープリター( Microsoft.Scripting.Interpreter にあります)を使用します。このインタープリターは、たとえばモバイルプラットフォームなど、動的コード生成が許されていない状況においても有効です。そうでなければ、DLR言語は全く実行できないことになります。

実行前に、式ツリー全体が、実行可能になるように、ひとつの関数の中にラップされなければなりません。DLRでは、関数は LambdaExpression ノードとして表現されます。ほとんどの言語において、ラムダは匿名関数ですが、DLRには名前の概念がありません。全ての関数は匿名です。この LambdaExpression だけが、 Compile() メソッドによってデリゲートにコンバートできるというユニークな特徴を有しています。delegateは、.NETが関数として呼び出すことができる一級市民です。デリゲートは、Cの関数ポインタに近い存在で、実態は呼び出し可能なコードの断片を指す、単なるハンドルです。

最初に、式ツリーは LightLambdaExpression の中にラップされ、これもまた実行可能なデリゲートを生成出来ますが、それはILコードを生成する(従ってJITを呼び出す)代わりに、式ツリーをコンパイルしてインタープリタの簡単なVMで実行できるような命令のリストを生成するものです。このインタープリターは、簡単なスタックベースのものです。命令は、スタックから値を取り出して、演算を実行し、結果をスタックに格納します。それぞれの命令は Microsoft.Scripting.Interpreter.Instruction から派生したクラスのインスタンスであり(たとえば AddInstructionBranchTrueInstruction )、それらは、スタックから値をいくつ取り出して、いくつ格納するかを示すプロパティや、スタックから取り出して命令を実行し値を格納して次の命令へのオフセットを返す Run() メソッドを有しています。このインタープリターは、命令のリストを受け取って、それらをひとつずつ実行し、 Run() メソッドの戻り値に応じて前後にジャンプします。

コードの断片が一定回数実行されると、これは LightLambdaExpression.Reduce() を呼び出すことによって完全な LambdaExpression にコンバートされ、そして(ちょっとした並列処理を伴うバックグラウンドスレッド上で) DynamicMethod デリゲートにコンパイルされ、古いデリゲートのコールサイトは、新しい高速なものに置き換えられます。これによって、プログラムのmain関数など、数回だけ実行される実行関数のコストが大幅に減少し、一方で共通的に呼び出される関数は可能な限り高速に実行できるようになります。デフォルトでは、このコンパイルの閾値は32回の実行ですが、これはコマンドラインオプションやホストプログラムによって変更でき、あるいは、コンパイルないしインタープリターを完全に禁止することもできます。

インタープリタで実行するか、あるいはILへコンパイルするかを、これらの言語の命令が、式ツリーコンパイラーによってハードコードされることはありません。むしろ、コンパイラは、動的かもしれない各命令(というのは実のところほぼすべて)について、コールサイトを生成します。これらのコールサイトは、パフォーマンスを高水準に維持しつつ動的な振る舞いを実装する機会を、オブジェクトに与えます。

8.7. 動的コールサイト

静的な.NET言語では、どのコードが呼び出されるかは、全てコンパイル時に決定されていました。例えば、次のようなC#のコードがあったとします:

var z = x + y;

C# コンパイラは、'x' および 'y' の型と、それらが加算可能であるかどうかを知っています。C#コンパイラは、演算子オーバーローや型変換、その他このコードを適切に処理するために必要となる、適切なコードを出力できます。これは、関連する型についての、完全に静的な情報に基づいています。ここで、以下のPythonコードを考えてみましょう:

z = x + y

IronPythonコンパイラは、これが出現した時に、何をする必要があるのか、全くわかりません。 xy が何であるかを知らず、仮に知ったとしても、 xy の加算の可否は、実行時には変わっているかもしれないからです。(原理的には可能かもしれませんが、IronPythonもIronRubyも、型推論を 行いません。) IronPythonは、数値を加算するILコードを生成する代わりに、これを実行時に解決するためのコールサイトを出力します。

コールサイトは、処理(operation)を実行時に決定するためのプレースホルダーです。これらは System.Runtime.CompilerServices.CallSite のインスタンスとして実装されています。RubyやPythonのような動的言語では、全ての処理が動的コンポーネントです。動的な処理は DynamicExpression ノードの式ツリーに表現されます。式ツリーコンパイラは、これをコールサイトに変換すべきであると知っています。コールサイトが生成された時点では、期待された処理をどのように行うかは、未知の状態です。しかし、これは現在使用中の言語に固有のコールサイト・バインダーのインスタンスを用いて生成され、その処理を行うために必要な情報を全て含んでいます。

Figure 8.1: コールサイトのクラス図

図8.1: コールサイトのクラス図

それぞれの言語には、それぞれの処理に応じた別々のコールサイト・バインダーが存在し、それらのバインダーは、そのコールサイトに渡される引数に応じて処理を実行する方法を、時として数多く、知っています。しかしながら、それらのルールを生成するのはコストが高くつくため(特に、実行できるデリゲートへの変換して.NETのJITの呼び出しを伴うなど)、このコールサイトには、複数レベルのコールサイト・キャッシュがあり、ここには後々の利用のために生成されたルールを格納します。

コールサイトのフローチャート

最初のレベルL0は、コールサイトのインスタンス自身をあらわす CallSite.Target プロパティです。ここには、このコールサイトで最も直近に使用したルールが保存されます。大半のコールサイトについては、ひと組の引数型について呼び出されるのみなので、これだけが必要です。このコールサイトには別のキャッシュL1があり、ここには他に10件のルールを格納できます。もし Target がこの呼び出しに対して有効でない場合(たとえば、引数型が異なった場合)、このコールサイトはまずそのルールキャッシュをチェックして、以前の呼び出しで既に適当なデリゲートが生成されていないかを見て、それを新しいものを生成する代わりに再利用します。

ルールをキャッシュに格納するというのは時間的な都合によるものです。新しいルールを実際にコンパイルするのは、既存のルールをチェックするよりも時間がかかります。大まかに言えば、ルールの述語として最も一般的である、ひとつの変数に対する型チェックを行うのには10ナノ秒かかります(二値関数のチェックには20ナノ秒、以下同様)。一方、doubleを加算する単純なメソッドをコンパイルするには、だいたい80マイクロ秒かかります。数千倍長いのです。このキャッシュのサイズは、全てのコールサイトで使用される全てのルールを記憶してメモリを浪費しなうよう、限られています。単純な加算については、それぞれのバリエーションで約1KBのメモリが必要になります。しかし、プロファイリングが示す結果では、10種類のバリエーションを要するコールサイトはほとんどありません。

最後に、バインダーのインスタンス自身に格納されるL2キャッシュがあります。ひとつのコールサイトに関連付けられるバインダーのインスタンスには、そのコールサイト固有の追加情報を格納するかもしれませんが、いずれにしろ、コールサイトの大半はユニークなものではなく、同一のバインダーインスタンスを共有できます。たとえば、Pythonで、加算の基本的なルールはプログラム全体を通して同一です。これは + の両端にある2つの型に依存する、それだけです。そのプログラム中では、全ての加算処理は同一のバインダーを共有し、もしL0とL1のキャッシュが無い場合、このL2キャッシュには、プログラム全体を通して収集された、ずっと多く(128件)の最新ルールが含まれています。もしあるコールサイトが初めて実行されるとしても、このL2キャッシュに適当なルールが見つかる可能性が十分にあります。これが最も効率的に機能するよう、IronPythonとIronRubyのいずれも、加算など共通の処理で使用される、典型的な(canonical)バインダーインスタンスの集合をもっています。

L2キャッシュが無かった場合、このバインダーは、そのコールサイトに、現在の引数型を(あるいはさらに値も)考慮した実装を作成するよう要求します。上記の例では、もし xy がdoubleであれば(あるいは他のネイティブ型であれば)、この実装は単純にそれらをdoubleにキャストして、ILの add 命令を呼び出します。このバインダーは、引数をチェックしてそれらがこの実装に適合することを保証するためのテストも生成します。この実装とテストが組み合わさって、ひとつのルールになります。ほとんどの場合、実装とテストは式ツリーとして生成され格納されます。(コールサイトのインフラストラクチャーは、式ツリーに依存しません。デリゲート単体で利用されることもあります。)

もしこの式ツリーがC#で表現されたとしたら、それは次のようなものになります:

if(x is double && y is double) {       // double型をチェック
  return (double)x + (double)y;    // doubleなら実行

} return site.Update(site, x, y); // doubleでないので、その型に
// 応じたルールを探索/作成

バインダーは、その後、この式ツリーからデリゲートを生成して、ルールをILに、さらにマシンコードに、コンパイルします。2つの数値を加算する場合、これは、型のクイックチェックと数値を加算するマシン命令になるでしょう。以上の全てを含めても、最終的な結果は静的なコードよりごくわずかに遅いだけでしょう。IronPythonとIronRubyには、プリミティブ型の加算など共通の処理のコンパイル済みルールが含まれており、実行時の生成を不要にして時間を節約して、代わりにディスクスペースを幾分か余計に消費しています。

8.8. メタオブジェクトプロトコル

言語インフラストラクチャーとは別の、DLRのもうひとつの主要な部分は、ある言語(ホスト言語)が、別の言語(ソース言語)で定義されたオブジェクトに、動的な呼び出しを行える能力です。これを可能にするために、DLRは、あるオブジェクト上でどの処理が有効であるかを、そのオブジェクトを作成した言語を問わずに、判断できなければなりません。PythonとRubyは類似のオブジェクトモデルを有していますが、JavaScriptは根本的に異なる、プロトタイプベースの(暮らすベースとは異なる)型システムを有しています。DLRでは、これらの様々な型システムを統合する代わりに、これらをSmalltalkスタイルのメッセージ渡しに基づいて扱います。

メッセージ私のオブジェクト指向システムでは、オブジェクトは他のオブジェクトに(普通はパラメータ付きで)メッセージを渡し、そのオブジェクトは結果としてまたオブジェクトを返します。こうして、オブジェクトの何たるかをそれぞれの言語において構想できるようにしつつ、メソッド呼び出しをオブジェクト間のメッセージとして見るだけで、それらのほぼすべてを同等に扱えるようになるのです。もちろん、静的なOO言語においてすらも、このモデルはある程度適合します。動的言語で違うのは、呼び出されるメソッドがコンパイル時に既知である必要はない、あるいはオブジェクト上に存在していなくても良い(たとえばRubyの method_missing )、あるいは、ターゲットオブジェクトが通常的に必要に応じてメッセージに介入して、異なるやり方で処理する機会があります(たとえばPythonの __getattr__ )。

DLRでは、以下のメッセージが定義されています:

  • {Get|Set|Delete}Member : オブジェクトのメンバーを操作する処理
  • {Get|Set|Delete}Index : インデックスのあるオブジェクトの処理(配列や辞書など)
  • Invoke , InvokeMember : オブジェクトのメンバーの呼び出し
  • CreateInstance : オブジェクトのインスタンスの生成
  • Convert : オブジェクトをある型から別の型に変換する
  • UnaryOperation , BinaryOperation : 否定( ! )や加算( ! )のような、演算子ベースの処理を実行する

これらがあれば、どんな言語のオブジェクトモデルを実装するにも十分であるはずです。

CLRは本来的に静的型付けなので、動的言語のオブジェクトもやはり静的なクラスによって表現されなければなりません。通常のテクニックは、 PythonObject のような静的なクラスを作り、実際のPythonオブジェクトがそのクラスあるいはその派生クラスのオブジェクトとなるようにする、というやり方です。相互運用性とパフォーマンスという理由から、DLRのメカニズムは、それよりはるかに複雑なものです。言語固有のオブジェクトを扱う代わりに、DLRでは、 System.Dynamic.DynamicMetaObject のサブクラスで、上記のメッセージを扱うメソッド群を含む、メタオブジェクトを扱います。それぞれの言語には、その言語のオブジェクトモデルを実装した DynamicMetaObject のサブクラスがあります。IronPythonでは MetaPythonObject です。これらのメタクラスは、対応する System.Dynamic.IDynamicMetaObjectProtocol インターフェースを実装するクラスをもちます。これが、DLRで動的オブジェクトを識別する方法として用いられます。

Figure 8.3: IDynamicMetaObjectProtocolのクラス図

DLRは、 IDynamicMetaObjectProtocol を実装するクラスから、GetMetaObject() を呼び出して DynamicMetaObject を取得できます。この DynamicMetaObject は言語によって提供され、そのオブジェクトに必要なバインディングの機能を実装します。それぞれの DynamicMetaObject には、もし提供されていれば、そのオブジェクトの内部的な値と型も含まれます。最後に、DynamicMetaObject は、コールサイト バインダーにも似ていますが、現時点でのコールサイトをあらわす式ツリーと、その式に対するあらゆる制約を格納しています。

DLRは、ユーザー定義クラス上のメソッドに対する呼び出しをコンパイルする時、まずはコールサイト(すなわち CallSite クラスのインスタンス)を生成します。このコールサイトは、上記の "Dynamic Call Sites" で説明される通りのバインディング プロセスを開始し、これは最終的に OldInstance のインスタンス上にあって MetaOldInstance を返す GetMetaObject() を呼び出します。(Pythonは古いスタイルと新しいスタイルのクラスがありますが、この話はそれとは無関係です。) 次に、バインダーが呼び出され( PythonGetMemberBinder.Bind() )、続いて MetaOldInstance.BindGetMember() が呼び出されます。これは、そのオブジェクトからメソッドを名前でルックアップする方法を記述した、新しい DynamicMetaObject を返します。すると、もうひとつのバインダー PythonInvokeBinder.Bind() が呼び出され、これは MetaOldInstance.BindInvoke() を呼び出し、先ほどの DynamicMetaObject に、ルックアップされたメソッドを呼び出す方法を、ラップします。つまり、ここには、元のオブジェクト、メソッド名をルックアップする式ツリー、そのメソッドへの引数を表す DynamicMetaObject 、が含まれます。

いったん式の中で最終的な DynamicMetaObject が生成されたら、この式ツリーおよび制約が、そのバインディングを開始したコールサイトへ返されます。そうすると、このコードは、そのコールサイト キャッシュの中に格納することができ、他の動的呼び出しと同等の速度で、そして静的呼び出しとほぼ同等の速度で、そのオブジェクト上の操作を行うことができるのです。

動的言語上の動的な処理を実行したいホスト言語は、そのバインダーを DynamicMetaObjectBinder から派生しなければなりません。この DynamicMetaObjectBinder は、まずターゲット オブジェクトに対して処理のバインドを要求して(これは前述の GetMetaObject() の呼び出しとそれ以降のバインディングのプロセスによります)、それからホスト言語のバインディングのセマンティクスにフォールバックします。つまり、たとえば IronRubyのオブジェクトがIronPythonのプログラムからアクセスされる場合、このバインディングはまずRuby(ターゲット言語)のセマンティクスを試みられ、もしそれが失敗したら、 この DynamicMetaObjectBinder はPython(ホスト言語)のセマンティクスにフォールバックしてきます。もしバインドされるオブジェクトが動的でなかった場合(つまり IDynamicMetaObjectProvider を実装していない場合)、たとえば.NETの基本クラスライブラリのクラスであった場合、これは.NETのリフレクションを用いて、ホスト言語のセマンティクスでアクセスされます。

言語側でこれをどのように実装するかについては、多少の自由があります。IronPythonの PythonInvokeBinderInvokeBinder から派生していません。Pythonオブジェクトに固有の追加処理が必要になるためです。Pythonオブジェクトのみを扱っている限り、これは問題ありません。もし IDynamicMetaObjectProvider を実装しているが Python のものではないオブジェクトに遭遇した場合、これは InvokeBinder を継承していて外部のオブジェクトも正常に処理できる CompatibilityInvokeBinder クラスに、処理を委譲します。

フォールバックによって処理をバインド出来なかった場合、例外は投げられません。代わりに、エラーを表す DynamicMetaObject が返されます。そうしたら、ホスト言語のバイダーは、そのホスト言語なりの適切な手法に基づいて、これを扱います。たとえば、仮定的なJavaScript実装において、IronPythonのオブジェクト上に対して、存在しないメンバーにアクセスしようとした場合は undefined が返されることになり、同様にIronPythonからJavaScriptのオブジェクトに対して同様の操作を行うと AttributeError が発生することになるでしょう。

8.9. ホスティング

DLRは、言語共通の実装の詳細に加えて、共有されたホスティング インターフェースも提供しています。このホスティング インターフェースは、ホスト言語によって(通常はC#のような静的言語です)、PythonやRubyといった別の言語で書かれたコードを実行するために使用されます。これは、ユーザーがアプリケーションを拡張できるようにするための一般的なテクニックであり、DLRはこれをさらに進めて、DLR実装を有する任意のスクリプト言語を簡単に使用できるようにしました。このホスティング インターフェースには、4つの主要な部品があります。ランタイムエンジン>ソース、>そしてスコープです。

ScriptRuntimeは、一般的には、ひとつのアプリケーション中のほぼ全ての動的言語で共有されます。このランタイムは、ロードされている言語で使用されている全ての現在利用可能なアセンブリ参照を扱い、ファイルのクイック実行を行うメソッドを提供し、新しいエンジンを作成するメソッドを提供します。単純なスクリプティングのタスクには、このランタイムのみが必要なインターフェースとなりますが、DLRではスクリプトの実行方法をより柔軟に制御できるようにするクラスも提供します。

通常、ひとつのスクリプト言語にはひとつの ScriptingEngine が使用されます。DLRのメタオブジェクトプロトコルは、ひとつのプログラムが複数の言語からのスクリプトをロードでき、それぞれの言語で作成されたオブジェクトがシームレスに相互運用できる、ということを意味します。このエンジンは、言語固有の LanguageContext (たとえば PythonContextRubyContext )をラップして、ファイルや文字列からコードを実行して、DLRをネイティブでサポートしない言語(たとえば.NET 4以前のC#)から、動的オブジェクトの処理を行うために使用されます。エンジンはスレッドセーフであり、各スレッドが独立したスコープにある限り、複数のスクリプトを同時に実行できます。スクリプトソースを作成するメソッドも提供されており、これによってスクリプトの実行をより細かく制御できるようになっています。

ScriptSource は、実行されるコード片を表します。これは、実際のコードを保持する SourceUnit オブジェクトを、そのソースを作成した ScriptEngine にバインドします。このクラスは、コードをコンパイルするか(そうすると CompiledCode オブジェクトが出力され、キャッシュ可能になります)、直接実行するかを選べるようにします。もしこのコード片が繰り返し実行されるようであれば、まずコンパイルして、そのコンパイル済みコードをスクリプト上で実行するのがベストです。一度しか実行されないコードは、直接実行するのがベストです。

しかし、最終的にコードが実行される際には、ScriptScope が実行されるコードについて提供されなければなりません。このスコープは、スクリプトの全変数を保持し、もし必要であれば、変数と合わせてホストから事前ロードできるようになっています。これによって、スクリプトの実行時にホスト側からカスタムオブジェクトが提供できるようになります。たとえば、画像エディタは、そのスクリプトで処理する画像のピクセルデータにアクセスする方法を提供するかもしれません。いったんスクリプトが実行されると、生成されたいかなる変数も、このスコープから読み取れます。スコープのもうひとつの主な用途は、独立化の実現です。これによって、複数のスクリプトが相互に干渉し合うこと無く同時にロードして実行できるようになります。

これらのクラス全てが、言語ではなくDLRから提供されているということが重要です。エンジンによって使用されている LanguageContext のみが、その言語実装に由来しています。この言語コンテキストが、コードのロード、スコープの生成、コンパイル、実行、動的オブジェクトの処理といった、ホスト言語によって必要とされる全ての機能を提供し、残りのDLRホスティングクラスが、それらの機能にアクセスするための便利機能を提供します。これによって、同じホスティングのコードが、任意のDLRベースの言語をホストできるというわけです。

Cで書かれた動的言語実装(オリジナルのPythonやRuby)については、動的言語で書かれていないコードにアクセスするためには、特別なラッパーコードが書かれなければならず、これはサポートするスクリプトごとに行われる必要があります。これを簡単にするSWIGのようなソフトwらは存在しますが、それでもPythonやRubyのスクリプティング インターフェースをプログラムに組み込んで、そのオブジェクトモデルを外部スクリプトによる実行に向けて公開するのは、簡単なことではありません。しかし、.NETのプログラムについては、ランタイムをセットアップして、プログラムのアセンブリをランタイムにロードして、 ScriptScope.SetVariable() を使用してプログラムのオブジェクトをスクリプトから利用可能にする、というだけのことで、スクリプティングが簡単にできます。.NETアプリケーションでスクリプティングのサポートを追加するのは、ほんの僅かな時間で可能であり、DLRの大きなボーナスポイントであります。

8.10. アセンブリ レイアウト

DLRは、CLRの一部とは独立して発展してきたため、CLRに含まれる部分(コールサイト、式ツリー、バインダー、コード生成、動的メタオブジェクト)とIronLanguagesオープンソースプロジェクトに含まれる部分(ホスティング、インタープリター、そしてここで議論しなかったいくつかの部分)が分かれています。CLRに含まれている部分は、IronLanguagesプロジェクトにも Microsoft.Scripting.Core として含まれています。DLRの部分はさらに2つのアセンブリに別れます。Microsoft.Script はホスティングAPIを、 Microsoft.Dynamic はCOM相互運用、インタープリター、その他動的言語の共通部品を、それぞれ含んでいます。

言語自体も、同様に2つに分かれていて、 IronPython.dllIronRuby.dll は言語自体(パーサー、バインダーなど)を実装していて、 IronPython.Modules.dllIronRuby.Libraries.dll は、クラシックPythonおよびRubyの実装において、Cで実装されている標準ライブラリの部分を実装しています。

8.11. ここから得られた教訓

DLRは、静的ランタイム上で構築された動的言語のための言語中立プラットフォームとして有用な例です。ここで高パフォーマンスな動的コードを実現するために用いられたテクニックは、適切に実装するのはトリッキーなものなので、DLRがそのテクニックを引き受けて全ての動的言語の実装で利用できるようにしたのです。

IronPythonおよびIronRubyは、DLR上で言語をビルドする良い例です。実装は近いチームが同時に開発していたために非常に類似していますが、実装にはやはりそれなりの違いが見られます。共同で開発された複数の異なる言語(IronPython、IronRuby、JavaScriptのプロトタイプ、完全に動的なバージョンのVBと言われる謎のVBx)と、C#およびVBのdynamicの機能によって、DLRの設計は開発中に多大なテストを得られることになりました。

IronPython、IronRuby、そしてDLRの、実際の開発は、同時期にMicrosoftで行われていたプロジェクトの大半とは、大きく異なるかたちで行われてきました。非常にアジャイルな反復的開発モデルであり、初日から継続的インテグレーションが稼動していました。これによって、必要に応じて非常に素早く物事を変更することが出来て、DLRをC#のdynamic機能として開発の早い時点で統合できたこともあって、良いことでした。DLRのテストは非常に速く、十数秒程度で終わるものですが、言語のテストを実行するには長い時間がかかります(IronPythonのテストスイートは、並列実行してもだいたい45分かかります)。この部分を改善することで、反復のスピードを改善することができたでしょう。最終的には、これらの反復は現在のDLRの設計に収束され、部分的には非常に複雑になるでしょうが、全体的には両者がきわめて良いかたちに適合することでしょう。

DLRがC#に統合されたことは、DLRの居場所が確保され「目的」をもったという点で、大変に重要なことでしたが、いったんC#のdynamic機能が完了すると、政治的な雰囲気も変わり(たまたま景気動向が変わったこともあり)、Iron言語は社内で急速に支持を失って行きました。たとえば、ホスティングAPIが.NET Frameworkに統合されることはありませんでした(そしてほぼ間違いなく、今後も無いでしょう)。これはつまり、PowerShell 3は、これもまたDLRに基づいているのですが、IronPythonやIronRubyとは全く異なるホスティングAPIの集合を使用し、しかし前述の通りそのオブジェクトは相互運用できる、ということになります。(DLRチームのメンバーの何人かは、IronPythonやIronRubyのホスティングAPIの魅力的な代替品を生み出す、C#のサービスとしてのコンパイラ、コードネーム "Roslyn"を実現するライブラリの仕事に回りました。) しかし、オープンソースライセンスの驚異の力によって、これらは生き残り続け、さらに繁栄し続けることでしょう。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment