CSDN博客

img flier_lu

使用未公开关键字在 C# 中导入外部 printf 等参数数量可变函数

发表于2004/7/7 20:11:00  1560人阅读

分类: .NET

http://www.blogcn.com/user8/flier_lu/index.html?id=2602611
http://www.blogcn.com/user8/flier_lu/index.html?id=2602647

    C++ 语言因为缺省使用 cdecl 调用方式,故而可以很方便实现参数可变参数。详细的原理可以参考我另外一篇文章《The history of calling conventions》。具体到使用上,就是我们最常用的 printf 系列函数:

以下内容为程序代码:

int printf(const char *format, ...);

    对应到 C# 中,则是通过 params 关键字模拟类似的语法:
以下内容为程序代码:

using System;
public class MyClass
{
   public static void UseParams(params int[] list)
   {
      for ( int i = 0 ; i < list.Length ; i++ [img]/images/wink.gif[/img]
         Console.WriteLine(list[i]);
      Console.WriteLine();
   }

   public static void UseParams2(params object[] list)
   {
      for ( int i = 0 ; i < list.Length ; i++ [img]/images/wink.gif[/img]
         Console.WriteLine(list[i]);
      Console.WriteLine();
   }

   public static void Main()
   {
      UseParams(1, 2, 3);
      UseParams2(1, 'a', "test"[img]/images/wink.gif[/img];

      int[] myarray = new int[3] {10,11,12};
      UseParams(myarray);
   }
}

    可以看到,这个 params 关键字实际上是将传递数组的语义,在 C# 编译器一级做了语法上的增强,以模拟 C++ 中 ... 的语法和语义。在 IL 代码一级仔细一看就一目了然了。
以下内容为程序代码:

.class public auto ansi beforefieldinit MyClass extends [mscorlib]System.Object
{
  .method public hidebysig static void  UseParams(int32[] list) cil managed
  {
    //...
  }

  .method public hidebysig static void  UseParams2(object[] list) cil managed
  {
    //...
  }

  .method public hidebysig static void  Main() cil managed
  {
    .entrypoint
    // Code size       93 (0x5d)
    .maxstack  3
    .locals init (int32[] V_0,
             int32[] V_1,
             object[] V_2)
    IL_0000:  ldc.i4.3
    IL_0001:  newarr     [mscorlib]System.Int32 // 构造一个 size 为 3 的 int 数组
    //...
    IL_0014:  call       void MyClass::UseParams(int32[])
    //...
  }
}

    这种 syntax sugar 在 C# 这个层面来说应该是足够满足需求了的,但如果涉及到与现有 C++ 代码的交互等问题,其模拟的劣势就暴露出来了。例如前面所提到的 printf 函数的 signature 就不是使用模拟语法的 params 能够处理的。MSDN 中给出的解决方法是:
以下内容为程序代码:

using System;
using System.Runtime.InteropServices;

public class LibWrap
{
  // C# doesn't support varargs so all arguments must be explicitly defined.
  // CallingConvention.Cdecl must be used since the stack is
  // cleaned up by the caller.

  // int printf( const char *format [, argument]... [img]/images/wink.gif[/img]

  [DllImport("msvcrt.dll", CharSet=CharSet.Ansi, CallingConvention=CallingConvention.Cdecl)]
  public static extern int printf(String format, int i, double d);

  [DllImport("msvcrt.dll", CharSet=CharSet.Ansi, CallingConvention=CallingConvention.Cdecl)]
  public static extern int printf(String format, int i, String s);
}

public class App
{
    public static void Main()
    {
        LibWrap.printf(" Print params: %i %f", 99, 99.99);
        LibWrap.printf(" Print params: %i %s", 99, "abcd"[img]/images/wink.gif[/img];
    }
}

    通过定义多个可能的函数原型,来枚举可能用到的形式。这种实现方式感觉真是 dirty 啊,用中文形容偶觉得“龌龊”这个词比较合适,呵呵。

    但是实际上 C# 或者说 CLR 的功能绝非仅此而已,在 CLR 一级实际上早已经内置了处理可变数量参数的支持。
    仔细查看 CLR 的库结构,会发现对函数的调用方式实际上有两种描述:
以下内容为程序代码:

namespace System.Runtime.InteropServices
{
  using System;

[Serializable]
public enum CallingConvention
  {
    Winapi          = 1,
    Cdecl           = 2,
    StdCall         = 3,
    ThisCall        = 4,
    FastCall        = 5,
  }
}

namespace System.Reflection
{
using System.Runtime.InteropServices;
using System;

  [Flags, Serializable]
  public enum CallingConventions
  {
   Standard   = 0x0001,
   VarArgs   = 0x0002,
   Any     = Standard | VarArgs,
    HasThis       = 0x0020,
    ExplicitThis  = 0x0040,
  }
}

    System.Runtime.InteropServices.CallingConvention 是在使用 DllImport 属性定义外部引用函数时用到的,故而使用的名字都是与现有编程语言命名方式类似的。而 System.Reflection.CallingConventions 则是内部用于 Reflection 操作的,故而使用的名字是直接与 CLR 中方法定义对应的。
    这儿的 CallingConventions.VarArgs 正是解决我们问题的关键所在。在随 .NET Framework SDK 提供的 Tool Developers Guide 中,Partition II Metadata.doc 文档中是这样介绍 VarArgs 调用方式的:

以下为引用:

vararg Methods

    vararg methods accept a variable number of arguments.  They shall use the vararg calling convention (see Section 14.3).
    At each call site, a method reference shall be used to describe the types of the actual arguments that are passed.  The fixed part of the argument list shall be separated from the additional arguments with an ellipsis (see Partition I).
    The vararg arguments shall be accessed by obtaining a handle to the argument list using the CIL instruction arglist (see Partition III). The handle may be used to create an instance of the value type System.ArgIterator which provides a typesafe mechanism for accessing the arguments (see Partition IV).




以下内容为程序代码:

[b]Example (informative): [/b]

    The following example shows how a vararg method is declared and how the first vararg argument is accessed, assuming that at least one additional argument was passed to the method:

.method public static vararg void MyMethod(int32 required) {
.maxstack 3
.locals init (valuetype System.ArgIterator it, int32 x)
ldloca it // initialize the iterator
initobj  valuetype System.ArgIterator
ldloca it
arglist // obtain the argument handle
call instance void System.ArgIterator::.ctor(valuetype System.RuntimeArgumentHandle) // call constructor of iterator
/* argument value will be stored in x when retrieved, so load
   address of x */
ldloca x
ldloca it
// retrieve the argument, the argument for required does not matter
call instance typedref System.ArgIterator::GetNextArg()
call object System.TypedReference::ToObject(typedref) // retrieve the object
castclass System.Int32 // cast and unbox
unbox int32
cpobj int32 // copy the value into x
// first vararg argument is stored in x
ret
}


    可以看到在 CLR 一级实际上是提供了对参数数目可变参数的支持的,只不过 C# 的 params 关键字因为某些原因并没有使用。而如果你考察 Managed C++ 的实现,就会发现其正是使用这个机制。
以下内容为程序代码:

// cl /clr param.cpp

#include <stdio.h>
#include <stdarg.h>

void show(const char *fmt, ...)
{
  va_list args;

  va_start(args, fmt);

  vprintf(fmt, args);

  va_end(args);
}

int main(int argc, const char *argv[])
{
  show("%s %d", "Flier Lu", 1024);
}

    编译成 Managed 代码后,其函数 signature 如下:
以下内容为程序代码:

.method public static pinvokeimpl(/* No map */)
        vararg void modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl)
        show(int8 modopt([Microsoft.VisualC]Microsoft.VisualC.NoSignSpecifiedModifier) modopt([Microsoft.VisualC]Microsoft.VisualC.IsConstModifier)* A_0) native unmanaged preservesig
{
  //...
}

   实际上,在 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

0 0

相关博文

我的热门文章

img
取 消
img