託管與非託管的混合程式設計

clelandgt發表於2015-12-12

託管與非託管混合程式設計

翻譯原文來自: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”是相當不容易的。
你應該確定是不是需要全部移植程式碼,那樣是不合理的。考慮重新設計這個應用。重寫託管程式碼可能比移植更划算。而且,最終的應用架構也是很清晰明瞭。

相關文章