Flier's Sky

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

导航

http://www.blogcn.com/user8/flier_lu/index.html?id=2602611&run=.09D4C2F

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
{
  //...
}