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

5

我正在将一些代码从.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.6开始有Buffer.MemoryCopy,它也可用于.NET Core 2.0。虽然不安全,但是可移植的。 - Jeroen Mostert
请注意,对于现代版本的Windows,kernel32.dll中的入口点确实是RtlCopyMemory而不是CopyMemory--没有这样的导出。在.NET CLR的P/Invoke层中可能会发生一些事情,以透明地修复这些问题,类似于ANSI/Unicode如何方便地转换,而.NET Core则不会浪费时间处理这种细节(如果您传递ExactSpelling=true,您会注意到它在.NET上也无法使用CopyMemory)。您可以为两个平台都使用RtlCopyMemory导入。 - Jeroen Mostert
@BradRobinson 你在同一台机器上运行 .net 4.5 程序和 .net core 程序,并且进程位数完全相同吗?我觉得这很难相信。 - David Heffernan
2
...但对于32位,它会失败。请仔细检查您是否将项目构建为“任何 CPU(首选32位)”,这是VS 2015+中.NET Framework的默认设置。 - Jeroen Mostert
2
有趣的是,我的机器上的32位kernel32.dll对于RtlMoveMemoryRtlZeroMemory有向前引用,但没有RtlCopyMemory。但更令人毛骨悚然的是,它也没有CopyMemory导出,所以我不知道CLR正在做什么来使这个工作...投资于让您的代码库使用Buffer.MemoryCopy似乎是一个推进的好方法。:-P - Jeroen Mostert
显示剩余7条评论
2个回答

9

如果想要可预测的结果,那么请求使用 CopyMemory 实际上是一个非常糟糕的主意。首先,没有任何非托管应用程序调用名为CopyMemory 的任何函数,因为它在Windows头文件中被定义为C语言memcpy函数的简单别名。根本没有在 kernel32.dll 中导出 CopyMemory 函数,而是否有可用的 RtlCopyMemory 取决于您的平台。当您请求 P/Invoke CopyMemory 时,所应用的导入函数逻辑(如果有)会因平台而异。以下是适用于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提供了一种可移植的替代方案,在.NET Core 2.0中也可用。如果你必须P/Invoke到本机函数(希望只是临时措施),最好P/Invoke到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系统)。


感谢Jeroen。是的,我已经转移到.NET 4.6,并将旧调用映射到Buffer.MemoryCopy现在。 - Brad Robinson
从技术上讲,count不应该仍然是UIntPtr吗?因为SIZE_T是无符号的! - 0xC0000022L
1
@0xC0000022L: 从技术上讲是的,我只是从 OP 复制了定义。实际上这可能并不重要。对于 C# 9+ 来说,明显的类型选择应该是 nuint,它更方便使用,也更能表达意图。 - Jeroen Mostert

4
它与你所针对的框架无关,而是进程的位数才是重要的。.NET 4.5项目在启动时默认勾选“优先使用32位”复选框,而.NETCore则更偏向于64位代码,并需要您跳过一些步骤来获取32位运行时。
CopyMemory()函数很古老,可以追溯到早期的16位Windows版本。为了保留32位winapi,他们不得不保留它,有许多VBx程序使用它。但是他们在64位上坚持自己的立场。否则,在MSDN文章中有很好的文档:“该函数被定义为RtlCopyMemory函数。其实现是提供内联的。有关详细信息,请参阅WinBase.h和WinNT.h”。这两个值得一看,您会看到“内联”的含义。RtlCopyMemory也没有被实际使用,反而使用了memcpy()代替它。
因此,对于任何一种类型,只需使用RtlCopyMemory即可。请记住,在Linux或MacOS上部署时,它无法工作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接