mono での NewGuid と RngCryptoServiceProvider
mono において Guid.NewGuid() で Guid がどうやって生成されているのかが気になったので、せっかくソースコードが公開されていることであるし、読んでみた。
また、NewGuid の実装には後述の通り RngCryptoServiceProvider が使われているので、結果として mono における乱数生成機(RNG)の造りも調べることになったという次第。
なお、全部丁寧に解説しようとするとモチベーションが折れるので、気分に応じてのおおざっぱな解説である。
前知識
Guid とは: GUID - Wikipedia
UUID とは: UUID - Wikipedia
Wikipedia を読めば分かるが、UUID や Guid は、
- 管理するサーバーなどなしに、一つのマシンの中だけで簡潔に生成できる
- (現実的な条件下では、ほぼ間違いなく)全世界において一意
という性質があり、Microsoft 関連でないオープンソースソフトウエアなどでも、すでに陰でかなり使われている代物である。
んで、要するに便利な一意 ID なのでもっと使おうよ、と思うのだが、やはり都合の良い話をするからには実装の詳細も知りたいよね、というのがコードを読んだきっかけ。
なお、.net における System.Guid は、GUID という名前だけど実際には UUID 値ならなんでも保持できる(mono なり .net framework なりのソースコードからも分かる)。
ここで、GUID は UUID に含まれる概念で、ある特定の生成方法によって作られた UUID が Guid である。が、実際には UUID なのに Guid と呼んでしまうことも少なくないし、.net framework / mono もその一例なのである。
UUID の生成手法は RFC 4122 に規定があり、おおざっぱに言うと
のどれかである*1。ちなみに、どの手法で生成されたのかは、UUID の先頭の方のビットで分かるようにもなっている。
mono のソースコードの流れ
で、mono での Guid の生成の流れを追っかけてみたのだが、以下に概要を示す。上から順にコードの流れで、付加されているソースコードは mono の svn head へのものである。
- static Guid.NewGuid()
- static RandomNumberGenerator.Create()
- static CryptoConfig.CreateFromName(string)
- RNGCryptoServiceProvider.GetBytes(byte[])
- RngOpen(), RngInitialize(byte), RngGetBytes(IntPtr, byte)
以下では、それぞれについて気になった点などをハイライトしてみた・・・つもりが、実際にはほとんど順をおった解説になってしまった。これだから現実逃避でコードを読むのは(ry
各論の前に: 長くなってしまったので概略
メモ: 自分への宿題 (= 読んだはいいが分からなかった点)
- mono のクラスライブラリ実装で、static readonly による singleton よりも、lock + if なシングルトンが用いられているのはなぜなのでしょう
- mono が libuuid を使わずに独自実装したのはなぜなのでしょう
- 非 Win32 環境の場合、RNGCryptoServiceProvider が AppDomain 単位で lock するのに対して、ファイルデスクリプタはプロセス内で一つ、とスレッドセーフ性*2に問題があるように見えるのは、実際どうなのでしょう
- http://www.linux.or.jp/JM/html/LDP_man-pages/man7/pthreads.7.html によると、read(2) は pthread-safe なようだ。
- むしろ、こうもしつこく lock している理由が気になる。
- 使っているインターフェース(型)のスレッドセーフ性が分からないから、なのかな?*3
*1:正確には他にもあったりするが、どうでもいいので続きは RFC で。
*2:AppDomain セーフ性?
*3:にしても、パフォーマンス上のオーバーヘッドが小さくない気がするのだがどうなのだろう。
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 は
- pointers to methods (first cache)
- 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。
ただし、現状では全ての関連コードを洗ったわけではないので、所によってはまだ読んでいないものがある。
自分への宿題 (= 不明な点)
ソースコードの流れ
具体的には、Delegate.DynamicInvoke を起点に、以下のようにコードを探索し、DynamicInvoke がどんなものかを見てみた。なお件のバグの原因だけを見るならば後半だけで十分である。
- Delegate.DynamicInvoke(params object[])
- Delegate.DynamicInvokeImpl (object[])
- Invoke(Object, Object[])
- MonoMethod.Invoke(Object, BindingFlags, Binder, Object[], ClutureInfo)
- ves_icall_InternalInvoke(...)
- mono_runtime_invoke_array(...)
- mono_runtime_invoke(...)
- default_mono_runtime_invoke(...)
- static MonoInvokeFunc default_mono_runtime_invoke
- mono_jit_runtime_invoke(...)
- mono_marshal_get_runtime_invoke(...)
これらは、大まかには以下のようになっている。
- 各種前処理をしながら関数呼び出しが深くなってゆく
- 過程で DynamicInvoke や Invoke、Activator.CreateInstance などや、さらには通常のメソッド呼び出しが合流してゆく
- 最終的には default_mono_runtime_invoke に行き着く
- あらゆるメソッド・コンストラクタの呼び出しがここにたどり着く
- 通常の JIT を使っている場合、これは mono_jit_runtime_invoke に相当する
- mono_jit_runtime_invoke では、以下の二つに分けて IL を生成 + コンパイルする (例外もある)
- 呼び出す対象のメソッドそのもの
- 呼び出す対象を呼び出すための各種処理
- 上で得られた二つ(コンパイル済み)を使うことで、コンパイル済みのメソッドを呼び出す、という処理を実現している
以下に、各部を読み解いた過程のメモ(?)を示す。