[C#] CopyMemory関数が使用する.NETのバージョンやプラットフォームの違いで使えない理由 (Win32 API)

この問題に遭遇している人を見かけることがあるためメモ。

Win32 APIの[CopyMemory 関数](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/aa366535(v=vs.85))が使用する.NETのバージョンやプラットフォームの違いで使えない理由は.NET Frameworkでは存在した`CopyMemory 関数`をP/Invokeした際の[フォールバック処理](https://github.com/dotnet/wpf/issues/586#issuecomment-483434892)が.NET Coreには存在しないため。

`CopyMemory`関数は実際には`kernel32.dll`には存在せず、アンマネージド (C/C++) の環境ではこの関数は実際には[RtlCopyMemory](https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-rtlcopymemory)という関数へのマクロであり、`RtlCopyMemory`関数は更にCランタイムライブラリの`memcpy`関数へのマクロになっている。

`CopyMemory`関数のMicrosoft Learnの公式[ドキュメント](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/aa366535(v=vs.85))にも以下の記載がある。
> This function is defined as the RtlCopyMemory function. Its implementation is provided inline. For more information, see WinBase.h and WinNT.h.

以下の画像は64ビットの`kernel32.dll`のエクスポート (`CopyMemory`関数ではなく`RtlCopyMemory`関数がエクスポートされている)
![k32_x64_exports_copymemory.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3780065/fe8cca06-6891-4684-b8be-f4c5d61b29a3.png "x64 kernel32.dll のエクスポート")

以下の画像は32ビットの`kernel32.dll`のエクスポート (`CopyMemory`関数や`RtlCopyMemory`関数はエクスポートされていない)
![k32_x86_exports_copymemory.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3780065/57b310b3-a131-404a-8ae9-709f7d473d27.png "x86 kernel32.dll のエクスポート")

プラットフォームが64ビットの場合は`RtlCopyMemory`関数のみ`kernel32.dll`からエクスポートされているが、実際には64ビットの`ntdll.dll`(他のRtlXxx関数やOSのネイティブAPI (NtXxx関数/システムコール) が実装されているDLL) からエクスポートされている同名の関数へのスタブ (`RtlCopyMemoryStub`) である。

以下の画像は64ビットの`ntdll.dll`の`RtlCopyMemory`関数のエクスポート
![ntdll_exports_copymemory.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3780065/8cb1b4da-562f-4b5d-937e-07c38dc92344.png "x64 ntdll.dll のエクスポート")

.NET Frameworkでは`CopyMemory 関数`をP/Invokeした場合は`RtlCopyMemory`関数か`memmove`関数に[フォールバック](https://stackoverflow.com/a/47016390)する仕様となっているが、.NET Coreにはこの特別処理は無い。

以下がこの関数に対応するWindows SDKに含まれるヘッダーファイル内の該当部分。

```cpp:minwinbase.h
#define CopyMemory RtlCopyMemory
```

```cpp:winnt.h / wdm.h (WDK)
#define RtlCopyMemory(Destination,Source,Length) memcpy((Destination),(Source),(Length))
```

ちなみにだが、`MoveMemory` `FillMemory` `ZeroMemory`関数も同様にマクロである。

```cpp:minwinbase.h
#define MoveMemory RtlMoveMemory
#define FillMemory RtlFillMemory
#define ZeroMemory RtlZeroMemory
```

```cpp:winnt.h / wdm.h (WDK)
#define RtlEqualMemory(Destination,Source,Length) (!memcmp((Destination),(Source),(Length)))
#define RtlMoveMemory(Destination,Source,Length) memmove((Destination),(Source),(Length))
#define RtlFillMemory(Destination,Length,Fill) memset((Destination),(Fill),(Length))
#define RtlZeroMemory(Destination,Length) memset((Destination),0,(Length))
```

`CopyMemory`関数を使用しなくても.NET Framework 4.6 (Coreは1.0) 以降は`System.Buffer`クラスの[MemoryCopy メソッド](https://learn.microsoft.com/en-us/dotnet/api/system.buffer.memorycopy?view=net-9.0)が用意されており。
.NET 7以降は`System.Runtime.InteropServices.NativeMemory`クラスにも同様の[メソッド](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.nativememory.copy?view=net-9.0#system-runtime-interopservices-nativememory-copy(system-void*-system-void*-system-uintptr))が用意されている。
また`System.Runtime.CompilerServices.Unsafe`クラスの[CopyBlock メソッド](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.unsafe.copyblock?view=net-9.0)も同様にコピーに使用できる。

どうしても`CopyMemory`関数を使用する必要がある場合は、代わりにCランタイムライブラリ (`msvcrt.dll` / `ucrtbase.dll`) の[memcpy 関数](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/memcpy-wmemcpy?view=msvc-170)の方をP/Invokeした方が良いと思われる。

```csharp:C#でのmemcpy関数P/Invoke例
[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true, SetLastError = false)]
// void*
public static extern IntPtr memcpy(
    IntPtr _Dst, // void* (or byte* or byte[])
    IntPtr _Src, // void* (or byte* or byte[])
    UIntPtr _Size // size_t (UIntPtr = UInt32 (on 32-Bit), UInt64 (on 64-Bit))
    );
```

併せてだが、`size_t`と定義されているバッファーサイズを受け取るパラメーターを`int`や`uint`型として定義している例が見受けられるが、これは間違い (プラットフォームを32ビットに固定する場合を除く)。

理由は`size_t`型はコンパイルするプラットフォームでサイズが異なり、32ビットでは32ビット符号なし整数である`unsigned int`(マネージド型は`uint`)型、64ビットでは64ビット符号なし整数である`unsigned __int64`(マネージド型は`ulong`)型となるので、マネージド型は`UIntPtr`か`nuint`(C# 9+)を使用するのが正しい。

```cpp:vcruntime.h (size_t)
// Definitions of common types
#ifdef _WIN64
    typedef unsigned __int64 size_t;
#else
    typedef unsigned int     size_t;
#endif
```

```cpp:basetsd.h (SIZE_T)
#if defined(_WIN64)
    typedef __int64 LONG_PTR, *PLONG_PTR;
    typedef unsigned __int64 ULONG_PTR, *PULONG_PTR;
#else
    typedef _W64 long LONG_PTR, *PLONG_PTR;
    typedef _W64 unsigned long ULONG_PTR, *PULONG_PTR;
#endif

//
// SIZE_T used for counts or ranges which need to span the range of
// of a pointer.  SSIZE_T is the signed variation.
//

typedef ULONG_PTR SIZE_T, *PSIZE_T;
typedef LONG_PTR SSIZE_T, *PSSIZE_T;
```

```cpp:intsafe.h / ntintsafe.h (WDK) (SIZE_T)
#ifdef _WIN64
typedef __int64             LONG_PTR;
typedef unsigned __int64    ULONG_PTR;
#else
typedef _W64 long           LONG_PTR;
typedef _W64 unsigned long  ULONG_PTR;

#ifdef _WIN64
typedef unsigned __int64    size_t;
#else
typedef _W64 unsigned int   size_t;
#endif

typedef LONG_PTR    SSIZE_T;
typedef ULONG_PTR   SIZE_T;
```

```cpp:minwinbase.h (CopyMemory)
void CopyMemory(
  _In_       PVOID  Destination,
  _In_ const VOID   *Source,
  _In_       SIZE_T Length // SIZE_Tはsize_tと同じ
);
```

```cpp:vcruntime_string.h (memcpy)
void* __cdecl memcpy(
    _Out_writes_bytes_all_(_Size) void* _Dst,
    _In_reads_bytes_(_Size)       void const* _Src,
    _In_                          size_t      _Size
    );
```