.NET 与 .NET Core 2 的不同 P/Invoke 入口点

Different P/Invoke entry point for .NET vs .NET Core 2

我正在将一些代码从 .NET (4.5) 移动到 .NET Core (2),并且有一个像这样的多目标项目...

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net45;netcoreapp2.0</TargetFrameworks>

代码库使用来自 kernel32 的 Win32 API 函数 CopyMemory,但我发现我需要根据我的目标框架使用不同的入口点名称。

#if NET45
    [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)]
#else
    [DllImport("kernel32.dll", EntryPoint = "RtlCopyMemory", SetLastError = false)]
#endif
    public static extern void CopyMemory(IntPtr dest, IntPtr src, IntPtr count);

我原以为这一切都在低于 .NET 的水平

那么,问题是……为什么?

它与您的目标框架没有任何关系,重要的是过程的细节。 .NET 4.5 项目以“项目”>“属性”>“构建”选项卡>“"Prefer 32-bit"”复选框开启。 .NETCore 非常喜欢 64 位代码,让你跳过一个圈套来获得 32 位运行时。

CopyMemory() 很古老,可以追溯到早期的 16 位 Windows 版本。他们必须为 32 位 winapi 保留它,有很多 VBx 程序使用它。但他们对 64 位的态度是坚决的。否则在 MSDN 文章中有详细记录:"This function is defined as the RtlCopyMemory function. Its implementation is provided inline. For more information, see WinBase.h and WinNT.h"。哪些值得一看,你会明白 "inline" 是什么意思。 RtlCopyMemory 实际上也没有被使用,而是用 memcpy() 代替它。

因此,只需使用 RtlCopyMemory 来代替这两种风格。请记住,在 Linux 或 MacOS 上部署时它无法工作。

如果您想要 predictable 结果,那么要求 CopyMemory 实际上是一个非常糟糕的主意。对于初学者来说,没有非托管应用程序调用任何名为 CopyMemory 的函数,因为它被定义为 Windows headers 中 C memcpy 函数的简单别名。 kernel32.dll 中根本没有 CopyMemory 导出,RtlCopyMemory 是否可用取决于您的平台。当您要求 CopyMemory 为 P/Invoked(如果有)时,用于导入什么函数的逻辑因平台而异。这里有一点 table 适用于 Windows 10:

+--------------+---------------+------------------------------+
|   Platform   | ExactSpelling | Resulting unmanaged function |
+--------------+---------------+------------------------------+
| .NET, 32-bit | true          | -                            |
| .NET, 64-bit | true          | -                            |
| .NET, 32-bit | false         | RtlMoveMemory                |
| .NET, 64-bit | false         | memmove                      |
+--------------+---------------+------------------------------+

对于.NET Core,逻辑要简单得多:.NET Core 不关心这种向后兼容的废话。如果您要求 kernel32!CopyMemory,天哪,它会尝试为您提供 kernel32!CopyMemory。由于根本没有这样的导出,它将失败。对于 64 位和 32 位运行时都是如此。

在 64 位上 Windows RtlCopyMemory 实际上作为导出存在,这就是它适用于 .NET Core(以及 64 位 .NET Framework)的原因。不过,值得注意的是,文档根本不保证它存在,所以依赖它似乎是不可取的——除了更基本的问题,它使你的代码在任何不是 Windows.

从 .NET 4.6 开始,Buffer.MemoryCopy 提供了一个 portable 替代方案,在 .NET Core 2.0 中也可用。如果你 必须 P/Invoke 到一个本地函数(希望只是作为权宜之计)你最好从 P/Invoking 到 RtlMoveMemory,因为它存在在 32 位和 64 位 Windows:

[DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory", ExactSpelling = true)]
public static extern void CopyMemory(IntPtr dest, IntPtr src, IntPtr count);

对于两种位数,这将在 .NET Core 和 .NET Framework 上正常工作(当然,只要您在 Windows 上 运行)。