經過一晚上的折騰,還是下點決心將些許的心得寫下來,以免以後重復勞動。
C#與C/C++相比,前者的優勢在於UI,後者的優勢在於算法,C++下的指針雖然惡心,若使用得當還是相當方便的,最重要的問題是,市面上很多流行的開發工具庫,幾乎沒有不支持C++的,但全面支持C#只能說是難得,在CPU發展到今天,若說C#的執行效率跟C++相比有很大的差距並不是那麼靠譜,若非萬不得已我還是寧願用C#來寫代碼,調試什麼的也很方便。
不得已的情況下,要在C#下使用C++的函數或類,最好的方式就是使用動態鏈接庫dll),至於COM什麼的我是至今沒弄明白其原理,也許主要是因為使用起來太麻煩了還要注冊什麼的),使用dll的話,可以很方便將一個工程細分到成為兩部分,協同編程可以加速進展。當然,用C#的代碼改寫一遍也不是不可能的,可當你有現成的幾萬行代碼那就真頭痛得要命,還是安心的使用動態鏈接庫吧。
當你打開VS2012的時候,新建工程項目,也許你可能發現會有一個“CLR類庫”的項目類型,直到今天我才知道,CLR原來指的是托管C++,托管C++跟非托管C++雖然有一定的關系,但很多人更願意將他倆看成為兩種不同的程序語言,托管C++是一種很惡搞的存在,它的唯一作用是用披著C++的馬甲來寫C#的內容,且最終是為C#服務的,使用CLR生成的dll可以直接在C#下引用,要用CLR還不如直接用C#更簡單一點純粹個人觀點)。
這是最常見的技倆,網上的資料是一大票,但為了我這健忘腦袋,我還是逐步貼圖講明吧,據本人一通宵的成果,幾經折騰,終於證明了想在native C++下導入類那是不可能的事,所以,以下講的僅是如何導入函數而已——就如你們在網上看到的文章一樣。
1)建立生成dll的工程
我用的是VS2012,不過好像跟前面的版本沒什麼太大的差別。打開VS,選擇"新建項目"-“VC++”-"Win32"-"Win32項目",工程的名字叫"MyNativeDll",配置如下圖所示,因為我有可能用到MFC的類,所以我就勾選了“MFC”的選項,在此需要注意的是,如果你新建時沒有勾選MFC,但在後面卻想動用MFC的內容,就會遇到“MFC apps must not #include <windows.h>”的Error,只是在工程的配置裡修改是根本沒有用的,必做要重建工程。
2)實現dll導出的函數
新建好工程後,在VS的“解決方案資源管理器”中可以看到如下圖的目錄,其實你完全可以不用管這些默認的文件,如果你要用,可以在看一下MyNativeDll.h裡的注釋說明,大概能看得懂的。
在工程裡添加幾個文件,Define.h,CFunction.h,CFunction.cpp,其內容如下所示:
//Define.h 用於導入dll的宏定義。
//Define.h /////////////////////////////////////////// ////////////////////////////////////////// #ifndef _DEFINE_H_ #define _DEFINE_H_ #define _EXTERN_C_ extern "C" _declspec(dllexport) #endif
//CFunction.h 函數定義,這裡我特意定義了一組結構,我的用意稍後再講。
//CFunction.h //////////////////////////////////////////// /////////////////////////////////////////// #ifndef _C_FUNCTION_H_ #define _C_FUNCTION_H_ #include "Define.h" #include <string> #include <istream> struct SystemTime { int year; int month; int day; int hour; int minute; int second; int millsecond; SystemTime & operator= (SystemTime st) { this->year = st.year; this->month = st.month; this->day = st.day; this->hour = st.hour; this->minute = st.minute; this->second = st.second; this->millsecond = st.millsecond; return *this; } }; _EXTERN_C_ int add(int x, int y); _EXTERN_C_ int sub(int x, int y); _EXTERN_C_ int testChar(char * src, char * res, int nCount); _EXTERN_C_ int testStruct(SystemTime & stSrc, SystemTime & stRes); #endif //_C_FUNCTION_H_
//CFunction.cpp dll函數的實現,簡單的賦值而已,大家應該看得明白的。
//CFunction.cpp //////////////////////////////////////////// //////////////////////////////////////////// #include "stdafx.h" #include "CFunction.h" #include <stdio.h> int add(int x, int y) { return x + y; } int sub(int x, int y) { return x - y; } int testChar(char * src, char * res, int nCount) { memcpy(res, src, sizeof(char) * nCount); return 1; } int testStruct(SystemTime & stSrc, SystemTime & stRes) { stRes = stSrc; return 1; }
添加好代碼之後,選擇“生成”的選項,因在工程目錄下的Debug文件就已經存在我們所需要的MyNativeDll.dll文件,一起的還有lib的靜態庫文件稍後要用到),及其他相關的調試文件,至此,我們已經成功的生成了native C++的動態鏈接庫,我只能說,這是相當簡單的第一步在而已。
3)在C#工程下使用生成的dll
新建一個C#的窗口工程我個人是很討厭控制台的程序的),工程命名為“DllTest”,這就不教了。
在新建的窗體工程中添加一個CFunction.cs的類,這個類主要是用於導出上面dll裡的函數,廢話不多說,直接貼代碼:
//CFunction.cs dll的函數接口
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Runtime.InteropServices; namespace DllTest { [StructLayout(LayoutKind.Sequential)] public struct SystemTime { public int year; public int month; public int day; public int hour; public int minute; public int second; public int millsecond; public SystemTime(DateTime dt) { this.year = dt.Year; this.month = dt.Month; this.day = dt.Day; this.hour = dt.Hour; this.minute = dt.Minute; this.second = dt.Second; this.millsecond = dt.Millisecond; } public override string ToString() { return this.year.ToString() + "-" + this.month.ToString() + "-" + this.day.ToString() + " " + this.hour.ToString() + ":" + this.minute.ToString() + "-" + this.second.ToString() + "-" + this.millsecond.ToString(); } }; public class CFunction { [DllImport("MyNativeDll.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)] public extern static int add(int x, int y); [DllImport("MyNativeDll.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)] public extern static int sub(int x, int y); [DllImport("MyNativeDll.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)] public extern static int testChar(ref byte src, ref byte res, int nCount); [DllImport("MyNativeDll.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl)] public extern static int testStruct(ref SystemTime stSrc, ref SystemTime stRes); } }
上面代碼的格式看起來不好看,大家自己下附件裡的文件看好了,上面的做法相當是作了一個CFunction的靜態類而已。然後在Form1.cs窗體裡直接寫測試代碼,我是直接寫在Form1的初始化函數裡,懶,沒辦法。
//Form1.cs 在C#的窗體初始化函數添加測試代碼
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace DllTest { public partial class Form1 : Form { public Form1() { InitializeComponent(); int a = CFunction.add(100, 50); int b = CFunction.sub(100, 50); Debug.WriteLine("add = " + a.ToString() + " b = " + b.ToString()); Debug.WriteLine("\r\n"); string src = "123456"; byte[] srcBytes = System.Text.Encoding.ASCII.GetBytes(src); byte[] resBytes = new byte[100]; a = CFunction.testChar(ref srcBytes[0], ref resBytes[0], src.Length); string res = (System.Text.Encoding.ASCII.GetString(resBytes, 0, resBytes.Length)).TrimEnd(); Debug.WriteLine(res.ToString()); Debug.WriteLine("\r\n"); SystemTime stSrc = new SystemTime(DateTime.Now); SystemTime stRes = new SystemTime(); a = CFunction.testStruct(ref stSrc, ref stRes); Debug.WriteLine(stRes.ToString()); Debug.WriteLine("\r\n"); } } }
在你進行調試之前,務必記得要將在第二步生成的MyNativeDll.dll拷貝至C#工程下的bin\Debug\目錄下,然後點擊“調試”,看輸出窗口,應該會有東西輸出的,我就不貼出來了。
4)總結
1)總體上來講,生成一個native C++的dll不是很困難的事,重點在於在C#下的dll導出函數那裡;
2)個人的經驗來看,使用native C++可以導入函數,至於導出C++類,通過指針的方式並非不可能,可是方法過於費解,建議不要那麼做;
3)在書寫dll導出函數時,變量的傳遞是關鍵,建議使用C++的基本類型,如int,float,double等,因為C#下指針的概念很糾結,在C++下的引用符“&”,在C#中則使用ref的標識,需要緊記的一點是,C#與C++的類型並不全然通用結構對齊問題),注意做變換。像上面的testChar函數,原本string(C#)對應的是char*(C++),但可能由於各種Unicode或多字節的關系,我是沒法返回正確的值,於是我采用了byte的傳入類型。關於C#與C++混編的類型問題,可以查看下面的文章:C++與C#的類型轉換,文章2,文章3,在網上google到好的文章也是不容易的啊。
4)觀察我寫的結構,在C++下使用的結構體,在C#必須要重新定義一次,使用 [StructLayout(LayoutKind.Sequential)]的標識用於結構的對齊,如果你變量中使用了string這樣的類型,還需要使用MarshalAs這樣的方法支定義其長度——才可以跟char *相對應;
5)函數的返回值別用什麼string了,我是找為到方法取得其正確的返回值,最好使用ref的引用方法回傳回來。
6)指針的參數的傳遞在C#下使用IntPtr類型作轉換,這我先不細說,網上相關文章還是不少的。
5)示例文件的下載
本文出自 “幾縷蕭雨鎖清秋” 博客,請務必保留此出處http://joeyliu.blog.51cto.com/3647812/1289614