5.6 平台调用
并不是Windows API调用的所有特性都可用于.NET Framework。旧的Windows API调用是这样,Windows 10或Windows Server 2016中的新功能也是这样。也许开发人员会编写一些DLL,导出非托管的方法,在C#中使用它们。
要重用一个非托管库,其中不包含COM对象,只包含导出的功能,就可以使用平台调用(P /Invoke)。有了P / Invoke, CLR会加载DLL,其中包含应调用的函数,并编组参数。
要使用非托管函数,首先必须确定导出的函数名。为此,可以使用dumpbin工具和 /exports选项。例如,命令:
dumpbin /exports c:\windows\system32\kernel32.dll | more
列出DLL kernel32.dll中所有导出的函数。这个示例使用Windows API函数CreateHardLink来创建到现有文件的硬链接。使用此API调用,可以用几个文件名引用相同的文件,只要文件名在一个硬盘上即可。这个API调用不能用于.NET Framework 4.5.1,因此必须使用平台调用。
为了调用本机函数,必须定义一个参数数量相同的C#外部方法,用非托管方法定义的参数类型必须用托管代码映射类型。
在C++中,Windows API调用CreateHardLink有如下定义:
BOOL CreateHardLink( LPCTSTR lpFileName, LPCTSTR lpExistingFileName, LPSECURITY_ATTRIBUTES lpSecurityAttributes);
这个定义必须映射到.NET数据类型上。非托管代码的返回类型是BOOL;它仅映射到bool数据类型。LPCTSTR定义了一个指向const字符串的long指针。Windows API给数据类型使用Hungarian命名约定。LP是一个long指针,C是一个常量,STR是以null结尾的字符串。T把类型标志为泛型类型,根据编译器设置为32还是64位,该类型解析为LPCSTR(ANSI字符串)或LPWSTR(宽Unicode字符串)。C字符串映射到.NET类型为String。LPSECURITY_ATTRIBUTES是一个long指针,指向SECURITY_ATTRIBUTES类型的结构。因为可以把NULL传递给这个参数,所以把这种类型映射到IntPtr是可行的。该方法的C#声明必须用extern修饰符标记,因为在C#代码中,这个方法没有实现代码。相反,该方法的实现代码在DLL kernel32.dll中,它用属性[DllImport]引用。.NET声明CreateHardLink的返回类型是bool,本机方法CreateHardLink返回一个布尔值,所以需要一些额外的澄清。因为C++有不同的Boolean数据类型(例如,本机bool和Windows定义的BOOL有不同的值),所以特性[MarshalAs]指定.NET类型bool应该映射为哪个本机类型:
[DllImport("kernel32.dll", SetLastError="true", EntryPoint="CreateHardLink", CharSet=CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool CreateHardLink(string newFileName, string existingFilename, IntPtr securityAttributes);
注意:网站http://www.pinvoke.net非常有助于从本机代码到托管代码的转换。
可以用[DllImport]特性指定的设置在表5-2中列出。
表5-2
为了使CreateHardLink方法更易于在.NET环境中使用,应该遵循如下规则:
● 创建一个内部类NativeMethods,来包装平台调用的方法调用。
● 创建一个公共类,给.NET应用程序提供本机方法的功能。
● 使用安全特性来标记所需的安全。
在接下来的例子中,类FileUtility中的公共方法CreateHardLink可以由.NET应用程序使用。这个方法的文件名参数,与本机Windows API方法CreateHardLink的顺序相反。第一个参数是现有文件的名称,第二个参数是新的文件。这类似于框架中的其他类,如File.Copy。
因为第三个参数用来传递新文件名的安全特性,此实现代码不使用它,所以公共方法只有两个参数。返回类型也改变了。它不通过返回false值来返回一个错误,而是抛出一个异常。如果出错,非托管方法CreateHardLink就用非托管API SetLastError设置错误号。要从.NET中读取这个值,[DllImport] 字段SetLastError设置为true。在托管方法CreateHardLink中,错误号是通过调用Marshal.GetLastWin32Error读取的。要从这个号中创建一个错误消息,应使用System.ComponentModel名称空间中的Win32Exception类。这个类通过构造函数接受错误号,并返回一个本地化的错误消息。如果出错,就抛出IOException类型的异常,它有一个类型Win32Exception的内部异常。应用公共方法CreateHardLink的FileIOPermission特性,检查调用程序是否拥有必要的许可。.NET安全性详见第24章(代码文件PInvokeSample / NativeMethods.cs)。
using System; using System.ComponentModel; using System.IO; using System.Runtime.InteropServices; using System.Security; using System.Security.Permissions; namespace Wrox.ProCSharp.Interop { [SecurityCritical] internal static class NativeMethods { [DllImport("kernel32.dll", SetLastError = true, EntryPoint = "CreateHardLinkW", CharSet = CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CreateHardLink( [In, MarshalAs(UnmanagedType.LPWStr)] string newFileName, [In, MarshalAs(UnmanagedType.LPWStr)] string existingFileName, IntPtr securityAttributes); internal static void CreateHardLink(string oldFileName, string newFileName) { if (! CreateHardLink(newFileName, oldFileName, IntPtr.Zero)) { var ex = new Win32Exception(Marshal.GetLastWin32Error()); throw new IOException(ex.Message, ex); } } } public static class FileUtility { [FileIOPermission(SecurityAction.LinkDemand, Unrestricted = true)] public static void CreateHardLink(string oldFileName, string newFileName) { NativeMethods.CreateHardLink(oldFileName, newFileName); } } }
现在可以使用这个类来轻松地创建硬链接。如果程序的第一个参数传递的文件不存在,就会得到一个异常,提示“系统无法找到指定的文件”。如果文件存在,就得到一个引用原始文件的新文件名。很容易验证它:在一个文件中改变文本,它就会出现在另一个文件中(代码文件PInvokeSample/ Program.cs):
using PInvokeSampleLib; using System.IO; using static System.Console; namespace PInvokeSample { public class Program { public static void Main(string[] args) { if (args.Length ! = 2) { WriteLine("usage: PInvokeSample " + "existingfilename newfilename"); return; } try { FileUtility.CreateHardLink(args[0], args[1]); } catch (IOException ex) { WriteLine(ex.Message); } } } }
调用本地方法时,通常必须使用Windows句柄。Windows句柄是一个32位或64位值,根据句柄类型,不允许使用一些值。在.NET 1.0中,句柄通常使用IntPtr结构,因为可以用这种结构设置每一个可能的32位值。然而,对于一些句柄类型,这会导致安全问题,可能还会出现线程竞态条件,在终结阶段泄露句柄。所以.NET 2.0引入了SafeHandle类。SafeHandle类是一个抽象的基类,用于每个Windows句柄。Microsoft.Win32.SafeHandles名称空间里的派生类是SafeHandleZeroOrMinus-OneIsInvalid和SafeHandleMinusOneIsInvalid。顾名思义,这些类不接受无效的0或1值。进一步派生的句柄类型是SafeFileHandle、SafeWaitHandle、SafeNCryptHandle和SafePipeHandle,可以供特定的Windows API调用使用。
例如,为了映射Windows API CreateFile,可以使用以下声明,返回一个SafeFileHandle。当然,通常可以使用.NET类File和FileInfo。
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] internal static extern SafeFileHandle CreateFile( string fileName, [MarshalAs(UnmanagedType.U4)] FileAccess fileAccess, [MarshalAs(UnmanagedType.U4)] FileShare fileShare, IntPtr securityAttributes, [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition, int flags, SafeFileHandle template);