mono Bug 463323, 475962 について調べてみた&動的なメソッド呼び出しの実装を追いかけてみた

最近 C# なコードを仕事でも使う縁に恵まれているのだが、そこで mono の Delegate.DynamicInvoke 関連で興味深いバグがあると聞いたので、調べてみた。また、せっかくなので Delegate.DynamicInvoke や MethodInfo.Invoke などに端を発するメソッド呼び出しの処理が、mono 内部でどのようにして処理されてゆくのかを追いかけてみた。

ref: Bug 463323 – Bug with delegates to dynamic methods
ref: Bug 475962 – exception thrown from CreateDelegate () when compiling Expression returning a delegate

後者(bug 475962)は前者(bug 463323)の調査過程で出た別のバグで、今(2009/03/19)は前者は Closed で後者は New 状態である。

Bug 475962 について

まず軽い方である 475962 について触れる。こちらは、以下のコードを gmcs でコンパイルするとおかしくなるという話である。

Expression<System.Func<Program, Action>> action = (d => d.testFunc);
var t = action.Compile();
Program p = new Program();

// ここで System.ArgumentException: method argument length mismatch
t(p);

これは、Bugzilla の Comment #4 にも書いた*1とおり、gmcs がラムダ式

d => d.testFunc

delegate(Program d){
	return (Action)Delegate.CreateDelegate(
		typeof(Action),
		null,				// ここが d になるはず
		...
	);
}

という風に誤ってコンパイルしてしまうのが原因のようである。

ちなみに、csc.exe でコンパイルすると、.net だろうと mono だろうと動くバイナリを得られることからも、これは C#ソースコードから ExpressionTree を生成するコンパイル処理におけるバグであると分かる(と筆者は考えたので bugzilla に書いてみたのであった)。

ので、csc.exe を使うなり、手で ExpressionTree を書くなりすれば問題ないはずである。

これについては、機会があればパーサやコードジェネレータも読んでみたいとは思う。が、本題はこちらではないのである。

Bug 463323

こちらは、再現可能性が低かったりで苦労されていたようだが、要するに動的に生成した Delegate について DynamicInvoke すると、希に失敗することがあるという話である。

Bug 463323 Comment #9 の原因と解決

この Bug はすでに fixed になっているのだが、その原因については Comment #9 に記述がある。

その記述によると、mono には "invoke wrapper" なるものがあり、その invoke wrappers は

  1. pointers to methods (first cache)
  2. method signatures (second cache)

の二つでキャッシュされるそうである。

ここで、dynamic method に対する "invoke wrapper" を考える。すると、

  • 元の dynamic method が free された
  • ことなる signature を持つ dynamic method が same memory location に配置された

という二つを満たした時に、"invoke wrapper" の想定している method signature と、pointer to methods の指している先のメソッドの method signature が等しくなくなる。

ここで、Bug 463323 のコメントで挙げられている

  • CheckMethodArguments からの ArgumentException
  • ArgumentException: Can not assign a ...
  • ArgumentException: method argument length mismatch
  • MissingMethodException: No constructor found

らは、確かに上の説で説明が付くのである。

この説を受けて、r126958 では、dynamic method が free された際に、二つのキャッシュをクリアするようになったそうである。

r126958 で治した旨を述べている Comment #21

Assemblies are not unloadable, and before net 2.0, neither were methods, lots of code was written assuming this

とある通り、アセンブリやその中にある(dynamic でない)メソッドが unload されないことを前提にしていたのが、dynamic で破棄可能なメソッドという物が入ったことによるバグ、という後知恵で見ればありがちに思えるものだった、とのことである。

ちなみにこの修正は今のところ trunk にしか入っていないが、

The relevant patches have been backported to the 2.4 branch.

とあり、また

We're hoping to release Mono 2.4 RC3 as the final, so only show stopper critical / blocker bugs will be considered for fixes.

な Mono 2.4 RC 3 が 2009/03/17 に出ていることから、もうすぐ出るであろう Mono 2.4 ではこのバグは治っているはずである。

