C# 托管和非托管混合編程


在非托管模塊中實現你比較重要的算法,然后通過 CLR 的平台互操作,來使托管代碼調用它,這樣程序仍然能夠正常工作,但對非托管的本地代碼進行反編譯,就很困難。

 

最直接的實現托管與非托管編程的方法就是使用C++/CLI

介紹

項目存檔一直是企業的采用的做法,而是事實證明他們也是對的!對於一個程序員,這是幾千men-days的工作量。為什么不開發一小段代碼去重新利用那段代碼,項目。
現在提供了一個漸漸的轉向C#的新技術: 使用托管與非托管的混合編程。這是一個可行的方案在top-down issue(from UI to low-level layers)or bottom-up(from low-level to UI)案例。
本文目的就是通過兩個簡單的例子來說明怎么一起使用這兩種技術:
* 在非托管中調用托管代碼。
* 在托管中調用非托管代碼。

非托管代碼中調用托管函數

這里寫圖片描述
這個例子主要展示了在非托管代碼(C++)中調用使用托管(C#)代碼實現類,通過托管代碼實現"mixed code"DLL 來導出API。

單一的非托管代碼

以下是一個控制台程序

#include "stdafx.h" #include <iostream> using namespace std; #ifdef _UNICODE #define cout wcout #define cint wcin #endif int _tmain(int argc, TCHAR* argv[]) { UNREFERENCED_PARAMETER(argc); UNREFERENCED_PARAMETER(argv); SYSTEMTIME st = {0}; const TCHAR* pszName = _T("John SMITH"); st.wYear = 1975; st.wMonth = 8; st.wDay = 15; CPerson person(pszName, &st); cout << pszName << _T(" born ") << person.get_BirthDateStr().c_str() << _T(" age is ") << person.get_Age() << _T(" years old today.") << endl; cout << _T("Press ENTER to terminate..."); cin.get(); #ifdef _DEBUG _CrtDumpMemoryLeaks(); #endif return 0; }

 

這段代碼沒有什么特殊的,這只是個再普通不過的非托管代碼。

單一的托管代碼

這是個典型的使用C#實現的裝配器

using System; namespace AdR.Samples.NativeCallingCLR.ClrAssembly { public class Person { private string _name; private DateTime _birthDate; public Person(string name, DateTime birthDate) { this._name = name; this._birthDate = birthDate; } public uint Age { get { DateTime now = DateTime.Now; int age = now.Year - this._birthDate.Year; if ((this._birthDate.Month > now.Month) || ((this._birthDate.Month == now.Month) && (this._birthDate.Day > now.Day))) { --age; } return (uint)age; } } public string BirthDateStr { get { return this._birthDate.ToShortDateString(); } } public DateTime BirthDate { get { return this._birthDate; } } } }

 

正如所見,這這是個單一的CLR

托管與非托管混合編程部分

這部分是最重要,也是最難的。VisualStudio環境提供了一些頭文件來幫助開發者鏈接這些關鍵詞。

#include <vcclr.h>

 

但是,並非就到這兒就結束了。我們還需要小心涉及的一些陷阱,尤其是是CLR(托管代碼)和native(非托管代碼)一些關鍵詞之間數據的傳遞。
以下是個類的頭文件輸出一個托管的部分

#pragma once #ifdef NATIVEDLL_EXPORTS #define NATIVEDLL_API __declspec(dllexport) #else #define NATIVEDLL_API __declspec(dllimport) #endif #include <string> using namespace std; #ifdef _UNICODE typedef wstring tstring; #else typedef string tstring; #endif class NATIVEDLL_API CPerson { public: // Initialization CPerson(LPCTSTR pszName, const SYSTEMTIME* birthDate); virtual ~CPerson(); // Accessors unsigned int get_Age() const; tstring get_BirthDateStr() const; SYSTEMTIME get_BirthDate() const; private: // Embedded wrapper of an instance of a CLR class // Goal: completely hide CLR to pure unmanaged C/C++ code void* m_pPersonClr; };

 

強調一點,盡量在頭文件里保證只有非托管代碼,混合編程在cpp中去實現,數據的傳遞。比如: 應該盡量避免使用vcclr.h中的函數, 進行混合編程。這就是為什么定義一個void指針來包裝CLR對象。
一個神奇的大門,就這樣打開了。。。
正如我說的那樣,神奇的事就從包含一個vcclr.h頭文件開始。但是,需要使用CLR編碼語言和使用一些復雜的類型(例如:strings, array, etc):

using namespace System; using namespace Runtime::InteropServices; using namespace AdR::Samples::NativeCallingCLR::ClrAssembly;

 

當然,需要申明一些使用的本地裝配器。
首先,我們來看這個類的構造器:

CPerson::CPerson(LPCTSTR pszName, const SYSTEMTIME* birthDate) { DateTime^ dateTime = gcnew DateTime((int)birthDate->wYear, (int)birthDate->wMonth, (int)birthDate->wDay); String^ str = gcnew String(pszName); Person^ person = gcnew Person(str, *dateTime); // Managed type conversion into unmanaged pointer is not // allowed unless we use "gcroot<>" wrapper. gcroot<Person^> *pp = new gcroot<Person^>(person); this->m_pPersonClr = static_cast<void*>(pp); }

 

在非托管代碼里允許使用一個指針指向一個托管的類,但是我們並不想直接到處一個托管的API給用戶。
所以, 我們使用了一個void指針來封裝這個對象,一個新的問題又出現了:我們是不被允許直接用非托管指針指向托管類型的。這就是為什么我們會使用gcroot<>模板類。
需要注意怎么使用指針指向托管代碼時需要加上^字符;這意味我們使用一個引用指針指向托管類。切記,類對象在.NET中被視為引用,當被用作函數成員時。
還需要注意一個在.NET中自動內存分配的關鍵詞:gcnew. 這意味我們在一個垃圾收集器保護環境中分配空間,而不是在進程堆里。
有時候需要小心的是:進程堆和垃圾收集器保護環境完全不一樣。我們將會看到一些封裝任務還得做: 在類的析構函數:

CPerson::~CPerson()
{
   if (this->m_pPersonClr) { // Get the CLR handle wrapper gcroot<Person^> *pp = static_cast<gcroot<Person^>*>(this->m_pPersonClr); // Delete the wrapper; this will release the underlying CLR instance delete pp; // Set to null this->m_pPersonClr = 0; } }

 

我們使用標准的c++類型轉化static_case. 刪除對象會釋放潛在封裝的CLR對象,允許它進入垃圾回收機制。
提醒: 申明一個析構函數的原因是實現了IDisposeable 接口 和自己的Dispose()方法。
關鍵: 不要忘了調用Dispose()在CPerson實例中。否則,會導致內存泄露,正如在C++中不能釋放(析構函數沒有被調用)。
調用基本的CLR類成員十分容易,和上文類似。

unsigned int CPerson::get_Age() const { if (this->m_pPersonClr != 0) { // Get the CLR handle wrapper gcroot<Person^> *pp = static_cast<gcroot<Person^>*>(this->m_pPersonClr); // Get the attribute return ((Person^)*pp)->Age; } return 0; }

 

但是,當我們必須要返回一個復雜類型時就麻煩一點,正如下面類成員:

tstring CPerson::get_BirthDateStr() const { tstring strAge; if (this->m_pPersonClr != 0) { // Get the CLR handle wrapper gcroot<Person^> *pp = static_cast<gcroot<Person^>*>(this->m_pPersonClr); // Convert to std::string // Note: // - Marshaling is mandatory // - Do not forget to get the string pointer... strAge = (const TCHAR*)Marshal::StringToHGlobalAuto( ((Person^)*pp)->BirthDateStr ).ToPointer(); } return strAge; }

 

我們不能直接返回一個System::String 對象給非托管的string。 必須使用一下幾步:
1. 得到 System::String 對象.
2. 使用 Marshal::StringToHGlobalAuto() 得到一個全局的句柄。我們在這里使用”auto”版本返回的是Unicode編碼的string. 然后盡可能的轉化為ANSI編碼的string;
3. 最后,得到一個指針指向潛在包含對象的句柄。
以上3步就實現了替換!
閱讀推薦的書關於C++/CLI, 你會看到其他的一些特別的關鍵詞,如pin_ptr<> 和 interna_ptr<>允許你得到指針隱藏的對象, 閱讀文檔可以獲取更多的細節。

大混合

這是個標准的例子展示了如何去創建一個本地的控制台程序使用MFC和CLR!

結論(非托管調用托管)

非托管中調用托管是一件復雜的事,這個例子很基本,普通。在例子中,你可以看到一些很復雜的考慮。希望你可以在今后混合編程中,碰到更多的其他的一些場景,獲取到更多經驗。

托管中調用非托管

這里寫圖片描述
這個例子展示了怎樣在CLR(C#)中調用非托管的C++類庫,通過起中間媒介的”mixed code”DLL,導出一個API來使用非托管代碼。

非托管的C++DLL

DLL導出:
1. A C++ 類
2. A C-風格的函數
3. A C-風格的變量
這一段介紹對象的申明,盡管他們很簡單,以至於沒有必要注釋。
C++ 類

class NATIVEDLL_API CPerson {
public: // Initialization CPerson(LPCTSTR pszName, SYSTEMTIME birthDate); // Accessors unsigned int get_Age(); private: TCHAR m_sName[64]; SYSTEMTIME m_birthDate; CPerson(); };

 

get_Age()函數簡單得計算從出生到現在的一個時間段。
導出 C 函數

int fnNativeDLL(void);

 

導出C變量

int nNativeDLL;

 

.NET 端

這里不詳細的介紹這個經典的案例。

筆記1:
.NET類不能直接從非托管的C++類中繼承。寫一個托管C++的類嵌入到c++實體對象內部。

筆記2:
申明一個成員CPerson_person2; 會導致生成C4368編譯錯誤(不能定義’member’ 作為一個托管類型的成員: 不支持混合類型)
這就是為什么在內部使用(在C#被視為’unsafe’)
技術文檔上是這么說的:
你不能直接嵌入一個非托管的數據成員到CLR中。但是,你可以申明一個本地化類型的指針,在構造函數,析構函數, 釋放托管的類里控制它的生命周期(看在Visual c++ 里有關於析構函數和終結器更多的信息)。
這就是嵌入的對象:

CPerson* _pPerson;

 

而不是:

CPerson person;

 

構造器中特殊的信息
公共的構造器有一個System::String string(托管類型)和一個SYSTEMTIME 結構體(Win32 API 類型,但是只是數值:很明顯是個數據集)
這個非托管的c++ CPerson 構造函數使用了LPCTSTR string 類型的指針, 這個托管的string不能直接轉化非托管的對象。
這是構造器的源代碼:

SYSTEMTIME st = { (WORD)birthDate.Year,
                  (WORD)birthDate.Month,
                  (WORD)birthDate.DayOfWeek,
                  (WORD)birthDate.Day,
                  (WORD)birthDate.Hour,
                  (WORD)birthDate.Minute,
                  (WORD)birthDate.Second,
                  (WORD)birthDate.Millisecond };

// Pin 'name' memory before calling unmanaged code pin_ptr<const TCHAR> psz = PtrToStringChars(name); // Allocate the unmanaged object _pPerson = new CPerson(psz, st);

 

注意這里使用pin_ptr關鍵詞來保護string可以在CRL中使用。
這個是一可以保護對象指向個內部的指針。當傳遞一個托管類的地址給一個非托管的的函數是很有必要的,因為地址不是在非托管代碼調用時異常的改變。

總結(托管中調用非托管)

如果我們覺得在托管中導入一個非托管的比非托管中導入一個托管更為常見,寫一個”intermediate assembly”是相當不容易的。
你應該確定是不是需要全部移植代碼,那樣是不合理的。考慮重新設計這個應用。重寫托管代碼可能比移植更划算。而且,最終的應用架構也是很清晰明了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM