我發起並創立了一個 VMBC 的 子項目 D#

你們好,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 語法分析器》            寫做中  。

相關文章
相關標籤/搜索