一般 P/Invoke 使用 [DllImport] 靜態宣告,但若 DLL 路徑、函數名稱在執行期才決定,或需呼叫非標準呼叫慣例的函數,可使用 LoadLibrary/GetProcAddress 取得函數指標,再透過 DynamicMethod 或 AssemblyBuilder 發射 IL 動態建立委派進行呼叫。本文提供完整可重用的 WinDllInvoke 方法。
DllImport 導致啟動時找不到 DLL 拋出例外。WinDllInvokeusing System;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.InteropServices;
public static class DynamicDllInvoker
{
[DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
private static extern IntPtr LoadLibrary(string lpLibFileName);
[DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
private static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
[DllImport("kernel32", ExactSpelling = true, SetLastError = true)]
private static extern bool FreeLibrary(IntPtr hModule);
///
/// 動態載入 DLL 並呼叫指定函數
///
/// DLL 完整路徑或檔名(需在 PATH 中)
/// 函數名稱(區分大小寫)
/// 引數陣列(順序需與原生簽章一致)
/// 回傳型別
/// 函數回傳值
public static object Invoke(string dllPath, string funcName, object[] args, Type returnType)
{
IntPtr hMod = LoadLibrary(dllPath);
if (hMod == IntPtr.Zero)
throw new DllNotFoundException($"無法載入 DLL: {dllPath}");
IntPtr procAddr = GetProcAddress(hMod, funcName);
if (procAddr == IntPtr.Zero)
{
FreeLibrary(hMod);
throw new EntryPointNotFoundException($"找不到函數: {funcName}");
}
try
{
// 1. 建立參數型別陣列
Type[] paramTypes = new Type[args.Length];
for (int i = 0; i < args.Length; i++)
paramTypes[i] = args[i]?.GetType() ?? typeof(object);
// 2. 發射動態方法
var dm = new DynamicMethod(
"DynamicInvoke", returnType, paramTypes, typeof(DynamicDllInvoker).Module, true);
ILGenerator il = dm.GetILGenerator();
// 3. 將引數推入堆疊
for (int i = 0; i < paramTypes.Length; i++)
{
if (paramTypes[i].IsValueType)
il.Emit(OpCodes.Ldarg, i); // by value
else
il.Emit(OpCodes.Ldarga, i); // by reference (address)
}
// 4. 載入函數指標 (視平台 32/64 位元)
if (IntPtr.Size == 4)
il.Emit(OpCodes.Ldc_I4, procAddr.ToInt32());
else
il.Emit(OpCodes.Ldc_I8, procAddr.ToInt64());
// 5. 發出 calli (stdcall)
var cc = CallingConventions.Standard;
il.EmitCalli(OpCodes.Calli, cc, returnType, paramTypes);
il.Emit(OpCodes.Ret);
// 6. 建立委派並呼叫
var del = dm.CreateDelegate(Expression.GetDelegateType(paramTypes.Concat(new[] { returnType }).ToArray()));
return del.DynamicInvoke(args);
}
finally
{
FreeLibrary(hMod);
}
}
}
// 靜態簽章對應: BOOL Beep(DWORD dwFreq, DWORD dwDuration)
object result = DynamicDllInvoker.Invoke(
"kernel32.dll", "Beep",
new object[] { 750u, 300u }, // uint 對應 DWORD
typeof(bool) // 回傳 BOOL -> bool
);
Console.WriteLine("Beep 回傳: " + result);
| 項目 | 說明 |
|---|---|
DynamicMethod vs AssemblyBuilder | DynamicMethod 輕量、無需儲存組件、效能優,適合單次/少量呼叫;AssemblyBuilder 可持久化、適合大量重複呼叫。 |
| 呼叫慣例 | Windows API 多為 StdCall (C# 預設)。C 執行期庫函數多為 Cdecl,需改 CallingConventions.Cdecl。 |
| 參數封送 | 範例僅處理基本值型別/參考型別。字串需 Marshal.StringToHGlobalAnsi/Uni、結構體需 Marshal.StructureToPtr、陣列需固定記憶體。 |
| 32/64 位元 | 指標大小由 IntPtr.Size 判斷,IL 中分別用 Ldc_I4/Ldc_I8 載入。 |
| 例外處理 | 原生例外 (SEH) 不會自動轉為 managed exception,需自行檢查回傳值或 GetLastError。 |
NativeLibrary.Load、GetExport 取得函數指標,搭配 Marshal.GetDelegateForFunctionPointer 直接轉委派,無需手寫 IL。StringMarshalling、ArrayMarshalling 等屬性。動態 DLL 呼叫屬於進階互通性技術,掌握 LoadLibrary/GetProcAddress + DynamicMethod/Delegate 流程,能在插件架構、選用元件、跨版本相容等場景提供極大彈性。現代 .NET 專案優先考慮 NativeLibrary 或 LibraryImport,僅在舊版框架或極特殊需求下使用本文 IL 發射方案。