并不是Windows API调用的所有特性都可用于.NET Framework。旧的Windows API调用是这样,Windows10或Windows Server2016中的新功能也是这样。也许开发人员会编写一些DLL,导出非托管的方法,在C#中使用它们。
要重用一个非托管库,其中不包含COM对象,只包含导出的功能,就可以使用平台调用(P/Invoke)。有了P/Invoke,CLR会加载DLL,其中包含应调用的函数,并编组参数。要使用非托管函数,首先必须确定导出的函数名。为此,可以使用dumpbin工具和/exports选项。例如,命令:dumpbin/exports c:\ windows\ system32\ kernel32. dll | more列出DLL kernel32. dll 中所有导出的函数。这个示例使用WindowsAPI函数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]特性指定的设置在下表中列出。
DLLIMPORT属性或字段 | 说明 |
EntryPoint | 可以给函数的C#声明指定与非托管库不同的名称。非托管库中方法的名称在EntryPoint字段中定义 |
CallingConvention | 根据编译器或用来编译非托管函数的编译器设置,可以使用不同的调用约定。调用约定定义了如何处理参数,把它们放在堆栈的什么地方。 可以设置一个可枚举的值,来定义调用约定。WindowsAPI在Windows操作系统上通常使用StdCall调用约定,在WindowsCE上使用Cdecl调用约定。把值设置为CallingConvention.Winapi,可让WindowsAPI用于Windows和WindowsCE环境 |
CharSet | 字符串参数可以是ANSI或Unicode。通过CharSet设置,可以定义字符串的管理方式。用CharSet枚举定义的值有Ansi、Unicode和Auto.CharSet。Auto在WindowsNT平台上使用Unicode,在微软的旧操作系统上使用ANSI |
SetLastError | 如果非托管函数使用Windows API SetLastError设置一个错误,就可以把SetLastError字段设置为true。这样,就可以使用Marshal.GetLastWin32Error读取后面的错误号 |
为了使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特性,检查调用程序是否拥有必要的许可。
using System; using System. ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Permissions;
namespace Rbsoft.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);
}
}
}
现在可以使用这个类来轻松地创建硬链接。如果程序的第一个参数传递的文件不存在,就会得到一个异常,提示“系统无法找到指定的文件”。如果文件存在,就得到一个引用原始文件的新文件名。很容易验证它:在一个文件中改变文本,它就会出现在另一个文件中:
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位值,根据句柄类型,不允许使用一些值。在.NET1.0中,句柄通常使用IntPtr结构,因为可以用这种结构设置每一个可能的32位值。然而,对于一些句柄类型,这会导致安全问题,可能还会出现线程竞态条件,在终结阶段泄露句柄。所以.NET2.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);