天天看點

C#與Native C++互相通路

    用C#做開發已經好幾年了,一直用得挺順手的,最近有一個項目需要用到DirectShow的相關COM元件,也就是想在C#的項目裡面實作一個基于記憶體流的Filter,這個卻讓我着實頭痛了好久。

      原因就是在C#裡面沒有現成的DirectShow的COM元件的定義,雖說在C#中可以使用一些特定的方式來操作COM元件(C#中COM操作(一)---執行個體化), 可是對于DirectShow這樣需要聲明一大批的COM接口,類型,枚舉卻是一件漫長又枯燥的事件,而且搞不好一個不小心中間出個錯誤,導緻最後調試總 不通過而抓狂。即便是現在有了DirectShow.NET提供的對絕大部分的DirectShow的C#翻譯,可是面對網上那麼多的C++開源代碼還有 少得可憐的C#操作DirectShow的代碼,你會怎麼辦?難道又是一行一行的翻譯成C#嗎,反正我是不會這樣幹的,于是這種方式首先被斃掉了。

     俗話說最優秀的程式員也是最懶的程式員,類和方法能公用的就盡量公用,已存在的實作就不需要自己再去寫一次了。既然有現成的C++的實作好的代碼為什麼不 可以再次拿來用一用呢。可是在C#裡面想要使用C++的實作(注意這裡我用的是實作,并沒有指定是類或方法),比較常見的是通過 DllImportAttribute特性導入相應的程式集,可是這種方式隻可以調用方法,而且是限定死了的隻能是C語言形式的導出方法。對于其它的方法 是不可以的,以前用的最多的就是對Windows API的調用了。這樣的使用形式極其不友善,C++和C#本都是都面向對象的語言,卻要通過面向過程式的方法調用來互相通信,感覺很别扭也很不爽,對于追 求完美的程式員來說,這也不是一個上上的選擇,于是這種方式也不是最好的,暫時作為候選吧,再找找看還有沒有其它的方式。

      後來我想到了以前看過一篇文章說的是在C++裡面通過托管C++作為媒價調用C#裡的代碼。把這個反過來就是用C#通過托管C++調用NativeC++ 這樣是否也是可行的呢?不過這都是好幾年前看到的了,隻怪那時候對于這種跨平台調用的方式研究不深,也不怎麼感興趣是以當時就沒有太在意,對于文中所說的 托管C++的印象也一直停留在中看不中用的程式上。如今也隻得找來看看,雖說信心不大,也報着死馬當活馬醫一回的心情試試先。

      其實前面已經提到過了,如果僅僅是對C++的導出方法的調用的話基本不是難事,可是C++是面向對象的語言,很多情況下都是以類的形式存在的,導出的也是 C++的類,就拿我找到的C++寫的記憶體流的Filter來說吧,網上的代碼都是基于類的,并且我想在C#裡面控制FilterGraph的建構,解決怎 麼在C#裡面直接或間接使用到這個類才是問題的本質和關鍵所在。網上找了幾份資料,也查了MSDN,裡面都說可以使用托管C++作Wrapper包裝 NativeC++然後編譯成CLI可識别的托管程式集就可以供C#調用了。

  1. class MemFilter : public IBaseFilter
  2. {
  3.    public:
  4.    private:
  5. }
  6. public ref class MemFilterWrapper : IBaseFilter
  7. {
  8.    private:
  9.       MemFilter* m_pFilter;
  10.    public:
  11. // 此處隻隻需要把MemFilter類的公有方法用托管方工聲明一下,在方法的實作中直接調用m_pFilter中相應的成員方法就可以了
  12. ..
  13. }

      把這兩個類都編譯到同一個CLI/C++項目中去,這樣在C#裡面就可以直接使用MemFilterWrapper這個類間接操作C++類了,與其它普通 的CLS類沒什麼差別。不過需要注意的是對于C++裡面的一些基元類型,比如int,char*,bool等,包括數組需要用CLS裡面相應的類型去替 換,然後再對外公布成方法,也就是說把所有的與C++特性相關的都封裝起來,對外隻顯示符合公共語言規範的格式,不然的話在使用的時候比較麻煩(有可能需 要使用C#的不安全代碼)。

     就像之前提到的那樣,我們也可以通過使用托管C++讓C#的類披上一層C++的皮,再聲明成導出類,這樣就可以供普通C++調用了(前是你必須先安裝Framework)。

  1. public class CSharpClass // C# 類
  2. {
  3. public void SayHello(){ Console.WriteLine("Hello"); }
  4. }
  5. class NativeCPP // C++原生類代理,注意這裡的參數傳回值什麼的都不能帶有托管類型的痕迹
  6. {
  7. public:
  8.  void* m_pCSharpClass;
  9.  NativeCPP(void);
  10.  SayHello(void);
  11. }
  12. NativeCPP::NativeCPP(void)
  13. {
  14.   CSharpClass^ obj = new CSharpClass();
  15.   GCHandle^ handle = GCHandle.Alloc(obj)
  16.   m_pCSharpClass = GCHandle::ToIntPtr(handle).ToPointer();
  17. }
  18. NativeCPP::SayHello(void)
  19. {
  20.   GCHandle handle = GCHandle::FromIntPtr(m_pCSharpClass);
  21.   CSharpClass^ obj = dynamic_cast<CSharpClass^>(handle.Target);
  22.   obj.SayHello();
  23. }

      不過這個時候有個問題:普通的C++類用new方式構造的執行個體可以用C++指針持有,在C++代碼中沒有調用delete的時候會一直存在于目前程序的地 址空間中。這樣在後面的使用中不會出現記憶體通路方面的問題,可是從C#到C++的過程就不是這的了。在C#中對象是由CLI自己管理的,當CLI發現一個 對象不有直接或間接的引用存在的時候(也就是所謂的無根對象)就會被回收,這裡的引用是指的CLI中的引用。另外C#的對象如果以引用的形式存在的話,那 麼在原始的C++裡面中兩個類之間傳送這個對象将會變得不可能。因為除了托管C++外,其它的代碼根本不能識别C#引用,隻認識指針,是以我們必須把C# 中的引用轉成指針。可是如果通過簡單的方式一旦轉成指針後C#中的對象很可能會在沒有引用的時候被GC回收了,造成C++代碼再去通路的時候出記憶體導常。 MS提供了一個名叫GCHandle的結構來解決這樣的問題。通過這個結構可以構造一塊記憶體區域代表這個引用對象,GC發現有這麼一塊記憶體區域存在的時候 不管什麼情況下都不會對該對象進行回收,用用類似的方法也可以從給定的記憶體指針上傳回所指代的托管對象的引用。而這塊記憶體本身不受托管代碼控制,反回的也 隻是一個指針,是以在整個C++的代碼生命周期裡都不用再擔心會引用到無效的C#對象了。

      這樣的處理通路方式,保留了C#代碼與C++代碼各自的書寫特别,真正做到了互不幹撓,假如我再從C#換到VB.NET也是很easy的事情,這正是我想 要的結果。到這裡才發現原來C#與C++互相通路是如些的簡單。其實作實就是這樣,你知道的就很簡單,不知道的就會覺得很難。