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

C# MFC 混合編程:在 MFC 專案中呼叫 C# DLL 完整指南

     編輯:關於C#

在既有 MFC (C++) 專案中整合 C# 組件,需啟用 `/clr` 混合模式編譯、正確引用組件、處理 Managed/Unmanaged 邊界。本文提供完整設定步驟、程式碼範例、常見問題解決方案。

前置需求

  • Visual Studio 2017/2019/2022 安裝「使用 C++ 的 .NET 桌面開發」工作負載
  • C# 專案目標 .NET Framework 4.x (非 .NET Core/.NET 5+,除非使用 .NET Core Hosting API)
  • C# 類別庫設定 `ComVisible(true)` 並註冊強名稱 (可選但建議)

步驟一:MFC 專案啟用 /clr

專案屬性設定

  1. 右鍵專案 → Properties (屬性)
  2. Configuration Properties → General
    • Common Language Runtime Support:Common Language Runtime Support (/clr)
    • Use MFC:Use MFC in a Shared DLL
  3. Configuration Properties → C/C++ → General
    • Debug Information Format:Program Database (/Zi) (非 /ZI)
  4. Configuration Properties → C/C++ → Code Generation
    • Runtime Library:Multi-threaded DLL (/MD) 或 Multi-threaded Debug DLL (/MDd)
    • **必須與 /clr 一致**,不能使用 /MT 或 /MTd

步驟二:C# DLL 準備

// C# 類別庫專案
using System;
using System.Runtime.InteropServices;

namespace CSharpLib
{
    [ComVisible(true)]
    [Guid("YOUR-GUID-HERE")]
    [ClassInterface(ClassInterfaceType.AutoDual)]
    public class Calculator
    {
        public int Add(int a, int b) => a + b;
        public int Subtract(int a, int b) => a - b;
        public double Multiply(double a, double b) => a * b;
        public string GetMessage(string name) => $"Hello, {name} from C#!";
    }
}

// AssemblyInfo.cs
[assembly: ComVisible(true)]
[assembly: Guid("ASSEMBLY-GUID-HERE")]

建置後事件:自動註冊 (可選)

// 專案屬性 → Build Events → Post-build event command line
"$(FrameworkSDKDir)bin\gacutil.exe" /i "$(TargetPath)"
"$(FrameworkSDKDir)bin\regasm.exe" "$(TargetPath)" /tlb /codebase

步驟三:MFC 程式碼引用與呼叫

// stdafx.h 或專用標頭檔
#using 
// 使用相對路徑或完整路徑引用 C# DLL
#using "..\CSharpLib\bin\Debug\CSharpLib.dll"

using namespace System;
using namespace CSharpLib; // C# 命名空間

// 在需要呼叫的 .cpp 檔案
#pragma managed // 啟用 Managed 程式碼區塊

void CMFCAppDlg::OnBnClickedCallCSharp()
{
    try
    {
        // 實例化 C# 類別
        Calculator^ calc = gcnew Calculator();
        
        // 呼叫方法
        int sum = calc->Add(10, 20);
        int diff = calc->Subtract(50, 30);
        double product = calc->Multiply(3.14, 2.0);
        String^ msg = calc->GetMessage("MFC User");
        
        // 轉換為 MFC 字串顯示
        CString strResult;
        strResult.Format(_T("Add: %d\nSubtract: %d\nMultiply: %.2f\nMessage: %s"),
            sum, diff, product, (LPCTSTR)Marshal::StringToHGlobalAuto(msg));
        
        MessageBox(strResult, _T("C# DLL 呼叫結果"), MB_OK);
        
        // 釋放 Marshal 記憶體
        Marshal::FreeHGlobal(Marshal::StringToHGlobalAuto(msg));
    }
    catch (Exception^ ex)
    {
        CString errMsg;
        errMsg.Format(_T("呼叫 C# DLL 失敗:%s"), (LPCTSTR)Marshal::StringToHGlobalAuto(ex->Message));
        MessageBox(errMsg, _T("錯誤"), MB_ICONERROR);
    }
}

#pragma unmanaged // 回復 Unmanaged 程式碼區塊

關鍵技術細節

1. #pragma managed/unmanaged

  • `#pragma managed`:後續程式碼編譯為 MSIL (Managed)
  • `#pragma unmanaged`:後續程式碼編譯為原生機器碼
  • **建議**:將呼叫 C# 程式碼包裹在最小範圍的 `#pragma managed` 區塊

