C#的三大難點之二:托管與非托管


相關文章:

C#的三大難點之前傳:什么時候應該使用C#?
C#的三大難點之一:byte與char,string與StringBuilder
C#的三大難點之二:托管與非托管
C#的三大難點之三:消息與事件

托管代碼與非托管代碼

眾所周知,我們正常編程所用的高級語言,是無法被計算機識別的。需要先將高級語言翻譯為機器語言,才能被機器理解和運行。
在標准C/C++中,編譯過程是這樣的:
enter description here
源代碼首先經過預處理器,對頭文件以及宏進行解析,然后經過編譯器,生成匯編代碼,接着,經過匯編,生成機器指令,最后將所有文件連接起來。
這種編譯方式的優點在於,最終直接生成了機器碼,可以直接被計算機識別和運行,無需任何中間運行環境,但缺點也在於,由於不同平台能夠識別的機器碼不同,因此程序的跨平台能力較差。
而在Java語言中,源代碼並沒有被直接翻譯成機器碼,而是編譯成了一種中間代碼(字節碼Bytecode)。因此,運行Java程序需要一個額外的JRE(Java Runtime Enviromental)運行環境,在JRE中存在着JVM(Java Virtual Mechinal,Java虛擬機),在程序運行的時候,會將中間代碼進一步解釋為機器碼,並在機器上運行。
使用中間代碼的好處在於,程序的跨平台性比較好,一次編譯,可以在不同的設備上運行。
托管/非托管是微軟的.net framework中特有的概念,其中,非托管代碼也叫本地(native)代碼。與Java中的機制類似,也是先將源代碼編譯成中間代碼(MSIL,Microsoft Intermediate Language),然后再由.net中的CLR將中間代碼編譯成機器代碼。
而C#與Java的區別在於,Java是先編譯后解釋,C#是兩次編譯。
托管的方式除了擁有跨平台的優點之外,對程序的性能也產生一定的影響。但程序性能不在本文討論的范圍,這里不在贅述。
此外,在.net中,C++也可以進行托管擴展,從而使C++代碼也依賴於.net和CLR運行,獲得托管代碼的優勢。

托管資源與非托管資源

在上一節中,我們講到,托管代碼與非托管代碼相比,有下列不同:

  1. 編譯運行過程不同
  2. 跨平台能力不同
  3. 程序性能不同

本節中,我們會涉及到托管和非托管的另一個區別:

  1. 釋放資源的方式不同

在C/C++中,資源都是需要手動釋放的,比如,你new了一個指針,用過之后就需要delete掉,否則就會造成內存泄露。
而在Java中,不必考慮資源釋放的問題,Java的垃圾回收機制(GC,Garbage Collection)會保證失效的資源被自動釋放。
而C#的機制與Java類似,運行於.net平台上的代碼,分配的資源一般會自動由平台的垃圾回收器釋放,這樣的資源就是托管資源。
但是一些例外的資源,如System.IO.StreamReader等各種流、各種連接所分配的資源,需要顯式調用Close()或Dispose()釋放,這種資源就叫做非托管資源。

托管與非托管的混合編程

C#的三大難點之前傳:什么時候應該使用C#?中我提到過,C#的一大優勢在於Windows平台下的界面編程。但由於C#並不是很普及,經常出現底層或后台代碼采用C/C++編寫的情況,此時,若選擇C#作為界面語言,則必然遇到一個C#調用C++代碼的問題。
比較普遍的解決方案就是,先將C/C++的代碼生成為DLL動態運行庫,再在C#中調用。
舉個例子
在C中:

#include 
#include 

void DisplayHelloFromDLL()
{
    printf ("Hello from DLL !\n");
}

void CallHelloFromDLL(char* cp)
{
    printf (cp);
    printf ("\n");
    *cp='a';
    cp++;
    printf (cp);
    printf ("\n");
}

在C#中:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TestConsole
{
    using System;
    using System.Runtime.InteropServices;     // DLL support

    class Program
    {
        [DllImport(@"TestLib.dll")]
        public static extern void DisplayHelloFromDLL();

        [DllImport(@"TestLib.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern void CallHelloFromDLL(StringBuilder s);

        static void Main()
        {
            Console.WriteLine("This is C# program");
            DisplayHelloFromDLL();
            StringBuilder sb = new StringBuilder(100);
            CallHelloFromDLL(sb);
            Console.WriteLine(sb);
    }
}

在混合編程中,涉及了幾個要點。

  1. 如何在DLL中將函數接口暴露出來?
    有兩種方式,一種是采用__declspec(dllexport)的聲明,另一種是編寫額外的def文件,如
    ;導出DLL函數
    LIBRARY testLib
    EXPORTS 
    DisplayHelloFromDLL
    CallHelloFromDLL
    
  2. DLL與C#之間如何進行數據傳送?
    這個問題其實很復雜,像int,double這種基本的數據類型,是很好傳遞的。到了byte和char,就有點復雜了,更復雜的還有string和stringBuilder,以及結構體的傳遞等。
    若傳遞的是指針,有兩種方法,一種是采用托管的方式,使用Intptr存儲指針,並使用ref獲得地址(&);另一種是在C#中編寫非托管的代碼,用unsafe聲明:

    unsafe
    {
    //非托管代碼
    }
    

    在非托管代碼中,即可進行指針相關的操作。
    若傳遞的是函數指針,由於C#中沒有函數指針的概念,因此采用委托(delegate)的方式。 
    若傳遞的是自定義結構體,也可以采用ref的方式傳遞。
    這個如果有機會的話,我會單獨整理一下。

  3. extern “C”、CallingConvention =CallingConvention.Cdecl)等必要聲明。
    這里面也牽涉到復雜的語言機制,本文不再贅述。

參考文獻:
[譯]C++, Java和C#的編譯過程解析
編譯原理

 


免責聲明!

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



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