Flier's Sky

天空,蓝色的天空,眼睛看不到的东西,眼睛看得到的东西

导航

http://www.blogcn.com/user8/flier_lu/index.html?id=2602647&run=.0A0B923

实际上,在 C# 中也提供了隐藏的对 vararg 类型方法定义和调用的支持,那就是 __arglist 关键字。
以下内容为程序代码:

public class UndocumentedCSharp
{
  [DllImport("msvcrt.dll", CharSet=CharSet.Ansi, CallingConvention=CallingConvention.Cdecl)]
  extern static int printf(string format, __arglist);

  public static void Main(String[] args)
  {
    printf("%s %d", __arglist("Flier Lu", 1024));
  }
}

    可以看到 __arglist 关键字实际上起到了和 C++ 中 va_list 类似的作用,直接将任意多个参数按顺序压入堆栈,并在调用时处理。而在 IL 代码一级,则完全类似于上述 IL 汇编和 Managed C++ 的例子:
以下内容为程序代码:

.method private hidebysig static pinvokeimpl("msvcrt.dll" ansi cdecl)
        vararg int32  printf(string format) cil managed preservesig
{
}

.method public hidebysig static void  Main(string[] args) cil managed
{
  IL_0033:  ldstr      "%s %d"
  IL_0038:  ldstr      "Flier Lu"
  IL_003d:  ldc.i4     0x400
  IL_0042:  call       vararg int32 UndocumentedCSharp::printf(string,
                                                               ...,
                                                               string,
                                                               int32)
}

    __arglist 除了可以用于与现有代码进行互操作,还可以在 C# 内作为与 params 功能上等同的特性来使用。只不过因为没有 C# 编译器在语义一级的支持,必须用相对复杂的方式进行操作。
以下内容为程序代码:

using System;
using System.Runtime.InteropServices;

public class UndocumentedCSharp
{
  private static void Show(__arglist)
  {
    ArgIterator it = new ArgIterator(__arglist);

    while(it.GetRemainingCount() >0)
   {
   TypedReference tr = it.GetNextArg();

   Console.Out.WriteLine("{0}: {1}", TypedReference.ToObject(tr), __reftype(tr));
   }
  }

  public static void Main(String[] args)
  {
    Show(__arglist("Flier Lu", 1024));
  }
}

    与 C++ 中不同,__arglist 参数不需要一个前导参数来确定其在栈中的起始位置。
    ArgIterator则是一个专用迭代器,支持对参数列表进行单向遍历。对每个参数项,GetNextArg 将会返回一个 TypedReference 类型,表示指向参数。
    要理解这里的实现原理,就必须单独先介绍一下 TypedReference 类型。
    我们知道 C# 提供了很多 CLR 内建值类型的名称映射,如 Int32 在 C# 中被映射为 int 等等。但实际上有三种 CLR 类型并没有在 C# 中被映射为语言一级的别名:IntPtr, UIntPtr 和 TypedReference。这三种类型在 IL 一级分别被称为 native int、native unsigned int 和 typedref。但在 C# 一级,则只能通过 System.TypedReference 类似的方式访问。而其中就属这个 TypedReference 最为奇特。
    TypedReference 在 MSDN 中的描述如下:
以下为引用:

    Describes objects that contain both a managed pointer to a location and a runtime representation of the type that may be stored at that location.

[CLSCompliant(false)]
public struct TypedReference

Remarks

    A typed reference is a type/value combination used for varargs and other support. TypedReference is a built-in value type that can be used for parameters and local variables.
    Arrays of TypedReference objects cannot be created. For example, the following call is invalid:

Assembly.Load("mscorlib.dll").GetType("System.TypedReference[]");


    也就是说,值类型 TypedReference 是专门用于保存托管指针及其指向内容类型的,查看其实现代码(bclsystemTypedReference.cs:28)可以验证这一点:
以下内容为程序代码:

public struct TypedReference
{
private int Value;
private int Type;

// 其他方法
}

    这儿 Value 保存了对象的指针,Type 保存了对象的类型句柄。
    使用的时候可以通过 __arglist.GetNextArg() 返回,也可以使用 __makeref 关键字构造,如:
