你們好,javascript
我發起並創立了一個 VMBC 的 子項目 D# 。html
有關 VMBC , 請參考 《我發起了一個 用 C 語言 做爲 中間語言 的 編譯器 項目 VMBC》 http://www.javashuo.com/article/p-djxdwein-hp.html ,java
和 《漫談 編譯原理》 http://www.javashuo.com/article/p-fhvkvvhp-hk.html 。c++
D# , 就是一個 簡單版 的 C# 。web
下面說一下 D# 項目 的 大概規劃 :算法
第 1 期, 實現 new 對象 的 機制, GC, 堆 。 (我作)數組
第 2 期, 實現 對象 的 函數(方法) 調用 。 (後人作)緩存
第 3 期, 實現 元數據, 簡單的 IL 層 基礎架構 。 (後人作)安全
第 4 期, 實現 簡單類型, 如 int, long, float, double 等 。 (後人作)架構
第 5 期, 實現 簡單的 表達式 和 語句, 如 變量聲明, 加減乘除, if else, for 循環 等 。 (後人作)
第 6 期, 實現 D# 代碼 翻譯爲 C 語言 中間代碼 。 (後人作)
第 7 期, 實現 將 C 語言 代碼 編譯 爲 本地代碼 。 (後人作)
第 8 期, 各類 高級 語法特性 逐漸 加入 。 (後人作)
第 9 期, 各類 完善發展 …… (後人作)
咱們來 具體 看一下 每一期 怎麼作 :
第 1 期, 對象 的 new 機制, 就是用 malloc() 在 內存 裏 申請一段 內存, 內存的 大小(Size) 是 對象 裏 全部字段 的 Size 宗和, 能夠用 C 語言的 sizeof() 根據 字段類型 取得 字段佔用的 內存長度, 加起來 就是 對象 佔用的 內存長度 。
GC, D# 的 GC 和 C# 有一點不一樣, C# 的 GC 會 作 2 件事 :
1 回收 對象 佔用的 內存
2 整理 堆 裏的 碎片空間
D# 只有 第 1 點, 沒有 第 2 點 。 就是說 D# 只 回收 對象佔用的 內存, 但不進行 碎片整理 。
C# GC 進行 碎片整理 須要 移動對象, 而後 修改 指向 這個對象 的 引用, 引用 是一個 結構體, 裏面 包含了 一個指針, 指向 對象 的 地址, 對象 被移動後, 地址 發生了 改變, 因此 引用 裏的 這個指針 也須要 修改 。
其實 不作 碎片管理 的 主要緣由 是 碎片整理 的 工做 很複雜, 我懶得寫了 。 ^^
碎片 整理 主要是 解決 碎片 佔用了 地址空間 和 內存空間 的 問題, 以及 碎片 增多時 堆 分配 效率變低 的 問題 。
固然還有 碎片 佔用了 操做系統 虛擬內存 頁 的 問題 。
首先, 關於 碎片佔用 地址空間 的問題, 如今 是 64 位 操做系統, 地址空間 能夠達到 16 EB, 不用擔憂 地址空間 用完 。
內存空間 的 問題, 如今 固態硬盤 已經普及, 內存 也 愈來愈大, 固態硬盤 能夠 讓 操做系統 虛擬內存 很快, 再加上 內存 也 愈來愈大, 因此 也不用擔憂 內存空間 不夠 的 問題 。
碎片 增多時 堆分配 效率變低 的 問題, 咱們打算本身實現一個 堆算法, 下面會 介紹 。
碎片 佔用了 操做系統 虛擬內存 頁 的 問題 是指 碎片 佔用了 較多 的 頁, 致使 操做系統 虛擬內存 可能 頻繁 的 載入載出 頁, 這樣 效率 會下降 。
這個問題 其實 和 碎片 佔用 內存空間 的 問題同樣, 固態硬盤 能夠 讓 操做系統 虛擬內存 很快, 內存 也 愈來愈大, 因此 基本上 也能夠 忽略 。
另外一方面, GC 整理碎片 移動對象 自己 就是一個 工做量 比較大 的 工做, 且 移動對象 時 須要 掛起 全部 線程 。
因此, 碎片整理 也是 有利有弊 的 。
D# GC 去掉了 整理碎片 的 部分, 也能夠說是 「空間換時間」 的作法,
另外, D# GC 工做時 不用 掛起 應用程序 線程, 能夠 和 應用程序 線程 正常的 併發 運行 。
相對於 C#, 實時性 也會 好一些 。
爲何 要 本身實現一個 堆 呢?
由於 C / C++ 的 堆 分配(malloc() , new) 是 有點 「昂貴」 的 操做,
C / C++ 是 「靜態語言」, 沒有 GC 來 整理碎片, 因此 就須要有一個 「精巧」 的 分配算法,
在 申請一塊內存(malloc() , new) 的 時候, 須要 尋找 和 申請的 內存塊 大小(size) 最接近 的 空閒空間,
當內存出現大量碎片,或者幾乎用到 100% 內存時, 分配 的 效率會下降, 就是說 分配操做 可能 會 花費 比較長 的 時間 。
見 《C++:在堆上建立對象,仍是在棧上?》 https://blog.csdn.net/qq_33485434/article/details/81735148 ,
原文是這樣:
「
首先,在堆上建立對象須要追蹤內存的可用區域。這個算法是由操做系統提供,一般不會是常量時間的。當內存出現大量碎片,或者幾乎用到 100% 內存時,這個過程會變得更久。
」
而 對於 java , C# 這樣的語言來講, new 操做 是 常規操做, 時間複雜度 應該 接近 O(1) 。
事實上 java , C# 的 new 操做 時間複雜度 可能就是 O(1), 由於有 GC 在 整理碎片, 因此 new 只須要從 最大的 空閒空間 分配一塊內存 就能夠 。
因此 D# 也須要 設計一種 O(1) 的 堆算法 。
D# 的 堆 算法 也會沿用 「空間換時間」 的 思路, new 直接從 最大的 空閒空間 分配 指定 size 的 內存塊, 由 另一個 線程 定時 或 不定時 對 空閒空間 排序,
好比 如今 在 堆 裏 有 10 個 空閒空間, 這個 線程 會對 這 10 個 空閒空間 排序, 把 最大的 空閒空間 放在 最前面,
這樣 new 只要在 最大的 空閒空間 裏 分配內存塊 就能夠了 。
這樣 new 的 時間複雜度 就是 O(1) 。
這個對 空閒空間 排序 的 線程 能夠是 GC 線程, 或者說, 對 空閒空間 排序 的 工做 能夠放在 GC 線程 裏 。
固然, 這樣對 內存空間 的 利用率 不是最高的, 但上面說了, 空間 相對廉價, 這裏是 「用 空間換時間」 。
這個 堆 算法 還有一個 特色 就是 簡單, 簡單 有什麼用 呢?
做爲一個 IL 層, 雖然 C / C++ 提供了 堆 算法, 可是本身仍是有可能本身實現一個 堆, 至少 要有這個 儲備力量,
上面這個 算法 的好處是, 由於 簡單, 因此 把 研發成本 下降了, 包括 升級維護 的 成本 也下降了 。 哈哈哈 。
我可不但願 後來人 學習 VMBC 的 時候, 看到 一堆 天書 同樣的 代碼,
我不以爲 像 研究 九陰真經 同樣 去 研究 Linux 內核 這樣 的 事 是一個 好事 。 ^^
接下來, 我再論證一下 GC 存在的 合理性, 這樣 第 1 期 的 部分 就結束了 。
過去有 觀點 認爲, GC 影響了 語言 的 實時性(好比 java, C#), 但若是從 另一個角度 來看, 應用程序 運行在 操做系統 上, 也會 切換回 系統進程, 系統進程 負責 進程調度 虛擬內存 IO 等 工做, 總的來講, 是 對 系統資源 的 管理 。
GC 也能夠看做是 應用程序 這個 「小系統」 裏 對 系統資源 管理 的 工做, 因此 GC 是一個 合理 的 併發, GC 是合理的 。
第 2 期, 實現 對象 的 函數(方法) 調用, 這很簡單, 就是 調用 函數, 給 函數 增長一個 參數, 這個 參數 做爲 第一個參數, 這個參數 就是 this 指針, 把 對象 本身的 地址 傳進去 就能夠了 。
第 3 期, 實現 元數據, 簡單的 IL 層 基礎架構 。 簡單的 IL 層 基礎架構 主要 就是 元數據 架構 。
元數據 就是 一堆 結構體, 聲明一堆 靜態變量 來 保存這些 結構體 就能夠了 。 不過 考慮到 元數據 是 能夠 動態加載 的, 這樣 能夠 用 D# 自身的 new 對象 機制 來實現 。 只要 聲明一個 靜態變量 做爲 元數據樹 的 根 就能夠了 。
元數據 實際上 也 包含了 第 2 期 的 內容, 元數據 會 保存 對象 的 方法(函數) 的 指針, 這還涉及到 IL 層 的 動態連接,
就跟 C# 同樣, 好比 用 D# 寫了 1 個 .exe 和 1 個 .dll, 用 .exe 調用 .dll , 涉及到一個 IL 層 的 動態連接 。
C# 或者 .Net 是 徹底 基於 元數據 的 語言 和 IL 平臺, java 應該也是這樣, java 剛出現時, 逐類編譯, 也就是說, 每一個類 編譯 爲 一個 class 文件, class 文件 是 最小單位 的 動態連接庫, 能夠 動態加載 class 文件, 這個 特性, 在 java 剛出現的時代, 是 「很突出」 的 , 也是 區別於 C / C ++ 的 「動態特性」 。
這個 特性 在 今天 看來 可能 已經 習覺得常, 不過在 當時, 這個特性 能夠用來 實現 「組件化」 、「熱插拔」 的 開發, 好比 Jsp 容器, 利用 動態加載 class 文件 的 特性, 能夠實現 動態 增長 jsp 文件, 在 web 目錄下 新增一個 jsp 文件,一個 新網頁 就上線了 。 固然 也能夠 動態 修改 jsp 文件 。
第 4 期, 實現 簡單類型, 如 int, long, float, double 等 。
C 語言 裏原本就有 int, long, float, double, 可是在 C# 裏, 這些 簡單類型 都是 結構體, 結構體 裏 除了 值 之外, 可能還有 類型信息 之類的 。
總之 會有一些 封裝 。
D# 也同樣, 用 結構體 把 C 語言 的 int, long, float, double 包裝一下 就能夠了 。
第 5 期, 實現 簡單的 表達式 和 語句, 如 變量聲明, 加減乘除, if else, for 循環 等 。
這些 也 不難, 上面說了, 值類型 會 包裝成 結構體, 那麼 變量聲明 就是 C 語言 裏 相應 的 結構體 聲明,
好比 int 對應的 結構體 是 IntStruct, 那麼, D# 裏 int i; 對應的 C 語言 代碼 就是 IntStruct i; ,
嚴格的講, 應該是
IntStruct i;
i.val = 0;
應該是 相似 上面這樣的 代碼, 由於 C 語言 裏 IntStruct i; 這樣不會對 i 初始化, i.val 的 值 是 隨機的 。
按照 C# 語法, int i; , i 的 值 是 默認值 0 。
也能夠用 IntStruct i = IntStruct(); 經過 IntStruct 的 構造函數 來 初始化 。
我在 網上 查了 這方面的文章, 能夠看看這篇 《c++的struct的初始化》 https://blog.csdn.net/rush_mj/article/details/79753259 。
加減乘除, if else, for 循環 基本上 能夠直接用 C 語言 的 。
第 6 期, 實現 D# 代碼 翻譯爲 C 語言 中間代碼 。
在 第 6 期 之前, 都尚未涉及 語法分析 的 內容, 都是 在 設計, 用 C 語言 怎樣 來 描述 和 實現 IL 層, 具體 會用 C 語言 寫一些 demo 代碼 。
第 6 期 會經過 語法分析 把 D# 代碼 翻譯爲 C 語言 中間代碼 。
具體的作法是,
經過 語法分析, 把 D# 代碼 轉換爲 表達式樹, 表達式 是 對象, 表達式樹 是 一棵 對象樹,
轉換爲 表達式樹 之後, 咱們就能夠進行 類型檢查 等 檢查, 以及 語法糖 轉換工做,
而後 讓 表達式 生成 目標代碼, 對於 一棵 表達式樹, 就是 遞歸生成 目標代碼,
一份 D# 代碼文件, 能夠 解析爲 一棵 表達式樹, 這棵 表達式樹 遞歸 生成 的 目標代碼 就是 這份 D# 代碼 對應的 C 語言 目標代碼 。
關於 語法分析, 能夠參考 《SelectDataTable》 https://www.cnblogs.com/KSongKing/p/9683831.html 。
第 7 期, 實現 將 C 語言 代碼 編譯 爲 本地代碼 。
這一期 並不須要 咱們本身 去 實現 一個 C 編譯器, 咱們只要和 一個 現有的 C 編譯器 鏈接起來 就能夠了 。
第 8 期, 各類 高級 語法特性 逐漸 加入 。
基本原理 就 上面 那些了, 按照 基本原理 來加入 各類 特性 就能夠 。
不過 別把 太多 C# 的 「高級特性」 加進來,
C# 已經變得 愈來愈複雜, 正好 乘此機會, 複雜的 不須要的 特性 就 不用 加進來了 。
C# 的 「高級特性」 增長了 不少複雜, 也增長了 不少 研發成本 。
恰好 咱們 不要 這些 特性, 咱們的 研發成本 也下降了 。
第 9 期, 各類 完善發展 ……
語法特性, 優化, IDE, 庫(Lib), 向 各個 操做系統 平臺 移植 ……
好了, 說的 有點遠 。
優化 是一個 重點, 好比 生成的 C 語言 中間代碼 的 效率, IL 層 架構 對 效率 的 影響, 等等, 這些是 重要的 評估 。
就像 C / C++ 的 目標 是 執行效率, 我認爲 D# 的 目標 也是 執行效率 。
D# 提供了 對象 和 GC,
對象 提供 了 封裝抽象 的 程序設計 的 語法支持,
GC 提供了 簡潔安全 的 內存機制,
這是 D# 爲 開發者 提供的 編寫 簡潔安全 的 代碼 的 基礎, 是 D# 的 基本目標 。
在此 基礎上, 就是 儘量 的 提高執行效率 。
還能夠看看 《漫談 C++ 虛函數 的 實現原理》 http://www.javashuo.com/article/p-wtpwxdib-du.html 。
上文中提到 IL 層 的 動態連接, 這是個問題, 也是個 課題 。
在 C# 中, IL 層 的 動態連接 是 JIT 編譯器 完成的 。
對於 D#, 能夠這樣來 動態連接, 假設 A.exe 會調用 B.dll, 那麼 在 把 A 的 D# 代碼 編譯成 C 語言 目標代碼 的 時候, 會聲明一個 全局變量 數組, 這個 全局變量 數組 做爲 「動態連接接口表」, 接口表 會保存 A 中調用到 B 的 全部 構造函數 和 方法 的 地址, 可是在 編譯 的時候 還不知道 這些 構造函數 和 方法 的 地址(在 運行時 才知道), 因此 這些 地址 都 預留 爲 空(0), 就是說 這個 接口表 在編譯時 是 爲 運行時 預留的, 具體的 函數地址 要在 運行時 填入 。
在 運行時, JIT 編譯器(內核是個 C 編譯器) 加載 B.dll, 將 B.dll 中的 C 語言 中間代碼 編譯爲 本地代碼, 而後 將 編譯後的 各個函數 的 地址 傳給 A, 填入 A 的 「動態連接接口表」,
A 中調用 B 的 函數的 地方在 編譯 時 會處理爲 到 接口表 中 指定 的 位置 得到 實際要調用的 函數地址, 而後根據這個 函數地址 調用函數 。
這有點像 虛函數 的 調用 。
接口表 中 爲何 要 保存 構造函數 呢? 由於若是要 建立 B 中定義的 類 的 對象, 就須要 調用 構造函數 。
其實 接口表 除了 構造函數, 還要保存 對象 的 大小(Size), 建立對象 的 時候, 先根據 Size 在 堆 裏 分配空間, 再 調用 構造函數 初始化 。
B.dll JIT 編譯 完成時, 須要把 本地代碼 中 各函數 的 地址 傳給 A, 對於 C# 來講, 這些是 JIT 編譯器 統一作的, 沒有 gap,
可是 對於 D# 來講, 若是咱們不想 修改 C 編譯器, 那麼 就有 gap,
這須要 在 B.dll 的 C 語言 中間代碼 裏 加上一個 能夠做爲 本地代碼 動態連接 的 函數(好比 win32 的 動態連接庫 函數), 經過這個函數, 來把 B 的 元數據 傳給 A, 好比 JIT 編譯後 本地代碼 中 各個函數 的 地址,
這樣 A 經過調用 B 的 這個函數, 獲取 元數據, 把 元數據 填入 接口表 。
上面說的 win32 動態連接庫 函數 是 經過 extern "C" 和 dllexport 關鍵字 導出 的 方法, 好比:
extern "C"
{
_declspec(dllexport) void foo();
}
這是 導出了一個 foo() 方法 。
這種方法 就是 純方法, 純 C 方法, 不涉及對象, 更和 Com 什麼的無關, 乾脆利落, 是 方法 中的 極品 。
這種方法 也 再次 體現了 C 語言 是 「高級彙編語言」 的 特色,
你能夠用 C 語言 作 任何事 。
爽, 很是爽 。
IL 層 動態連接 和 本地代碼庫 動態連接 的 區別 是:
IL 層 動態連接 的 2 個 dll 是 用 一樣的語言 寫的(好比 D# 的 dll 是 C 語言 寫的), 又是 同一個 編譯器 編譯成 本地代碼 的, 2 個 dll 編譯後 的 本地代碼 的 寄存器 和 堆棧 模型 相同, 只要知道 函數地址, 就能夠 相互調用 函數 。 其實 就跟 把 A.exe 和 B.dll 裏包含的 C 文件所有放在一塊兒編譯 的 效果 是同樣的 。
本地代碼庫 動態連接 的 話, 2 個 dll 多是用 不一樣的語言 寫的, 也多是 不一樣的編譯器 編譯的, 2 個 dll 的 寄存器 和 堆棧 模型 可能 不相同, 須要 按照 操做系統 定義 的 規範 調用 。
在 上文提到的 《漫談 編譯原理》 中, 也 簡單的討論了 連接 原理 。
這個道理 搞通了, D# 要搞成 JIT 也是能夠的 。
事實上 也 應該 搞成 JIT, 不搞成 JIT 估計沒人用 。
JIT 還真不是 跨平臺 的 問題,
我想起了, C++ 寫了 3 行代碼, 就須要一個 幾十 MB 的 「Visual Studio 2012 for C++ Distribute Package」 ,
看到這些, 就知道是 怎麼回事 了 。
通過 上面的 討論, 一些 細節 就 更清楚了 。
D# 編譯產生的 dll, 其實是個 壓縮文件, 解壓一看, 裏面是 一些 .c 文件 或者 .h 文件, 至關因而一個 C 語言 項目 。
這樣是否是 很容易 被 反編譯 ?
實際上 不存在 反編譯, 直接打開看就好了 。 ^^
若是怕被 反編譯 的話, 能夠把 C 代碼 裏的 回車 換行 空格 去掉, 這樣 字符 都 密密麻麻 的 排在一塊兒,
再把 變量名 和 函數名 混淆一下 。
感受好像 javascript ……
若是跟 Chrome V8 引擎 相比, VMBC / D# 確實像 javascript 。
try catch 能夠本身作, 也能夠 用 C++ 的, 但我建議 本身作,
由於 VMBC 是 Virtual Machine Base on C, 不是 Virtual Machine Base on C++ 。
try catch 可能會用到 goto 語句 。
昨天網友提起 C 語言 的 編譯速度 相對 IL 較低, 由於 C 語言 是 文本分析, IL 是 肯定格式 的 二進制數據,
我以前也想過這個問題, 我還想過 像 .Net Gac 同樣搞一個 本地代碼程序集 緩存, 這樣, 運行一個 D# 程序時, 能夠先 用 Hash 檢查一下 C 中間代碼程序集 文件 是否 和 以前的同樣, 若是同樣就 直接運行 緩存裏的 本地代碼程序集 就能夠 。
由這個問題, 又想到了, D# 應該支持 靜態編譯(AOT), 這也是 C 語言 的 優點 。
D# 應該 支持 JIT 和 AOT, JIT 和 AOT 能夠 混合使用 。
好比, 一個 D# 的 程序, 裏面一些 模塊 是 AOT 編譯好的, 一些 模塊 是 JIT 在 運行時 編譯的 。
爲此, 咱們提出一個 ILBC 的 概念, ILBC 是 Intermediate Language Base on C 的 意思 。
ILBC 不是一個 語言, 而是一個 規範 。
ILBC 是 指導 C 語言 如何構建 IL 層 的 規範, 以及 支持 這個 規範 的 一組 庫(Lib) 。
ILBC 規範草案 大概是這樣 :
ILBC 程序集 能夠提供 2 個 C 函數 接口,
1 ILBC_Main(), 這是 程序集 的 入口點, 和 C# 裏的 Main() 是同樣的,
2 ILBC_Link() , 這就是 上面 討論的 IL 層 的 動態連接 的 接口, 這個 函數 返回 程序集 的 元數據, 其它 ILBC 程序集 得到 元數據後,能夠 根據 元數據 調用 這個 程序集 裏的 類 和 方法 。 元數據 裏 的 內容 主要是 類 的 大小(Size)、 構造函數地址 、 成員函數地址 。
哎? 不過說到這裏, 若是要訪問 另一個 程序集 裏的 類 的 公有字段 怎麼辦 ? 嘿嘿嘿,
好比 A.dll 要 訪問 B.dll 裏的 Person 類的 name 字段, 這須要在 把 A 項目 的 D# 代碼 編譯成 A.dll 時 從 B.dll 的 元數據 裏 知道 name 字段 在 Person 類 裏的 偏移量, 這樣就能夠把 這個 偏移量 編譯到 A.dll 裏, A.dll 裏 訪問 Person 類 name 字段 的 代碼 會被 處理成 *( person + name 的 偏移量 ) , person 是 Person 對象 的 指針 。
這是 在把 D# 代碼 編譯成 A.dll 的 時候 根據 B.dll 裏的 元數據 來作的工做, 這不是 動態連接, 那算不算 「靜態連接」 ? 由於 字段 的訪問 的 處理 比較簡單, 「連接」 包含的 工做 可能 更復雜一些, 固然, 你要把 字段 的 處理 叫作 連接 也能夠, 怎麼叫均可以 。
那 函數調用 能不能 也 這樣處理 ?
訪問字段 的 時候, 是 對象指針 + 字段偏移量,
函數 則是 編譯器 編譯 爲 本地代碼, 函數 的 本地代碼 的 入口地址 是 編譯器 決定的, 須要 編譯器 把 C 中間代碼 編譯 爲 本地代碼 後才知道, 因此 函數 須要 動態連接 。
從上面的討論咱們也看到, ILBC 程序集 會有一個 .dat 文件(數據文件), 用來存放 能夠 靜態知道 的 元數據, 好比 類 字段 方法,類的大小(Size), 字段的偏移量(Offset) 。 元數據 的 做用 是 類型檢查 和 根據 偏移量 生成 訪問字段 的 C 中間代碼 。
元數據 裏的 類的大小(Size) 和 字段偏移量 是 D# 編譯器 計算 出來的, 這須要 D# 編譯器 知道 各類 基礎類型(int, long, float, double, char 等) 在 C 語言 裏的 佔用空間大小(Size), 這是 D# 編譯器 的 參數, 須要 根據 操做系統平臺 和 C 編譯器 來 設定 。
類(Class) 在 ILBC 裏 是用 C 語言 的 結構體(Struct) 來表示, 結構體 由 基礎類型 和 結構體 組成, 因此 只要 知道了 基礎類型 的 Size, 就能夠 計算出 結構體 的 Size, 固然 也就知道了 類 的 Size 和 字段偏移量 。
但有一個 問題 是, D# 編譯器 對 字段 的 處理順序 和 C 編譯器 是否同樣 ? 若是不同, 那 D# 把 name 字段 放在 age 以前, C 編譯器 把 age 字段 放在 name 字段 以前, 那計算出來的 字段偏移量 就不同了, 就錯誤了 。 這就 呵呵 了 。
不過 C 編譯器 好像是 按照 源代碼 裏 寫的 字段順序 來 編譯 的, 這個能夠查證確認一下 。
好比, 有一個 結構體 Person ,
struct Person
{
char[8] name;
int age;
}
那麼, 編譯後的結果 應該是 Person 的 Size 是 12 個 byte, 前 8 個 byte 用來 存儲 char[8] name; , 後 4 個 字節 用來 存儲 int age; , (假設 int 是 32 位整數) 。
若是是這樣, 那就沒問題了 。 D# 編譯器 和 C 編譯器 都 按照 源代碼 裏 書寫 的 順序 來 編譯字段 。
C# 好像也沿襲了這樣的作法, 在 反射 裏 用 type.GetFields() 方法 返回 Field List, Field 的 順序 好像 就是 跟 源代碼 裏 書寫的順序 同樣的 。
並且在 C# 和 非託管代碼 的 交互中(P / Invoke), C# 裏 定義一個 字段名 字段順序 和 C 裏的 Struct 同樣的 Struct, 好像也直接能夠傳給 C 函數用, 好比有一個 C 函數 的 參數 是 struct Person, 在 C# 裏 定義一個 和 C 裏的 Person 同樣的 Struct 能夠直接傳過去用 。
咱們來看一下 方法 的 動態連接 的 具體過程:
假設 A 項目 裏 會調用到 B.dll 的 Person 類 的 方法, Person 類 有 Sing() 和 Smile() 2 個 方法, D# 代碼 是這樣:
public class Person
{
public Sing()
{
// do something
}
public Smile()
{
// do something
}
}
那麼 A 項目 裏 調用 這 2 個 方法 的 C 中間代碼 是:
Person * person ; // Person 對象 指針
……
ilbc_B_MethodList [ 0 ] ( person ); // 調用 Sing() 方法
ilbc_B_MethodList [ 1 ] ( person ); // 調用 Smile() 方法
你們注意, 這裏有一個 ilbc_B_MethodList , 這是 A 項目 的 D# 代碼 編譯 生成的 C 中間代碼 裏的 一個 全局變量:
uint ilbc_B_MethodList ;
是一個 uint 變量 。
uint 變量 能夠 保存 指針, ilbc_B_MethodList 實際上 是一個 指針, 表示一個 數組 的 首地址 。
這個數組 就是 B.dll 的 函數表 。 函數表 用來 保存 B.dll 裏 全部類 的 全部方法 的 地址(函數指針), D# 編譯器 在 編譯 B 項目 的 時候 會給 每一個類的每一個方法 編一個 序號 。
編號規則 仍是 跟 編譯器 對 源代碼 的 語法分析 過程 有關, 基本上 可能仍是 跟 書寫順序 有關, 不過 無論 這個 編號規則 如何, 這都沒有關係 。
總之 D# 編譯器 會給 全部方法 都 編一個號(Seq No), 每一個方法 的 編號 是多少, 這些信息 會 記錄在 B.dll 的 元數據 裏(metadata.dat),
D# 編譯器 在 編譯 A 項目 時, 會根據 A 引用的 B.dll 裏的 元數據 知道 B.dll 裏的 方法 的 序號,
這樣, D# 編譯器 就能夠 把 調用 Sing() 方法 的 代碼 處理成 上述的 代碼:
ilbc_B_MethodList [ 0 ] (); // 調用 Sing() 方法
注意, ilbc_B_MethodList [ 0 ] 裏的 「0」 就是 Sing() 方法 的 序號, 經過 這個 序號 做爲 ilbc_B_MethodList 數組 的 下標(index), 能夠取得 Sing() 方法 的 函數地址(函數指針), 而後 就能夠 調用 Sing() 方法 了 。
上文說了, ilbc_B_MethodList 表示 B.dll 的 函數表 的 首地址,
那麼, B.dll 的 函數表 從哪裏來 ?
函數表 是在 加載 B.dll 時生成的 。
運行時 會把 B.dll 編譯爲 本地代碼 並加載到內存, 而後 調用 上文定義的 ILBC_Link() 函數,
ILBC_Link() 函數 會 生成 函數表, 並 返回 函數表 的 首地址 。
ILBC_Link() 函數 的 代碼 是這樣的:
uint ilbc_MethodList [ 2 ] ; // 這是一個 全局變量
uint ILBC_Link()
{
ilbc_MethodList [ 0 ] = & ilbc_Method_Person_Sing ;
ilbc_MethodList [ 1 ] = & ilbc_Method_Person_Smile ;
return ilbc_MethodList ;
}
void ilbc_Method_Person_Sing ( thisPtr )
{
// do something
}
void ilbc_Method_Person_Smile ( thisPtr )
{
// do something
}
uint ilbc_MethodList [ 2 ] ; 就是 B.dll 的 函數表, 這是一個 全局變量 。
裏面的 數組長度 「2」 表示 B.dll 裏 有 2 個方法, 如今 B.dll 裏只有 1 個 類 Person, Person 類 有 2 個方法, 因此 整個 B.dll 只有 2 個方法 。
若是 B.dll 有 多個類, 每一個類有 若干個 方法, 那 D# 編譯器 會 先對 類 排序, 再對 類裏的方法 排序, 總之 會給 每一個 方法 一個 序號 。
uint ILBC_Link() 函數 的 邏輯 就是 根據 方法 的 序號 把 方法 的 函數地址 填入 ilbc_MethodList 數組 對應的 位置,
再返回 ilbc_MethodList 數組 的 首地址 。
也就是 先 生成 函數表, 再 返回 函數表 首地址 。
上文說了, 運行時 加載 B.dll 的 過程 是, 先把 B.dll 編譯成 本地代碼, 加載到 內存, 再調用 ILBC_Link() 函數, 這樣 B 的 本地代碼 函數表 就生成了 。
而後 運行時 會把 ILBC_Link() 函數 返回 的 函數表 首地址 賦值給 A 的 ilbc_B_MethodList , 這樣 A 就能夠 調用 B 的 方法了 。
由於 函數 是 動態連接 的, 函數表 裏 函數 的 順序 是 由 D# 編譯器 決定的, 因此 和 C 編譯器 無關, 不須要像 字段 那樣 考慮 C 編譯器 對 函數 的 處理順序 。
以上就是 ILBC 的 草案 。 還會 陸續補充 。
IL 層 動態連接 是 ILBC 的 一個 基礎架構 。
ILBC 的 一大特色 是 同時支持 AOT 和 JIT , AOT 和 JIT 能夠混合使用, 也能夠 純 AOT, 或者 純 JIT 。
我查了一下, 「最小的 C 語言編譯器」, 查到 一個 Tiny C, 能夠看下 這篇文章 《TCC(Tiny C Compiler)介紹》 http://www.cnblogs.com/xumaojun/p/8544083.html ,
還查到一篇 文章 《讓你用C語言實現簡單的編譯器,新手也能寫》 https://blog.csdn.net/qq_42167135/article/details/80246557 ,
他們 還有個 羣, 我打算去加一加 。
還查到一篇 文章 《手把手教你作一個 C 語言編譯器:設計》 https://www.jianshu.com/p/99d597debbc2 ,
看了一下他們的文章, 主要是 我 對 彙編 和 操做系統 環境 不熟, 否則 我也能夠寫一個 小巧 的 C 語言編譯器 。
ILBC 會 自帶 運行時, 若是是 純 AOT, 那麼 運行時 裏 不用 帶 C 語言編譯器, 這樣 運行時 就能夠 小一些 。
若是 運行時 不包含 龐大的 類庫, 又不包含 C 語言編譯器, 那麼 運行時 會很小 。
我建議 ILBC 不要用 在 操做系統 上 安裝 運行時 的 方式, 而是 每一個 應用程序 隨身攜帶 運行時,
ILBC 採用 簡單的 、即插即用 的 方式, 引用到的 ILBC 程序集 放在 同一個 目錄下 就能夠找到 。
程序集 不須要 安裝, 也不須要 註冊 。
D# 能夠 編寫 操做系統 內核 層 以上的 各類應用,
其實 除了 進程調度 虛擬內存 文件系統 外, 其它 的 內核 模塊 能夠用 D# 編寫, 好比 Socket 。
這有 2 個 緣由:
1 GC 須要運行在一個 獨立的 線程裏, GC 負責 內存回收 和 空閒空間排序 。 因此 D# 須要有一個 線程 的 架構 。
2 D# 的 堆 算法 是 不嚴格的 、鬆散的, 須要運行在 虛擬內存 廣大的 地址空間 和 存儲空間 下, 不適合 用於 物理內存 。
因此, D# 的 適用場景 是 在 進程調度 虛擬內存 文件系統 的 基礎上 。
爲何 和 文件系統 有關係 ?
由於 虛擬內存 會用到 文件系統, 因此 ~ 。
D# / ILBC 的 目標 是 跨平臺 跨設備 。
後面會把 進一步 的 設計 放在 系列文章 裏, 文章列表 以下:
《我發起並創立了一個 C 語言編譯器 開源項目 InnerC》 http://www.javashuo.com/article/p-gpskqdni-bo.html
《ILBC 運行時 (ILBC Runtime) 架構》 http://www.javashuo.com/article/p-vvybjngc-be.html
《ILBC 規範》 http://www.javashuo.com/article/p-hsmtjoox-s.html
《堆 和 GC》 寫做中 。
《InnerC 語法分析器》 寫做中 。