また、Mono 2.4 や svn 上のコードを使わない場合、

  • 動的に Compile したコード(free されうるコード)を
  • Delegate として取得して
  • それを DynamicInvoke する

といった風に、動的なメソッド + ダイナミックな呼び出し という 2 条件が重ならなければ、原理的に問題にならないはずである。

せっかくだから俺はこのコードのリーディングを選ぶぜ*2

で、後知恵で見れば bugzilla の記述だけでも上記のように説明できてハイ良かったね、で済ませなくもないだろうが、ここは一丁ソースコードを追ってみるとする*3

ただし、現状では全ての関連コードを洗ったわけではないので、所によってはまだ読んでいないものがある。

自分への宿題 (= 不明な点)

  • CreateDelegate_internal の中身を見てみる
  • static なメソッドでありながら、Delegate.m_target が非 null になる条件を明らかにする
    • たぶん拡張メソッドの関係だとおもうのだけど。
  • mono_marshal_get_runtime_invoke の中身やその先も見てみる

ソースコードの流れ

具体的には、Delegate.DynamicInvoke を起点に、以下のようにコードを探索し、DynamicInvoke がどんなものかを見てみた。なお件のバグの原因だけを見るならば後半だけで十分である。

  1. Delegate.DynamicInvoke(params object[])
  2. Delegate.DynamicInvokeImpl (object[])
  3. Invoke(Object, Object[])
  4. MonoMethod.Invoke(Object, BindingFlags, Binder, Object[], ClutureInfo)
  5. ves_icall_InternalInvoke(...)
  6. mono_runtime_invoke_array(...)
  7. mono_runtime_invoke(...)
  8. default_mono_runtime_invoke(...)
  9. static MonoInvokeFunc default_mono_runtime_invoke
  10. mono_jit_runtime_invoke(...)
  11. mono_marshal_get_runtime_invoke(...)

これらは、大まかには以下のようになっている。

  1. 各種前処理をしながら関数呼び出しが深くなってゆく
    • 過程で DynamicInvoke や Invoke、Activator.CreateInstance などや、さらには通常のメソッド呼び出しが合流してゆく
  2. 最終的には default_mono_runtime_invoke に行き着く
    • あらゆるメソッド・コンストラクタの呼び出しがここにたどり着く
  3. 通常の JIT を使っている場合、これは mono_jit_runtime_invoke に相当する
  4. mono_jit_runtime_invoke では、以下の二つに分けて IL を生成 + コンパイルする (例外もある)
    1. 呼び出す対象のメソッドそのもの
    2. 呼び出す対象を呼び出すための各種処理
  5. 上で得られた二つ(コンパイル済み)を使うことで、コンパイル済みのメソッドを呼び出す、という処理を実現している

以下に、各部を読み解いた過程のメモ(?)を示す。

Delegate.DynamicInvoke から ves_icall_InternalInvoke まで。

Delegate.DynamicInvoke から MonoMethod.Invoke を経て ves_icall_InternalInvoke に行き着いているが、ここまでは単に処理をたらい回しているだけである。

一応見てみると、DynamicInvoke は

return DynamicInvokeImpl (args);

しているだけで、DynamicInvokeImpl は以下のように、MethodInfo の取得と、引数の数の帳尻あわせをおこなっている。

if (Method == null) {
	// 中略
	method_info = m_target.GetType ().GetMethod (data.method_name, mtypes);
}

if ((m_target != null) && Method.IsStatic) {
	// The delegate is bound to m_target	
	// 略
		object[] newArgs = new object [args.Length + 1];
	// 略
	return Method.Invoke (null, args);
}

return Method.Invoke (m_target, args);

まず先頭の Method の取得についてだが、これは単にメソッドの名前と引数の型から MethodInfo を持ってきているだけである。逆に言うと、インスタンスメソッドを mono で DynamicInvoke すると、Delegate 一つにつき必ず一回 GetMethod が発生することになる*4

なお、先頭のコードは、メソッドが static な場合に m_target が null になるので危ないように見えるが、static なメソッドに対する Delegate の場合、Method フィールドは Delegate 生成時点で CreateDelegate_internal 関数によって埋められているので問題がないようだ。正直ちょっとトリッキーなコードである。

