託管與非託管混合編程
翻譯原文來自:http://www.codeproject.com/Articles/35041/Mixing-NET-and-native-code
源代碼
最直接的實現託管與非託管編程的方法就是使用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”是相當不容易的。
你應該確定是不是需要全部移植代碼,那樣是不合理的。考慮重新設計這個應用。重寫託管代碼可能比移植更划算。而且,最終的應用架構也是很清晰明瞭。