如何使用未公开关键字在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调用方式,故而可以很方便实现参数可变参数。详细的原理可以参考我另外一篇文章《Thehistoryofcallingconventions》。具体到使用上,就是我们最常用的printf系列函数:
以下内容为程序代码:
intprintf(constchar*format,...); |
|
对应到C#中,则是通过params关键字模拟类似的语法:
以下内容为程序代码:
usingSystem;
publicclassMyClass
{
publicstaticvoidUseParams(paramsint[]list)
{
for(inti=0;i<list.Length;i++[img]/images/wink.gif[/img]
Console.WriteLine(list[i]);
Console.WriteLine();
}
publicstaticvoidUseParams2(paramsobject[]list)
{
for(inti=0;i<list.Length;i++[img]/images/wink.gif[/img]
Console.WriteLine(list[i]);
Console.WriteLine();
}
publicstaticvoidMain()
{
UseParams(1,2,3);
UseParams2(1,'a',"test"[img]/images/wink.gif[/img];
int[]myarray=newint[3]{10,11,12};
UseParams(myarray);
}
} |
|
可以看到,这个params关键字实际上是将传递数组的语义,在C#编译器一级做了语法上的增强,以模拟C++中...的语法和语义。在IL代码一级仔细一看就一目了然了。
以下内容为程序代码:
.classpublicautoansibeforefieldinitMyClassextends[mscorlib]System.Object
{
.methodpublichidebysigstaticvoidUseParams(int32[]list)cilmanaged
{
//...
}
.methodpublichidebysigstaticvoidUseParams2(object[]list)cilmanaged
{
//...
}
.methodpublichidebysigstaticvoidMain()cilmanaged
{
.entrypoint
//Codesize93(0x5d)
.maxstack3
.localsinit(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:callvoidMyClass::UseParams(int32[])
//...
}
} |
|
这种syntaxsugar在C#这个层面来说应该是足够满足需求了的,但如果涉及到与现有C++代码的交互等问题,其模拟的劣势就暴露出来了。例如前面所提到的printf函数的signature就不是使用模拟语法的params能够处理的。MSDN中给出的解决方法是:
以下内容为程序代码:
usingSystem;
usingSystem.Runtime.InteropServices;
publicclassLibWrap
{
//C#doesn'tsupportvarargssoallargumentsmustbeexplicitlydefined.
//CallingConvention.Cdeclmustbeusedsincethestackis
//cleanedupbythecaller.
//intprintf(constchar*format[,argument]...[img]/images/wink.gif[/img]
[DllImport("msvcrt.dll",CharSet=CharSet.Ansi,CallingConvention=CallingConvention.Cdecl)]
publicstaticexternintprintf(Stringformat,inti,doubled);
[DllImport("msvcrt.dll",CharSet=CharSet.Ansi,CallingConvention=CallingConvention.Cdecl)]
publicstaticexternintprintf(Stringformat,inti,Strings);
}
publicclassApp
{
publicstaticvoidMain()
{
LibWrap.printf(" Printparams:%i%f",99,99.99);
LibWrap.printf(" Printparams:%i%s",99,"abcd"[img]/images/wink.gif[/img];
}
} |
|
通过定义多个可能的函数原型,来枚举可能用到的形式。这种实现方式感觉真是dirty啊,用中文形容偶觉得“龌龊”这个词比较合适,呵呵。
但是实际上C#或者说CLR的功能绝非仅此而已,在CLR一级实际上早已经内置了处理可变数量参数的支持。
仔细查看CLR的库结构,会发现对函数的调用方式实际上有两种描述:
以下内容为程序代码:
namespaceSystem.Runtime.InteropServices
{
usingSystem;
[Serializable]
publicenumCallingConvention
{
Winapi=1,
Cdecl=2,
StdCall=3,
ThisCall=4,
FastCall=5,
}
}
namespaceSystem.Reflection
{
usingSystem.Runtime.InteropServices;
usingSystem;
[Flags,Serializable]
publicenumCallingConventions
{
Standard =0x0001,
VarArgs =0x0002,
Any =Standard|VarArgs,
HasThis=0x0020,
ExplicitThis=0x0040,
}
} |
|
System.Runtime.InteropServices.CallingConvention是在使用DllImport属性定义外部引用函数时用到的,故而使用的名字都是与现有编程语言命名方式类似的。而System.Reflection.CallingConventions则是内部用于Reflection操作的,故而使用的名字是直接与CLR中方法定义对应的。
这儿的CallingConventions.VarArgs正是解决我们问题的关键所在。在随.NETFrameworkSDK提供的ToolDevelopersGuide中,PartitionIIMetadata.doc文档中是这样介绍VarArgs调用方式的:
以下为引用:
varargMethods
varargmethodsacceptavariablenumberofarguments.Theyshallusethevarargcallingconvention(seeSection14.3).
Ateachcallsite,amethodreferenceshallbeusedtodescribethetypesoftheactualargumentsthatarepassed.Thefixedpartoftheargumentlistshallbeseparatedfromtheadditionalargumentswithanellipsis(seePartitionI).
ThevarargargumentsshallbeaccessedbyobtainingahandletotheargumentlistusingtheCILinstructionarglist(seePartitionIII).ThehandlemaybeusedtocreateaninstanceofthevaluetypeSystem.ArgIteratorwhichprovidesatypesafemechanismforaccessingthearguments(seePartitionIV).
|
以下内容为程序代码:
[b]Example(informative):[/b]
Thefollowingexampleshowshowavarargmethodisdeclaredandhowthefirstvarargargumentisaccessed,assumingthatatleastoneadditionalargumentwaspassedtothemethod:
.methodpublicstaticvarargvoidMyMethod(int32required){
.maxstack3
.localsinit(valuetypeSystem.ArgIteratorit,int32x)
ldloca it //initializetheiterator
initobj valuetypeSystem.ArgIterator
ldloca it
arglist //obtaintheargumenthandle
call instancevoidSystem.ArgIterator::.ctor(valuetypeSystem.RuntimeArgumentHandle) //callconstructorofiterator
/*argumentvaluewillbestoredinxwhenretrieved,soload
addressofx*/
ldloca x
ldloca it
//retrievetheargument,theargumentforrequireddoesnotmatter
call instancetypedrefSystem.ArgIterator::GetNextArg()
call objectSystem.TypedReference::ToObject(typedref) //retrievetheobject
castclassSystem.Int32 //castandunbox
unbox int32
cpobj int32 //copythevalueintox
//firstvarargargumentisstoredinx
ret
} |
|
可以看到在CLR一级实际上是提供了对参数数目可变参数的支持的,只不过C#的params关键字因为某些原因并没有使用。而如果你考察ManagedC++的实现,就会发现其正是使用这个机制。
以下内容为程序代码:
//cl/clrparam.cpp
#include<stdio.h>
#include<stdarg.h>
voidshow(constchar*fmt,...)
{
va_listargs;
va_start(args,fmt);
vprintf(fmt,args);
va_end(args);
}
intmain(intargc,constchar*argv[])
{
show("%s%d","FlierLu",1024);
} |
|
编译成Managed代码后,其函数signature如下:
以下内容为程序代码:
.methodpublicstaticpinvokeimpl(/*Nomap*/)
varargvoidmodopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl)
show(int8modopt([Microsoft.VisualC]Microsoft.VisualC.NoSignSpecifiedModifier)modopt([Microsoft.VisualC]Microsoft.VisualC.IsConstModifier)*A_0)nativeunmanagedpreservesig
{
//...
} |
|
实际上,在C#中也提供了隐藏的对vararg类型方法定义和调用的支持,那就是__arglist关键字。
以下内容为程序代码:
publicclassUndocumentedCSharp
{
[DllImport("msvcrt.dll",CharSet=CharSet.Ansi,CallingConvention=CallingConvention.Cdecl)]
externstaticintprintf(stringformat,__arglist);
publicstaticvoidMain(String[]args)
{
printf("%s%d",__arglist("FlierLu",1024));
}
} |
|
可以看到__arglist关键字实际上起到了和C++中va_list类似的作用,直接将任意多个参数按顺序压入堆栈,并在调用时处理。而在IL代码一级,则完全类似于上述IL汇编和ManagedC++的例子:
以下内容为程序代码:
.methodprivatehidebysigstaticpinvokeimpl("msvcrt.dll"ansicdecl)
varargint32printf(stringformat)cilmanagedpreservesig
{
}
.methodpublichidebysigstaticvoidMain(string[]args)cilmanaged
{
IL_0033:ldstr"%s%d"
IL_0038:ldstr"FlierLu"
IL_003d:ldc.i40x400
IL_0042:callvarargint32UndocumentedCSharp::printf(string,
...,
string,
int32)
} |
|
__arglist除了可以用于与现有代码进行互操作,还可以在C#内作为与params功能上等同的特性来使用。只不过因为没有C#编译器在语义一级的支持,必须用相对复杂的方式进行操作。
以下内容为程序代码:
usingSystem;
usingSystem.Runtime.InteropServices;
publicclassUndocumentedCSharp
{
privatestaticvoidShow(__arglist)
{
ArgIteratorit=newArgIterator(__arglist);
while(it.GetRemainingCount()>0)
{
TypedReferencetr=it.GetNextArg();
Console.Out.WriteLine("{0}:{1}",TypedReference.ToObject(tr),__reftype(tr));
}
}
publicstaticvoidMain(String[]args)
{
Show(__arglist("FlierLu",1024));
}
} |
|
与C++中不同,__arglist参数不需要一个前导参数来确定其在栈中的起始位置。
ArgIterator则是一个专用迭代器,支持对参数列表进行单向遍历。对每个参数项,GetNextArg将会返回一个TypedReference类型,表示指向参数。
要理解这里的实现原理,就必须单独先介绍一下TypedReference类型。
我们知道C#提供了很多CLR内建值类型的名称映射,如Int32在C#中被映射为int等等。但实际上有三种CLR类型并没有在C#中被映射为语言一级的别名:IntPtr,UIntPtr和TypedReference。这三种类型在IL一级分别被称为nativeint、nativeunsignedint和typedref。但在C#一级,则只能通过System.TypedReference类似的方式访问。而其中就属这个TypedReference最为奇特。
TypedReference在MSDN中的描述如下:
以下为引用:
Describesobjectsthatcontainbothamanagedpointertoalocationandaruntimerepresentationofthetypethatmaybestoredatthatlocation.
[CLSCompliant(false)]
publicstructTypedReference
Remarks
Atypedreferenceisatype/valuecombinationusedforvarargsandothersupport.TypedReferenceisabuilt-invaluetypethatcanbeusedforparametersandlocalvariables.
ArraysofTypedReferenceobjectscannotbecreated.Forexample,thefollowingcallisinvalid:
Assembly.Load("mscorlib.dll").GetType("System.TypedReference[]");
|
也就是说,值类型TypedReference是专门用于保存托管指针及其指向内容类型的,查看其实现代码(bclsystemTypedReference.cs:28)可以验证这一点:
以下内容为程序代码:
publicstructTypedReference
{
privateintValue;
privateintType;
//其他方法
} |
|
这儿Value保存了对象的指针,Type保存了对象的类型句柄。
使用的时候可以通过__arglist.GetNextArg()返回,也可以使用__makeref关键字构造,如:
以下内容为程序代码:
inti=21;
TypedReferencetr=__makeref(i); |
|
而其中保存的对象和类型,则可以使用__refvalue和__reftype关键字来获龋
以下内容为程序代码:
inti=32;
TypedReferencetr1=__makeref(i);
Console.Out.WriteLine("{0}:{1}",__refvalue(tr,int),__reftype(tr1)); |
|
注意这儿的__refvalue关键字需要指定目标TypedReference和转换的目标类型,如果结构中保存的类型不能隐式转换为目标类型,则会抛出转换异常。相对来说,TypedReference.ToObject虽然要求强制性box目标值,但易用性更强。
从实现角度来看,__refvalue和__reftype是直接将TypedReference的内容取出,因而效率最高。
以下内容为程序代码:
inti=5;
TypedReferencetr=__makeref(i);
Console.Out.WriteLine("{0}:{1}",__refvalue(tr,int),__reftype(tr)); |
|
上面这样一个代码片断,将被编译成:
以下内容为程序代码:
IL_0048:ldc.i4.5
IL_0049:stloc.0
IL_004a:ldloca.sV_0
IL_004c:mkrefany[mscorlib]System.Int32
IL_0051:stloc.1
IL_0052:callclass[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:callclass[mscorlib]System.Type[mscorlib]System.Type::GetTypeFromHandle(valuetype[mscorlib]System.RuntimeTypeHandle)
IL_0070:callvirtinstancevoid[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)如下:
以下内容为程序代码:
FJitResultFJit::compileCEE_REFANYTYPE()
{
//Thereshouldbearefanyonthestack
CHECK_STACK(1);
//Therehastobeatypedrefonthestack
//Thisshouldbeavaliditycheckaccordingtothespec,becausethespecsays
//thatREFANYTYPEisalwaysverifiable.However,V1.NETFrameworkthrowsverificationexception
//sotomatchthisbehaviorthisisaverificationcheckaswell.
VERIFICATION_CHECK(topOpE()==typeRefAny);
//PopofftheRefany
POP_STACK(1);
_ASSERTE(offsetof(CORINFO_RefAny,type)==sizeof(void*));//Typeisthesecondthing
emit_WIN32(emit_POP_I4())emit_WIN64(emit_POP_I8());//Justpopoffthedata,leavingthetype.
CORINFO_CLASS_HANDLEs_TypeHandleClass=jitInfo->getBuiltinClass(CLASSID_TYPE_HANDLE);
VALIDITY_CHECK(s_TypeHandleClass!=NULL);
pushOp(OpType(typeValClass,s_TypeHandleClass));
returnFJIT_OK;
} |
|
从以上代码可以看到,JIT在处理refanyval指令时,并没有对堆栈内容进行任何操作,而是直接操作堆栈。
如果希望进一步了解相关信息,可以参考以下介绍:
UndocumentedC#TypesandKeywords
UndocumentedTypedReference
ASampleChapterfromC#ProgrammersReference-Valuetypes
ps:实测了一下发现,MS不公开vararg这种调用方式,大概是因为考虑效率方面的原因。与params相比,使用vararg的调用方式,纯粹函数调用的速度要降低一个数量级:(
下面这篇文章也讨论了这个问题,结论是不到万不得已情况下尽量少用,呵呵
Why__arglistisundocumented