そして後者の引数の数のつじつま合わせがあるのだが、これはいささか興味深い。メソッドが static でない場合は単にたらい回すだけなのだが、static なメソッドでありながらも m_target*5 がある場合、デリゲートの引数群の先頭に m_target が足され、引数が一個増えるのである。おそらく、この m_target を第一引数に付け足す挙動*6は、拡張メソッド(カリー化されたデリゲート)のためにあるのだと思われるが、機会があったら追ってみたいところである。

ちなみに、拡張メソッドの実体は static メソッドであるものの、拡張メソッドの Delegate は普通に作ることができる(ref: 拡張メソッドのデリゲートへの代入)。

ともあれ、こうして Delegate.DynamicInvoke は引数をごにょごにょしつつも MethodInfo.Invoke(object, object[]) 呼び出しに帰着する。ここから ves_icall_InternalInvoke まではほぼ一直線で、ずばり以下のようになる。

MethodBase.cs より

[DebuggerHidden]
[DebuggerStepThrough]		
public Object Invoke(Object obj, Object[] parameters) {
	return Invoke (obj, 0, null, parameters, null);
}

public abstract Object Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture);

MonoMethod.cs (MethodBase を継承) より

[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal extern Object InternalInvoke (Object obj, Object[] parameters, out Exception exc);

public override Object Invoke (Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) {
	// 中略 (Binder の処理やセキュリティの検証など)
	Exception exc;
	object o = null;
	try {
		// The ex argument is used to distinguish exceptions thrown by the icall
		// from the exceptions thrown by the called method (which need to be
		// wrapped in TargetInvocationException).
		o = InternalInvoke (obj, parameters, out exc);
	} // 中略(いくつかの例外は TargetInvocationException にラップしないで透過する)
	} catch (Exception e) {
		throw new TargetInvocationException (e);
	}
	if (exc != null) throw exc;
	return o;
}

このように、MethodInfo.Invoke は、Binder による引数の変換やセキュリティのチェック、Generic 型のチェック*7などを行った上で、InternalInvoke に丸投げされる。

なお、上に引用したコードは #ifdef などを削ってあるが、実際にはいくつもの #ifdef があり、細かい例外の扱いの違いなどを準拠する .net framework のバージョンに応じてちまちま変えてある辺りに苦労を感じられた・・・。

ves_icall_InternalInvoke

こうして Delegate.DynamicInvoke や MethodInfo.Invoke は ves_icall_InternalInvoke に行き着いたわけだが、この ves_icall_InternalInvoke は以下の処理を行っている。

  • Reflection セキュリティ許可のチェック
  • 対象インスタンス(つまり this)のチェック
  • 引数の数のチェック
  • abstract 型のコンストラクションでないかのチェック
  • Reflection での参照専用のアセンブリでないかのチェック
  • 配列コンストラクタ専用の処理
  • 上記を全てパスしたら、mono_runtime_invoke_array に丸投げする

上記には自明でないであろう処理がいくつかあるが、それらは ves_icall_InternalInvoke が .ctor つまりコンストラクタの Invoke にも対応していることに起因している。

実際、ves_icall_InternalInvoke は以下のようなコンストラクタの Invoke に対応している。

  • this を与えられていない状況で、非 abstract 型のコンストラクタ呼び出し
  • this を与えられている状況での任意のコンストラクタ適用
  • 配列型のコンストラクタ呼び出し

これらコンストラクタ周りの処理も見ていると面白いのだが、本筋でないので深入りは避けておく。

mono_runtime_invoke_array

この関数は、基本的には MonoArray* として渡された引数を gpointer* の形に変換してから、mono_runtime_invoke に渡すだけである。

MonoArray**8 として渡された引数の i 番目を gpointer**9 に変換する処理は、

  • 値型なら、その値をアンボクシングして gpointer[i] 上に展開する
  • 参照型なら、ポインタを gpointer[i] に置く
  • Generic な型(MONO_TYPE_GENERICINST)の場合、値の本体が入れ子になっているので、入れ子を展開してリトライ

