首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 呼叫 Win32 DLL 完整指南:建立原生 DLL、P/Invoke 簽名、資料封送、除錯技巧

     編輯:關於C語言

架構概覽

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++ 專案)

專案設定

  1. 新增專案 → **Windows Desktop → Dynamic-Link Library (DLL)**
  2. 屬性 → **C/C++ → Code Generation → Runtime Library**:Multi-threaded (/MT) (靜態連結 CRT,避免部署 VC++ Runtime) 或 Multi-threaded DLL (/MD) (需安裝對應 VC++ Redist)
  3. **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 / Int32Windows 長整數皆 32 位
long long / __int64long / Int64
floatfloat / Single
doubledouble / Double
char (ANSI)byte / sbyte單一字元
wchar_t / WCHARchar / CharUnicode 字元
char* / LPSTRstring (In) / StringBuilder (Out)[MarshalAs(UnmanagedType.LPStr)]
wchar_t* / LPWSTRstring / StringBuilder預設 Unicode
void* / HANDLE / HWNDIntPtr / HandleRef平台無關指標
BOOL (4 bytes)int / bool ([MarshalAs(UnmanagedType.Bool)])Win32 BOOL = 4 bytes
BYTEbyte

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 動態連結 CRTDLL 較小、共用 CRT目標機需安裝對應版本 VC++ Redistributable
延遲載入 /DELAYLOAD啟動快、缺少 DLL 才報錯第一次呼叫較慢、需處理例外
內嵌資源 + 執行期釋放單一 EXE 部署需寫入磁碟、權限/防毒軟體可能攔截

除錯技巧

  1. Dependency Walker / dumpbin / Dependencies:檢查 DLL 匯出函式名稱是否含修飾 (如 ?Add@@YAHHH@Z 表示缺 extern "C")。
  2. Visual Studio 混合模式除錯:專案屬性 → Debug → **Enable native code debugging**,可同時中斷 C# 與 C++ 程式碼。
  3. 啟用非受控程式碼偵錯:Debug → Windows → Modules 確認 DLL 載入位址、版本。
  4. 記憶體檢視:Debug → Windows → Memory 檢查結構體記憶體佈局是否對齊。
  5. 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。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved