アジョブジ星通信

進捗が出た頃に更新されるブログ。

System.Stringのコンストラクタを許すな

というわけで始まりました深夜の CoreCLR ソースコードリーディングのお時間。司会は早くこの記事を書き終えてアニメを見たいazyobuzinがお送りいたします。

普通のコンストラクタ

コンストラクタは、名前「.ctor」、戻り値の型 void で定義されるインスタンスメソッドと考えることができます。そしてオペコード newobj でコンストラクタが指定されると、そのクラスのインスタンスが作成され、第0引数に入れられコンストラクタが呼び出されます。つまり、「メモリ確保 → コンストラクタ呼び出し」という順番になります。

String のコンストラクタ

mscorlib の String クラスのコンストラクタは、すべて [MethodImplAttribute(MethodImplOptions.InternalCall)] が付与されていています(ILでは internalcall 属性, 通称 FCall)。 FCall は ecalllist.hマッピングされた関数を呼び出します。

マネージドメソッドとして定義されたコンストラクタ

では、 String のコンストラクタを呼び出した時に、実際に呼び出される関数を見ていきましょう。まずは FCDynamicSig で定義されているこの5つ。

FCDynamicSig(COR_CTOR_METHOD_NAME, &gsig_IM_ArrChar_RetVoid, CORINFO_INTRINSIC_Illegal, ECall::CtorCharArrayManaged)
FCDynamicSig(COR_CTOR_METHOD_NAME, &gsig_IM_ArrChar_Int_Int_RetVoid, CORINFO_INTRINSIC_Illegal, ECall::CtorCharArrayStartLengthManaged)
FCDynamicSig(COR_CTOR_METHOD_NAME, &gsig_IM_PtrChar_RetVoid, CORINFO_INTRINSIC_Illegal, ECall::CtorCharPtrManaged)
FCDynamicSig(COR_CTOR_METHOD_NAME, &gsig_IM_PtrChar_Int_Int_RetVoid, CORINFO_INTRINSIC_Illegal, ECall::CtorCharPtrStartLengthManaged)
FCDynamicSig(COR_CTOR_METHOD_NAME, &gsig_IM_Char_Int_RetVoid, CORINFO_INTRINSIC_Illegal, ECall::CtorCharCountManaged)

https://github.com/dotnet/coreclr/blob/release/1.0.0/src/vm/ecalllist.h#L214-L218

これらは最後に「Managed」がついてることからわかるように、マネージドメソッドを呼び出します。最初の「CtorCharArrayManaged」に対応するメソッドを探すと String クラスに CtorCharArray メソッドがありました。(ecall.h, ecall.cpp を読むと、 String 型のメンバーを舐めているのがわかります。)

CtorCharArray はインスタンスメソッドで、 String を返します。……は?とりあえず、この戻り値が newobj の実行結果となるようです。

アンマネージドのほう

C++で書かれたメソッドを呼び出すものは FCFuncElementSig で定義されています。

FCFuncElementSig(COR_CTOR_METHOD_NAME, &gsig_IM_PtrSByt_RetVoid, COMString::StringInitCharPtr)
FCFuncElementSig(COR_CTOR_METHOD_NAME, &gsig_IM_PtrSByt_Int_Int_RetVoid, COMString::StringInitCharPtrPartial)

https://github.com/dotnet/coreclr/blob/v1.0.0/src/vm/ecalllist.h#L219-L220

COMString クラスのメンバーの実装は stringnative.cpp にあります。

StringInitCharPtr を見てみると

_ASSERTE(stringThis == 0);      // This is the constructor

なんてコードがありますね。つまり this として null が渡されているようです。ということはマネージドのほうも this は null なのだと思われます。

戻り値はオブジェクトの参照、つまり String です。

まとめると

コンストラクタを internalcall にすると、 FCall で this が null なインスタンスメソッドとして呼び出され、戻り値が生成されたインスタンスとして扱われます。

Decimal のコンストラクタ

他にもコンストラクタが internalcall なものはないかと探していたら Decimal の float, double を引数にとるコンストラクタが internalcall でした。 decimal.cpp の InitSingle と InitDouble が呼び出されるメソッドです。今度は戻り値が void ですね。値型だと戻り値は不要のようです。そもそも値型のコンストラクタは newobj のほかに、普通に call で呼び出すこともあります(コンパイラ依存案件)し、そういうものなのでしょうか。よくわからん。