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

原创 2004年07月07日 20:11:00

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

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

http://www.blogcn.com/user8/flier_lu/index.html?id=2602647&run=.0A0B923 实际上,在 C# 中也提供了隐藏的对 vararg...
  • heliang6291
  • heliang6291
  • 2015年08月11日 16:30
  • 260

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

http://www.blogcn.com/user8/flier_lu/index.html?id=2602611&run=.09D4C2F C++ 语言因为缺省使用 cdecl 调用方式,故...
  • heliang6291
  • heliang6291
  • 2015年08月11日 16:28
  • 263

c语言中可变参数的原理---printf()函数

函数原型: int printf(const char *format[,argument]...)        返 回 值: 成功则返回实际输出的字符数,失败返回-1.  函数说明:    ...
  • tangcong29
  • tangcong29
  • 2014年01月18日 10:28
  • 989

可变参数的使用-printf简单实现

我们在写一个程序的时候,经常用到一些函数,例如printf函数,在我们用的时候觉得并没有什么觉得他很简单啊,我们使用的时候都没有注意过,它其实有很多种调用方法。 例如: 其实这就是可变...
  • zhouchaoya142526
  • zhouchaoya142526
  • 2017年04月23日 11:20
  • 334

C# params可变数量参数

为了将方法声明为可以接受可变数量参数的方法,我们可以使用params关键字来声明数组,如下所示:public static Int32Add(params Int32[] values) { ...
  • qq_23833037
  • qq_23833037
  • 2016年12月10日 23:09
  • 1242

【C语言】printf函数的简单实现(可变参数列表)

stdarg宏: 可变参数列表是通过宏来实现的,这些宏定义于stdarg.h头文件中,它是标准库的一部分。这个头文件声明一个类型va_list 和三个宏va_start、va_arg和va_end。我...
  • liuxiaoqian_
  • liuxiaoqian_
  • 2015年05月14日 22:56
  • 872

【C/C++】利用可变参数模拟实现printf

一、printf 功能 printf()函数是格式化输出函数,一般用于向标准输出设备按规定格式输出信息。 原型 int printf( const char* format , [argument]...
  • qq_31828515
  • qq_31828515
  • 2016年07月27日 15:45
  • 935

php可变参数实现

编者按:最开始想到的办法是利用数组的方式,或者是利用C语言的方法利用宏,不过在网上发现了php的实现方法不同,就把文章转载过来 首先,我们需要编写一个PHP函数来计算两个数的和,可以如下编写: ...
  • u011822516
  • u011822516
  • 2015年08月25日 10:19
  • 504

C语言可变参数函数的使用方法讲解

http://blog.chinaunix.net/uid-21736802-id-1817658.html 本文主要介绍可变参数的函数使用,然后分析它的原理,程序员自己如何对它们实现和封装...
  • zangchaodotcnatgmail
  • zangchaodotcnatgmail
  • 2015年12月28日 11:02
  • 1124

C语言之传递可变参数

C语言之传递可变参数文章链接:知识点 传递int可变参数个数; 传递char类型可变参数个数; 传递logn类型可变参数个数; 传递可变参数个数特例; 新名词记录{stdarg.h, va_list,...
  • qq_16628781
  • qq_16628781
  • 2017年05月24日 22:18
  • 304
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:使用未公开关键字在 C# 中导入外部 printf 等参数数量可变函数
举报原因:
原因补充:

(最多只允许输入30个字)