專案中需在 WinForm 程式中透過 P/Invoke 調用 C 語言編寫的 DLL 讀取 IC 卡號,過程中遇到 DLL 路徑解析、結構體封送、參數傳遞約定等典型問題,現整理如下供參考。
CLR 尋找非託管 DLL 的順序:
bin\Debug 或 bin\Release)。C:\Windows\System32)。PATH 指定的目錄。DllImport(@"E:\Source\GPTagReaderDll")),不推薦(發布時路徑不固定,且若該 DLL 還依賴其他 DLL,硬編碼無法解決依賴鏈)。最佳實踐:將所有非託管 DLL(含依賴項)放在與 .exe 同級目錄,或在專案屬性 → Build Events → Post-build event 中添加複製命令(xcopy /y "$(SolutionDir)NativeDlls\*" "$(TargetDir)")。
typedef struct {
unsigned char DeviceIdentify[30]; // 設備標識字符串
unsigned short res; // 保留字段
} FindDeviceAck_Struct;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
public struct FindDeviceAck_Struct {
// 固定長度字符串 → ByValTStr(注意編碼)
// [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 30)]
// public string DeviceIdentify; // 易產生亂碼
// 建議用 byte[] 對應 unsigned char[],再手動解碼
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 30)]
public byte[] DeviceIdentify;
public ushort res;
}
LayoutKind.Sequential:按聲明順序緊湊排列。Pack = 1:1 字節對齊,對應 C 端 #pragma pack(1) 或預設無填充。CharSet.Ansi:字符串以 ANSI 編碼封送(C 端為 char)。ByValTStr 封送為定長 ANSI 字符串,若 C 端數據含非 ASCII 或非空終止,極易亂碼;改用 byte[] + Encoding.Default.GetString() 手動轉換最穩健。BOOL FindDevice(FindDeviceAck_Struct *finddeviceack); // 輸出參數,指針傳遞
[DllImport("GPTagReaderDll", EntryPoint = "FindDevice", CallingConvention = CallingConvention.Cdecl)]
public static extern bool FindDevice(ref FindDeviceAck_Struct finddeviceack);
*)且為輸出參數,C# 必須用 ref(或 out)標記,否則發生 Attempted to read or write protected memory 錯誤。CallingConvention.Cdecl:C/C++ 預設調用約定(調用者清棧),若 DLL 為 __stdcall 則用 StdCall(預設)。EntryPoint:若 C++ 函數經歷 Name Mangling,需用 extern "C" 導出或用 Dependency Walker 確認實際導出名。var ack = new FindDeviceAck_Struct();
bool ok = NativeMethods.FindDevice(ref ack);
if (ok) {
string devId = Encoding.Default.GetString(ack.DeviceIdentify).TrimEnd('\0');
Console.WriteLine($"Device: {devId}, Res: {ack.res}");
}
Pack、CharSet、LayoutKind 嚴格對應 C 端定義。byte[] 封送,應用層自行解碼。ref/out。Cdecl/StdCall)與導出名(EntryPoint)雙重確認。extern "C" 工廠函數)供 P/Invoke 調用。