C#通過P/Invoke調用C++ DLL時,字符串參數(特別是輸出參數`char*`/`char[]`)的封送處理是最易出錯的環節。本文針對`CT_ReadPin`類接口,詳解`StringBuilder`正確用法、內存佈局對齊與常見錯誤。
// C++ 導出函數
// 讀取PIN碼到調用者提供的緩衝區
// 返回:0成功,非0錯誤碼
extern "C" __declspec(dllexport) LONG CT_ReadPin(
int iPortNo,
char cExtendPort,
int iBaudRate,
int iVoiceType,
int iTimeOut,
char* pPinBuffer, // 輸出緩衝區,調用者分配
int iBufferSize // 緩衝區大小
);
using System;
using System.Runtime.InteropServices;
using System.Text;
public class CentermWrapper
{
// 1. 精確簽名:CallingConvention.Cdecl 必須與C++一致
// CharSet.Ansi 對應 char*;若C++用wchar_t*則用 Unicode
[DllImport("CT_InfoTerm.dll", EntryPoint = "CT_ReadPin",
CallingConvention = CallingConvention.Cdecl,
CharSet = CharSet.Ansi)]
// 2. 輸出字符串參數用 StringBuilder,由CLR分配管理內存
public static extern int CT_ReadPin(
int iPortNo,
char cExtendPort, // 單字符按值傳遞
int iBaudRate,
int iVoiceType,
int iTimeOut,
StringBuilder pPinBuffer, // 輸出緩衝區
int iBufferSize // 緩衝區容量
);
public string ReadPin(int port, char extPort, int baud, int voiceType, int timeout)
{
// 3. 預分配足夠容量(如256),防止截斷
var pinBuffer = new StringBuilder(256);
int ret = CT_ReadPin(port, extPort, baud, voiceType, timeout, pinBuffer, pinBuffer.Capacity);
if (ret != 0)
throw new Exception($"DLL調用失敗,錯誤碼: {ret}");
// 4. 直接獲取字符串,無需手動轉換
return pinBuffer.ToString();
}
}
| 錯誤做法 | 後果 | 正確做法 |
|---|---|---|
| 用 `string` 接收輸出參數 | 字符串不可變,無法接收DLL寫入的數據,可能導致內存訪問違規 | 用 `StringBuilder` (可變字符緩衝區) |
| 用 `byte[]` 或 `IntPtr` 手動分配 | 易發生內存洩漏、釋放時機錯誤、編碼轉換麻煩 | `StringBuilder` 由CLR自動管理生命周期與編碼轉換 |
| 忽略 `CallingConvention` | 棧平衡錯誤,導致參數錯亂、崩潰(C++默認 `__cdecl`,StdCall需顯式指定) | 明確指定 `CallingConvention.Cdecl` 或 `StdCall` |
| 忽略 `CharSet` | 默認Unicode(雙字節),與C++ `char*`(單字節)不匹配,內容亂碼或截斷 | 指定 `CharSet.Ansi` |
| 緩衝區容量不足 | DLL寫入超出邊界,導致緩衝區溢出、數據截斷 | 預分配足夠大容量(如256、1024),傳入 `Capacity` |
若DLL接口涉及結構體,需用`[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]`定義對應C#類/結構體,字段順序、類型、大小完全一致。
P/Invoke核心原則:簽名精確匹配、內存由CLR管理、編碼顯式指定、調用約定一致。