この問題に遭遇している人を見かけることがあるためメモ。
Win32 APIのCopyMemory 関数が使用する.NETのバージョンやプラットフォームの違いで使えない理由は.NET Frameworkでは存在したCopyMemory 関数
をP/Invokeした際のフォールバック処理が.NET Coreには存在しないため。
CopyMemory
関数は実際にはkernel32.dll
には存在せず、アンマネージド (C/C++) の環境ではこの関数は実際にはRtlCopyMemoryという関数へのマクロであり、RtlCopyMemory
関数は更にCランタイムライブラリのmemcpy
関数へのマクロになっている。
CopyMemory
関数のMicrosoft Learnの公式ドキュメントにも以下の記載がある。
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
関数がエクスポートされている)
以下の画像は32ビットのkernel32.dll
のエクスポート (CopyMemory
関数やRtlCopyMemory
関数はエクスポートされていない)
プラットフォームが64ビットの場合はRtlCopyMemory
関数のみkernel32.dll
からエクスポートされているが、実際には64ビットのntdll.dll
(他のRtlXxx関数やOSのネイティブAPI (NtXxx関数/システムコール) が実装されているDLL) からエクスポートされている同名の関数へのスタブ (RtlCopyMemoryStub
) である。
以下の画像は64ビットのntdll.dll
のRtlCopyMemory
関数のエクスポート
.NET FrameworkではCopyMemory 関数
をP/Invokeした場合はRtlCopyMemory
関数かmemmove
関数にフォールバックする仕様となっているが、.NET Coreにはこの特別処理は無い。
以下がこの関数に対応するWindows SDKに含まれるヘッダーファイル内の該当部分。
#define CopyMemory RtlCopyMemory
#define RtlCopyMemory(Destination,Source,Length) memcpy((Destination),(Source),(Length))
ちなみにだが、MoveMemory
FillMemory
ZeroMemory
関数も同様にマクロである。
#define MoveMemory RtlMoveMemory
#define FillMemory RtlFillMemory
#define ZeroMemory RtlZeroMemory
#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 メソッドが用意されており。
.NET 7以降はSystem.Runtime.InteropServices.NativeMemory
クラスにも同様のメソッドが用意されている。
またSystem.Runtime.CompilerServices.Unsafe
クラスのCopyBlock メソッドも同様にコピーに使用できる。
どうしてもCopyMemory
関数を使用する必要がある場合は、代わりにCランタイムライブラリ (msvcrt.dll
/ ucrtbase.dll
) の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+)を使用するのが正しい。
// Definitions of common types
#ifdef _WIN64
typedef unsigned __int64 size_t;
#else
typedef unsigned int size_t;
#endif
#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;
#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;
void CopyMemory(
_In_ PVOID Destination,
_In_ const VOID *Source,
_In_ SIZE_T Length // SIZE_Tはsize_tと同じ
);
void* __cdecl memcpy(
_Out_writes_bytes_all_(_Size) void* _Dst,
_In_reads_bytes_(_Size) void const* _Src,
_In_ size_t _Size
);