以下内容为程序代码:

int i = 21;

TypedReference tr = __makeref(i);

    而其中保存的对象和类型,则可以使用 __refvalue 和 __reftype 关键字来获取。
以下内容为程序代码:

int i = 32;

TypedReference tr1=__makeref(i);

Console.Out.WriteLine("{0}: {1}", __refvalue(tr, int), __reftype(tr1));

    注意这儿的 __refvalue 关键字需要指定目标 TypedReference 和转换的目标类型,如果结构中保存的类型不能隐式转换为目标类型,则会抛出转换异常。相对来说,TypedReference.ToObject 虽然要求强制性 box 目标值,但易用性更强。

    从实现角度来看,__refvalue 和 __reftype 是直接将 TypedReference 的内容取出,因而效率最高。
以下内容为程序代码:

int i=5;
TypedReference tr = __makeref(i);
Console.Out.WriteLine("{0}: {1}", __refvalue(tr, int), __reftype(tr));

    上面这样一个代码片断,将被编译成:
以下内容为程序代码:

  IL_0048:  ldc.i4.5
  IL_0049:  stloc.0
  IL_004a:  ldloca.s   V_0
  IL_004c:  mkrefany   [mscorlib]System.Int32
  IL_0051:  stloc.1
  IL_0052:  call       class [mscorlib]System.IO.TextWriter [mscorlib]System.Console::get_Out()
  IL_0057:  ldstr      "{0}: {1}"
  IL_005c:  ldloc.1
  IL_005d:  refanyval  [mscorlib]System.Int32
  IL_0062:  ldind.i4
  IL_0063:  box        [mscorlib]System.Int32
  IL_0068:  ldloc.1
  IL_0069:  refanytype
  IL_006b:  call       class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
  IL_0070:  callvirt   instance void [mscorlib]System.IO.TextWriter::WriteLine(string,
                                                                               object,
                                                                               object)

    可以看到 __makeref、__refvalue 和 __reftype 是通过 IL 语言的关键字 mkrefany、refanyval 和 refanytype 直接实现的。而这样的实现是通过直接对堆栈进行操作完成的,无需 TypedReference.ToObject 那样隐式的 box/unbox 操作,故而效率最高。
    JIT 中对 refanyval 的实现(fjit jit.cpp:8361)如下:
以下内容为程序代码:

FJitResult FJit::compileCEE_REFANYTYPE()
{

    // There should be a refany on the stack
    CHECK_STACK(1);
    // There has to be a typedref on the stack
    // This should be a validity check according to the spec, because the spec says
    // that REFANYTYPE is always verifiable. However, V1 .NET Framework throws verification exception
    // so to match this behavior this is a verification check as well.
    VERIFICATION_CHECK( topOpE() == typeRefAny );
    // Pop off the Refany
    POP_STACK(1);
    _ASSERTE(offsetof(CORINFO_RefAny, type) == sizeof(void*));      // Type is the second thing

    emit_WIN32(emit_POP_I4()) emit_WIN64(emit_POP_I8());            // Just pop off the data, leaving the type.

    CORINFO_CLASS_HANDLE s_TypeHandleClass = jitInfo->getBuiltinClass(CLASSID_TYPE_HANDLE);
    VALIDITY_CHECK( s_TypeHandleClass != NULL );
    pushOp(OpType(typeValClass, s_TypeHandleClass));
    return FJIT_OK;
}

    从以上代码可以看到,JIT 在处理 refanyval 指令时,并没有对堆栈内容进行任何操作,而是直接操作堆栈。

    如果希望进一步了解相关信息,可以参考以下介绍:

    Undocumented C# Types and Keywords

    Undocumented TypedReference

    A Sample Chapter from C# Programmers Reference - Value types

ps: 实测了一下发现,MS不公开 vararg 这种调用方式,大概是因为考虑效率方面的原因。与 params 相比,使用 vararg 的调用方式,纯粹函数调用的速度要降低一个数量级 :(
    下面这篇文章也讨论了这个问题,结论是不到万不得已情况下尽量少用,呵呵

    Why __arglist is undocumented