C#高级编程(第10版) C# 6 & .NET Core 1.0 (.NET开发经典名著)
上QQ阅读APP看书,第一时间看更新

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);