在VS2010上使用C#呼叫非託管C++生成的DLL檔案(圖文講解) 背景
背景
在專案過程中,有時候你需要呼叫非C#編寫的DLL檔案,尤其在使用一些第三方通訊元件的時候,通過C#來開發應用軟體時,就需要利用DllImport特性進行方法呼叫。本篇文章將引導你快速理解這個呼叫的過程。
步驟
1. 建立一個CSharpInvokeCPP的解決方案:
2. 建立一個C++的動態庫專案:
3. 在應用程式設定中,選擇“DLL”,其他按照預設選項:
最後點選完成,得到如圖所示專案:
我們可以看到這裡有一些檔案,其中dllmain.cpp作為定義DLL應用程式的入口點,它的作用跟exe檔案有個main或者WinMain入口函式是一樣的,它就是作為DLL的一個入口函式,實際上它是個可選的檔案。它是在靜態連結時或動態連結時呼叫LoadLibrary和FreeLibrary時都會被呼叫。詳細內容可以參考(
4. 現在我們開啟CSharpInvokeCPP.CPPDemo.cpp檔案:
現在我們加入以下內容:
// CSharpInvokeCPP.CPPDemo.cpp : 定義 DLL 應用程式的匯出函式。 // #include "stdafx.h" extern "C" __declspec(dllexport) int Add(int x, int y) { return x + y; } extern "C" __declspec(dllexport) int Sub(int x, int y) { return x - y; } extern "C" __declspec(dllexport) int Multiply(int x, int y) { return x * y; } extern "C" __declspec(dllexport) int Divide(int x, int y) { return x / y; }
extern "C" 包含雙重含義,從字面上即可得到:首先,被它修飾的目標是“extern”的;其次,被它修飾的目標是“C”的。而被extern "C"修飾的變數和函式是按照C語言方式編譯和連線的。
__declspec(dllexport)的目的是為了將對應的函式放入到DLL動態庫中。
extern "C" __declspec(dllexport)加起來的目的是為了使用DllImport呼叫非託管C++的DLL檔案。因為使用DllImport只能呼叫由C語言函式做成的DLL。
5. 編譯專案程式,最後在Debug目錄生成CSharpInvokeCPP.CPPDemo.dll和CSharpInvokeCPP.CPPDemo.lib
我們用反編譯工具PE Explorer檢視下該DLL裡面的方法:
可以發現對外的公共函式上包含這四種“加減乘除”方法。
6. 現在來演示下如何利用C#專案來呼叫非託管C++的DLL,首先建立C#控制檯應用程式:
7. 在CSharpInvokeCSharp.CSharpDemo專案上新建一個CPPDLL類,編寫以下程式碼:
public class CPPDLL
{
[DllImport("CSharpInvokeCPP.CPPDemo.dll")]
public static extern int Add(int x, int y);
[DllImport("CSharpInvokeCPP.CPPDemo.dll")]
public static extern int Sub(int x, int y);
[DllImport("CSharpInvokeCPP.CPPDemo.dll")]
public static extern int Multiply(int x, int y);
[DllImport("CSharpInvokeCPP.CPPDemo.dll")]
public static extern int Divide(int x, int y);
}
DllImport作為C#中對C++的DLL類的匯入入口特徵,並通過static extern對extern “C”進行對應。
8. 另外,記得把CPPDemo中生成的DLL檔案拷貝到CSharpDemo的bin目錄下,你也可以通過設定【專案屬性】->【配置屬性】->【常規】中的輸出目錄:
這樣編譯專案後,生成的檔案就自動輸出到CSharpDemo中了。
9. 然後在Main入口編寫測試程式碼:
static void Main(string[] args)
{
int result = CPPDLL.Add(10, 20);
Console.WriteLine("10 + 20 = {0}", result);
result = CPPDLL.Sub(30, 12);
Console.WriteLine("30 - 12 = {0}", result);
result = CPPDLL.Multiply(5, 4);
Console.WriteLine("5 * 4 = {0}", result);
result = CPPDLL.Divide(30, 5);
Console.WriteLine("30 / 5 = {0}", result);
Console.ReadLine();
}
執行結果:
方法得到呼叫。
10. 以上的方法只能通過靜態方法對於C++中的函式進行呼叫。那麼怎樣通過靜態方法去呼叫C++中一個類物件中的方法呢?現在我在CPPDemo專案中新增一個頭檔案userinfo.h:
class UserInfo {
private:
char* m_Name;
int m_Age;
public:
UserInfo(char* name, int age)
{
m_Name = name;
m_Age = age;
}
virtual ~UserInfo(){ }
int GetAge() { return m_Age; }
char* GetName() { return m_Name; }
};
在CSharpInvokeCPP.CPPDemo.cpp中,新增一些程式碼:
#include "malloc.h"
#include "userinfo.h"
typedef struct {
char name[32];
int age;
} User;
UserInfo* userInfo;
extern "C" __declspec(dllexport) User* Create(char* name, int age)
{
User* user = (User*)malloc(sizeof(User));
userInfo = new UserInfo(name, age);
strcpy(user->name, userInfo->GetName());
user->age = userInfo->GetAge();
return user;
}
這裡宣告一個結構,包括name和age,這個結構是用於和C#方面的結構作個對映。
注意:程式碼中的User*是個指標,返回也是一個物件指標,這樣做為了防止方法作用域結束後的區域性變數的釋放。
strcpy是個複製char陣列的函式。
11. 在CSharpDemo專案中CPPDLL類中補充程式碼:
[DllImport("CSharpInvokeCPP.CPPDemo.dll")]
public static extern IntPtr Create(string name, int age);
[StructLayout(LayoutKind.Sequential)]
public struct User
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string Name;
public int Age;
}
其中這裡的結構User就和C++中的User對應。
12. 在Program.cs中補充程式碼:
IntPtr ptr = CPPDLL.Create("李平", 27);
<strong><font color="#ff0000">CPPDLL.User user = (CPPDLL.User)Marshal.PtrToStructure(ptr, typeof(CPPDLL.User));</font></strong>
Console.WriteLine("Name: {0}, Age: {1}", user.Name, user.Age);
注意:紅色字型部分,這裡結構指標首先轉換成IntPtr控制代碼,然後通過Marshal.PtrToStructrue轉換成你所需要的結構。
執行結果:
最後附上我的原始碼:CSharpInvokeCPP.rar,希望對大家有所幫助:)