C/C++ 是電腦世界的共同語言。它速度快,又能直接與系統溝通,因此許多語言都能透過它來串接底層功能。有時候,也會將關鍵的程式邏輯用 C/C++ 寫好,只需實作一次,就能讓其他語言重複使用。
在 C# 中,串接 C/C++ 的方式有很多種。本次想記錄其中一種做法:透過 DllImport 讓 C# 呼叫原生函式。從使用 Visual Studio 編譯 C/C++ 程式碼成 DLL 開始,一步步到實際呼叫為止。如果手上已經有現成的 DLL 和對應的 .h 檔,也能藉此了解整個過程。
本次的開發環境,如下
- IDE : Visual Studio 2022
- .NET版本 : . NET 8.0
事前準備與方案架構
在開始前,除了需要安裝 Visual Studio、C# 和 .NET 之外,
因為這次會用到 C++ 來建立原生 DLL,所以還需要安裝 C++ 的開發工具。
如果 Visual Studio 安裝時沒有勾選 C++ 相關項目,
可以開啟 Visual Studio Installer 來進行安裝。
理論上最少要安裝這些,
- 使用 C++ 的桌面開發
- MSVC v143 - VS 2022 C++ x64/x86 建置工具
- C++ 核心功能
因為個人測試過一些功能,
安裝的項目也比較混亂,
所以並不是這麼確定 ,
(哪天有機會確定後再更新)
以下附上個人的安裝清單與畫面供參考,
如有需要可依照個人需求自行調整環境。


本次的預計的方案架構如下,
1 | Solution: CsUseCppNativeSample |
整體分成三個主要部分,有下至上分別是,
- CppNativeDll:C++ 專案
- CSharpLibrary:C# 類別庫
- CSharpTestApp: C# 專案
當然,如果想直接在 C# 專案去使用 C++ 專案,也是可行的,
架構可以依需求自行調整。
不過個人習慣在實務上這樣分三層,方便維護與測試,也更容易擴充功能。
接下來,將從建立專案開始,
並一步步完成整個流程。
建立 C++ DLL 專案與程式
建立C++ Native專案
首先,從建立 C++ Native 專案 開始。
Visual Studio 建立好方案後,
在方案上按右鍵 → 加入 → 新增專案。

語言篩選部分可以選擇C++,
個人習慣從 空白專案 開始,所以這邊選擇「空白專案」,

接著設定專案名稱後即可建立,
建立完成後,畫面會像下圖這樣,

接下來,建立一個空白的標頭檔(.h),
在「標頭檔」資料夾上按右鍵 → 加入 → 新增項目。

接著選擇「標頭檔 (.h)」,輸入名稱後按下「新增」。

同樣的步驟,在「來源檔案」資料夾中,
建立一個 C++ 原始檔 (.cpp)。

上述步驟完成後,
整個專案的架構與狀況如下圖所示,

到這邊為止,專案的初步建立就完成了。
C++ 原生程式碼範例
再來是編寫 .h 與 .cpp 的程式內容。
下面提供的程式碼可以直接貼進專案中使用。
這個範例示範了一些基本的 C++ 方法,
各功能方法的細節這裡就不特別說明,
NativeDll.h
1 |
|
NativeDll.cpp
1 |
|
整份程式裡,最需要注意的是 .h 檔中的兩個部分:
extern "C":用來告訴編譯器這些函式使用 C 語言的命名方式,讓之後 C# 在使用 DllImport 時能正確對應函式名稱。__declspec(dllexport):用來將這些函式 導出到 DLL,使外部應用程式(例如 C#)可以呼叫。
這兩個關鍵字是讓 C# 能順利呼叫 C++ 程式的必要設定。
到此為 C++ 程式碼部分的基本說明。
下面為額外的補充,
如果覺得每個函式都重複寫這兩個關鍵字比較繁瑣,
可以使用 #ifdef 或 macro 統一管理。例如:
1 |
|
那如果希望程式能跨平台共用,則可以 導出符號與呼叫約定一起包起來,例如,
1 |
|
這樣就能統一管理函式導出與呼叫方式,
不管在 Windows 或其他平台都能維持一致,程式碼更乾淨,也更容易維護。
編譯 C++ DLL
C++ 程式碼撰寫完成後,接下來要編譯出 Native DLL。
首先,在專案上按右鍵 → 屬性。

會出現一個在 C++ 專案中常見的屬性設定視窗,
因為前面建立專案時選擇了空白專案,
所以需要做一些設定調整。
找到組態類型後,可以看到目前選擇的是「應用程式(.exe)」,
將它改為「動態程式庫(.dll)」,然後按下「套用」即可。