2. 字串轉換

// C# String^ → MFC CString
String^ managedStr = ...;
CString mfcStr = (LPCTSTR)Marshal::StringToHGlobalAuto(managedStr);
// 使用後必須釋放
Marshal::FreeHGlobal((IntPtr)(LPCTSTR)mfcStr); // 或儲存 IntPtr 稍後釋放

// MFC CString → C# String^
CString mfcStr = _T("Hello");
String^ managedStr = gcnew String(mfcStr); // 自動轉換

3. 例外處理

try
{
    // Managed 程式碼
}
catch (System::Exception^ ex) // 捕獲 Managed 例外
{
    // 處理
}
catch (...) // 捕獲 Unmanaged 例外 (SEH)
{
    // 處理
}

常見問題與解決方案

| 問題 | 原因 | 解決方案 | |------|------|----------| | `error C1189: #error: /clr 不支援 /ZI` | Debug 格式設為 Edit & Continue | 改為 `/Zi` (Program Database) | | `error C3861: 'gcnew': identifier not found` | 未在 `#pragma managed` 區塊 | 包裹在 `#pragma managed` 區塊 | | `FileNotFoundException: Could not load file or assembly` | DLL 路徑錯誤、未註冊 GAC | 確認 `#using` 路徑、或放在 exe 同目錄、或 GAC 註冊 | | `TypeLoadException` | 版本不匹配、缺少依賴 | 確認 .NET Framework 版本一致、依賴組件存在 | | `AccessViolationException` | Managed/Unmanaged 邊界記憶體損毀 | 檢查字串轉換、陣列固定、委派固定 | | 呼叫極慢 | 首次呼叫載入 CLR、JIT 編譯 | 預熱呼叫、NGen、ReadyToRun | | 記憶體洩漏 | `Marshal::StringToHGlobalAuto` 未釋放 | 必須配對 `FreeHGlobal`、使用 `pin_ptr` 固定陣列 |

進階:反向呼叫 (C# 呼叫 MFC)

// C# 定義委派介面
public delegate void ProgressCallback(int percent, string message);

[ComVisible(true)]
public interface IProgressReporter
{
    void Report(int percent, string message);
}

// MFC 實作介面
#pragma managed
public ref class MfcProgressReporter : public IProgressReporter
{
    HWND _hWnd;
public:
    MfcProgressReporter(HWND hWnd) : _hWnd(hWnd) {}
    
    virtual void Report(int percent, String^ message)
    {
        // 發送訊息到 MFC 視窗
        ::PostMessage(_hWnd, WM_USER_PROGRESS, percent, 
            (LPARAM)Marshal::StringToHGlobalAuto(message));
    }
};
#pragma unmanaged

// C# 使用
public void DoWork(IProgressReporter reporter)
{
    for (int i = 0; i <= 100; i += 10)
    {
        reporter.Report(i, $"Processing {i}%");
        Thread.Sleep(100);
    }
}

現代替代方案比較

| 方案 | 優點 | 缺點 | 適用場景 | |------|------|------|----------| | **C++/CLI 混合模式** | 直接呼叫、效能最好、雙向互通 | 複雜、需 /clr、除錯困難 | 緊密整合、高效能需求 | | **COM Interop** | 標準化、語言中立、支援 VB6/脚本 | 效能較差、註冊複雜、版本管理難 | 現有 COM 元件、跨語言 | | **Named Pipe / TCP / HTTP** | 進程隔離、跨機器、技術棧無關 | 序列化開銷、延遲較高 | 微服務、進程隔離、異構系統 | | **C# 宿主 .NET Core (hostfxr)** | 現代 .NET、無 /clr 限制 | 較新、API 較低階 | .NET 6+ 整合、不想用 /clr |

總結

MFC 呼叫 C# DLL 的關鍵在於:

  1. 專案正確設定 `/clr` + `/MD` + MFC Shared DLL
  2. C# 組件 `ComVisible(true)` 並正確引用 (`#using`)
  3. 呼叫程式碼包裹在 `#pragma managed` 區塊
  4. 字串/陣列/物件跨邊界正確封送 (Marshal)
  5. 例外處理區分 Managed/Unmanaged

此技術適用於漸進式現代化既有 MFC 系統,但新專案建議考慮進程隔離架構降低耦合複雜度。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved