使用.net做系统整合是一个很有技术含量的活,从代码到架构都有很多值得考虑的问题和一大堆陷阱。
最近看了一些关于这个方面的文章,现在整理一些笔记希望对大家有所帮助。
今天是第一篇,编码级别的C#调用WinAPI(可以是Windows提供的,也可以是其他系统提供的,比如另一个应用程序)的常见问题点。在VS的安装目录下的frameworksdk\samples\technologies\interop\platforminvoke\winapis\cs 目录中有大量的调用api的例子。
一、声明函数时的Attrbutes
在使用API的函数时,我们需要先声明这个函数,在以后的代码中这个函数将可以像这个class的一个正常的static method一样进行使用了。
1. 最基础的声明
[dllimport("SampleWinApi.dll")]
public static extern int SampleApi1();
2. 进一步说明特性,用逗号隔开,如:
[ dllimport( "SampleWinApi.dll", entrypoint="SampleApi1" )]
[dllimport("SampleWinApi.dll")]
public static extern int SampleApi1();
2. 进一步说明特性,用逗号隔开,如:
[ dllimport( "SampleWinApi.dll", entrypoint="SampleApi1" )]
public static extern int SampleApiFriendName();
dllimport的attribute有如下几个,最常用的是charset,setlasterror和entrypoint :
a callingconvention 指示向非托管实现传递方法参数时所用的 callingconvention 值。
callingconvention.cdecl : 调用方清理堆栈。它使您能够调用具有 varargs 的函数。
callingconvention.stdcall : 被调用方清理堆栈。它是从托管代码调用非托管函数的默认约定。
b charset 控制调用函数的名称版本及指示如何向方法封送 string 参数。
此字段被设置为 charset 值之一。如果 charset 字段设置为 unicode,则所有字符串参数在传递到非托管实现之前都转换成 unicode 字符;如果此字段设置为 ansi,则字符串将转换成 ansi 字符串;如果 charset 设置为 auto,则这种转换就是与平台有关的(在 windows nt 上为 unicode,在 windows 98 上为 ansi)。charset 的默认值为 ansi。charset 字段也用于确定将从指定的 dll 导入哪个版本的函数。charset.ansi 和 charset.unicode 的名称匹配规则大不相同。对于 ansi 来说,如果将 entrypoint 设置为“mymethod”且它存在的话,则返回“mymethod”。如果 dll 中没有“mymethod”,但存在“mymethoda”,则返回“mymethoda”。对于 unicode 来说则正好相反。如果将 entrypoint 设置为“mymethod”且它存在的话,则返回“mymethodw”。如果 dll 中不存在“mymethodw”,但存在“mymethod”,则返回“mymethod”。如果使用的是 auto,则匹配规则与平台有关(在 windows nt 上为 unicode,在 windows 98 上为 ansi)。如果 exactspelling 设置为 true,则只有当 dll 中存在“mymethod”时才返回“mymethod”。
c entrypoint 指示要调用的 dll 入口点的名称或序号。
如果你的方法名不想与api函数同名的话,一定要指定此参数,例如:
[ dllimport( "SampleWinApi.dll", entrypoint="SampleApi1" )]
dllimport的attribute有如下几个,最常用的是charset,setlasterror和entrypoint :
a callingconvention 指示向非托管实现传递方法参数时所用的 callingconvention 值。
callingconvention.cdecl : 调用方清理堆栈。它使您能够调用具有 varargs 的函数。
callingconvention.stdcall : 被调用方清理堆栈。它是从托管代码调用非托管函数的默认约定。
b charset 控制调用函数的名称版本及指示如何向方法封送 string 参数。
此字段被设置为 charset 值之一。如果 charset 字段设置为 unicode,则所有字符串参数在传递到非托管实现之前都转换成 unicode 字符;如果此字段设置为 ansi,则字符串将转换成 ansi 字符串;如果 charset 设置为 auto,则这种转换就是与平台有关的(在 windows nt 上为 unicode,在 windows 98 上为 ansi)。charset 的默认值为 ansi。charset 字段也用于确定将从指定的 dll 导入哪个版本的函数。charset.ansi 和 charset.unicode 的名称匹配规则大不相同。对于 ansi 来说,如果将 entrypoint 设置为“mymethod”且它存在的话,则返回“mymethod”。如果 dll 中没有“mymethod”,但存在“mymethoda”,则返回“mymethoda”。对于 unicode 来说则正好相反。如果将 entrypoint 设置为“mymethod”且它存在的话,则返回“mymethodw”。如果 dll 中不存在“mymethodw”,但存在“mymethod”,则返回“mymethod”。如果使用的是 auto,则匹配规则与平台有关(在 windows nt 上为 unicode,在 windows 98 上为 ansi)。如果 exactspelling 设置为 true,则只有当 dll 中存在“mymethod”时才返回“mymethod”。
c entrypoint 指示要调用的 dll 入口点的名称或序号。
如果你的方法名不想与api函数同名的话,一定要指定此参数,例如:
[ dllimport( "SampleWinApi.dll", entrypoint="SampleApi1" )]
public static extern int SampleApiFriendName();
d exactspelling 指示是否应修改非托管 dll 中的入口点的名称,以与 charset 字段中指定的 charset 值相对应。如果为 true,则当 dllimportattribute.charset 字段设置为 charset 的 ansi 值时,向方法名称中追加字母 a,当 dllimportattribute.charset 字段设置为 charset 的 unicode 值时,向方法的名称中追加字母 w。此字段的默认值是 false。
e preservesig 指示托管方法签名不应转换成返回 hresult、并且可能有一个对应于返回值的附加 [out, retval] 参数的非托管签名。
f setlasterror 指示被调用方在从属性化方法返回之前将调用 win32 api setlasterror。 true 指示调用方将调用 setlasterror,默认为 false。运行时封送拆收器将调用 getlasterror 并缓存返回的值,以防其被其他 api 调用重写。用户可通过调用 getlastwin32error 来检索错误代码。
二、参数类型:
d exactspelling 指示是否应修改非托管 dll 中的入口点的名称,以与 charset 字段中指定的 charset 值相对应。如果为 true,则当 dllimportattribute.charset 字段设置为 charset 的 ansi 值时,向方法名称中追加字母 a,当 dllimportattribute.charset 字段设置为 charset 的 unicode 值时,向方法的名称中追加字母 w。此字段的默认值是 false。
e preservesig 指示托管方法签名不应转换成返回 hresult、并且可能有一个对应于返回值的附加 [out, retval] 参数的非托管签名。
f setlasterror 指示被调用方在从属性化方法返回之前将调用 win32 api setlasterror。 true 指示调用方将调用 setlasterror,默认为 false。运行时封送拆收器将调用 getlasterror 并缓存返回的值,以防其被其他 api 调用重写。用户可通过调用 getlastwin32error 来检索错误代码。
二、参数类型:
WinApi使用的数据类型和.net的数据类型相比是完全不同的体系,所以我们需要在声明API函数时将API的数据类型转换为.net的类型。以下是常见的数据类型对应表
http://msdn.microsoft.com/zh-cn/library/aa720411(en-us,VS.71).aspx
值得特殊说明的有以下两个,StringBuilder和Struct
值得特殊说明的有以下两个,StringBuilder和Struct
1、stringbuilder,不要使用ref或out,否则会被当作CHAR**
2、api中结构 -> .net中结构或者类。注意这种情况下,要先用structlayout特性限定声明结构或类
公共语言运行库利用structlayoutattribute控制类或结构的数据字段在托管内存中的物理布局,即类或结构需要按某种方式排列。如果要将类传递给需要指定布局的非托管代码,则显式控制类布局是重要的。它的构造函数中用layoutkind值初始化 structlayoutattribute 类的新实例。 layoutkind.sequential 用于强制将成员按其出现的顺序进行顺序布局。layoutkind.explicit 用于控制每个数据成员的精确位置。利用 explicit, 每个成员必须使用 fieldoffsetattribute 指示此字段在类型中的位置。这样做往往需要用到mashalas特性,它用于描述字段、方法或参数的封送处理格式。用它作为参数前缀并指定目标需要的数据类型。例如,以下代码将两个参数作为数据类型长指针封送给 windows api 函数的字符串 (lpstr):
[marshalas(unmanagedtype.lpstr)]
string existingfile;
[marshalas(unmanagedtype.lpstr)]
string newfile;
注意结构作为参数时候,一般前面要加上ref修饰符,否则会出现错误:对象的引用没有指定对象的实例。
[ dllimport( "kernel32", entrypoint="getversionex" )]
public static extern bool getversionex2( ref osversioninfo2 osvi );
三、防止被不恰当的回收
如果在调用平台 invoke 后的任何位置都未引用托管对象,则垃圾回收器可能将完成该托管对象。这将释放资源并使句柄无效,从而导致平台invoke 调用失败。用 handleref 包装句柄可保证在平台 invoke 调用完成前,不对托管对象进行垃圾回收。
例如下面:
filestream fs = new filestream( "a.txt", filemode.open );
stringbuilder buffer = new stringbuilder( 5 );
int read = 0;
readfile(fs.handle, buffer, 5, out read, 0 ); //调用win api中的readfile函数
由于fs是托管对象,所以有可能在平台调用还未完成时候被垃圾回收站回收。将文件流的句柄用handleref包装后,就能避免被垃圾站回收:
[ dllimport( "kernel32.dll" )]
public static extern bool readfile(
handleref hndref,
stringbuilder buffer,
int numberofbytestoread,
out int numberofbytesread,
ref overlapped flag );
......
......
filestream fs = new filestream( "handleref.txt", filemode.open );
handleref hr = new handleref( fs, fs.handle );
stringbuilder buffer = new stringbuilder( 5 );
int read = 0;
// platform invoke will hold reference to handleref until call ends
readfile( hr, buffer, 5, out read, 0 );
2、api中结构 -> .net中结构或者类。注意这种情况下,要先用structlayout特性限定声明结构或类
公共语言运行库利用structlayoutattribute控制类或结构的数据字段在托管内存中的物理布局,即类或结构需要按某种方式排列。如果要将类传递给需要指定布局的非托管代码,则显式控制类布局是重要的。它的构造函数中用layoutkind值初始化 structlayoutattribute 类的新实例。 layoutkind.sequential 用于强制将成员按其出现的顺序进行顺序布局。layoutkind.explicit 用于控制每个数据成员的精确位置。利用 explicit, 每个成员必须使用 fieldoffsetattribute 指示此字段在类型中的位置。这样做往往需要用到mashalas特性,它用于描述字段、方法或参数的封送处理格式。用它作为参数前缀并指定目标需要的数据类型。例如,以下代码将两个参数作为数据类型长指针封送给 windows api 函数的字符串 (lpstr):
[marshalas(unmanagedtype.lpstr)]
string existingfile;
[marshalas(unmanagedtype.lpstr)]
string newfile;
注意结构作为参数时候,一般前面要加上ref修饰符,否则会出现错误:对象的引用没有指定对象的实例。
[ dllimport( "kernel32", entrypoint="getversionex" )]
public static extern bool getversionex2( ref osversioninfo2 osvi );
三、防止被不恰当的回收
如果在调用平台 invoke 后的任何位置都未引用托管对象,则垃圾回收器可能将完成该托管对象。这将释放资源并使句柄无效,从而导致平台invoke 调用失败。用 handleref 包装句柄可保证在平台 invoke 调用完成前,不对托管对象进行垃圾回收。
例如下面:
filestream fs = new filestream( "a.txt", filemode.open );
stringbuilder buffer = new stringbuilder( 5 );
int read = 0;
readfile(fs.handle, buffer, 5, out read, 0 ); //调用win api中的readfile函数
由于fs是托管对象,所以有可能在平台调用还未完成时候被垃圾回收站回收。将文件流的句柄用handleref包装后,就能避免被垃圾站回收:
[ dllimport( "kernel32.dll" )]
public static extern bool readfile(
handleref hndref,
stringbuilder buffer,
int numberofbytestoread,
out int numberofbytesread,
ref overlapped flag );
......
......
filestream fs = new filestream( "handleref.txt", filemode.open );
handleref hr = new handleref( fs, fs.handle );
stringbuilder buffer = new stringbuilder( 5 );
int read = 0;
// platform invoke will hold reference to handleref until call ends
readfile( hr, buffer, 5, out read, 0 );