接下來,只要對著專案按下「建置」,
就可以在對應的組態資料夾中看到編譯出的 DLL,如下圖所示:

接下來,就輪到 C# 的部分了。
在 C# 中呼叫 C++ 函式庫
建立呼叫 C++ 的 C# 程式碼
接下來,我們要撰寫讓 C# 能呼叫 C++ DLL 的程式。
這裡建立了一個 C# 類別庫,
並在專案中新增一個名為 CsWrapper.cs 的檔案。
如前面所說,
專案架構可以依需求自行調整,
不一定要建立類別庫。
此時整個方案的架構如下圖所示,

再來是整個 C# 程式碼的部分,
CsWrapper.cs
1 | public static class CsWrapper |
大致上可以分成兩大部分:
串接 C++ 的結構與方法
包含
struct、enum、delegate以及使用DllImport呼叫 C++ DLL 的原生函式。對外封裝給使用者的方法
將原生函式封裝成靜態方法,提供簡單易用的介面給其他 C# 專案呼叫。
關於 DllImport Attribute 的部分,
以下以 Add 與 Divide 為例做個說明,
1 | [ |
詳細的設定說明可以自行上網查閱,
這裡要特別注意的是 C++ 原生 DLL 的檔案名稱與位置。
在程式執行時,DLL 必須放在所設定的路徑,
否則程式可能會發生 Crash。
至於要放在哪裡,則取決於你的專案策略。
例如,有些廠商雖然提供 .NET DLL,
但實際上原生 C++ DLL 是隨驅動程式一起安裝,
透過安裝程式放置到指定位置的。
另外,如果 C++ 原始碼弱較為複雜、
或直接呼叫時需要處理許多轉換 (例如結構體或字串),
那麼可以在這一層 C# 類別中,
做適當的封裝與轉換,
讓外層程式在使用時能保持乾淨且一致的介面。
C# 串接 C++ 執行結果
接下來進入實際執行的部分。
這裡建立一個 C# 主控台專案,
並將前面建立的 C# 類別庫 加入至專案參考中。
完成後,整個方案的架構如下圖所示,

再來,在 Program.cs 中撰寫一些簡單的測試程式,
實際呼叫前面封裝的 CsWrapper 方法。
Program.cs
1 | using CSharpLibrary; |
撰寫完成後,可以先執行一次建置。
由於在前面的 C# 類別庫中,
DllImport 是設定 C++ DLL 與執行檔位於同一層目錄,
因此建置完成後,需將編譯出來的 C++ DLL 手動複製到輸出資料夾中,
如下圖所示,

最後執行整個程式,
便可看到執行的結果如下圖,

上述步驟做完,到能看到結果輸出,
到這邊便能掌握一些基礎的從建立 C++ DLL 到 C# 的使用的其中一種方法。
上述步驟完成後,就能順利看到執行結果,
到這邊為止,應該已能掌握從建立 C++ DLL 到在 C# 中使用的基本流程。
隨著實際開發與使用的過程中,
或許產生一些想法,
接下來也會分享幾個可以參考的方向。
額外補充
補充1:C++ 如何進入中斷點
在開發過程中,難免需要 debug。
即使像本次範例一樣,
將 C++ 與 C# 專案放在同一個方案中,
可能會發現,即便在 C++ 程式碼中插入了中斷點,
在 Debug 模式 執行 C# 專案時,中斷點仍然不會被觸發。
那麼,要如何對 C++ 程式碼進行除錯呢?
其實只要稍作設定,
就能讓程式在執行時進入 C++ 的中斷點。
首先,在「執行檔專案」上按右鍵 → 屬性,
在選單中找到 「偵錯」,
接著點選 「開啟 debug 啟動設定檔 UI」。

在啟動設定檔中,
找到 「啟用機器碼偵錯」的選項,
將它勾選後關閉視窗,這樣設定就完成了。

接著回到 C++ 程式碼中設定中斷點,
再以 Debug 模式 執行程式,
此時中斷點就會如預期生效,
也能檢視 C++ 端的變數內容與執行流程。

補充2:利用 Costura.Fody 將 C++ DLL 做打包
前面提到,C# 專案在呼叫 C++ DLL 時,
通常需要把對應的 C++ DLL 放在指定的目錄下,
否則在執行階段會找不到對應的函式庫。
當遇到一些狀況時,
像是不希望使用者看到這個 C++ DLL,
或者希望最終只發佈單一的 .NET DLL 檔案,
這時就會稍微麻煩一些。
這時候,可以考慮使用 Costura.Fody 套件,
該套件會在編譯時,
可以將所設定的 C++ DLL 檔案包起來。
詳細操作如下,
在專案中開啟 NuGet 套件管理器,
搜尋並安裝 Costura.Fody 套件。

著從專案結構來看,
由於建置環境設定為 x64,
需要建立一個名為 costura-win-x64 的資料夾,
(若使用舊版本套件,則為 Costura64 )
並將 C++ DLL 複製到該資料夾中。
設定該 DLL 的屬性,將「建置動作」設為「內嵌資源」。
至於專案中的 FodyWeavers.xml,
這個檔案是自動生成,無需特別理會。

接著,可以試著 刪除 bin 目錄中的所有檔案,
然後重新建置 C# 執行檔專案。
即使輸出資料夾中沒有原本的 C++ DLL,
執行程式仍能正常運作。
這就代表 C++ DLL 已經被 自動包入 C# 類別庫的 DLL 之中。

如果使用 發佈功能,並且設定為單一檔案發佈,
原本只會將 .NET DLL 包進去,而不會包含 C++ DLL。
不過,如果搭配 Costura.Fody 套件,
C++ DLL 也會被自動包入進去,
最終就可以只產生單一執行檔,直接執行。

如果想進一步了解 單一檔案發佈 的細節,
可以參考下列網址中的另一篇文章:
發佈 .NET 單一執行檔到本地資料夾
如果 NuGet 套件中本身就包含 C++ DLL,
發佈後仍可能會在輸出資料夾看到該 DLL,
不論是否有將它放入指定的資料夾中,
這都是正常現象,屬於發佈功能的行為。
前面示範的是將 C++ DLL 包在 C# 類別庫那一層。
如果希望在執行檔專案這一層使用,也是完全可行的。
只不過,相關的 Costura 設定就需要在執行檔專案中進行設定。
補充3:C++ DLL 輸出路徑設定
如果在方案中修改了 C++ 程式碼,
每次要執行整個程式時,都需要手動複製 C++ DLL 到對應目錄,
這樣做起來會很麻煩,有沒有辦法簡化這個流程呢?
這時候,可以透過建置後事件來解決這個問題。
在 C++ 專案上按右鍵 → 屬性,
左側選單找到「建置事件」並找到「建置後事件」,
點下去後,接著點選右邊視窗中的「命令列」,

接著輸入以下指令與參數,確認評估值 OK 後,按下「確定」
1 | xcopy /Y /I "$(TargetPath)" "..\CSharpLibrary\costura-win-x64\" |
配合評估值來看,大家應該可以猜出,
這是一個將編譯好的 DLL,自動複製到指定位置的指令,

最後在屬性頁按下按下「套用」。

這樣一來,未來每次 C++ 編譯完成後,
對應的 DLL 就會自動複製到指定路徑,
相關參數與路徑可以依實際專案需求自行調整。
總結
以上就是利用 DllImport 讓 C# 呼叫 C++ 原生函式的整體流程。
其實只要是 .NET 架構下的語言,例如 VB.NET,
也都能用相同的方式進行,
就算是在 C# 類別庫 中使用,也完全沒問題,
因為 .NET 各模組之間本身就能相互呼叫。
若是從他人手中取得的 C++ DLL 或既有的 DllImport 程式碼,
在理解了這些原理後,
相信對整體架構的運作方式就不會那麼陌生。
而若是自行開發的 C++ 函式庫,
在實際串接的過程中若遇到一些小麻煩,
也可以參考後面的補充章節,
有幾個能讓整體開發流程更加順暢的方法。
另外,在實際部署或讓使用者端執行軟體時,
電腦端有時也需要安裝對應的 C++ 執行階段元件(Runtime)。
這部分依照編譯方式與環境設定而定,
實務上有遇過必須安裝的情況,也有不需要的狀況。
若未來有更完整的了解,
會再針對這部分做進一步的補充。
自言自語543
依稀記得在學時期,
第一個接觸的程式語言(應該)就是 C++ ,
也有印象還有用過什麼 CMake 。
以前曾稍微碰過 MFC 專案,
當時有用個第三方函示庫,
雖然是編譯好的 C++,
但卻不是 DLL,而是 .h 和 lib 檔。
又或者是也有用過其他的程式語言,
去串接過 C++ 的程式碼,
其實還滿多程式語言的底層,
都是 C/C++ 的呢…
(指標什麼的我不想知道)
總之,除了 C# 有各種方式可以接 C/C++ 以外,
其他語言也是各種五花八門的串接方式呢…