有時候,我們也會有在C++中調用.NET的需求,C++/CLI就是這樣一種技術,它能夠與本地代碼混合編程,從而提供強大的功能,本文將介紹如何使用反射的一些實踐。 ...
為什麼要在C++中調用.NET
一般情況下,我們常常會在.NET程式中調用C/C++的程式,使用P/Invoke方式進行調用,在編寫代碼代碼的時候,首先要導入DLL文件,然後在根據C/C++的頭文件編寫特殊的C#平臺調用代碼,例如像下麵這個樣子:
[DllImport("Interop.dll",EntryPoint = "Multiply",CharSet = CharSet.Ansi)] static extern int Multiply(int factorA, int factorB);
詳細的過程,可以參考之前我這篇文章:《C#調用C和C++函數的一點區別》
有時候,我們也會有在C++中調用.NET的需求,比如我們在維護一個大型的C++應用程式,它年代久遠,現在需要增加一些新功能,而這些功能在.NET中已經有了,只需要調用它即可,如果為了方便想要用.NET重寫這個C++應用程式是不太現實的,幸好,C++/CLI提供了一個簡便的方案使得可以在C++中直接編寫.NET程式,所以C++/CLI代表托管和本地編程的結合,可以在托管代碼中直接使用本地代碼,也可以反過來,這樣結合了C++本地代碼的高效性和.NET代碼的強大性,看起來是非常有潛力的。
使用C++/CLI進行.NET編程
要進行C++/CLI編程,只需要進行下麵的步驟:
1,添加.NET程式集的應用;
2,修改C++項目屬性,配置屬性->公共語言運行時支持-公共語言運行時支持(/clr)
然而,為了保持C++與.NET應用程式的獨立性,要求不能將.NET的DLL文件放到C++的應用程式目錄下,因此上述步驟1不可行,需要在C++代碼中使用反射來調用.NET。
註意,本文說的C++反射調用,不是對C++自身進行封裝的反射功能,而是在C++/CLI代碼中反射調用.NET代碼,原理上跟你在.NET應用中反射調用另外一個.NET的程式集一個道理。
首先,我們建立一個名字叫CppNetTest的解決方案,添加3個項目:
1,CppConsoleTest---一個C++控制台項目,在項目中更改屬性支持CLR;
2,NetApp--一個.NET控制台應用程式,作為對比示例代碼,方便編寫C++/CLI代碼參考;
3,NetLib--一個.NET類庫程式集,它將被1和2項目進行反射調用。
我們先在NetLib項目寫一個簡單的.NET 類,這個類的方法內部沒有複雜的業務邏輯代碼,僅僅用來供反射調用測試:
namespace NetLib { public class User { static List<IUserInfo> UserDb = new List<IUserInfo>(); public int GetUserID(string IdString) { int result = 0; int.TryParse(IdString, out result); return result; } public DateTime GetUserBirthday(int userId) { return new DateTime(1980, 1, 1); } public IUserInfo GetUserByID(int userId) { IUserInfo userinfo= EntityBuilder.CreateEntity<IUserInfo>(); userinfo.ID = userId; userinfo.Name = "姓名_" + userId; userinfo.Birthday = new DateTime(1980, 1, 1); return userinfo; } //返回List或者數組,不影響 C++調用 public List<IUserInfo> GetUsers(string likeName) { List<IUserInfo> users = new List<NetLib.IUserInfo>(); for (int i = 0; i < 10; i++) { IUserInfo userinfo = GetUserByID(i); userinfo.Name += likeName; users.Add(userinfo); } //return users.ToArray(); return users; } public bool SaveUsers(IList<IUserInfo> users) { UserDb.AddRange(users); return true; } public IUserInfo CreateUserObject() { return EntityBuilder.CreateEntity<IUserInfo>(); } public bool SaveUsers2(IEnumerable<Object> para) { var users = from u in para select u as IUserInfo; return SaveUsers (users.ToList()); } } }
在CppConsoleTest項目的頭文件中,添加一個 UserProxy.h 的C++頭文件,在文件中添加下麵的命名空間:
using namespace System; using namespace System::Reflection; using namespace Runtime::InteropServices; using namespace System::Collections;
這樣我們就可以使用反射相關的類型了。
在UserProxy類中,先編寫我們需要的構造函數:
public ref class UserProxy { private: String^ assemblyFile; //"..\\NetLib\\bin\\Debug\\NetLib.dll" Object^ dotnetObject; Type^ entityBuilderType; String^ className = "NetLib.User"; EntityHelper^ helper; public: UserProxy(String^ assemblyFile) { this->assemblyFile = assemblyFile; Assembly^ ass = Assembly::LoadFrom(this->assemblyFile); this->dotnetObject = ass->CreateInstance(className); String^ sodPath = System::IO::Path::Combine(System::IO::Path::GetDirectoryName(this->assemblyFile), "PWMIS.Core.dll"); /*Assembly^ ass_sod = Assembly::LoadFrom(sodPath); this->entityBuilderType = ass_sod->GetType("PWMIS.DataMap.Entity.EntityBuilder");*/ helper = gcnew EntityHelper(sodPath); } }
註意我們的 C++/CLI的類必須是“引用”類型,所以需要加關鍵字 ref,即:
public ref class UserProxy{}
所有的.NET引用類型,在使用的時候,都必須在類型名字後加 ^ 符號,例如下麵定一個.NET字元串類型變數:
String^ assemblyFile;
帶^符號的變數,在C++/CLI中稱為 “句柄”對象,用來跟C++本地代碼的“指針”相區別。
在C++中,類的成員用 -> 符號調用,命名空間或者類的靜態成員,用::調用,例如上面的構造函數中的代碼:
Assembly^ ass = Assembly::LoadFrom(this->assemblyFile);
註意:在本例中需要.NET類庫項目引用 PDF.NET SOD框架,在項目的“管理Nuget程式包”裡面搜索 PDF.NET.SOD.Core 添加此引用即可。
學會了這些C++的基礎語法,那麼編寫C++/CLI代碼就沒有主要的障礙了。
在C++/CLI中使用反射
反射調用第一個.NET類的方法
下麵的方法,將會反射調用 User類的一個最簡單的方法 :
public int GetUserID(string IdString){}
該方法只有一個一個參數和一個簡單的返回值,下麵是C++/CLI的反射調用代碼:
int GetUserID(String^ iDstring) { MethodInfo^ method = this->dotnetObject->GetType()->GetMethod("GetUserID", BindingFlags::Public | BindingFlags::Instance); Func<String^, int>^ fun = (Func<String^, int>^)Delegate::CreateDelegate(Func<String^, int>::typeid, this->dotnetObject, method); int result = fun(iDstring); return result; }
註意這裡創建了一個 Func<String,int>的委托方法,使用委托能夠簡化我們的反射調用並且有時候還能夠提高效率,在這段代碼中,有1個要註意的地方:
Func<String^, int>::typeid
這是C++/CLI特殊的語法,表示獲取“句柄”類型的類型ID,實際上它的結果就Type對象,等同於C#的
typeof(Func<string,int>)
PS:非常遺憾的是,typeid方式,沒法得到下麵類型的類型值:
typeof(Func<,>),這給我們在動態構造泛型對象的時候造成了很大的困惑。
再看一個簡單方法的反射:
DateTime GetUserBirthday(int userId) { MethodInfo^ method = dotnetObject->GetType()->GetMethod("GetUserBirthday", BindingFlags::Public | BindingFlags::Instance); Func<int, DateTime>^ fun = (Func<int, DateTime>^)Delegate::CreateDelegate(Func<int, DateTime>::typeid, this->dotnetObject, method); DateTime result = fun(userId); return result; }
註意:由於DateTime是值類型,因此在進行類型申明的時候,不需要加^符號,僅需要對Func委托加上^句柄標記。
有了這2個簡單的方法,我們來看看如何調用這個.NET方法“代理類”:
NetLibProxy::UserProxy^ proxy = gcnew NetLibProxy::UserProxy("..\\NetLib\\bin\\Debug\\NetLib.dll"); int result= proxy->GetUserID("123456"); DateTime date = proxy->GetUserBirthday(result); System::Console::WriteLine("C++/CLI .Net Proxy Class Call Test Result:\r\n UserID={0},\r\n Birthday={1}", result,date.ToShortDateString());
OK,第一個C++/CLI代碼調用成功,而且還是反射調用的,心情小激動一下。
有關C++/CLI的反射,委托的詳細資料,可以參考MSDN的介紹:
https://msdn.microsoft.com/zh-cn/library/2x8kf7zx.aspx 使用 C++ 互操作(隱式 PInvoke)
https://msdn.microsoft.com/zh-CN/library/213x8e7w.aspx 泛型委托
在下一篇,我們將繼續探究C++/CLI 反射調用.NET中可能遇到"深坑",因此僅打算吧本篇文章作為一個“入門”,免得大家心生恐懼,錯過了挑戰艱險的機會。
(未完待續)