聊聊 .NET9 FCall/QCall 調用約定
前言
FCall/Qcall是托管與非托管之間的調用約定,雙方需要一個契約,以彌合彼此的互相/單向調用。
非托管調用約定
先了解下非托管約定,一般有四種,分別為thiscall,stdcall ,cdecl ,fastcall
thiscall:用特定的寄存器傳遞當前類指針this,由編譯器決定哪個寄存器傳遞this。自身清理堆棧,從右往左傳遞參數。
stdcall:一般用于win32 API函數的傳遞方式,自身清理堆棧,從右往左一次傳參。
cdecl:一般用于微軟古老的MFC框架的類的函數傳遞方式,調用者清理堆棧,從右往左依次傳參。
fastcall :用于快速調用方式,規定前幾個參數用寄存器傳遞,多余的參數用棧來傳遞。比如x64前四個參數rcx,rdx,r8,r9等。自身清理堆棧,從右往左傳參。
FCall
.NET9里面需要在托管和非托管進行相互調用,如果需要調用有效,就必須雙方互有約定。使托管代碼與CLR保持一致。比如FCall會通過一些宏定義打亂堆棧或者寄存器里面的參數進行重新排序,再比如FCall會對返回值,參數,函數名稱進行重新構造。FCall就是做這些的,下面看個例子----函數重構。
例子:
C# code: GC.CollectionCount(0);
定義:
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern int _CollectionCount(int generation, int getSpecialGCCount);
非托管:
FCIMPL2(int, GCInterface::CollectionCount, INT32 generation, INT32 getSpecialGCCount)
{
FCALL_CONTRACT;
_ASSERTE(generation >= 0);
int result = (INT32)GCHeapUtilities::GetGCHeap()->CollectionCount(generation, getSpecialGCCount);
FC_GC_POLL_RET();
return result;
}
FCIMPLEND
一般來說FCall用FCIMPL宏定義開頭,這么做的主要目的是:We align the native code shape to CoreCLR by implementing and using the and macros. These macro are responsible for using correct calling convention and shuffling the order of parameters on the stack. The macros also handle export of undecorated names using the alternatename linker/pragma trick. The downside of the trick is that linker doesn't see the comment pragma if there's no other reference to the .obj file inside a static library. There happened to be exactly two files that have only methods and no other referenced code. As a workaround I added a dummy reference from the .asm files for one function from each of those two files.FCIMPLxFCDECLxFCIMPLx。參考:https://github.com/dotnet/runtime/pull/99430
FCIMPL部分定義:
#define FCIMPL0(rettype, funcname) rettype funcname() { FCIMPL_PROLOG(funcname)
#define FCIMPL1(rettype, funcname, a1) rettype funcname(a1) { FCIMPL_PROLOG(funcname)
#define FCIMPL1_V(rettype, funcname, a1) rettype funcname(a1) { FCIMPL_PROLOG(funcname)
#define FCIMPL2(rettype, funcname, a1, a2) rettype funcname(a1, a2) { FCIMPL_PROLOG(funcname)
#define FCIMPL2VA(rettype, funcname, a1, a2) rettype funcname(a1, a2, ...) { FCIMPL_PROLOG(funcname)
下面代碼:
源碼:FCIMPL2(int, GCInterface::CollectionCount, INT32 generation, INT32 getSpecialGCCount)
宏定義:
#define FCIMPL2(rettype, funcname, a1, a2) rettype funcname(a1, a2) { FCIMPL_PROLOG(funcname)
#define FCIMPLEND FCIMPL_EPILOG(); }
展開如下:
int GCInterface::CollectionCount(int generation,INT32 getSpecialGCCount)
{
//FCIMPL2開頭
FCIMPL_PROLOG(funcname)
//函數主體部分
FCALL_CONTRACT;
_ASSERTE(generation >= 0);
int result = (INT32)GCHeapUtilities::GetGCHeap()->CollectionCount(generation, getSpecialGCCount);
FC_GC_POLL_RET();
return result;
//FCIMPL2結尾
FCIMPL_EPILOG();
}
QCall
QCall一般使用導出標記extern,用托管匹配 CLR調用,運行出結果。調用約定遵循平臺標準.
例子:把長度為len個字節從str復制到desc
[DllImport("QCall", CharSet = CharSet.Unicode)]
private unsafe static extern void Buffer_MemMove(byte* dest, byte* src, [NativeInteger] UIntPtr len);
非托管Qcall
extern "C" void QCALLTYPE Buffer_MemMove(void *dst, void *src, size_t length)
{
QCALL_CONTRACT;
memmove(dst, src, length);
}
總結
簡單點來說FCall意思:調用托管函數的時候,可能會調用非托管,FCall就是從托管調用非托管的C#代碼與CLR之間的約定,約定它們如何調用。
QCall的意思:QCall一般用于非托管導出(extern)的函數,在托管里面的調用。