C# DllImport完全ガイド|DLL呼び出しの基本からエラー対策・マーシャリングまでわかる入門教程

はじめに

C#でアプリケーションを開発していると、Windows API、自作のC/C++ DLL、既存のネイティブライブラリなどを呼び出したい場面があります。そのときに使われる代表的な仕組みがDllImportです。

C#は通常、.NETランタイム上で動くマネージドコードとして実行されます。一方、C/C++で作られたDLLやOSが提供するAPIは、アンマネージドコードとして動作します。DllImportは、この2つの世界をつなぐための機能です。

この記事では、C# DllImportの基本構文、DLL呼び出しの仕組み、マーシャリング、エラー対策、.NET Frameworkと.NET Core以降の違い、実用サンプルまでを入門者向けに解説します。

1. C# DllImportとは?DLL呼び出しの基本を初心者向けに解説

1-1. DllImportの役割とP/Invokeとの関係

DllImportは、C#からアンマネージドDLLの関数を呼び出すために使う属性です。正式にはDllImportAttributeというクラスで、System.Runtime.InteropServices名前空間に含まれています。

C#でDllImportを使う処理は、一般的にP/Invoke、つまりPlatform Invocation Servicesと呼ばれます。P/Invokeは、.NETのマネージドコードからネイティブコードの関数を呼び出すための仕組みです。

Microsoftの公式ドキュメントでも、DllImportAttributeはアンマネージドDLLからエクスポートされた関数を呼び出すために必要な情報を提供する属性と説明されています。最低限必要なのは、エントリポイントを含むDLL名です。Microsoft Learn

1-2. C#からネイティブDLLを呼び出す仕組み

C#からネイティブDLLを呼び出す場合、まずC#側で「このDLLのこの関数を、この戻り値と引数で呼び出す」という宣言を行います。実際の関数本体はC#には書かず、DLLの中に存在します。

基本的な流れは次のとおりです。

  1. C/C++などで作られたDLLが存在する

  2. DLL内の関数が外部から呼び出せるようにエクスポートされている

  3. C#側でDllImportを使って関数シグネチャを定義する

  4. C#コードから通常のメソッドのように呼び出す

  5. .NETランタイムが必要な型変換やDLL呼び出しを行う

たとえば、Windowsのuser32.dllに含まれるMessageBox関数をC#から呼び出す場合、C#側では次のように宣言します。

C#
using System.Runtime.InteropServices;

class Program
{
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int MessageBox(
IntPtr hWnd,
string text,
string caption,
uint type);

static void Main()
{
MessageBox(IntPtr.Zero, "Hello from C#", "DllImport Sample", 0);
}
}

このように、C#のメソッドとして宣言しているように見えますが、実体はuser32.dll内のネイティブ関数です。

1-3. DllImportが必要になる代表的なケース

C# DllImportが必要になる代表的なケースは、次のような場面です。

Windows APIを直接呼び出したい場合、既存のC/C++ DLLを再利用したい場合、ハードウェア制御用のメーカー提供DLLを利用したい場合、古い業務システムのネイティブライブラリを.NETアプリから使いたい場合、画像処理・音声処理・数値計算などの高速なネイティブライブラリを呼び出したい場合などです。

たとえば、バーコードリーダー、計測器、カメラ、プリンター、POS機器などのSDKでは、C/C++向けのDLLだけが提供されていることがあります。このような場合、C#からDllImportを使うことで、提供されたDLLを呼び出せます。

1-4. マネージドコードとアンマネージドコードの違い

C#の通常のコードは、.NETランタイムによって管理されるマネージドコードです。メモリ管理、型安全性、例外処理、ガベージコレクションなどは.NETが面倒を見てくれます。

一方、C/C++で作られたDLLなどは、多くの場合アンマネージドコードです。メモリ確保や解放、ポインタ操作、呼び出し規約、構造体のメモリ配置などを開発者が正しく管理する必要があります。

DllImportは、このマネージドコードとアンマネージドコードの境界を越えるための機能です。そのため、単純なC#メソッド呼び出しとは異なり、型の対応、文字コード、構造体のレイアウト、ビット数、メモリ所有権などに注意する必要があります。

1-5. DllImportでできること・できないこと

DllImportでできることは、アンマネージドDLLからエクスポートされた関数を呼び出すことです。Windows APIの呼び出し、自作C/C++ DLLの関数呼び出し、ポインタや構造体を使ったデータ受け渡し、コールバック関数の受け渡しなどが可能です。

一方で、DllImportは万能ではありません。C#で作成された通常の.NET DLLを直接呼び出すための仕組みではありません。また、C++クラスのメンバー関数をそのまま自然に呼び出せるわけでもありません。C++の名前修飾、クラスインスタンス、テンプレート、例外などはC#側から直接扱いにくいため、通常はC言語形式の関数として公開します。

2. C# DllImportの基本構文と最小サンプル

2-1. DllImportAttributeの基本的な書き方

C#でDllImportを使うには、System.Runtime.InteropServicesを読み込み、外部関数にDllImport属性を付けます。

C#
using System.Runtime.InteropServices;

[DllImport("Sample.dll")]
private static extern int Add(int a, int b);

基本形は次のとおりです。

C#
[DllImport("DLL名")]
private static extern 戻り値の型 メソッド名(引数);

DllImportにはDLL名を指定します。メソッド名は、既定ではDLL内のエクスポート関数名と一致している必要があります。名前が異なる場合は、EntryPointを使ってDLL内の実際の関数名を指定します。

2-2. static externメソッドを定義する理由

DllImportを使うメソッドには、通常static externを付けます。

staticを付けるのは、DLL内の関数がC#オブジェクトのインスタンスメソッドではなく、外部に存在する関数だからです。

externを付けるのは、メソッドの実装がC#コード内には存在せず、外部DLL側にあることを示すためです。

つまり、次の宣言は「このメソッドの本体はC#にはない。Sample.dllにある関数を呼び出す」という意味になります。

C#
[DllImport("Sample.dll")]
private static extern int Add(int a, int b);

2-3. DLL名・関数名・戻り値・引数の指定方法

DLL名はDllImportの第1引数に指定します。

C#
[DllImport("kernel32.dll")]

