架構概覽
C# (Managed) ↔ P/Invoke ↔ Win32 DLL (Unmanaged C/C++)
- C++ 專案:建立 **Dynamic-Link Library (DLL)**,匯出
extern "C" __declspec(dllexport) 函式。
- C# 專案:使用
[DllImport] 匯入,**靜態 extern** 方法對應原生簽名,**封送** 參數與回傳值。
- 部署:原生 DLL 需置於輸出目錄 (x86/x64 對應) 或系統 PATH。
步驟一:建立 Win32 DLL (Visual Studio C++ 專案)
專案設定
- 新增專案 → **Windows Desktop → Dynamic-Link Library (DLL)**
- 屬性 → **C/C++ → Code Generation → Runtime Library**:
Multi-threaded (/MT) (靜態連結 CRT,避免部署 VC++ Runtime) 或 Multi-threaded DLL (/MD) (需安裝對應 VC++ Redist)
- **Platform Target** 必須與 C# 專案一致 (x86/x64/ARM64)
原始碼範例 (MathLib.cpp)
// MathLib.h
#ifdef MATHLIB_EXPORTS
#define MATHLIB_API __declspec(dllexport)
#else
#define MATHLIB_API __declspec(dllimport)
#endif
extern "C" // 關鍵:防止 C++ Name Mangling,讓 C# 依名稱找到函式
{
MATHLIB_API int Add(int a, int b);
MATHLIB_API double Calculate(double x, double y, char op);
MATHLIB_API void GetSystemTimeEx(int* year, int* month, int* day, int* hour, int* min, int* sec);
MATHLIB_API int ProcessArray(int* data, int length, int* outSum, double* outAvg);
MATHLIB_API int FillStruct(MyStruct* s);
}
// 結構體定義 (需與 C# 端完全一致記憶體佈局)
typedef struct MyStruct
{
int id;
double value;
char name[32]; // 固定長度字元陣列
bool flag;
} MyStruct;
// MathLib.cpp
#include "MathLib.h"
#include
extern "C"
{
MATHLIB_API int Add(int a, int b) { return a + b; }
MATHLIB_API double Calculate(double x, double y, char op)
{
switch (op)
{
case '+': return x + y;
case '-': return x - y;
case '*': return x * y;
case '/': return y != 0 ? x / y : 0;
default: return 0;
}
}
MATHLIB_API void GetSystemTimeEx(int* year, int* month, int* day, int* hour, int* min, int* sec)
{
time_t now = time(NULL);
struct tm local;
localtime_s(&local, &now);
*year = local.tm_year + 1900;
*month = local.tm_mon + 1;
*day = local.tm_mday;
*hour = local.tm_hour;
*min = local.tm_min;
*sec = local.tm_sec;
}
MATHLIB_API int ProcessArray(int* data, int length, int* outSum, double* outAvg)
{
if (!data || length <= 0) return -1;
long long sum = 0;
for (int i = 0; i < length; i++) sum += data[i];
*outSum = (int)sum;
*outAvg = (double)sum / length;
return 0;
}
MATHLIB_API int FillStruct(MyStruct* s)
{
if (!s) return -1;
s->id = 12345;
s->value = 3.14159;
strcpy_s(s->name, "Hello from C++");
s->flag = true;
return 0;
}
}
編譯產出
MathLib.dll (原生程式碼)
MathLib.lib (匯入庫,C++ 專案參考用)
步驟二:C# P/Invoke 宣告與封送
基本型別對照表
| C/C++ | C# | 備註 |
int / long (32-bit) | int / Int32 | Windows 長整數皆 32 位 |
long long / __int64 | long / Int64 | |
float | float / Single | |
double | double / Double | |
char (ANSI) | byte / sbyte | 單一字元 |
wchar_t / WCHAR | char / Char | Unicode 字元 |
char* / LPSTR | string (In) / StringBuilder (Out) | [MarshalAs(UnmanagedType.LPStr)] |
wchar_t* / LPWSTR | string / StringBuilder | 預設 Unicode |
void* / HANDLE / HWND | IntPtr / HandleRef | 平台無關指標 |
BOOL (4 bytes) | int / bool ([MarshalAs(UnmanagedType.Bool)]) | Win32 BOOL = 4 bytes |
BYTE | byte | |
C# 簽名完整範例
using System;
using System.Runtime.InteropServices;
using System.Text;
public static class NativeMethods
{
const string DllName = "MathLib.dll"; // 需置於輸出目錄
// 1. 簡單數值運算
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern int Add(int a, int b);
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern double Calculate(double x, double y, char op);
// 2. 多輸出參數 (指標 → out/ref)
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern void GetSystemTimeEx(
out int year, out int month, out int day,
out int hour, out int min, out int sec);
// 3. 陣列處理 (固定大小 → 記憶體固定)
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern int ProcessArray(
[In] int[] data, int length,
out int outSum, out double outAvg);
// 4. 結構體封送
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
public struct MyStruct
{
public int id;
public double value;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string name; // 對應 char name[32]
[MarshalAs(UnmanagedType.U1)] // 1 byte bool
public bool flag;
}
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern int FillStruct(ref MyStruct s);
// 5. 字串緩衝區 (StringBuilder)
[DllImport(DllName, CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
public static extern int GetVersionString(StringBuilder buffer, int bufferSize);
}
關鍵屬性說明
CallingConvention.Cdecl:C/C++ 預設呼叫慣例 (呼叫者清理堆疊)。Win32 API 多為 StdCall,但自建 DLL 通常用 Cdecl。
ExactSpelling=true:禁止自動嘗試 AddA/AddW 後綴,加快繫結。
CharSet.Ansi:對應 C++ char*,若 C++ 用 wchar_t* 則用 CharSet.Unicode (預設)。
StructLayout.Sequential, Pack=1:依宣告順序、1 位元組對齊,符合 C++ #pragma pack(1) 或預設對齊。
MarshalAs(UnmanagedType.ByValTStr, SizeConst=32):固定長度字串內嵌結構體,非指標。
[In] / [Out]:明確標示方向,優化封送效能 (陣列大時顯著)。
步驟三:C# 呼叫與測試
public class Program
{
public static void Main()
{
try
{
Console.WriteLine($"Add(10, 20) = {NativeMethods.Add(10, 20)}");
Console.WriteLine($"Calculate(10, 5, '*') = {NativeMethods.Calculate(10, 5, '*')}");
NativeMethods.GetSystemTimeEx(out int y, out int m, out int d, out int h, out int mi, out int s);
Console.WriteLine($"Time: {y}-{m:D2}-{d:D2} {h:D2}:{mi:D2}:{s:D2}");
int[] arr = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int sum; double avg;
int ret = NativeMethods.ProcessArray(arr, arr.Length, out sum, out avg);
Console.WriteLine($"ProcessArray: ret={ret}, sum={sum}, avg={avg:F2}");
var st = new NativeMethods.MyStruct();
ret = NativeMethods.FillStruct(ref st);
Console.WriteLine($"FillStruct: ret={ret}, id={st.id}, value={st.value}, name='{st.name}', flag={st.flag}");
var sb = new StringBuilder(256);
ret = NativeMethods.GetVersionString(sb, sb.Capacity);
Console.WriteLine($"Version: {sb}");
}
catch (DllNotFoundException)
{
Console.Error.WriteLine("找不到 MathLib.dll,請確認:");
Console.Error.WriteLine("1. DLL 置於輸出目錄 (bin/Debug/netX.Y/ 或 bin/Release/...)");
Console.Error.WriteLine("2. 平台目標一致 (x64 專案需 x64 DLL)");
Console.Error.WriteLine("3. 相依 VC++ Runtime 已安裝 (/MD) 或靜態連結 (/MT)");
}
catch (EntryPointNotFoundException ex)
{
Console.Error.WriteLine($"找不到匯出函式: {ex.Message} - 檢查 extern \"C\" 與 __declspec(dllexport)");
}
catch (Exception ex)
{
Console.Error.WriteLine($"呼叫失敗: {ex.Message}");
}
}
}
部署與相依性管理
| 策略 | 優點 | 缺點 |
/MT 靜態連結 CRT | 無需安裝 VC++ Redist、單一 DLL 部署 | DLL 體積較大、多 DLL 共用 CRT 時各自複製 |
/MD 動態連結 CRT | DLL 較小、共用 CRT | 目標機需安裝對應版本 VC++ Redistributable |
延遲載入 /DELAYLOAD | 啟動快、缺少 DLL 才報錯 | 第一次呼叫較慢、需處理例外 |
| 內嵌資源 + 執行期釋放 | 單一 EXE 部署 | 需寫入磁碟、權限/防毒軟體可能攔截 |
除錯技巧
- Dependency Walker / dumpbin / Dependencies:檢查 DLL 匯出函式名稱是否含修飾 (如
?Add@@YAHHH@Z 表示缺 extern "C")。
- Visual Studio 混合模式除錯:專案屬性 → Debug → **Enable native code debugging**,可同時中斷 C# 與 C++ 程式碼。
- 啟用非受控程式碼偵錯:
Debug → Windows → Modules 確認 DLL 載入位址、版本。
- 記憶體檢視:
Debug → Windows → Memory 檢查結構體記憶體佈局是否對齊。
- SetLastError + Marshal.GetLastWin32Error():原生函式呼叫
SetLastError,C# 端 [DllImport(SetLastError=true)] 取得錯誤碼。
進階主題
回呼函式 (Function Pointer → Delegate)
// C++: typedef void (__stdcall *Callback)(int progress);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate void ProgressCallback(int progress);
[DllImport(DllName)]
public static extern void DoLongWork(ProgressCallback callback);
// C# 使用
static void OnProgress(int p) => Console.WriteLine($"Progress: {p}%");
NativeMethods.DoLongWork(OnProgress); // 委派實例必須保持存活 (防 GC)
結構體陣列輸出
[DllImport(DllName)]
public static extern int GetItems([Out] MyStruct[] buffer, int maxCount, out int actualCount);
// 呼叫
var buffer = new MyStruct[100];
int count;
int ret = NativeMethods.GetItems(buffer, buffer.Length, out count);
// buffer[0..count-1] 包含資料
64 位元相容性
- 所有指標/控制代碼 **必須用
IntPtr**,不可用 int/long。
- 結構體包含指標欄位時,
Pack 對齊在 64 位元下不同,需分別定義或用 IntPtr 代替指標。
- C# 專案 **取消勾選 Prefer 32-bit** 以原生 64 位元執行,或強制 x86 相容舊 DLL。