という単純な処理で実現されている。

なお、その基本的な処理以外には、以下のこともおこなっている。

  • コンストラクタ周りのつじつまあわせ
  • Nullable 型のつじつまあわせ(元になっている型のメンバを使うため)
  • ボクシング/アンボクシングのつじつまあわせ
  • TransprentProxy 専用の処理(Invoke を RealProxy に処理させる)
    • mono_marshal_get_remoting_invoke 関数に丸投げ

mono_runtime_invoke, default_mono_runtime_invoke

こうして大量の前処理*10を経つつも、Delegate.DynamicInvoke なり MethodInfo.Invoke は最終的に mono_runtime_invoke に行き着く。

ここで肝心のソースコードを見てみると、

MonoObject*
mono_runtime_invoke (MonoMethod *method, void *obj, void **params, MonoObject **exc)
{
	if (mono_runtime_get_no_exec ()) g_warning (...);
	return default_mono_runtime_invoke (method, obj, params, exc);
}
static MonoObject*
dummy_mono_runtime_invoke (MonoMethod *method, void *obj, void **params, MonoObject **exc)
{
	g_error ("runtime invoke called on uninitialized runtime");
	return NULL;
}

static MonoInvokeFunc default_mono_runtime_invoke = dummy_mono_runtime_invoke;

といった案配で、実は mono_runtime_invoke は MonoInvokeFunc な関数ポインタに投げるためのラッパーに過ぎないのである。

ここで、static な default_mono_runtime_invoke は、以下の関数によって設定することができる。

void
mono_install_runtime_invoke (MonoInvokeFunc func)
{
	default_mono_runtime_invoke = func ? func: dummy_mono_runtime_invoke;
}

また、default_mono_runtime_invokegrep すると分かるが、mono 内部での例えばプロパティの読み書きといった処理も、必ず default_mono_runtime_invoke を介して行われている。

つまり、オブジェクトに対する操作を具体的にどう実行するのか、を default_mono_runtime_invoke 関数ポインタが抽象化しているのである。

実際、mono のソースコードツリーには、mono の処理を実行するための系として

があるが、そのどれもが MonoInvokeFunc 型を持った関数を持っている(上に示したリンク先のコードで、実際に mono_install_runtime_invoke している様を見ることが出来る)。

mono_jit_runtime_invoke

ここで、今回興味があるのは mono の標準の JIT が動作している環境なので、mini で mono_install_runtime_invoke されている mono_jit_runtime_invoke を見てみる。

mono_jit_runtime_invoke は、おおまかには以下の処理で成り立っている。

  1. 呼び出す対象のメソッドを明らかにする
  2. 呼び出す対象のメソッドを呼び出す処理を行うための runtime_invoke を得る
  3. 呼び出す対象のメソッドをコンパイルしたものを得る
  4. runtime_invoke を使って、コンパイル済みの対象メソッドを呼び出す

具体的には以下の通り。

  1. インスタンスメソッドについて、インスタンスの null チェック
  2. static メソッドに runtime generic context (RGCTX) を渡すための、メソッドのラッピング
    • static メソッドの場合、そのまま関数として呼び出すと、ジェネリック型引数の情報が失われてしまうので
    • mono_method_needs_static_rgctx_invoke および mono_marshal_get_static_rgctx_invoke
  3. SpecialName かつ引数を全く持たないコンストラクタの高速化処置
    • コメントによると、Activator.CreateInstance () を高速化する意図があるそうだ
  4. 普通のメソッドについて、mono_marshal_get_runtime_invoke と mono_jit_compile_method で runtime_invoke を得る
    • 後述
  5. vtable を用いてメソッドを解決する
    • mono_marshal_get_runtime_invoke can be place the helper method in System.Object and not the target class
  6. 呼び出す対象のメソッドを mono_jit_compile_method して compiled_method を得る
  7. runtime_invoke を使って compiled_method を実行する

