このエントリはWindows & Microsoft技術 基礎 Advent Calendar 2015の6日目です。
.NETのトラブルシュートをしたり、実装に興味があったり、日々の仕事に疲れた心への癒やしとして、.NETの実装を見たいことがあると思います。このエントリでは、そういった方向けに簡単なガイドを提供します。
.NETの実装は色々あるのですが、この記事では.NET 5向けの実装についての話をします。それ以前のMicrosoft実装については、クラスライブラリ部分のみリファレンスソースとして公開されているので、そちらを参照すると良いでしょう(ランタイムそのもののソースコードは公開されていません)。ライセンスもMITライセンスなので安心です。ただし、クライアント側(Windows FormsやWPF)のコードはgithubには公開されておらず、Microsoftのサイトで公開されており、そのライセンスはMicrosoft Reference Source Licenseとなっていますので、デバッグや相互運用性の向上を目的としない使用には注意が必要です。XamarinやUnityで使っているMonoについては、素直にMonoのソースツリーを見ればいいでしょう。ただし、クラスライブラリはMITライセンスですが、ランタイムはLGPLなので、人によっては注意が必要かもしれません。
この記事で紹介する.NET 5(.NET Core)のソースコードは、ランタイムとライブラリのコア部分がCoreCLRとして https://github.com/dotnet/coreclr に、クラスライブラリ全般がCoreFXとして https://github.com/dotnet/corefx にあります。
なお、この後、.NET 5/.NET CoreのことはCoreCLRと書きます。また、クラスライブラリなどC#で書かれている部分をマネージド、ランタイムのC++やアセンブラで書かれている部分をアンマネージドと言っていきます。
.NETのソースコードは、C#またはC++(一部アセンブラ)で記述され、githubに置いてあります。なので、最初に必要なツールは、モダンなWebブラウザかgitクライアントです。gitクライアントでソースコードで落としてローカルで見る場合、エディタがあるといいでしょう。ランタイム側のC++コードは、ソースコードのサイズが大きくなりがちなので、githubのWebサイトでは構文のハイライトが効かなかったり、そもそも閲覧できなかったりするので、ランタイムを追う場合は、3.5万行のC++ファイルをストレスなく閲覧できるエディタがあるといいでしょう。
さて、CoreCLRのソースコードには、マネージド側にもアンマネージド側にも、#ifdef
や#if
が至るところに存在しています。CoreCLRのソースコードは、基本的に既存の.NETの実装(CLRやSilverlight等)とコードを共有したまま公開されているようで、以下のようなコンパイラ定数を使用して、ビルドされるコードを分けています。
コンパイル定数 | 補足 |
---|---|
SILVERLIGHT | |
APPX_MODEL | Windowsランタイム用の.NET向け |
FEATURE_CORE_CLR | CoreCLR向け |
FEATURE_REMOTING | .NET Remotingのインフラストラクチャ向け。 透過プロキシやMarshalByRefObject はもちろん、 CallContext /LogicalCallContext や、 Context も含まれる |
FEATURE_ASYNC_IO | Taskベースの非同期I/O |
他にもいろいろありますが、上記あたりをおさせておけばいいでしょう。
私自身全体を見れているわけではありませんが、知っている情報だけはシェアしようと思います。
- クラスライブラリ
コアとなるライブラリは https://github.com/dotnet/coreclr/tree/master/src/mscorlib/src にあります。ここにないクラスは、.NET Coreにそもぞも存在しない(コードアクセスセキュリティや.NET Remoting関連、クライアントGUI系など)か、corefx側にあるかのいずれかです。.NET Coreに含まれていないクラスライブラリについては、前述のリファレンスソースリポジトリを見てみてください。
さて、CoreCLRにある、つまりデスクトップCLRではmscorlibに実装されているクラスライブラリのソースを読むに当たっての知識をいくつか紹介します。これらのコア型はランタイムの実装と密接に関連していたり、歴史的な理由(昔はマネージド実装とアンマネージド実装の性能差が大きかった)により、以下のようにアンマネージドコードと連携するものがあります。
- ランタイム内のアンマネージドコードからも参照されるものがあります。たとえば、
System.Object
、System.String
、System.Threading.Thread
などです。これらの型にはアンマネージド側のカウンターパートがあり、それらは https://github.com/dotnet/coreclr/tree/master/src/vm/object.h 、object.ink、またはobject.cppにあります。 - 以下のように、メソッドによっては実装がアンマネージド側にあります。
[MethodImpl(MethodImplOptions.InternalCall)]
で修飾されているextern
メソッド(FCall)。たとえば、String.InternalMarvin32HashString
。[DllImport(JitHelpers.QCall)]
で修飾されているextern
メソッド(QCall)。たとえば、String.InternalUseRandomizedHashing
。 これらは https://github.com/dotnet/coreclr/tree/master/src/vm 以下にあるアンマネージドコード内のどこかで実装されています。ただし、マネージド側のメソッドシグネチャとアンマネージド側のシグネチャは必ずしも一致していません。ではどうやって探すのかというと、vm/ecalllist.h にマッピングあるので、マネージド側のextern
メソッドの名前をキーにして検索して、目的となるアンマネージド関数を調べることになります。たとえば、上記のString
のメソッド(いずれもString.GetHashCode()
の実装です)は以下のようになっています。
- ランタイム内のアンマネージドコードからも参照されるものがあります。たとえば、
FCFuncElement("InternalMarvin32HashString", COMString::Marvin32HashString)
QCFuncElement("InternalUseRandomizedHashing", COMString::UseRandomizedHashing)
この場合、COMString
アンマネージドクラスのMarvin32HashString
とUseRandomizedHashing
に実体があるので、これらを検索することになります。前述のように、関数名が一致しているとは限りません。たとえば、
FCFuncElement("nativeCompareOrdinalIgnoreCaseWC", COMString::FCCompareOrdinalIgnoreCaseWC)
のようになっている場合があります。 なお、FCall、QCall、ECallについては、Book of the Runtime(日本語訳)に説明があります。
GCは https://github.com/dotnet/coreclr/tree/master/src/gc にその実体があります。実際にGCの実装を追うときには、GCに必要な情報やGC呼び出しそのものを埋め込むJITコンパイラや、スレッドの動作を制御する実行エンジンのコードも調べる必要があるでしょうが、『ガベージコレクションのアルゴリズムと実装』などで華麗に省かれていたCLRの実装についての正確な情報(ソース)がMITライセンスでここにあります。 さて、このGC、開発者のblogやBoTR(日本語訳)にも情報はありますが、コードを追うにもどこから手を付けていいものやらという話になります。なにしろ、GCの処理が実装されているgc.cppは3.5万行もあるからです。ところが、https://github.com/dotnet/coreclr/blob/master/src/gc/sample/ ディレクトリの下に、JITコンパイラが出力するGC関連のコードをC++で書き直したようなサンプルがあります。GCを追うにはからこの辺りから入っていくといいのではないでしょうか。たとえば、オブジェクトの割り当て処理は以下のようになります(BoTRのGCの記事に書いてありますが、可能であればスレッドローカルなメモリを確保し、ダメなときにスレッド間で共有されるGCヒープからメモリを確保しています)。
Object * AllocateObject(MethodTable * pMT)
{
alloc_context * acontext = GetThread()->GetAllocContext();
Object * pObject;
size_t size = pMT->GetBaseSize();
uint8_t* result = acontext->alloc_ptr;
uint8_t* advance = result + size;
if (advance <= acontext->alloc_limit)
{
acontext->alloc_ptr = advance;
pObject = (Object *)result;
}
else
{
pObject = GCHeap::GetGCHeap()->Alloc(acontext, size, 0);
if (pObject == NULL)
return NULL;
}
pObject->RawSetMethodTable(pMT);
return pObject;
}
それでは、happy reading!