使用未公開關鍵字在 C# 中導入外部 printf 等參數數量可變函數

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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章