ここで、メソッドを呼び出す処理を行うための runtime_invoke を得るために、mono_marshal_get_runtime_invoke と mono_jit_compile_method が使われている。

具体的には、mono_marshal_get_runtime_invoke がメソッドを呼び出す処理を行うための MonoMethod* *11を返すので、これを mono_jit_compile_method でコンパイルすることで、メソッドを呼び出す処理をネイティブに行ってくれる関数、すなわち runtime_invoke を得られるのである。

また、呼び出す対象のメソッドを mono_jit_compile_method して得たネイティブな関数へのポインタが compiled_method である。

こうして得た runtime_invoke に、compiled_method を噛ませることで、呼び出したいメソッドを実際に呼び出す、という処理が実行されているのである。

mono_marshal_get_runtime_invoke

ここで、素朴な疑問として、「メソッドを呼び出す処理」をわざわざ IL で生成してコンパイルするという手続きを踏んでいるのはなぜか、というものがあるが、これについて文章に起こそうかとも思ったのだが、疲れてきたのでこれは後日気が向いたら、にしたい。

なお、具体的な処理の内容は http://anonsvn.mono-project.com/viewvc/tags/mono-2-2/mono/mono/metadata/marshal.c?view=markup で見ることが出来る。やっていることは CLI の動作の根幹に関わるものであるが、それほど複雑でも難読でもないので、目を通すだけなら辛くない感じである。ただ、詳細に見てゆくとなかなか勉強になりそうであるので、後日見てみようと思う。

して、具体的な内容を棚上げしたのだが、元々のきっかけだった Bug 463323 (mono_marshal_get_runtime_invoke のキャッシュにゴミが残っているせいで、シグニチャがおかしくなる)についてだが、これは mono_marshal_get_runtime_invoke の先頭で get_cache を二回ほど行っていることから、bugzilla に書かれていた説が確かに発生しうるものであると伺える。

また、mono_marshal_get_runtime_invoke を見ていると、type initializer の関係もあって runtime invoke wrapper の流用には注意が必要である*12といった旨が書かれているといった具合に、runtime invoke wrapper 周りの問題は最適化が絡む故に注意すべき点が少なくないことがよく分かる。

例えば、先述のキャッシュが 2 つの件についても、Bugzilla ではキャッシュが 2 つあってもパフォーマンス的には 1 つと大して変わらないから不要といったことも言われていたが、type initializer の問題*13や、キャッシュのキー*14といったものがあるためにこうなっているのではあるまいか(あくまで筆者の推測)、といった風にパフォーマンスだけではない事情も多々絡んでいるのであろう。

それにしても、そういった慎重さが要求されるほどに、処理系の中枢なのだという気分にもなって読んでいて楽しいものである。

まとめ?

gdgdにコードを読みながら垂れ流しているだけであったが、一応。

  • Bug 463323 の件は Mono 2.4 を待つか、svn head を使うか、動的にコンパイルしたメソッドを DynamicInvoke しないようにしよう。
  • Bug 475962 の件は、csc.exe を使うか、ExpressionTree を手で構築するかしよう。
  • 処理系のコードを読むのは楽しいね!

*1:英語がとっても不安だ・・・というか駄目そうな予感がひしひしと・・・

*2:see: デスクリムゾン - Wikipedia

*3:そこにコードがあるから

*4:Reflection はいかんせん遅い。DynamicInvoke している時点でしょうがないのだけど。

*5:デリゲートの対象となっているインスタンス

*6:これはカリー化になっている

*7:束縛されていない型引数がある場合、当然メソッド呼び出しは出来ない。

*8:System.Object[] みたいなもの。

*9:ネイティブな配列

*10:こうしてみると Dynamic な Invoke が重い理由が何となくではなくて、よーく分かる。

*11:メソッド情報と IL で出来ているようだ

*12:別の型の type initializer を実行する runtime invoke wrapper を使い回してしまうことがありうる

*13:type initializer があるかないかでも、invoke wrapper の扱いが変わりうるはず

*14:シグニチャをキーにするより関数ポインタの方が、キーの計算コストが遙かに小さいはず