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#的一大優點在於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#中:ui

  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); } } 

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

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

      unsafe
      {
      //非託管代碼 } 

      在非託管代碼中,便可進行指針相關的操做。
      若傳遞的是函數指針,因爲C#中沒有函數指針的概念,所以採用委託(delegate)的方式。
      若傳遞的是自定義結構體,也能夠採用ref的方式傳遞。
      這個若是有機會的話,我會單獨整理一下。翻譯

    3. extern 「C」、CallingConvention =CallingConvention.Cdecl)等必要聲明。
      這裏面也牽涉到複雜的語言機制,本文再也不贅述。指針

1.1.1 摘要

咱們知道計算機不能直接理解高級語言,它只能理解機器語言,因此咱們必需要把高級語言翻譯成機器語言,這樣計算機才能執行高級語言編寫的程序,在接下來的博文中,咱們將介紹非託管和託管語音的編譯過程。

1.1.2正文

非託管環境的編譯過程(C/C++)

純C/C++的程序一般運行在一個非託管環境中,類是由頭文件(.h)和實現文件(.cpp)組成,每一個類造成了一個單獨的編譯單元,當咱們編譯程序時,幾個基本組件會把咱們的源代碼翻譯成二進制代碼,接下來咱們經過如下圖片說明非託管環境的編譯過程:

GCC_CompilationProcess

圖1 C/C++編譯過程

首先是預處理器,若是在項目中有頭文件和宏表達式,那麼它將負責包含頭文件和翻譯全部的宏觀表達式。

接下來是編譯器,它不是直接生成二進制代碼,而是生成彙編代碼(.s),這基本上是全部現代的非結構化語言的共同基礎。

而後,彙編程序把彙編代碼翻譯成目標代碼(.o和.obj文件,機器指令)。

最後連接器,它把全部彼此相關的目標文件和生成的可執行文件或庫連接起來。

總而言之,在通常狀況下,咱們的代碼首先翻譯成彙編代碼,接着翻譯成機器指令(二進制代碼)。

什麼是宏?

在C/C++中,宏是預處理指令,它有多種應用技術:包括預約義、建立關鍵字和條件編譯等等。在通常狀況下,這些技術在C++中使用被認爲是很差的作法,主要緣由是有可能濫用C++提供的語法變化功能,甚至有可能在不知情狀況下建立了非標準的語言,宏不遵循通常的源代碼編譯規則,因爲它經過預處理來處理,而不是編譯器。

託管環境的編譯過程(C#/Java)

在託管環境中,編譯的過程略有不一樣,咱們熟知的託管語言有C#和Java,接下來,咱們將以C#和Java爲例介紹在託管環境中的編譯過程。

當咱們在喜好的IDE中編寫代碼時,第一個檢測咱們代碼的就是IDE(詞法分析),而後,編譯成目標文件和連接到動態/靜態庫或可執行文件進行再次檢查(語法分析),最後一次檢查是運行時檢查。託管環境的共同特色是:編譯器不直接編譯成機器碼,而是中間代碼,在.NET中稱爲MSIL - Microsoft Intermediate Language,Java是字節碼(Bytecode)

在那以後,在運行時JIT(Just In Time)編譯器將MSIL翻譯成機器碼,這意味着咱們的代碼在真正使用的時候才被解析,這容許在CLR(公共語言運行時)預編譯和優化咱們的代碼,實現程序性能的提升,但增長了程序的啓動時間,咱們也可使用Ngen(Native Image Generator)預編譯咱們的程序,從而縮短程序的啓動時間,但沒有運行時優化的優勢。(JeffWong的補充Java是先經過編譯器編譯成Bytecode,而後在運行時經過解釋器將Bytecode解釋成機器碼;C#是先經過編譯器將C#代碼編譯成IL,而後經過CLR將IL編譯成機器代碼。因此嚴格來講Java是一種先編譯後解釋的語言,而C#是一門純編譯語言,且須要編譯兩次。)

 Dot_Net_Application_Compilation-707676

圖2 C#的編譯過程

.Net Framework就是在Win32 core上添加了一個抽象層,它提供的一個好處就是支持多語言、JIT優化、自動內存管理和改進安全性;另一個完整解決方案是WinRT,但這涉及到另一個主題了,這裏不做詳細介紹。

MicrosoftBoxologyDiagram

圖3 Windows API

JIT編譯的優勢和缺點

JIT編譯帶來了許多好處,最大的一個在我看來是性能的優點,它容許CLR(通用語言運行時扮演Assembler組件)只執行須要的代碼,例如:假設咱們有一個很是大的WPF應用程序,它不是當即加載整個程序,而是CLR開始執行時,咱們代碼的不一樣部分將經過一個高效的方法翻譯成本地指令,由於它可以檢查系統JIT和生成優化的代碼,而不是按照一個預約義的模式。不幸的是,有一個缺點就是啓動的過程比較慢,這意味着它不適用於加載時間長的包。

JIT的替代方案使用NGen

若是Visual Studio由JIT建立,那麼它的啓動咱們將須要等待幾分鐘,相反,若是它是使用Ngen(Native Image Generator)編譯,它將建立純二進制可執行文件,若是隻考慮速度的問題,那是絕對是正確的選擇。

1.1.3總結

在非託管環境中,咱們須要知道編譯的過程分紅編譯和鏈接兩個階段,編譯階段將源程序(*.c,*.cpp或*.h)轉換成爲目標代碼(*.o或*.obj文件),至於具體過程就是上面說的C/C++編譯過程的前三個階段;連接階段是把前面轉成成的目標代碼(obj文件)與咱們程序裏面調用的庫函數對應的代碼連接起來造成對應的可執行文件(exe文件)。

託管環境中,編譯過程能夠分爲:詞法分析、語法分析、中間代碼生成、代碼優化和目標代碼生成等等過程;不管是.NET仍是Java,它們都會生成中間代碼(MSIL或Bytecode),而後把優化後的中間代碼翻譯成目標代碼,最後在程序運行時,JIT將IL翻譯成機器碼。

不管是託管或非託管語言,它們的編譯編譯過程是把高級語言翻譯成計算機能理解的機器碼,因爲編譯過程涉及的知識面很廣(編譯的原理和硬件知識),並且本人的能力有限,也只能簡單的描述一下這些過程,若是你們但願深刻了解編譯的原理,我推薦你們看一下《編譯原理》。

相關文章
相關標籤/搜索