.NET交互操作服务
前言
.NET是微软最新推出的编程平台,它通过公共语言运行库将基于.NET Framework的托管代码(Managed code)承载运行,以简化Internet 环境中的应用程序开发和部署。
Microsoft .NET最核心的特征是交互性,包括多种程序语言的交互、与非托管代码的交互。其中与非托管代码又包括与现有原生代码(Native code)的交互、与COM的互相交互操作。与非托管代码交互使得.NET能够与操作系统服务,应用开发商现有程序库、Microsoft COM组件技术等进行无缝的整合,从而促进.NET编程平台的推广。
本人在此把全文分为《平台调用P/Invoke进阶》、《在.NET中使用COM组件》、《从COM中调用.NET托管类型》三部分来一一详细解析相关使用方法,结合一些例子(均由C#编写),由浅入深地分析相关运行时机制。
平台调用P/Invoke进阶
平台调用P/Invoke用于从.NET托管代码中调用在动态链接库(DLL)中的函数。这里的动态链接库是指原生的非托管动态链接库,如操作系统的DLL、由C/C++生成的DLL等,而并非.NET的托管程序库。在公共语言运行时中它的主要作用是:
1.提供一种更便利的方式与开发商现存代码交互。
2.调用系统API来实现公共语言运行时库目前没有提供的功能。
可见,利用平台调用这种服务,我们可以再利用本公司现有的程序库,以及直接使用操作系统API来实现我们的.NET解决方案。
初阶:认识平台调用P/Invoke
一、从Hello world例子开始
我们从”Hello world”这个简单例子开始我们的平台调用之旅。这里我们从自己编写的一个动态链接库mylib.dll中调用Dll函数Print()输出”Hello world!”。这个函数的声明如下:
void WINAPI Print (
const char* str // 输出的字符串
);
我们可以用以下一个用C#写的方法来将这个函数引进以供在托管代码中使用。这个方法就是.NET中的平台调用方法。
[DllImport("mylib.dll")]
public static extern void Print(String text);
这样声明后,我们就能如用其它任何方法一样的使用这个静态方法。所有如加载动态链接库、查找函数地址,数据封送调度等工作均由.NET在运行时自动完成。我们所要做的仅仅就是正确地声明一个平台调用函数。
public static void Main(String[] args)
{
Print ("Hello world!");
}
我们编译后并运行,就可得到如下结果:
二、解析平台调用P/Invoke方法
我们从刚刚这个例子可以看到,平台调用在.NET中调用非托管函数代码是如此的简单。它只需要我们正确地声明一个平台调用方法就完成了整个调用包装过程。从.NET内部机制来看,平台调用方法其实只是一个让公共语言运行时去寻找真正的非托管函数的元数据,因而它不需要方法体定义。
现在我们来仔细分析它的结构。它可分为两个部分。方法头声明和DllImport属性声明。我们再来看Helloworld这个例子中的平台调用函数。
[DllImport("mylib.dll")]
public static extern void Print(String text);
方法头
在C#中,平台调用方法必须带有修饰符static和extern, static是因为非托管函数并没有对象实例,而extern指示这是一个外部实现方法,而一个外部方法是没有方法体,它通常与DllImport属性一起使用,来说明一个方法为平台调用方法。
其次,大家注意到,这个方法的原型结构对应于动态链接库里的函数声明,它返回类型,参数类型是一一相对应的,不同的是,这里的字符串用的是通用类型系统的String类型。从String类型到C格式的字符串转换是由.NET在运行时自动完成的。(这里有一个术语叫调度(Marshal),就是专门来描述不同数据格式模型的封送、转换的,在本文的后面,将有详细讲解)下面这个表格列出了平台调用中C#托管类型、通用类型系统类型与C式样的非托管数据类型的对应关系。
表1.非托管类型与托管类型的对应关系
非托管类型 |
C#类型 |
通用类型系统类型 |
说明 |
unsigned char |
byte |
System.Byte |
无符号 8 位整数 |
short |
short |
System.Int16 |
有符号 16 位整数 |
unsigned short |
ushort |
System.UInt16 |
无符号 16 位整数 |
int, long |
int |
System.Int32 |
有符号 32 位整数 |
unsigned int, unsigned long |
uint |
System.UInt32 |
无符号 32 位整数 |
__int64 或 long long |
long |
System.Int64 |
有符号 64 位整数 |
unsigned __int64 或 unsigned long long |
Ulong |
System.UInt64 |
无符号 64 位整数 |
char |
char |
System.Char |
8 位 ANSI 字符 |
wchar_t |
Char |
System.Char |
16 位 Unicode 字符 |
char*, const char* |
string |
System.String |
ANSI字符串 |
wchar_t*, const wchar_t* |
String |
System.String |
Unicode字符串 |
float |
float |
System.Single |
32位浮点数 |
double |
double |
System.Double |
64位浮点数 |
void*,const void*,其它指针 |
System.IntPtr |
System.IntPtr |
指针类型 |
DllImport属性
我们曾说过,平台调用方法只是一个让公共语言运行时去寻找真正的非托管代码的元数据,这些元数据信息就是来自DllImport这个自定义属性。由此可见,DllImport属性在平台调用中起着非常重要的作用。我们来看它的C#版本定义(源代码来自Mono):
namespace System.Runtime.InteropServices {
[AttributeUsage (AttributeTargets.Method)]
public sealed class DllImportAttribute: Attribute {
public CallingConvention CallingConvention;
public CharSet CharSet;
public string EntryPoint;
public bool ExactSpelling;
public bool PreserveSig;
public bool SetLastError;
private string Dll;
public string Value {
get {return Dll;}
}
public DllImportAttribute (string dllName) {
Dll = dllName;
}
}
}
由上面的代码我们可以得出,一个DllImport属性的最小化形式就是我们在Helloworld中使用形式,它必须指明我们的非托管函数所在有动态链接库名。你所指定的动态链接库可以为绝对路径,如DllImport("D://pinvoke sample//mylib.dll"),也可以为相对路径,这时公共语言运行时沿以下路径顺序搜索这个库:
当前路径。
Windows系统目录。
PATH环境变量所指定的目录。
如果所指定的动态链接库没有在可搜索路径范围内,公共语言运行时就会引发DllNotFoundException.
而其它六个属性成员都是可选的,在我们未指明它们的时候,公共语言运行时为我们加上一些默认的值。接下来我们一一来认识其它属性成员。
CallingConvention 指定非托管函数的调用规则,它指示公共语言运行时如何进行参数堆栈的清理。比较常用的CallingConvention.StdCall和CallingConvention.Cdecl,在C/C++中它分别对应于__stdcall和__cdecl。该属性成员的默认值是CallingConvention.WinAPI,它实际上就是CallingConvention.StdCall,它表示由被调用方清理堆栈。通常我们用默认值都能很好的工作,即使我们的非托管函数是__cdecl,但是这种情况并不总是有效,如果调用规则不一致,可能导致堆栈不能正确清理而影响应用程序的稳定性,甚至可能直接导致应用程序无法运行。所以,在编写平台调用方法时,我们一定要检查是否与非托管函数的调用规则保持一致。
CharSet 指定封送的字符串的字符集类型。我们熟悉最常见的字符集是Unicode和ANSI,而.NET的字符串所用的字符集是Unicode,从表1中我们看到无论非托管函数中所用的字符集是ANSI还是Unicode,在平台调用方法中我们都用String类型。显然我们只通过指定CharSet属性来区分我们的非托管函数所用的字符集。它的默认值是CharSet.Ansi,也就是ANSI字符集。我们还可以使用CharSet.Auto值,它表示根据操作系统自动识别所用的字符集,即公共语言运行时在WinNT, Win2000等系列操作系统用CharSet.Unicode字符集,在Win98,WinMe等系列操作系统就用CharSet.ANSI.
EntryPoint 指定非托管函数名称。如果我们没有指定在DllImport属性中指定EntryPoint值,公共语言运行时就会以我们的平台调用方法名为名称,如Hello world例子中的Print.如果公共语言运行时在动态链接库中没有找到非托管函数,就会引发EntryPointNotFoundException。但是公共语言运行时对名字进行解析时做一些精致的动作,我们在ExactSpelling中可以看到详细的分析。
ExactSpelling 指定是否通过修改我们所指定的非托管函数名来寻找非托管函数。我们知道,在Windows系统API中,如果API的参数或返回类型中有字符串类型,Windows就会提供两个API,一个用于Uncode字符集,一个用于ANSI字符集。如MessageBox API,它实际上只是MessageBoxA或MessageBoxW的宏。而在动态链接库user32.dl中也只提供了MessageBoxA和MessageBoxW两个函数。公共语言运行时为了我们平台调用的方便,就提供了一个自动添加尾缀的操作,它根据我们所指定的CharSet属性值来添加。当ExactSpelling为假值false时,如果我们在平台调用方法中用的字符集为CharSet.ANSI, 就会在我们所指定的非托管函数名尾部添加一个A字符;如果我们在平台调用方法中用的字符集为CharSet.Unicode, 就会在我们所指定的非托管函数名尾部添加一个W字符。如果在添加了尾缀后从动态链接库中没有找到非托管函数,就会以非添加尾缀的名称再找一次,如果仍然没有找到,就引发EntryPointNotFoundException. 当ExactSpelling为真值true时,公共语言运行时不会做任何操作,它直接用我们指定的非托管函数名称。如果没有找到,就会引发EntryPointNotFoundException.
ExactSpelling的默认值因编译器不同而不同,微软的C#编译器的默认值为false.
PreserveSig 指定是否保留托管函数的签名不作变化。这个属性值是针对COM方法来的。在COM规范中,所有的方法都必须返回HRESULT类型值,以指示这个调用是否成功,而一个方法欲返回的其它值则在参数中以[out,retval]标出。公共语言运行时为我们又提供一种便利,可以自动转换函数返回值,在我们的平台调用方法中直接返回[out,retval]参数值而不是HRESULT值。如果PreserveSig为假值false,则作出这种转换,但是必须以这个非托管函数是返回HRESULT值为前提。它的默认值为true
SetLastError 指定是否保留平台调用的错误值,这里的错误值是指Windows API GetLastError所返回的值。如是保留,则我们可以通过调用Marshal.GetLastWin32Error获得此值。该属性值的默认值是假值false.
我们来看一个对DllImport属性的综合运用,它用MSOLE API LoabTypeLib装载一个COM类型库。这里用到了两到了两个系统API的原型为:
HRESULT CoInitialize(
LPVOID pvReserved //保留
);
我们用以下平台调用方法来引进它,因为它没有[out,ret]参数,我们就不能变化它的函数签名:
[DllImport("ole32.dll")]
public static extern int CoInitialize (IntPtr v);
HRESULT LoadTypeLib(
const OLECHAR FAR* szFile, //类型库文件名
ITypeLib FAR* FAR* pptlib //装载后得到的ITypeLib接口指针
);
我们用以下平台调用方法来引进它,注意OLECHAR的为Unicode字符集
[DllImport("oleaut32.dll", PreserveSig = false, ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr LoadTypeLib (string szFile);
声明之后,在我们的.NET应用程序中,我们就可以直接使用这些API:
int err = CoInitialize (IntPtr.Zero);
if (err == 0) {
pTypeLib = Test.LoadTypeLib (argv[0]);
if (pTypeLib == IntPtr.Zero) {
Console.WriteLine ("Failed call LoadTypeLib with " + err);
err = Marshal.GetLastWin32Error();
Marshal.ThrowExceptionForHR (err);
} else {
Console.WriteLine ("Success");
}
} else {
Console.WriteLine ("Failed CoInitialize with " + err);
}
现在,我们已经一一了解了DllImport各成员的作用,并对公共语言运行时的一些操作进行了分析。下面我们来进一步来认识平台调用的数据封送调度。