関数名は、C#側のメソッド名とDLL側のエクスポート名が同じであれば、そのまま使えます。

C#
[DllImport("Sample.dll")]
private static extern int Add(int a, int b);

DLL側の関数名とC#側のメソッド名を変えたい場合は、EntryPointを使います。

C#
[DllImport("Sample.dll", EntryPoint = "Add")]
private static extern int AddFromDll(int x, int y);

戻り値と引数は、DLL側の型に合わせてC#側で定義します。ここが一致していないと、値が壊れたり、AccessViolationExceptionが発生したりする原因になります。

2-4. Windows APIをDllImportで呼び出すサンプル

Windows APIのMessageBoxを呼び出す最小サンプルです。

C#
using System;
using System.Runtime.InteropServices;

class Program
{
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int MessageBox(
IntPtr hWnd,
string lpText,
string lpCaption,
uint uType);

static void Main()
{
MessageBox(IntPtr.Zero, "C#からDLLを呼び出しました", "DllImport", 0);
}
}

user32.dllはWindowsが提供するDLLです。CharSet.Unicodeを指定することで、文字列をUnicodeとして渡します。

2-5. 自作C/C++ DLLをC#から呼び出すサンプル

C++側で次のようなDLL関数を作成したとします。

C++
extern "C" __declspec(dllexport) int __stdcall Add(int a, int b)
{
return a + b;
}

C#側では次のように呼び出します。

C#
using System;
using System.Runtime.InteropServices;

class Program
{
[DllImport("NativeMath.dll", CallingConvention = CallingConvention.StdCall)]
private static extern int Add(int a, int b);

static void Main()
{
int result = Add(10, 20);
Console.WriteLine(result);
}
}

この例では、C++側で__stdcallを使っているため、C#側でもCallingConvention.StdCallを指定しています。

3. DllImportで指定できる主要オプション

3-1. EntryPoint:DLL内の関数名を指定する

EntryPointは、DLL内の実際のエクスポート関数名を指定するためのオプションです。

C#
[DllImport("NativeMath.dll", EntryPoint = "Add")]
private static extern int AddFromNative(int a, int b);

C#側のメソッド名をわかりやすく変えたい場合や、DLL側の関数名がC#の命名規則に合わない場合に便利です。

Windows APIでは、MessageBoxWMessageBoxAのように、Unicode版とANSI版で関数名が分かれていることがあります。この場合もEntryPointを使って明示できます。

C#
[DllImport("user32.dll", EntryPoint = "MessageBoxW", CharSet = CharSet.Unicode)]
private static extern int MessageBox(
IntPtr hWnd,
string text,
string caption,
uint type);

3-2. CallingConvention:呼び出し規約を指定する

CallingConventionは、関数呼び出し時に引数をどのようにスタックに積み、誰がスタックを片付けるかを決める呼び出し規約です。

代表的な値には、StdCallCdeclWinapiなどがあります。

