聊聊 .NET9 FCall/QCall 调用约定

2024年 3月 11日 72.7k 0

前言

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)的函数,在托管里面的调用。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论