本文是 VMBC / D# 項目 的 系列文章,html
有關 VMBC / D# , 見 《我發起並創立了一個 VMBC 的 子項目 D#》(如下簡稱 《D#》) http://www.javashuo.com/article/p-zziqptgy-s.html 。小程序
ILBC 運行時 架構圖 以下:數組
爲了便於講解, 圖中 一些位置 標註了 紅色數字 。架構
ILBC 運行時 包含 3 個 部分: 調度程序 、 InnerC(Byte Code to Native Code) 、 GC 。函數
1 處, 調度程序 調用 入口程序集 的 ILBC_Main() 函數, 開始執行程序 。性能
若是 入口程序集 是 ILBC 程序集, 就會 調用 InnerC(Byte Code to Native Code) 編譯 ILBC 程序集 爲 本地程序集(2 處) 。優化
ILBC 程序集 就是 ILBC Byte Code 程序集, 本地程序集 就是 本地代碼 程序集 。spa
若是 入口程序集 是 ILBC 程序集, 就直接調用 ILBC_Main() 函數, 開始執行程序 。操作系統
3 處 表示 A 程序集 引用了 B 程序集, 在 調度程序 加載 A 程序集 的 時候, 會調用 A 本地程序集 的 ILBC_GetAssembly() 函數,線程
ILBC_GetAssembly() 函數 以前沒有提到, 如今補充上來 。
ILBC_GetAssembly() 函數 會返回 A 程序集 引用 的 程序集 列表, 包含了 這些 程序集 的 名字 。
程序集 列表 是一個 數組, 數組元素 是 一個 字符數組 的 首地址, 這個 字符數組 就是 程序集 的 名字 。
調度程序 會 根據 程序集列表 去 加載 列表 裏的 程序集,
假設 A 程序集 引用了 B 程序集, 則 程序集 列表 裏有 B, 調度程序 會先把 B 加載到內存, 若是 B 是 本地代碼程序集, 則 直接加載到內存, 若是 B 是 ILBC 程序集, 則 先 JIT 編譯 爲 本地代碼程序集, 再加載到內存 。
4 處 表示 ILBC 程序集 JIT 編譯 爲 本地程序集 後 投入使用 。
把 B 加載到 內存後, 調用 B 的 ILBC_GetMethodList() 函數, 返回 B 的 函數表 首地址, 另外一方面, 調度程序 會 調用 A 的 ILBC_GetMethodListList() 函數, 返回 「函數表 列表」 的 首地址, 「函數表 列表」 是 一個數組, 數組元素 是 函數表 首地址, 因此是 「函數表 的 列表」 。
這樣, 把 B 的 函數表 首地址 存到 函數表 列表 中 B 的 位置, 加載 A 和 「依賴項」 B 的 過程 就完成了 。
若是 A 還引用了 其它 程序集, 或者 B 引用了 其它 程序集, 也是 按照 這個 過程 依次加載 。
上面這個 過程 說的有點囉嗦, 沒事, 咱們先來看一下 InnerC 的架構, 等下再把這個流程 總結一遍 。
InnerC 的 架構以下:
InnerC 分爲 2 個 模塊 :
1 InnerC to Byte Code
2 Byte Code to Native Code
InnerC to Byte Code 的 職責 是 語法分析 和 類型檢查, 語法分析 包含了 語法檢查 。
經過 語法分析, 把 C 代碼 解析 爲 表達式對象樹, 而後 對 表達式對象樹 進行 類型檢查,
類型檢查 經過後, 就能夠 返回 表達式對象樹 了,
表達式對象樹 能夠直接 傳給 Byte Code to Native Code,
Byte Code to Native Code 負責 將 表達式 生成爲 目標代碼 和 連接(連接外部庫), 最終 生成 本地庫,
這就是 AOT 編譯 。
表達式對象樹 也能夠 序列化, 序列化 獲得的 byte 數組(byte [ ]) 就是 Byte Code, Byte Code 保存爲 文件 就是 ILBC 程序集 。
ILBC 程序集 能夠 讀取爲 byte 數組(byte [ ]), byte 數組 反序列化 就是 表達式對象樹, 表達式對象樹 傳給 Byte Code to Native Code 編譯爲 本地庫,
這就是 JIT 編譯 。
C 代碼 是 第一級 中間代碼, Byte Code 是 第二級 中間代碼 。
這就是 InnerC 的 架構, 以及 AOT 編譯 和 JIT 編譯 的 原理 。
咱們能夠把 C 中間代碼 文件 的 擴展名 定義爲 .ilc , 意思是 「ILBC C Code」,
把 ILBC 程序集 (Byte Code 文件) 的 擴展名 定義爲 .ilb, 意思是 「ILBC Byte Code」 。
本地代碼 程序集 的 擴展名 遵循 操做系統 的 規定, 好比 Windows 上 就是 動態連接庫 .dll, 由於 本地程序集 就是 操做系統 定義的 動態連接庫 。
咱們 接下來 把 ILBC 運行時 加載 程序集 和 運行 應用程序 的 流程 總結一下 :
1 調度程序 加載 入口程序集, 若是 入口程序集 是 本地程序集, 就 直接加載到內存,
若是 入口程序集 是 ILBC 程序集, 則 先 JIT 編譯, 把 入口程序集 編譯爲 本地程序集 再加載到內存 。
2 調度程序 調用 入口程序集 的 ILBC_GetAssemblyList() 函數 , ILBC_GetAssemblyList() 函數 返回 AssemblyList 首地址 。
AssemblyList 是一個 數組, 數組元素 是一個 char 數組(char [ ]) 的 首地址, 表示 Assembly 的 名字 (文件名, 不包含擴展名) 。
3 調度程序 用 Assembly 名字 查找 當前目錄下 的 程序集, 先查找 本地程序集, 好比 「程序集名字.dll」, 若是找到, 直接加載到內存,
若是找不到 本地程序集, 就找 ILBC 程序集, 好比 「程序集名字.ilb」, 若是找到, 先 JIT 編譯 爲 本地程序集, 再把 本地程序集 加載到內存 。
若是 ILBC 程序集 也沒有找到, 就 報錯 「找不到 某某 程序集 。」 。
怎麼把 本地程序集 加載到內存 ? 這 遵循 操做系統 提供的 方式, 好比 Windows 把 .dll 庫 加載到 應用程序 裏的 方式 。
總的來講, 加載程序集 的 流程 如上, 從 入口程序集 開始依次加載, 加載完成後, 調用 入口程序集 的 ILBC_Main() 開始 執行程序 。
另外, ILBC_GetMethodListList() 函數 應該是 ILBC_InitializeMethodListList() , 具體 邏輯 不長, 但講起來煩瑣, 以後看 Demo 代碼就清楚了 。
能夠看到, ILBC 運行時 加載 程序集 會 將 全部 引用到的 程序集 所有加載 完成, 纔會開始 執行程序 。
這是 和 .Net / C# 不一樣的 , .Net / C# 應該是 用到 這個 程序集 的時候 纔會 加載, 用到這個 程序集 是指 第一次 調用到 這個 程序集 裏的 類 的時候 。
實際上, .Net / C# 的 動態加載 的 粒度 可能 更細, 多是 Class 這一級別 的,
咱們在 調試 .Net / C# 程序 的 時候 能夠 觀察到, 只有 第一次 用到 某個 Class 的 時候, 這個 Class 的 靜態構造函數 纔會被 調用 。
從這一點上來看, .Net / C# 的 動態性 比 ILBC 更強, 更加動態 。
進一步, ILBC 加載 的 單位 是 整個 程序集, 而不是 類(Class), 若是是 本地程序集, 則將 整個 本地程序集 加載到內存,
若是 是 ILBC 程序集, 則 對 整個 ILBC 程序集 進行 JIT 編譯, 編譯爲 本地程序集 後, 再把 整個 本地程序集 加載到內存 。
也所以, D# / ILBC 不提供 類 的 靜態構造函數, 而是 提供一個 ILBC_AssemblyLoad() 函數, ILBC 運行時 會在 加載 程序集 完成時 調用 ILBC_AssemblyLoad() 函數, 整個程序集 全部 類 的 初始化 工做 能夠在 ILBC_AssemblyLoad() 裏 來 完成 。
.Net / C# 的 動態性 須要 更加 複雜 的 設計 和 實現, 這不是 ILBC 的 定位 。
咱們能夠 探討 一下, 若是要實現 .Net / C# 的 動態性, 好比 第一次 new 類的對象 或者 第一次調用類的靜態方法 時, 加載類(若是 Assembly 未加載 則 先加載 Assembly 再加載 Class) 並 調用 類的靜態構造函數 這個 動態加載 怎麼實現:
咱們能夠寫一段 僞碼:
簡單起見, 咱們假設 Assembly 已經加載了, 只要 判斷 類 是否已加載, 若未加載 則 加載 類 。
編譯器 會 把 new 類 的 對象, 以及 調用 類的 靜態方法 的 代碼 處理成 一段 臨時代碼, 咱們稱之爲 「連接代碼」,
假設 該 類 是 A Class,
僞碼以下:
bool ifAClassLoad = false;
if ( ! ifAClassLoad )
{
lock ( ifAClassLoad )
{
if ( ! ifAClassLoad )
{
加載 A Class ;
調用 類 的 靜態構造函數 ;
ifAClassLoad = true ;
}
}
}
new () 或者 A.靜態方法() ;
按照這個 代碼 的 邏輯, 第一次 new A() 或者 調用 A.靜態方法() 時, 會 判斷 A Class 是否已加載, 若是未加載, 會有一個 線程 通知 CLR 加載 A Class, 其它 線程 等待(若是 有 其它線程 也在 new A() 或者 調用 A.靜態方法() 的話), CLR 加載完成後, 就執行 真正的 new A() 或者 A.靜態方法() ,
以後, 再 new A() 或者 調用 A.靜態方法() 的時候, 在 連接代碼 的 第一句,
if ( ! ifAClassLoad )
就能夠 判斷 出來 A Class 已經加載, 因而就直接執行 new A() 或者 A.靜態方法() 。
但 這樣的 作法, 每次 new A() 或者 A.靜態方法() 都要有 一個 判斷, 雖然 只是一個 判斷, 但從 微觀 上來講, 也形成了 性能消耗 。
這樣的 性能消耗, 應該是 「應該被優化掉的」 。
若是 .Net / C# 已經 把 這個 判斷 優化掉了, 那麼 應該用到了 「修改已經編譯好的本地代碼」 的 操做, 形象的講, 就是給 「已經編譯好的本地代碼」 作了個 「微創手術」 。
具體就是 在 第一次 加載 成功後, .Net CLR 會 把 這段 「連接代碼」 替換掉, 替換爲 new A() 和 A.靜態函數() 的 代碼,
在 新的 new A() 和 A.靜態函數() 代碼中, A() 構造函數 和 A.靜態方法() 已經替換爲 A Class 加載後的 實際的 函數地址 。
這樣, 替換後的 代碼 和 訪問 同一個 程序集 中的 類 的 代碼 是 同樣的 。
性能 也和 訪問 同一個 程序集 中的 類 同樣 。
順便加一句, 原本 連接代碼 中 new A() 和 A.靜態函數() 的 部分 還有一個 相似 調用 虛函數 的 查函數表 的 操做, 也被這個 替換 優化掉了 。
這個 技術 很底層, ILBC 不打算 涉及 這個 技術,
ILBC 仍然 把 C 語言 和 C 編譯器(InnerC) 看做一個總體, 不會 介入 C 編譯器 的 工做細節 。
不過, 從上面的討論也能夠知道, 若是 ILBC 想實現 和 .Net / C# 同樣的 「動態特性」, 好比 用到 A Class 的時候 才 加載 A Class, 若是 A Class 所在的 程序集 未加載 則 先加載程序集 再 加載 A Class,
若是要作到 這樣 的 動態特性 的話, 簡單點 也能夠用 上面的 「連接代碼」 的 作法, 只是每次調用 new A() 構造函數 和 A.靜態方法() 都要多一個
if ( ! ifAClassLoad )
的 判斷 了 。
還有 就是 查函數表 的 操做 也是要有的 。
固然, 即便不實現這個 「動態特性」, 查函數表 的 操做 也是有的 。
ILBC 的 動態連接 就 至關於 調用 虛函數 。
不過 即便用了 上面 「連接代碼」 的 方式, 也只能 「用到某個 程序集 的 時候 才加載 程序集」, 還不能達到 Class 的 粒度,
由於 上文 也說了, ILBC 是 把 整個 ILBC 程序集 編譯成 本地程序集 的,
這是由於 ILBC 程序集 是 C 語言 寫的, C 語言 只能 整個項目(程序集) 一塊兒編譯, 不能把 裏面的 .c 文件 一個一個 拿出來編譯 。
就算能把 若干 .c 文件 任意 的 拿出來 編譯, 根據 ILBC 規範, 這些 單獨 拿出來的 .c 文件 編譯成的 程序集 裏 必需要 提供 ILBC_GetAssemblyList(), ILBC_InitializeMethodList(), ILBC_Link() 函數, 這就亂套了 。 由於 本來的程序集 已經 爲 本來的整個項目 生成了 一份 這些 函數 。
假設 A 引用 B, A 裏 編譯好的 邏輯 是 引用 B, 如今 把 B 拆成了 若干個 小程序集, 你讓 A 怎麼引用 ?