C#
[DllImport("NativeLib.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int Calculate(int value);

C/C++側が__cdeclならCallingConvention.Cdecl__stdcallならCallingConvention.StdCallを指定します。ここが一致していないと、特に32bit環境でスタック破壊やStack imbalanceの原因になります。

3-3. CharSet:文字列の文字コードを指定する

CharSetは、文字列をどの文字コードとしてDLLに渡すかを指定します。

C#
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int MessageBox(
IntPtr hWnd,
string text,
string caption,
uint type);

主な値は次のとおりです。

CharSet.UnicodeはUnicode文字列として渡します。Windows APIのW版関数と相性がよい指定です。

CharSet.AnsiはANSI文字列として渡します。古いDLLやC言語のchar*を前提とするDLLで使われます。

CharSet.Autoは実行環境に応じて自動選択しますが、挙動を明確にするため、実務ではUnicodeAnsiを明示することが多いです。

3-4. SetLastError:Win32エラー情報を取得する

SetLastError = trueを指定すると、ネイティブ関数が設定した最後のエラー情報をC#側から取得できます。

C#
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess(
uint dwDesiredAccess,
bool bInheritHandle,
uint dwProcessId);

呼び出し後にMarshal.GetLastWin32Error()を使うと、Win32エラーコードを取得できます。

C#
IntPtr handle = OpenProcess(0x0010, false, 999999);

if (handle == IntPtr.Zero)
{
int error = Marshal.GetLastWin32Error();
Console.WriteLine($"Error: {error}");
}

Marshal.GetLastWin32Errorは、DllImportAttribute.SetLastErrortrueにしたP/Invoke呼び出しの後に、最後のシステムエラーを取得するために使います。Microsoft Learn

.NET 6以降では、用途によってMarshal.GetLastPInvokeError()も選択肢になります。公式ドキュメントでは、SetLastError = trueを指定したP/Invokeに対して最後のP/Invokeエラーを取得する例が示されています。Microsoft Learn

3-5. ExactSpelling:関数名の自動補完を制御する

ExactSpellingは、関数名を厳密に一致させるかどうかを指定します。

C#
[DllImport("user32.dll", EntryPoint = "MessageBoxW", ExactSpelling = true)]
private static extern int MessageBoxW(
IntPtr hWnd,
string text,
string caption,
uint type);

CharSetを指定した場合、環境や設定によって関数名の末尾にAWが付いた候補を探すことがあります。ExactSpelling = trueにすると、指定した名前そのものを探します。

Windows APIでMessageBoxWのように明示的な関数名を指定する場合は、ExactSpelling = trueを使うと意図が明確になります。

3-6. BestFitMappingとThrowOnUnmappableCharの使い方

BestFitMappingThrowOnUnmappableCharは、主にANSI文字列への変換時に関係するオプションです。

BestFitMappingは、変換できないUnicode文字を近い文字に置き換えるかどうかを制御します。

ThrowOnUnmappableCharは、変換できない文字があった場合に例外をスローするかどうかを制御します。

C#
[DllImport(
"Legacy.dll",
CharSet = CharSet.Ansi,
BestFitMapping = false,
ThrowOnUnmappableChar = true)]
private static extern int SendText(string text);

古いANSI前提のDLLに日本語や記号を渡す場合、文字化けや意図しない文字変換が起きることがあります。そのようなケースでは、これらのオプションを検討します。

.NETのP/Invokeソース生成に関する公式ドキュメントでは、BestFitMappingThrowOnUnmappableCharはWindows上のANSI文字列マーシャリングに関連する項目として説明されています。Microsoft Learn

4. C# DllImportでよく使う型変換とマーシャリング

4-1. マーシャリングとは何か

マーシャリングとは、C#側のデータ型をネイティブDLL側で扱える形式に変換したり、逆にネイティブ側のデータをC#側に戻したりする処理です。

たとえば、C#のstringは.NET管理下の文字列ですが、C言語ではchar*wchar_t*として扱われることが多く、内部表現が異なります。この差を吸収するのがマーシャリングです。

基本型だけであれば自動変換できることもありますが、文字列、配列、構造体、ポインタ、コールバックなどでは明示的な指定が必要になることがあります。

4-2. int・double・boolなど基本型の対応関係

基本型の対応例は次のとおりです。

C/C++側C#側の例
intint
doubledouble
floatfloat
shortshort
char*stringまたはStringBuilder
void*IntPtr
BOOLboolまたはint
BYTEbyte
DWORDuint
LONGintまたは環境に応じた型

注意点として、C/C++のlongはOSやコンパイラ環境によってサイズが異なることがあります。.NETの型マーシャリングに関する公式ドキュメントでは、Cのlongを扱う場合、.NET 6以降ではCLongCULongの利用が案内されています。Microsoft Learn

4-3. string・StringBuilderを渡す方法

DLLに文字列を入力として渡すだけなら、C#側ではstringを使うことが多いです。

C#
[DllImport("NativeText.dll", CharSet = CharSet.Unicode)]
private static extern int PrintText(string text);

一方、DLL側で文字列バッファに値を書き込む場合は、StringBuilderを使うのが一般的です。

C#
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int GetWindowText(
IntPtr hWnd,
StringBuilder lpString,
int nMaxCount);

呼び出し例です。

C#
StringBuilder buffer = new StringBuilder(256);
GetWindowText(handle, buffer, buffer.Capacity);
Console.WriteLine(buffer.ToString());

stringは基本的に不変なので、DLL側で内容を書き換える用途には向きません。出力バッファとして使う場合はStringBuilder、またはbyte[]char[]を使います。

4-4. 配列をDLLに渡す方法

整数配列をDLLに渡す例です。

C++側です。

C++
extern "C" __declspec(dllexport) int __stdcall SumArray(int* values, int length)
{
int sum = 0;
for (int i = 0; i < length; i++)
{
sum += values[i];
}
return sum;
}

C#側です。

C#
[DllImport("NativeArray.dll", CallingConvention = CallingConvention.StdCall)]
private static extern int SumArray(
[In] int[] values,
int length);

呼び出し例です。

C#
int[] values = { 1, 2, 3, 4, 5 };
int result = SumArray(values, values.Length);
Console.WriteLine(result);

配列の受け渡しでは、配列の長さを別引数で渡すことが重要です。C/C++側のポインタには、C#配列の要素数情報が自動では伝わりません。

Microsoftのドキュメントでも、マネージド配列とネイティブ配列はメモリ上の扱いが異なるため、境界を越えて渡すにはマーシャリングが必要になると説明されています。Microsoft Learn

4-5. struct構造体を渡す方法

C/C++の構造体をC#で扱う場合、メモリ配置を一致させる必要があります。

C++側です。

C++
struct Point
{
int X;
int Y;
};

extern "C" __declspec(dllexport) int __stdcall GetLength(Point p)
{
return p.X * p.X + p.Y * p.Y;
}

C#側です。

C#
[StructLayout(LayoutKind.Sequential)]
public struct Point
{
public int X;
public int Y;
}

[DllImport("NativeStruct.dll", CallingConvention = CallingConvention.StdCall)]
private static extern int GetLength(Point p);

[StructLayout(LayoutKind.Sequential)]を指定することで、C#側のフィールド順を構造体のメモリ配置に反映します。

構造体に文字列や配列が含まれる場合は、さらにMarshalAsでサイズや文字列形式を明示することがあります。

4-6. ポインタ・IntPtr・ref・outの使い分け

ポインタを扱う場合、C#側では主にIntPtrrefoutを使います。

IntPtrは、ネイティブハンドル、メモリアドレス、void*のようなポインタ値を扱うときに使います。

C#
[DllImport("kernel32.dll")]
private static extern bool CloseHandle(IntPtr hObject);

refは、C#側から値を渡し、DLL側で変更された結果も受け取りたい場合に使います。

C#
[DllImport("Native.dll")]
private static extern void UpdateValue(ref int value);

outは、DLL側から値を出力してもらう場合に使います。

C#
[DllImport("Native.dll")]
private static extern bool TryGetValue(out int value);

C/C++側がint*を受け取る場合でも、その意味が入力なのか出力なのか入出力なのかによって、C#側の定義は変わります。

4-7. MarshalAs属性の使い方

MarshalAs属性は、C#の型をネイティブ側でどのように表現するかを明示するために使います。

たとえば、C++側のBOOLは多くの場合4バイト整数です。C#のboolをそのまま使うと意図と異なることがあるため、次のように指定します。

C#
[DllImport("Native.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool IsEnabled();

構造体内の固定長文字列では次のように指定できます。

C#
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct UserInfo
{
public int Id;

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
public string Name;
}

この場合、Nameは固定長64文字の文字列としてマーシャリングされます。

4-8. UnmanagedTypeの代表的な指定例

UnmanagedTypeにはさまざまな指定があります。代表例は次のとおりです。

指定用途
UnmanagedType.BoolWin32 BOOL
UnmanagedType.I11バイトboolやsigned char
UnmanagedType.U1unsigned charや1バイト値
UnmanagedType.LPStrANSI文字列
UnmanagedType.LPWStrUnicode文字列
UnmanagedType.LPUTF8StrUTF-8文字列
UnmanagedType.ByValTStr構造体内の固定長文字列
UnmanagedType.ByValArray構造体内の固定長配列
UnmanagedType.FunctionPtr関数ポインタ

DllImportで値がおかしい場合、MarshalAsの指定漏れが原因になっていることがあります。特にbool、文字列、構造体内配列は注意が必要です。

5. DllImportでC/C++ DLLを呼び出す実践パターン

5-1. C++側でDLL関数をexportする方法

C++でDLL関数をC#から呼び出せるようにするには、関数をエクスポートする必要があります。

C++
extern "C" __declspec(dllexport) int __stdcall Add(int a, int b)
{
return a + b;
}

__declspec(dllexport)は、その関数をDLLの外部から呼び出せるように公開する指定です。

C#側では次のように定義します。

C#
[DllImport("NativeMath.dll", CallingConvention = CallingConvention.StdCall)]
private static extern int Add(int a, int b);

5-2. extern "C"を付ける理由

C++では、関数名に引数情報などを含めた名前修飾が行われます。これを名前マングリングと呼びます。

たとえば、C++側でAddという関数を書いても、DLLにエクスポートされる名前がAddそのものではなく、別の装飾された名前になることがあります。

extern "C"を付けると、C言語形式の関数名でエクスポートされるため、C#のDllImportから見つけやすくなります。

C++
extern "C" __declspec(dllexport) int __stdcall Add(int a, int b);

EntryPointNotFoundExceptionが発生する場合は、まずDLL側のエクスポート名が想定どおりかを確認しましょう。

5-3. __stdcallと__cdeclの違い

__stdcall__cdeclは呼び出し規約です。

__stdcallは、主にWindows APIでよく使われる呼び出し規約です。スタックの後始末は呼び出された側が行います。

__cdeclは、C/C++の標準的な呼び出し規約として使われることが多く、スタックの後始末は呼び出し側が行います。可変長引数の関数では__cdeclが使われます。

C#側では次のように合わせます。

C#
[DllImport("Native.dll", CallingConvention = CallingConvention.StdCall)]
private static extern int FuncStdCall(int x);

[DllImport("Native.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern int FuncCdecl(int x);

呼び出し規約が一致していないと、32bit環境でスタック不整合が発生することがあります。64bit環境では呼び出し規約の差が表面化しにくい場合もありますが、DLL側の定義とC#側の定義は必ず合わせるべきです。

5-4. 構造体をC#とC++で一致させる方法

構造体を一致させるには、次の点を確認します。

フィールドの順番、各フィールドの型サイズ、アラインメント、パディング、文字列や配列のサイズ、文字コードを合わせます。

C++側です。

C++
#pragma pack(push, 1)
struct DeviceInfo
{
int Id;
double Value;
};
#pragma pack(pop)

extern "C" __declspec(dllexport) void __stdcall GetDeviceInfo(DeviceInfo* info)
{
info->Id = 100;
info->Value = 12.34;
}

C#側です。

C#
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct DeviceInfo
{
public int Id;
public double Value;
}

[DllImport("Device.dll", CallingConvention = CallingConvention.StdCall)]
private static extern void GetDeviceInfo(out DeviceInfo info);

C++側で#pragma packを指定している場合、C#側でもStructLayoutPackを合わせます。

5-5. コールバック関数をDllImportで扱う方法

DLL側からC#側の関数を呼び返してもらう場合は、デリゲートを使います。

C++側です。

C++
typedef void (__stdcall *Callback)(int value);

extern "C" __declspec(dllexport) void __stdcall ExecuteCallback(Callback callback)
{
callback(123);
}

C#側です。

C#
using System;
using System.Runtime.InteropServices;

class Program
{
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate void CallbackDelegate(int value);

[DllImport("CallbackLib.dll", CallingConvention = CallingConvention.StdCall)]
private static extern void ExecuteCallback(CallbackDelegate callback);

private static CallbackDelegate? _callback;

static void Main()
{
_callback = OnCallback;
ExecuteCallback(_callback);
}

private static void OnCallback(int value)
{
Console.WriteLine($"Callback value: {value}");
}
}

重要なのは、デリゲートをローカル変数だけにしないことです。ガベージコレクションで回収されると、DLL側が無効な関数ポインタを呼び出してクラッシュする可能性があります。そのため、上記のように静的フィールドなどで保持します。

5-6. DLL側で確保したメモリをC#側で扱う注意点

DLL側で確保したメモリをC#側で受け取る場合、誰が解放するのかを明確にする必要があります。

悪い例です。

C++
extern "C" __declspec(dllexport) const char* __stdcall GetText()
{
char* p = new char[100];
strcpy_s(p, 100, "Hello");
return p;
}

この場合、C#側で受け取ったあと、どの方法で解放すべきかが不明確です。C++のnew[]で確保したなら、C++側でdelete[]する関数を用意するのが安全です。

C++
extern "C" __declspec(dllexport) void __stdcall FreeText(char* p)
{
delete[] p;
}

C#側ではIntPtrで受け取り、文字列化したあと、必ずDLL側の解放関数を呼びます。

C#
[DllImport("NativeText.dll", CallingConvention = CallingConvention.StdCall)]
private static extern IntPtr GetText();

[DllImport("NativeText.dll", CallingConvention = CallingConvention.StdCall)]
private static extern void FreeText(IntPtr p);

メモリの確保と解放は同じランタイム、同じ規約で行うのが基本です。

5-7. 32bit・64bit DLLを使い分ける方法

C#アプリのプロセスが32bitなら32bit DLL、64bitなら64bit DLLを読み込む必要があります。ビット数が一致していないと、BadImageFormatExceptionが発生する代表的な原因になります。

実務では、次のような配置にすることがあります。

runtimes/
win-x86/native/NativeLib.dll
win-x64/native/NativeLib.dll

または、実行時にプロセスのビット数を判定してDLL名を切り替えます。

C#
string dllName = Environment.Is64BitProcess
? "NativeLib_x64.dll"
: "NativeLib_x86.dll";

ただし、DllImportのDLL名は属性引数としてコンパイル時に固定されるため、柔軟に切り替えたい場合は.NET Core以降のNativeLibrarySetDllImportResolverの利用も検討します。Native library loadingに関する公式ドキュメントでは、P/Invoke時にランタイムがネイティブライブラリを探索するパスや、SetDllImportResolverによる解決方法が説明されています。Microsoft Learn

6. C# DllImportで発生しやすいエラーと原因

6-1. Unable to load DLLが出る原因と対処法

Unable to load DLLは、指定したDLLを読み込めないときに発生する代表的なエラーです。

主な原因は、DLLが実行ディレクトリに存在しない、PATHが通っていない、依存DLLが不足している、32bit/64bitが一致していない、DLL名が間違っている、アクセス権がない、などです。

対処法として、まずDLLがアプリケーションの実行フォルダーに配置されているか確認します。次に、DLLが依存している別のDLLが不足していないか確認します。さらに、C#アプリのビルド設定とDLLのビット数が一致しているかを確認します。

6-2. DllNotFoundExceptionの原因と確認ポイント

DllNotFoundExceptionは、指定されたDLLが見つからない場合に発生します。

確認すべきポイントは次のとおりです。

DLL名のスペルが正しいか、拡張子の有無が適切か、実行ディレクトリに配置されているか、PATH環境変数にDLLの場所が含まれているか、発行時にDLLがコピーされているか、OSごとのライブラリ名が正しいかを確認します。

WindowsではSample.dll、LinuxではlibSample.so、macOSではlibSample.dylibのように、ネイティブライブラリの名前が異なる場合があります。

6-3. EntryPointNotFoundExceptionの原因と対処法

EntryPointNotFoundExceptionは、DLL自体は見つかったものの、指定した関数がDLL内に見つからない場合に発生します。

主な原因は、関数名のスペルミス、C++の名前マングリング、extern "C"の付け忘れ、__declspec(dllexport)の指定漏れ、EntryPointの指定ミス、ANSI版・Unicode版の関数名違いなどです。

対処法として、dumpbin /exportsなどのツールでDLLのエクスポート関数名を確認します。C++ DLLの場合は、C#から呼び出す関数にextern "C"を付けることを検討します。

6-4. BadImageFormatExceptionが出る原因

BadImageFormatExceptionは、読み込もうとしたDLLの形式が現在のプロセスに合っていない場合によく発生します。

特に多い原因は、32bitアプリから64bit DLLを読み込んでいる、または64bitアプリから32bit DLLを読み込んでいるケースです。

C#側のビルド設定がAny CPUになっている場合、実行環境によって64bitプロセスとして起動することがあります。32bit DLLしかない場合は、プロジェクト設定でx86を指定するか、「Prefer 32-bit」の設定を確認します。

6-5. AccessViolationExceptionが出る原因

AccessViolationExceptionは、ネイティブ側で不正なメモリアクセスが発生した場合に起こります。

よくある原因は、引数の型が間違っている、構造体のレイアウトが一致していない、バッファサイズが不足している、解放済みメモリを参照している、DLL側が期待するポインタにIntPtr.Zeroを渡している、コールバックデリゲートがGCで回収されている、などです。

このエラーは単なるC#例外というより、メモリ破壊の結果として発生することが多いため、DllImport定義を最初から見直す必要があります。

6-6. Stack imbalanceが発生する原因

Stack imbalanceは、主に呼び出し規約の不一致で発生します。

C++側が__cdeclなのにC#側でCallingConvention.StdCallを指定している、またはその逆のケースです。特に32bit環境では、スタックを呼び出し側が片付けるのか、呼び出された側が片付けるのかが重要になります。

対策は、C/C++側の関数宣言を確認し、C#側のCallingConventionを一致させることです。

6-7. 文字化け・文字列が正しく渡らない原因

文字化けが起きる場合、CharSetMarshalAs、DLL側の文字型が一致していない可能性があります。

C++側がwchar_t*を期待しているなら、C#側ではCharSet.UnicodeUnmanagedType.LPWStrを使います。

C++側がchar*を期待しているなら、CharSet.AnsiUnmanagedType.LPStr、またはUTF-8を想定したUnmanagedType.LPUTF8Strなどを検討します。

ただし、char*がANSIなのかUTF-8なのかはDLLの仕様次第です。C#側だけで判断せず、DLLのドキュメントやC/C++の実装を確認しましょう。

6-8. 戻り値や引数の値がおかしい場合の確認ポイント

戻り値や引数の値がおかしい場合は、型サイズ、符号あり・符号なし、boolのサイズ、構造体のPack、配列の長さ、文字コード、呼び出し規約を確認します。

たとえば、C++側のBOOLをC#側で1バイトのboolとして扱ってしまうと、戻り値の解釈がずれることがあります。C++側のunsigned intをC#側でintにしている場合、値域の違いで問題が出ることもあります。

DllImportでは、C#側の宣言がコンパイルできても、ネイティブ側と一致しているとは限りません。動作がおかしいときは、C/C++側のヘッダーファイルとC#側の宣言を1つずつ照合することが大切です。

7. DllImportエラーを解決するための確認手順

7-1. DLLの配置場所を確認する

まず、DLLがアプリケーションの実行ディレクトリに存在するか確認します。

Visual Studioで実行している場合、実行ディレクトリは通常、次のような場所です。

bin/Debug/net8.0/
bin/Release/net8.0/

.NET Frameworkの場合は次のような場所です。

bin/Debug/
bin/Release/

DLLをプロジェクトに追加している場合は、プロパティで「出力ディレクトリにコピー」を「常にコピーする」または「新しい場合はコピーする」に設定します。

7-2. PATH環境変数と実行ディレクトリを確認する

DLLが実行ディレクトリにない場合、OSのDLL探索パスに従って検索されます。Windowsでは、アプリケーションディレクトリ、システムディレクトリ、PATH環境変数に含まれるディレクトリなどが関係します。

.NETでは、P/Invokeによるネイティブライブラリ読み込み時にランタイムが複数の場所を探索します。公式ドキュメントでは、ライブラリ名のバリエーションや検索パス、DllImportSearchPathSetDllImportResolverなどの仕組みが説明されています。Microsoft Learn

7-3. 依存DLLが不足していないか確認する

対象DLLそのものが存在していても、そのDLLが依存している別のDLLが不足していると読み込みに失敗します。

たとえば、NativeLib.dllvcruntime140.dllや別の独自DLLに依存している場合、それらも実行環境に必要です。

確認には、Visual StudioのDeveloper Command Promptでdumpbin /dependentsを使ったり、Dependenciesなどのツールを使ったりします。

dumpbin /dependents NativeLib.dll

依存DLL不足はDllNotFoundExceptionUnable to load DLLとして見えることがあるため、対象DLLだけでなく依存関係も確認しましょう。

7-4. x86・x64のビルド設定を確認する

DLLのビット数とC#アプリのプロセスのビット数は一致している必要があります。

確認ポイントは次のとおりです。

C#プロジェクトのプラットフォームターゲットがx86x64Any CPUのどれになっているかを確認します。32bit DLLを使うならC#側もx86にします。64bit DLLを使うならC#側もx64にします。

実行時には次のコードで確認できます。

C#
Console.WriteLine(Environment.Is64BitProcess ? "64bit process" : "32bit process");

7-5. 関数名のエクスポート状態を確認する

DLLに関数が正しくエクスポートされているか確認します。

Windowsでは次のコマンドが使えます。

dumpbin /exports NativeLib.dll

ここに表示される関数名と、C#側のEntryPointまたはメソッド名が一致しているか確認します。

C++の名前修飾が入っている場合、Addではなく?Add@@YAHHH@Zのような名前で表示されることがあります。この場合は、C++側にextern "C"を付けるか、C#側のEntryPointに実際のエクスポート名を指定します。

7-6. 呼び出し規約と引数定義を見直す

DLLが読み込めて関数も見つかるのにクラッシュする場合、呼び出し規約や引数定義が間違っている可能性があります。

C/C++側のヘッダーを確認し、次の項目を照合します。

戻り値の型、各引数の型、ポインタか値渡しか、constの有無、構造体の定義、呼び出し規約、文字列の文字コード、配列の長さの渡し方を確認します。

C#側の定義は、C/C++側のヘッダーを見ながら作るのが基本です。推測で定義すると、動作しているように見えてもメモリ破壊を起こすことがあります。

7-7. Marshal.GetLastWin32Errorで詳細エラーを取得する

Windows APIのようにGetLastErrorを使う関数では、SetLastError = trueを指定してからMarshal.GetLastWin32Error()を呼びます。

C#
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool DeleteFile(string lpFileName);

static void Main()
{
bool result = DeleteFile("not_found.txt");

if (!result)
{
int error = Marshal.GetLastWin32Error();
Console.WriteLine($"Win32 Error: {error}");
}
}

SetLastError = trueを指定していないと、期待したエラーコードが取得できないことがあります。エラー調査では、まずDllImport宣言にSetLastErrorが必要かを確認しましょう。

8. .NET Framework・.NET Core・.NET 5以降でのDllImportの違い

8-1. .NET FrameworkでのDllImportの基本

.NET Frameworkでは、WindowsアプリケーションでWindows APIやネイティブDLLを呼び出す用途でDllImportが広く使われてきました。

典型的には、user32.dllkernel32.dllgdi32.dllなどのWindows DLLを呼び出します。

C#
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();

.NET Frameworkは基本的にWindows向けのため、Windows APIとの連携では現在でも多くの既存コードがあります。

8-2. .NET Core以降でのネイティブDLL読み込み

.NET Core以降、.NETはWindowsだけでなくLinuxやmacOSでも動作します。そのため、DllImportでもOSごとのネイティブライブラリ名や配置場所を考慮する必要があります。

Windowsでは.dll、Linuxでは.so、macOSでは.dylibが使われます。

C#
[DllImport("mylib")]
private static extern int Execute();

ライブラリ名を拡張子なしで指定した場合、実行環境に応じて適切な名前解決が行われることがあります。ただし、複雑な構成では明示的な制御が必要です。

8-3. RuntimeInformationを使ったOS判定

OSごとに呼び出すDLL名や処理を分けたい場合は、RuntimeInformationを使います。

C#
using System.Runtime.InteropServices;

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Console.WriteLine("Windows");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Console.WriteLine("Linux");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
Console.WriteLine("macOS");
}

OSごとに異なるDllImport宣言を用意する場合は、ラッパークラスを分けると保守しやすくなります。

8-4. Windows・Linux・macOSでDLL名を切り替える方法

単純な方法は、OSごとに別々のDllImport宣言を用意することです。

C#
internal static class NativeMethods
{
[DllImport("NativeLib.dll", EntryPoint = "execute")]
internal static extern int ExecuteWindows();

[DllImport("libNativeLib.so", EntryPoint = "execute")]
internal static extern int ExecuteLinux();

[DllImport("libNativeLib.dylib", EntryPoint = "execute")]
internal static extern int ExecuteMac();
}

呼び出し側でOSを判定します。

C#
int result;

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
result = NativeMethods.ExecuteWindows();
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
result = NativeMethods.ExecuteLinux();
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
result = NativeMethods.ExecuteMac();
}
else
{
throw new PlatformNotSupportedException();
}

より高度な方法として、.NET Core以降ではNativeLibrarySetDllImportResolverを使って、ライブラリ解決をカスタマイズできます。

8-5. NativeLibraryとの使い分け

DllImportは、静的に関数宣言を書いてネイティブ関数を呼び出す方法です。関数名やシグネチャが決まっている場合に使いやすいです。

一方、NativeLibraryは、実行時にネイティブライブラリを読み込んだり、関数ポインタを取得したりするためのAPIです。公式ドキュメントでは、NativeLibrary.LoadはOSローダーをラップしてネイティブライブラリを読み込むAPIとして説明されています。Microsoft Learn

たとえば、プラグインのように実行時にDLLパスが変わる場合や、OS・CPUアーキテクチャごとに読み込むライブラリを細かく制御したい場合はNativeLibraryが向いています。

通常のWindows API呼び出しや、決まったDLLの決まった関数を呼び出すだけなら、DllImportのほうが簡潔です。

9. DllImportを安全に使うためのベストプラクティス

9-1. C#側とC/C++側の型定義を必ず一致させる

DllImportで最も重要なのは、C#側の宣言とC/C++側の実際の関数定義を一致させることです。

戻り値、引数、呼び出し規約、文字コード、構造体レイアウト、配列サイズ、ポインタの意味を確認しましょう。

特に注意が必要なのは、boollong、文字列、構造体、ポインタです。C#の型名とC/C++の型名が似ていても、サイズや表現が一致するとは限りません。

Microsoftのネイティブ相互運用のベストプラクティスでも、マーシャリングや型の扱いを明示し、相互運用コードを慎重に設計することが推奨されています。Microsoft Learn

9-2. DLLのビット数とアプリのビット数を合わせる

32bit DLLは32bitプロセスから、64bit DLLは64bitプロセスから読み込む必要があります。

Visual Studioのプロジェクト設定で、プラットフォームターゲットを確認します。Any CPUは便利ですが、ネイティブDLLを使う場合は意図しないビット数で起動することがあるため注意が必要です。

配布時には、x86用DLLとx64用DLLを明確に分けて管理しましょう。

9-3. 文字列・構造体・配列は明示的にマーシャリングする

文字列、構造体、配列は、DllImportでトラブルが起きやすい代表的な型です。

文字列ではCharSetMarshalAsを指定します。構造体ではStructLayoutを指定します。配列では[In][Out]MarshalAs、長さ引数を適切に使います。

曖昧な自動マーシャリングに頼るより、DLL側の仕様に合わせて明示するほうが安全です。

9-4. 例外発生時に原因を切り分けやすいコードにする

DllImport宣言をアプリケーション全体に散らばらせると、エラーの原因を追いにくくなります。

おすすめは、NativeMethodsWin32Nativeのような専用クラスにDllImport宣言をまとめることです。

C#
internal static class NativeMethods
{
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool CloseHandle(IntPtr hObject);
}

さらに、その上にC#向けの安全なラッパーメソッドを作ると、呼び出し側はネイティブの詳細を意識せずに使えます。

C#
public static void SafeCloseHandle(IntPtr handle)
{
if (handle == IntPtr.Zero)
{
return;
}

if (!NativeMethods.CloseHandle(handle))
{
int error = Marshal.GetLastWin32Error();
throw new InvalidOperationException($"CloseHandle failed: {error}");
}
}

9-5. DLLの依存関係を管理する

ネイティブDLLは、別のDLLやランタイムに依存していることがあります。開発環境では動くのに本番環境で動かない場合、依存DLL不足が原因になっていることがあります。

配布時には、対象DLLだけでなく依存DLL、Visual C++ランタイム、OSバージョン、CPUアーキテクチャを確認しましょう。

NuGetパッケージとしてネイティブライブラリを含める場合は、runtimes/{rid}/native配下に配置する構成が使われます。MicrosoftのNuGetドキュメントでも、.NET 5以降のプロジェクトでP/Invokeから使うネイティブファイルをパッケージに含める方法が説明されています。Microsoft Learn

9-6. セキュリティリスクを理解して利用する

DllImportは、マネージドコードの安全な世界から外に出て、ネイティブコードを直接実行します。そのため、DLLの信頼性は非常に重要です。

信頼できないDLLを読み込むと、任意コード実行、メモリ破壊、情報漏えい、権限悪用などのリスクがあります。

対策として、DLLの入手元を確認する、DLLの配置場所を固定する、不要なPATH探索に頼らない、ユーザーが書き換え可能な場所からDLLを読み込まない、署名やハッシュを確認する、最小権限で実行する、といった点を意識しましょう。

10. C# DllImportの実用サンプル集

10-1. MessageBoxを呼び出すサンプル

C#
using System;
using System.Runtime.InteropServices;

class Program
{
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int MessageBox(
IntPtr hWnd,
string lpText,
string lpCaption,
uint uType);

static void Main()
{
MessageBox(IntPtr.Zero, "こんにちは", "C# DllImport", 0);
}
}

Windows APIをC# DllImportで呼び出す最も基本的な例です。

10-2. GetWindowTextを呼び出すサンプル

C#
using System;
using System.Runtime.InteropServices;
using System.Text;

class Program
{
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();

[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int GetWindowText(
IntPtr hWnd,
StringBuilder lpString,
int nMaxCount);

static void Main()
{
IntPtr handle = GetForegroundWindow();

StringBuilder title = new StringBuilder(256);
GetWindowText(handle, title, title.Capacity);

Console.WriteLine(title.ToString());
}
}

DLL側が文字列を書き込むため、StringBuilderを使っています。

10-3. C++ DLLの加算関数を呼び出すサンプル

C++側です。

C++
extern "C" __declspec(dllexport) int __stdcall Add(int a, int b)
{
return a + b;
}

C#側です。

C#
using System;
using System.Runtime.InteropServices;

class Program
{
[DllImport("NativeMath.dll", CallingConvention = CallingConvention.StdCall)]
private static extern int Add(int a, int b);

static void Main()
{
Console.WriteLine(Add(3, 5));
}
}

10-4. 文字列をDLLに渡すサンプル

C++側です。

C++
#include <windows.h>

extern "C" __declspec(dllexport) void __stdcall ShowText(const wchar_t* text)
{
MessageBoxW(nullptr, text, L"Native DLL", MB_OK);
}

C#側です。

C#
using System.Runtime.InteropServices;

class Program
{
[DllImport("NativeText.dll",
CallingConvention = CallingConvention.StdCall,
CharSet = CharSet.Unicode)]
private static extern void ShowText(string text);

static void Main()
{
ShowText("C#から文字列を渡しました");
}
}

C++側がwchar_t*を受け取るため、C#側ではCharSet.Unicodeを指定しています。

10-5. 構造体をDLLに渡すサンプル

C++側です。

C++
struct Point
{
int X;
int Y;
};

extern "C" __declspec(dllexport) int __stdcall SumPoint(Point point)
{
return point.X + point.Y;
}

C#側です。

C#
using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential)]
public struct Point
{
public int X;
public int Y;
}

class Program
{
[DllImport("NativeStruct.dll", CallingConvention = CallingConvention.StdCall)]
private static extern int SumPoint(Point point);

static void Main()
{
Point p = new Point { X = 10, Y = 20 };
Console.WriteLine(SumPoint(p));
}
}

10-6. 配列をDLLに渡すサンプル

C++側です。

C++
extern "C" __declspec(dllexport) int __stdcall Sum(int* values, int length)
{
int total = 0;

for (int i = 0; i < length; i++)
{
total += values[i];
}

return total;
}

C#側です。

C#
using System;
using System.Runtime.InteropServices;

class Program
{
[DllImport("NativeArray.dll", CallingConvention = CallingConvention.StdCall)]
private static extern int Sum(
[In] int[] values,
int length);

static void Main()
{
int[] values = { 10, 20, 30 };
Console.WriteLine(Sum(values, values.Length));
}
}

配列だけを渡してもC++側には長さが伝わらないため、lengthも一緒に渡します。

10-7. コールバック関数を受け取るサンプル

C++側です。

C++
typedef void (__stdcall *NotifyCallback)(int code);

extern "C" __declspec(dllexport) void __stdcall StartProcess(NotifyCallback callback)
{
callback(200);
}

C#側です。

C#
using System;
using System.Runtime.InteropServices;

class Program
{
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate void NotifyCallback(int code);

[DllImport("NativeCallback.dll", CallingConvention = CallingConvention.StdCall)]
private static extern void StartProcess(NotifyCallback callback);

private static NotifyCallback? _callback;

static void Main()
{
_callback = OnNotify;
StartProcess(_callback);
}

private static void OnNotify(int code)
{
Console.WriteLine($"通知コード: {code}");
}
}

デリゲートをフィールドに保持している点が重要です。保持しないと、ガベージコレクションで回収され、DLL側から呼び出したときに問題が起きる可能性があります。

11. C# DllImportに関するよくある質問

11-1. DllImportとDeclareの違いは?

DllImportはC#で使う属性ベースのP/Invoke宣言です。一方、DeclareはVisual Basicで使われる外部関数宣言です。

C#では通常、次のようにDllImportを使います。

C#
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();

Visual BasicではDeclare構文を使えますが、詳細なオプションを指定する場合はDllImportAttributeを使うこともあります。

11-2. DllImportでC#のDLLは呼び出せる?

通常のC# DLL、つまり.NETアセンブリを呼び出すためにDllImportは使いません。

C# DLLをC#から使う場合は、プロジェクト参照やNuGet参照を使います。DllImportは、アンマネージドDLLのエクスポート関数を呼び出すための仕組みです。

ただし、C#側でUnmanagedCallersOnlyなどを使ってネイティブエクスポート可能な形にする特殊なケースはあります。一般的な入門段階では、「C# DLLは参照、C/C++ DLLはDllImport」と考えるとわかりやすいです。

11-3. DLLはどこに配置すればよい?

最も簡単なのは、アプリケーションの実行ファイルと同じフォルダーに配置する方法です。

Visual Studioのプロジェクトでは、DLLをプロジェクトに含めて「出力ディレクトリにコピー」を設定します。

.NET Core以降でOSやCPUごとにネイティブDLLを分ける場合は、runtimes/win-x64/nativeのようなRID別の配置も使われます。

11-4. Any CPU設定でDllImportは使える?

使えますが、注意が必要です。

Any CPUの場合、実行環境によって32bitまたは64bitプロセスとして起動します。読み込むネイティブDLLが片方のビット数しか対応していない場合、実行時に失敗することがあります。

32bit DLLしかないなら、C#側のプラットフォームターゲットをx86にします。64bit DLLしかないならx64にします。x86とx64の両方のDLLを用意できるなら、実行時に適切なDLLを読み込む構成にします。

11-5. DllImportとCOM参照の違いは?

DllImportは、ネイティブDLLのエクスポート関数を直接呼び出す仕組みです。

COM参照は、COMコンポーネントとして公開されたオブジェクトを.NETから利用する仕組みです。COMではインターフェイス、GUID、レジストリ登録、タイプライブラリなどが関係します。

単純なC関数を呼ぶならDllImport、COMコンポーネントとして提供されている機能を使うならCOM参照、という使い分けになります。

11-6. DllImportとNativeLibraryはどちらを使うべき?

関数名、DLL名、引数、戻り値があらかじめ決まっているなら、DllImportが簡単です。

実行時にDLLパスを選びたい、プラグイン形式で読み込みたい、OSやCPUごとに細かくロード処理を変えたい、関数ポインタを動的に取得したい場合はNativeLibraryが向いています。

一般的なC# DllImport入門では、まずDllImportを理解し、より高度なロード制御が必要になったらNativeLibraryを検討するとよいでしょう。

11-7. DllImportがうまく動かないときに最初に確認すべきことは?

最初に確認すべきことは、DLLの場所、ビット数、関数名、呼び出し規約、引数と戻り値の型です。

特に、次の5つを優先して確認しましょう。

  1. DLLは実行ディレクトリまたは探索パスにあるか

  2. 依存DLLは不足していないか

  3. C#アプリとDLLのx86/x64は一致しているか

  4. DLLに対象関数が正しい名前でエクスポートされているか

  5. C#側のDllImport宣言はC/C++側のヘッダーと一致しているか

この5つを確認するだけで、多くのDllImportエラーは原因を絞り込めます。

まとめ

C# DllImportは、C#からWindows APIやC/C++ DLLなどのネイティブ関数を呼び出すための重要な仕組みです。基本構文はシンプルですが、実際には型変換、文字コード、構造体レイアウト、呼び出し規約、DLLの配置、32bit・64bitの違いなど、多くの注意点があります。

DllImportを安全に使うためには、C/C++側の関数定義を正確に確認し、C#側の宣言を一致させることが大切です。特に、文字列、配列、構造体、ポインタ、コールバックはトラブルが起きやすいため、CharSetMarshalAsStructLayoutCallingConventionを明示的に指定しましょう。

エラーが発生した場合は、DLLの配置場所、依存DLL、ビット数、エクスポート関数名、呼び出し規約、型定義を順番に確認します。Windows APIではSetLastError = trueMarshal.GetLastWin32Error()を使うことで、詳細な原因を調べやすくなります。

C# DllImportを理解すれば、.NETだけでは扱いにくいOS機能、既存のC/C++資産、ハードウェアSDK、高速なネイティブライブラリをC#アプリケーションから活用できます。最初は小さなサンプルから始め、型とメモリの境界を意識しながら、安全で保守しやすい相互運用コードを書いていきましょう。