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

2

5

I'm in the process of moving some code from .NET (4.5) to .NET Core (2) and have a multi-targeted project like so...

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

The code base uses the Win32 API function CopyMemory from kernel32, but I've found I need to use a different entry point name depending on which framework I'm targeting.

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

I would have thought this was all at a lower level than .NET

So, the question is... why?

Bessbessarabia answered 30/10, 2017 at 11:52 Comment(12)
It's certainly nothing at all to do with .net versions. Why do you think that .net version is the key? More likely it's different Windows versions and/or 32/64 bit differences. Anyway, in .net core you use System.Buffer.MemoryCopyIrisirisa
If your project uses Windows specific stuff, why do you want to port it to .NET Core? Isn't the whole point of .NET Core to be multi platform?Fiberglass
On an almost but not completely unrelated note, from .NET 4.6 onwards there's Buffer.MemoryCopy, which is also available in .NET Core 2.0. Unsafe, but portable.Immunotherapy
The only reason this came up is because I'm trying to understand multi-targeting a bit better so the project I'm porting I thought I'd try to get working under .NET45 and .NETCore. The #ifdefs on the DllImport are the only way I've found to get it to work. (and it's only on this one function out of many that I'm importing that gives the problem). I'm just wondering why.Bessbessarabia
Note that the entry point in kernel32.dll for modern versions of Windows really is RtlCopyMemory, and not CopyMemory -- there is no such export. There may be something going on in the P/Invoke layer of the .NET CLR that fixes things up transparently, similarly to how ANSI/Unicode get conveniently translated, while .NET Core doesn't bother with such niceties (you will notice it fails on .NET too for CopyMemory if you pass ExactSpelling = true). You may as well use the RtlCopyMemory import for both platforms.Immunotherapy
@JeroenMostert - what's weird though is only CopyMemory works on .NET 4.5 and only RtlCopyMemory works on .NET Core.Bessbessarabia
@BradRobinson You are running the .net 4.5 program and the .net core program on the exact same machine, with exact same process bitness? I find that very hard to believe.Irisirisa
@BradRobinson: at least on my machine (Windows 10, .NET 4.7, 64-bit JIT), this is not true. RtlCopyMemory works fine there, as I'd expect it to.Immunotherapy
Yep, same machine, just tweaking the config and rebuilding.Bessbessarabia
...but for 32-bit, it fails. Double-check that you haven't built your project as "Any CPU (prefer 32-bit)", which is the default for .NET Framework in VS 2015+.Immunotherapy
@JeroenMostert - Yep, that was it! Thanks. Makes sense now.Bessbessarabia
Curiously, the 32-bit kernel32.dll on my machine has forwards for RtlMoveMemory and RtlZeroMemory, but not RtlCopyMemory. But on an even spookier note, it has no CopyMemory export either, so I have no idea what the CLR is doing to make this work... investing in making your code base use Buffer.MemoryCopy seems like a really good way to move forward. :-PImmunotherapy
I
11

Asking for CopyMemory is actually a pretty bad idea, if you want predictable results. For starters, no unmanaged application calls any function named CopyMemory, as it's defined as a simple alias for the C memcpy function in the Windows headers. There is no CopyMemory export in kernel32.dll at all, and whether RtlCopyMemory is available is dependent on your platform. The logic applied for what function gets imported when you ask for CopyMemory to be P/Invoked (if any) varies by platform. Here's a little table that applies to 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                      |
+--------------+---------------+------------------------------+

For .NET Core, the logic is much simpler: .NET Core doesn't care about this backwards compatibility nonsense. If you ask for kernel32!CopyMemory, by golly it will try and get you kernel32!CopyMemory. And since there is no such export at all, it will fail. This is true for both 64-bit and 32-bit runtimes.

On 64-bit Windows RtlCopyMemory actually exists as an export, which is why that works for .NET Core (and 64-bit .NET Framework as well). It's worth noting, though, that the documentation does not guarantee that it exists at all, so it seems inadvisable to rely on this -- aside from the more basic problem that it makes your code unportable on anything that's not Windows.

From .NET 4.6 onwards, Buffer.MemoryCopy provides a portable alternative, available in .NET Core 2.0 as well. If you must P/Invoke to a native function (hopefully only as a stopgap measure) you're better off P/Invoking to RtlMoveMemory, since it exists on both 32-bit and 64-bit Windows:

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

This will work correctly on both .NET Core and .NET Framework, for both bitnesses (as long as you're running on Windows, of course).

Immunotherapy answered 30/10, 2017 at 13:23 Comment(3)
Thanks Jeroen. Yep I've moved to .NET 4.6 and mapping the old call to Buffer.MemoryCopy now.Bessbessarabia
Shouldn't the count still be a UIntPtr technically, because SIZE_T would be unsigned?!Seventeen
@0xC0000022L: technically, yes. I just copied the definition from the OP. It's not likely to matter much in practice. For C# 9+ the obvious choice of type would be nuint, which is more convenient to use and expresses the intent better as well.Immunotherapy
W
4

It does not have anything to do with the framework you target, it is the bitness of the process that matters. A .NET 4.5 project starts life with the Project > Properties > Build tab > "Prefer 32-bit" checkbox turned on. .NETCore favors 64-bit code heavily and makes you jump through a hoop to get the 32-bit runtime.

CopyMemory() is olden, dates back to early 16-bit Windows versions. They had to retain it for the 32-bit winapi, there are lots of VBx programs that use it. But put their foot down for the 64-bit. Otherwise well documented in the MSDN article: "This function is defined as the RtlCopyMemory function. Its implementation is provided inline. For more information, see WinBase.h and WinNT.h". Which are worth a look, you'll see what "inline" means. The RtlCopyMemory isn't actually being used either, substituting it with memcpy() instead.

So just use RtlCopyMemory instead for either flavor. Do keep in mind that it can't work when you deploy on Linux or MacOS.

Watthour answered 30/10, 2017 at 12:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.