ILBC 規範 2

接上篇 《ILBC 規範》  http://www.javashuo.com/article/p-hsmtjoox-s.html  ,html

 

ILBC    的 目標 是    跨平臺  跨設備 。java

 

D# / ILBC  能夠 編寫 操做系統 內核 層 以上的 各類應用, 程序員

其實 除了 進程調度 虛擬內存 文件系統  外,  其它 的 內核 模塊 能夠用  D#  編寫, 好比 Socket 。docker

 

D# / ILBC  的 設計目標 是 保持簡單, 好比  D#  支持  Lambda 表達式,  可是  LinQ  應該 由 庫 來 支持,  與 語言 無關 。數組

另外一方面,  ILBC  不打算 發展一個 龐大 的 優化體系 。   C++ ,  .Net / C#  的 優化體系 已經 龐大複雜 到 成爲 大公司 也很難 承受 之重 了 。安全

咱們不會這麼幹 。數據結構

ILBC 認爲   「簡單 就是 優化」  。架構

 

保持 簡單設計 和 模塊化,  模塊化 會 帶來一些 性能損耗,  這些 性能損耗 是 合理 的 。ide

 

保持 簡單設計 和 模塊化,  對於  ILBC / D# / c3 / ……  以及 應用程序 都是 有益的 。模塊化

 

ILBC  的 目標 是 創建一個 基礎設施 平臺 。

就像 容器(好比 docker,    kubernetes),  容器 打算 在 操做系統 之上 創建一個 基礎設施 平臺,

咱們的 作法 不一樣,

ILBC  是 用 語言 創建一個 基礎設施 平臺 。

 

爲了 避開 「優化陷阱」, 我決定仍是 啓用 以前的 「ValueBox」 的 想法 。 ValueBox 的 想法 以前 想過, 但後來又放棄了 。

ValueBox 相似 java C# 裏的 「裝箱」 、 「拆箱」 。

ValueBox 就是 對於 int long float double char 等 值類型 (或者說 簡單類型) , 用一個 對象(ValueBox) 裝起來, 用於 須要 按照 對象 的 方式 處理 的 場合 。

原本我以前是放棄了 這個 想法, 以爲 仍是 按照 C# 的 「一切都是對象」 的 作法, 讓 值類型 也 做爲 對象, 繼承 Object 類, 而後 讓 編譯器 在 不須要 做爲對象, 只是 對 值 計算 的 場合 把 值類型對象 優化回 值類型 (C 語言 裏的 int long float double char 等) 。

 

但 如今 既然談到 優化陷阱, 上面說的 「一切都是對象」 的 架構 就 有點 呵呵 了 。

這有一個問題, 把 值對象 優化回 值類型, 這個 優化 是 放在 C 中間代碼 裏 仍是 InnerC 編譯器 裏,

放在 C 中間代碼 是指 由 高級語言(D# c3 等) 編譯器 來 優化, 這樣 高級語言 編譯 生成 的 C 中間代碼 裏面 就已是 優化過的 代碼, 好比 在 值 計算的 地方 就是 C 語言 的 int long float double char 等, 而不是 值對象 。

但 這樣 要求 高級語言 的 編譯器 都 按照 這個 標準 進行優化, 否則 在 各 高級語言 寫的 庫 之間 動態連接 時 會 發生問題 。

好比 D# 調用 c3 寫的 庫 的 Foo(int a) 方法, c3 作過優化, 因此 須要的 a 參數是 一個 C 語言 裏的 int 類型, 而 D# 未做優化, 傳給 Foo(int a) 的 a 參數 是 一個 int 對象, 這就 出錯了, 這是 不安全的 。

 

但 要求 高級語言 的 編譯器 都 按照 標準 優化, 這是一個比較 糟糕 的 事情 。

這會 讓 高級語言 編譯器 變得 麻煩 和 作 重複工做, 且 ILBC 會因 規則 累贅 而 缺少活力 。

 

若是 把 優化 放在 InnerC 編譯器 裏 優化 , 那 會 和 咱們的一些想法 不符 。 咱們但願 InnerC 是一個 單純的 C 編譯器, 不要把 IL 層 的 東西 摻雜 到 裏面 。

InnerC 是 一個 單純的 C 編譯器, 這也是 ILBC 的 初衷 和 本意 。

 

因此, 咱們採用這樣的設計, 值類型 就是 值類型, 對應到 C 語言 裏的 基礎類型(int long float double char 等), 值類型 不是 對象, 也不 繼承 Object 類, 對象 是 引用類型, 繼承 Object 類 。

 

當 須要 以 對象 的 方式來 處理 時, 把 值類型 包到 ValueBox 裏 。

每一個 值類型 會 對應一個 ValueBox, 好比 int 對應 IntBox, long 對應 LongBox, float 對應 FloatBox, double 對應 DoubleBox, char 對應 CharBox, bool 對應 BoolBox 等等 。

 

ValueBox 的 使用 代碼 好比:

 

IntBox i = new IntBox( 10 ); // 10 就是 IntBox 包裝的 Value

 

或者,

 

int i = 10;

IntBox iBox = new IntBox( i ); // 把 int 類型的 變量 i 的 值 包裝到 IntBox

 

何時須要 把 值類型 包到 ValueBox 裏 ? 或者說, 何時須要 以 對象 的 方式 來 處理 值類型 ?

通常是在 須要 動態傳遞參數 的 時候,

好比, Foo ( object o ) 方法 的 o 參數 可能 傳入 各類類型, 那麼能夠把 o 參數 聲明爲 object 類型, 這樣在 Foo() 方法內部 判斷 o 參數 的 類型, 根據類型執行相關操做 。

又好比, 反射, 經過 反射 調用 方法, 參數 是 經過 object [ ] 數組 傳入,

這 2 種 狀況 對於 參數 都是 以 對象 的 方式 處理, 若是 參數 是 值類型 的話, 就須要 包裝 成 ValueBox 再傳入 。

 

D# / ILBC 支持 值類型 數組 、 值類型 泛型 容器 。

值類型 數組 就是 數組元素 就是 值類型, 假設 int 類型 佔 4 個 字節, 那麼 int [ ] 數組 的 每一個元素 佔用空間 也是 4 個 字節, 這和 C 語言 是同樣的 。

值類型 泛型 容器 好比 List<int> , List<int> 的 內部數組 就是 int [ ] 。

 

值類型 數組, 值類型 泛型 容器 直接存取 值類型, 不須要 對 值類型 裝箱 。

 

可是要注意, 好比 Dictionary<TKey, TValue> , value 能夠是 值類型, 但 key 須要是 對象類型, 由於會 調用 key.GetHashCode() 方法 。

因此, 若是 key 是 值類型, 須要 裝箱 成 ValueBox 。

 

好比

 

Dictionary < string , int > , value 能夠是 值類型 ,

Dictionary < IntBox , object > , key 須要是 對象類型, 若是是 int , 須要 裝箱 成 IntBox

 

若是聲明 Dictionary < int , object > , 則 編譯器 會對 key 的 類型 報錯, 提示 應 聲明 爲 引用類型(對象類型) 。

 

值類型 又稱 簡單類型 ,

引用類型 又稱 對象類型 ,

(這有點 呵呵)

 

編譯器 是 依據 什麼 檢查 key 類型 應爲 引用類型 呢 ?

 

咱們能夠在 D# 裏 加入一個 語法, 好比, Dictionary 的 定義 是這樣:

 

public class Dictionary < object TKey , TValue >

{

           …… 

           public void Add ( TKey key , TValue value )

           {

                      int hash = key.GetHashCode() ;

                      ……

            }

}

 

能夠看到, TKey 的前面 加了一個 object , 這表示 TKey 的 類型 應該是 object 類型 或者 object 的 子類,

這個 object 能夠 換成 其它 的 類型, 好比 其它 的 類 或者 接口 。

 

這樣的話, 若是 TKey 被 聲明 爲 值類型, 好比 Dictionary < int , object > , 因爲 int 不是 引用類型, 固然 也就不是 object 或者 object 的 子類, 因而 不知足 TKey 的 類型約束, 因而 編譯器 就 報錯了 。

 

若是 TKey 的 前面 不聲明 object , 會怎麼樣 ? 仍是會報錯 。

由於在 Add ( TKey key , TValue value ) 方法 裏 調用了 key.GetHashCode() 方法, 調用方法 意味着 必須是 引用類型(對象類型), 因此 編譯器 會要求 Dictionary 的 定義 裏 要 聲明 TKey 的 類型 , 且 TKey 的 類型 必須是 引用類型(對象類型) 。

這 也有點 呵呵 。

 

IntBox override(重寫) 了 Object 類的 GetHashCode() 方法, 用於 返回 IntBox 包裝的 int 值 的 HashCode, 不過 int 類型 的 GetHashCode() 方法 多是 最簡單的了, 直接返回 int 值 就能夠 。 ^^

 

String 類 會 override(重寫) Object 類 的 Equals(object o) 方法, 而且會 增長 一個 Equals(string s) 方法, Equals( object o ) 方法內部會調用 Equals( string s ) 方法 。 Equals ( object o ) 方法 先 判斷 o 是否是 String 類型, 若是不是, 則 返回 false, 若是是, 則 調用 Equals( string s ) 判斷 是否相等 。

D# 裏 用 「 == 」 號 比較 2 個 String 的 代碼 會被 編譯器 處理成 調用 Equals( string s ) 方法 。

 

除了 最底層 的 模塊 用 C 編寫, D# / ILBC 能夠編寫 各個層次 各個種類 的 軟件 ,

用 C 寫 能夠用 InnerC 寫, 只要 符合 ILBC 規範, InnerC 寫的 代碼 就能夠 和 ILBC 程序集 同質連接 。

從這個 意義 來看, ILBC / InnerC 能夠 編寫 包括 操做系統 在內 的 各個層次 各個種類 的 軟件 ,

從這個 意義 來看, ILBC 是 一個 軟件 基礎設施 平臺 。

 

今天 看了 C# 8.0 新特性 https://mp.weixin.qq.com/s?__biz=MzAwNTMxMzg1MA==&mid=2654074187&idx=1&sn=e0a6d9c963c3405dcae232a70434f225&chksm=80dbd11eb7ac58085d5357785cae13bbd4a3ccf92e876cd12c1f8faa9ada7629e5f8b2ff030e&mpshare=1&scene=23&srcid=#rd

 

能夠看出, C# 8.0 標誌着 C# 開始成爲 「保姆型」 語言 , 而不是 程序員 的 語言 。

D# 將 一直 會是 程序員 的 語言 , 這是 D# 的 設計目標 和 使命 。

 

補充一點, ValueBox 的 使用 小技巧 ,

在一段代碼中, ValueBox 能夠只 new 一個, 而後 重複使用 。

ValueBox 有一個 public value 字段, 就是 ValueBox 包裝的 值, 對 value 字段 賦上新值 就能夠 從新使用 了 。

好比, IntBox ,有 public int value 字段,

 

IntBox i = new IntBox( 1 );

i.value = 2;

i.value = 3;

i.value = 4;

 

重複使用 ValueBox 能夠 減小 new ValueBox 和 GC 回收 的 開銷 。

 

有 網友 提議 D# 的 名字 能夠叫 Dava , 這名字 挺好聽, 挺美麗的, 和 女神(Diva) 相近, 好吧, 就叫 Dava 吧, D# 又名 Dava 。

 

接下來 咱們 討論 泛型 原理 / 規範 ,

 

泛型 在 ILBC 裏 和 C++ 相似 , 由 高級語言 編譯器 生成 具體類型,

 

假設 有 一個 List<T> 類, 這個類 的 C 中間代碼 以下:

 

struct List<T>

{

            T arr [ 20 ] ; // 20 是 內部數組 的 初始化 長度

            int length = 0 ;

}

 

void List<T><>Add<>T ( List<T> * this , T element )

{

             this -> arr [ this -> length ] = element ;

             this -> length ++ ;

}

 

T List<T><>Get<>T ( List<T> * this , int index )

{

             return this -> arr [ index ] ;

}

 

若是在 代碼 中 使用 了

 

List<int> list1 = new List<int>();

List<string> list2 = new List<string>();

 

那麼 編譯器 會 爲 List<int> 生成一個 具體類型 List~int 類, 也會爲 List<string> 生成一個 List~string 類 , 代碼以下:

 

struct List~int

{

           int arr [ 20 ] ; // 20 是 內部數組 的 初始化 長度

           int length = 0 ;

}

 

void List~int<>Add<>int ( List~int * this , int element )

{

            this -> arr [ this -> length ] = element ;

            this -> length ++ ;

}

 

int List~int<>Get<>int ( List~int * this , int index )

{

            return this -> arr [ index ] ;

}

 

struct List~string

{

           string * arr [ 20 ] ;       //    20 是 內部數組 的 初始化 長度

           int length = 0 ;

}

 

void List~string<>Add<>string ( List~int * this , string * element )

{

            this -> arr [ this -> length ] = element ;

            this -> length ++ ;

}

 

int List~string<>Get<>int ( List~int * this , int index )

{

            return this -> arr [ index ] ;

}

 

能夠看出來, 把 泛型類型 裏的 List<T> 替換成 具體類型(List<int>, List<string>), 把 T 替換成 泛型參數類型 (int , string *) 就是 具體類型 。

注意 , 值類型 把 T 替換爲 值類型 就能夠, 好比 int, 引用類型 要把 T 替換成 引用(指針), 好比 string * 。

 

這部分 由 高級語言 編譯器 完成 。

 

複雜一點的狀況是, 跨 程序集 的 狀況, 假設 有 程序集 A , B , A 引用了 B 裏的 List<T> , 那 …… ?

這個須要 把 List<T> 的 C 中間代碼 放在 B 的 元數據 文件 (B.ild) 裏, A 引用 B.ild , 編譯器 會 從 B.ild 中 獲取到 List<T> 的 C 中間代碼, 根據 List<T> 的 C 中間代碼 生成 具體類型 的 C 中間代碼 。

這好像 又 有點 呵呵 了 。

 

不過 這樣看來的話, 上文 關於 泛型 對 值類型 和 引用類型 的 不一樣處理 好像 不必了 。

上文 舉例 的 Dictionary<object TKey , TValue> 要把 TKey 聲明爲 object ,

這其實已經不必了 。

 

public class Dictionary < TKey , TValue >

{

            ……

            public void Add ( TKey key , TValue value )

            {

                       int hash = key.GetHashCode() ;

                       ……

             }

}

 

若是在 代碼 中 寫了

 

Dictionary< int , object > dic ;

 

則 編譯器 會 報錯 「TKey 的 具體類型 int 不包含 GetHashCode() 方法, int 是 值類型, 值類型 不支持 方法, 建議改成 引用類型 。」

 

假設 有 class Foo<T> , 代碼以下:

 

class Foo<T>

{

           void M1 ( T t )

           {

                      t.Add();

           }

}

 

Foo<A> foo = new Foo<A>();

A a = new A();

foo.M1 ( a ) ;

 

A 是 引用類型(對象類型), 若是 A 沒有 Add() 方法, 編譯器 會 報錯 「泛型參數類型 A 不包含 Add() 方法 。」

 

咱們還能夠把 代碼 改爲:

 

class Foo<T>

{

           T M1 ( T t )

           {

                       return t ++ ;

            }

}

 

Foo<int> foo = new Foo<int>();

int i = 0 ;

int p = foo.M1 ( i ) ;

 

這 能夠 編譯 經過, 由於 int 支持 ++ 運算符, 實際上, 只要 支持 ++ 運算符 的 類型 均可以 使用 Foo<T> , 或者說, 只要 支持 ++ 運算符 的 類型 都 能夠做爲 Foo<T> 的 泛型參數類型 T 。

 

其實 說白了,  你 按照  C++ 模板 來 理解  ILBC 泛型 就能夠了 。  哈哈哈哈

 

接下來 討論 繼承 ,   繼承 就是 繼承 基類 的 字段 和 方法, 進一步 是 重寫 虛方法 。

 

咱們先來看  繼承 基類 的 字段 和 方法 ,

 

假設  

 

class A1

{

         int  f1;

}

 

class A2 : A1

{

         int f2;

}

 

那麼, A2 佔用的 內存空間 就是  A1 的 空間 加上 A2 的 空間, 就是  f1  和  f2  的 空間,

由於  f1,  f2  都是 int ,  假設 int 是 4 個字節,   那麼 f1 ,  f2  共 佔用  8  個字節 的空間,  這就是 A2 佔用 的 空間 。

因此 new A2()  的 時候,  就是 先 從 堆 裏 申請  8 個 字節 的 空間,  而後 再 調用  A2 的 構造函數 初始化,  A2 的 構造函數 會 先調用 A1 的 構造函數 初始化  。

假設  A3 繼承 A2,  A2 繼承 A1 ,    那麼 new A3()  時 會 先 申請 A3 的 空間,  而後  調用  A3 的 構造函數, A3 的 構造函數 是這樣:

 

A3( A3 *   this)

{

          A2( this );

          A3  的 初始化 工做

}

 

A2( A2 *   this)

{

          A1( this );

          A2  的 初始化 工做

}

 

A1( A1 *   this)

{

          A1  的 初始化 工做

}

 

能夠看出, 會 沿 繼承鏈 依次 調用 基類 的 構造函數 。

 

若是 基類 在 另外一個 程序集 裏,  那麼 對 基類 構造函數 的 調用 會 編譯成 動態連接 的 方式, 和 普通方法 的 動態連接 同樣 。

 

對於 方法 的 繼承,  編譯器 會 把 調用 基類 方法 的 地方 直接 編譯成 調用 基類方法, 傳入 子類對象 的 this 指針,  這個跟 基類對象 調用 自己的 方法 同樣 。

若是 是 基類 在 另外一個 程序集 裏, 就會 編譯成 動態連接 的 方式,  跟 基類對象 調用 自己的 方法  仍然同樣 。

 

對於 虛方法,   假設 有 程序集 A ,  B,     B 裏有 A1 , A2  類,    A2 是 A1 的 子類 ,   並  override(重寫) 了   M1() ,  M2()   方法 。

 

虛方法  經過   引用  實現,  引用 裏 有一個字段 是 虛函數表 。

 

因此, 咱們要對 引用 作一點 改進,

以前 咱們 在  C 中間代碼 裏 寫的 引用 都是 指針,  但爲了實現 虛方法 , 須要 把 引用 改進成一個 結構體 :

 

struct    ILBC<>Reference

{

           void *    objPtr   ;           //     對象指針

           void *    virtualMethods   ;       //    虛函數表 指針

}

 

A  裏 的 代碼:

 

A1  a  =  new A2();

a.M1();

 

這段 代碼 會編譯成:

 

ILBC<>Reference  a   ;         //   建立 引用  a

a.objPtr = ILBC_gcNew( sizeof(ILBC<>Class<>A2 ) )   ;      //  給  A2 對象 分配空間

(* ILBC<>Class<>A2<>Constructor)  ( a.objPtr )   ;       //   調用  A2 構造函數 初始化  a

a.virtualMethods = ILBC_GetVirtualMethods( "B.A2",   "B.A1" );        //    寫入 A2 對於 A1 虛函數表 指針

 

(  *  (  a.virtualMethods [ ILBC<>Class<>A1<>VirtualMethodNo<>M1 ]  )  )   ( )    ;              //   調用   a.M1()   ;

 

//   ILBC<>Class<>A1<>VirtualMethodNo<>M1  是一個 全局變量, 保存  A1.M1()  方法 的 虛方法號,  虛方法號 由 ILBC 在 加載 A1 類 時產生 並 寫入 這個 全局變量

 

以上就是 編譯器 產生 的 代碼  。

 

ILBC_GetVirtualMethods( "B.A2",   "B.A1" )    方法 返回  A2 對於 A1 的 虛函數表 指針,

參數  "B.A2"  表示 A2 的 全名,  "B.A1" 表示 A1 的 全名, 全名 包含了 名字空間  。

 

ILBC_GetVirtualMethods( subClassFullName,   baseClassFullName )   方法 是 ILBC 調度程序 提供的 ILBC 系統方法,

這個方法 會 先根據  subClassFullName,   baseClassFullName   查找  子類 對於 父類 的 虛函數表 是否存在, 若是 不存在 , 則 生成一份,  下次直接返回 。

虛函數表 是一個 數組, 數組元素 是 子類 對於 父類 虛函數 重寫 的 函數 的 地址, ILBC 在 加載類 時 會對 類 的 虛函數 排一個序, 而後 對於 該類的 每一個 子類 的 虛函數表, 都 按照 這個 順序 把 相應 的 虛函數 重寫 的 函數 的 地址 放到 數組(虛函數表) 裏 。

若是 子類 沒有 重寫函數,  則 存放 基類 的 函數地址 。

 

虛函數 排序 的 序號(從 0 開始) 就是 虛方法號(VirtualMethodNo),

以 虛方法號 做爲 下標(index) 從 虛函數表 裏 取出 的 就是 這個 虛方法 的 函數地址 。

 

加載類 是 在 ILBC_GetType( assemblyName,  className )  方法 裏 進行的,  實際上 應該改爲  ILBC_GetType( classFullName ) , 由於 classFullName 已經包含了 名字空間,  不須要 assemblyName 了 ,  事實上 在 ILBC 運行時  對於 類(Class) 的 識別 就是 用  Full Name,  不須要涉及 assemblyName , 也能夠說,  在 一個 運行時 內,  不能 有 相同 Full Name 的  2 個 類 ,  無論 這 2 個 類 是否是 在 一個 程序集 裏 。

 

ILBC_Type( classFullName ) 方法 會 檢查 類 是否 已加載, 若是 已加載 就 直接返回  ILBC_Type * ,  若是 沒有 則 加載 並 返回  ILBC_Type *   。

 

ILBC_GetVirtualMethods( 「B.A2」,   "B.A1" )   方法 會 查找 A1 中 全部的 虛方法, 排一個序, 並 建立一個 長度 等於 虛方法個數 的 數組(虛方法表), 而後 從 A2 中 按名稱 逐個 查找 A2 對 虛方法 的 重寫實現 的 函數地址, 按 順序 填入 虛方法表 中, 若是 未重寫, 則 直接使用 基類 的 實現, 即 填入 基類 的 函數地址 。

 

好比  A2 繼承 A1,  A1 繼承 Object ,  A2 重寫了 Object.GetHashCode() 方法, 那麼 A2 對於 A1 的 虛函數表 中 GetHashCode() 方法 對應的 位置 就會 寫入 A2.GetHashCode()  的 函數地址,

若是 A1 重寫了 Object.GetHashCode()  而  A2 未重寫, 則 會 填入  A1.GetHashCode()  的 函數地址,

若是 A1 A2 都沒有 重寫 Object.GetHashCode() ,    則 會 填入  Object.GetHashCode()  的 函數地址 。

 

也就是說, ILBC 會 沿着 繼承鏈 向上 查找  虛函數 的 重寫實現 。

 

好比  有 如下 繼承關係 :  

 

A3 -> A2 -> A1 -> Object

 

又有 這樣的 代碼:

 

A1 a1 = new A3();

A2 a2 = new A3();

A3 a3 = new A3();

 

對於 引用 a1 ,    a1.virtualMethods 應該是  「A3 對於 A1 的 虛函數表」,

什麼是  「A3 對於 A1 的 虛函數表」,  就是  「A3 對象 以 A1 的 身份 運行」  的 虛函數表 。

因此   a1.virtualMethods  指向 的 虛函數表  應 包含  A1  的 所有 虛方法 ,

a2.virtualMethods  指向 的 虛函數表  應 包含  A2  的 所有 虛方法  ,

a3.virtualMethods  指向 的 虛函數表  應 包含  A2  的 所有 虛方法  ,

 

A1 的 所有 虛方法 包括 A1 本身 聲明 的 虛方法 和 Object 的 虛方法 ,

A2 的 所有 虛方法 包括 A2 本身 聲明 的 虛方法 和 A1 的 虛方法 和 Object 的 虛方法 。

A3 的 所有 虛方法 包括 A3 本身 聲明 的 虛方法 和 A2 的 虛方法 和 A1 的 虛方法 和 Object 的 虛方法 。

 

因此, 虛函數表  裏的 方法  也是 沿着 繼承鏈 向上 查找 的 。

 

接口 也是 同樣的 處理方式  。

 

好比

 

IFoo foo = new A();

 

表示  A 對象 foo  以  IFoo 的 身份 運行 。

 

接口 能夠 區分 顯示實現 和 隱式實現 ,  這在  元數據  中能夠 區分,  在 建立 虛函數表 查找 元數據 的 時候 能夠 判斷 出來 。

 

能夠看出, 查找 和 建立 虛函數表  用到 較多 根據 名字 查找 成員 的 操做,  因此 前文 在 動態連接 的 篇幅 也 提到 能夠用 HashTable 來實現 快速 查找, 提高 反射 和 動態連接 的 效率 。

 

查找 和 建立 虛函數表  也是  反射 和 動態連接  。

 

咱們還能夠 順便 看一下    Object 類  的 結構 :

 

struct  Object

{

          ILBC_Type  *         type    ;      //      類型信息

          char        lock           ;             //      用於  IL Lock ,  當  鎖定 該對象時,   lock 字段 寫入 1,   未鎖定時 lock 字段 是 0

}

 

昨天 一羣 網友 嚷嚷着  「沒有 結構體(Struct) 是 如何如何 的 糟糕,,」     ,

ILBC  能夠支持 結構體, 這很容易,  結構體 有方法, 能夠繼承,  但不能多態 。

不能 多態 是指 結構體 不能聲明 虛方法, 子類結構體 也不能 重寫 基類結構體 的 方法 。

 

加入 結構體 能夠 讓 程序員 本身 選擇 棧 存儲數據 仍是 堆 存儲數據 ,  能夠 由 程序員 本身 決定 這個 設計策略 或者說 架構 。

這很清晰 。

 

目前 不打算 讓 Struct 支持 可爲空(Nullable)類型,  即  Struct ?  類型 ,  能夠用 一個字段 來 表示 初始 等狀態,

若是實在想要  null ,    那就用  Class 吧  ,    Oh  ……

 

Struct 經過 關鍵字 struct 聲明,  不繼承 ValueType,  也不繼承 Struct,  實際上也沒有  ValueType ,  Struct  這樣的 基類 。

在  ILBC 裏,    「一切都是對象是不成立的」 ,      對象(Class) 只是 數據類型 的 一種 。

 

DateTime  能夠用 Struct 來實現, 由於 DateTime 可能就是一個  64 位 整數, 表示 公元元年 到 某時 的  Ticks  數,

若是是這樣的話, 如 網友 所說   「引用 都 比 Struct(DateTime) 大」  。

 

討論到這裏,  能夠看出來,  C# 爲了實現  「一切都是對象」  付出了多大的代價 ,   

並且 C# 還支持 Struct 能夠是 可爲空(Nullable) 類型,   這讓人無語, 只想 呵呵 。 ^^ ^^ ^^

 

到 目前爲止,  ILBC 裏的 數據類型 有 3 種 :

1   簡單類型 (值類型) ,  int long float double char  等等

2   結構體 Struct (值類型)

3   對象 Class (引用類型) 

 

值類型 的 優勢 是:

1   一次尋址, 不須要 經過 引用 二次尋址

2   只包含 值, 不包含 類型信息 等 數據, 不冗餘

3   存儲 在 棧空間, 分配快 不須要回收, 事實上 對於 靜態分配 的 棧 變量, 函數 入棧 的 時候 修改了 棧頂, 則 該 函數 中 全部的 棧 變量 都被 分配 了  。

 

如今有個 問題 是,  一個 參數 是 值類型 的 方法, 若是要經過 反射 調用,  怎麼調用?

反射 須要 把 參數 放到  object[ ]   數組,    object[ ]  數組 的 元素 是 引用 。

 

我懷疑  C# 中 把 Struct 放到  object[ ] 裏時, 會對 Struct 裝箱 。

 

因此 咱們 也能夠 對 Struct 進行 裝箱,  能夠用  ValueBox  對  Struct  裝箱, 好比:

 

[  ValueBox( typeof ( ABox ) )  ]              //   告訴 ILBC 運行時 A Struct 對應的 ValueBox 是 ABox

struct     A

{

}

 

class     ABox    :    ValueBox<A>

{

}

 

ValueBox 是一個 泛型類, 由 ILBC 基礎庫 提供, 代碼以下:

 

class     ValueBox<T>

{

           T    value    ;

}

 

那麼, 在 動態傳遞參數 的 場合, 好比:

 

void    Foo( object  o )

{

            ……

}

 

能夠這樣寫:

 

void    Foo  ( object  o )

{

           Type type = o.GetType();

 

           if ( type.IsValueBox )       //   IsValueBox  是  Type  的 屬性, 若是 Type 表示的類型 是 ValueBox 或者 ValueBox 的 子類, 則 IsValueBox 返回 true

           {

                      Type valueType = type.GetValueType() ;       //   GetValueType() 方法 是 Type 的 方法, 若是 Type 表示的類型 是 ValueBox 或者 ValueBox 的 子類, 則 返回 ValueBox 包裝的 值 的 類型, 即 value 字段 的 類型

                      

                       if   ( valueType == typeof(int) )       //    typeof(int)  返回的 Type 對象 由 編譯器 生成

                               //  do something for  int

                       else if ( valueType == typeof(A) )       //    typeof(A)  返回的 Type 對象 由 編譯器 生成

                               //  do something for   A Struct

                       else if  (  ……  )

                               ……

 

                        return    ;

           }

           

           //    do something for   Object (引用類型)

}

 

咱們能夠這樣調用 Foo() 方法:

 

Foo ( 1 );

 

A a = new A() ;       //  A 是 Struct

Foo ( a );

 

Foo ( "a string" ) ;

 

Person person = new Person() ;      //   Person 是 Class

Foo ( person ) ;

 

對於 反射 的 狀況, 能夠這樣寫:

 

class   Class1

{

          void Foo ( Struct1 s1 )

          {

                     ……

          }

}

 

MethodInfo mi = typeof ( Class1 ).GetMethod( "Foo" )  ;

 

Struct1 s1 =  new Struct1()  ;

Struct1Box s1Box = new Struct1Box( s1 )  ;

 

mi.Invoke ( new object [ ]  { s1Box } )  ;

 

把 s1 裝箱 到 s1Box 裏,  再把 s1Box 放到  object [ ]  裏,  這樣  MethodInfo  內部會 「拆箱」 把 s1 傳給 Foo() 方法 。

若是 直接 把  s1  放到   object [ ]  裏,  好比  new object [] { s1 }   會怎麼樣?    會 編譯 報錯  「s1 不是 對象,  不能轉換爲 object 類型, 請考慮用 ValueBox 裝箱 。」  。

 

把  反射 調用 方法 的 參數 放到  object [ ]  數組 裏傳入, 這一方面是爲了 統一處理,  另外一方面 也是 爲了 安全,  引用 是 一個 固定格式 的 Struct, 因此 ILBC 能夠 安全 規範 的 從  object [ ]  中 訪問 每一個 引用 。   若是能夠直接傳遞 值 的話,   object [ ]   就會變成  C 的  void * 的 狀況 ,   void *  容易致使 訪問內存錯誤,  好比 方法 訪問 的 地址 已經 超過了 對象 的 地址範圍, 或者 訪問了 錯誤的 地址(好比 訪問 A 字段 可能變成了 訪問 B 字段, 或者是 把 B 字段 中的 某個字節 的 地址 做爲 A 字段 的 首地址) 。  這會形成 意想不到 的 錯誤 或者 程序 崩潰 。    也可能 被 用於 攻擊 。

 

而在 上面  Foo( object o )   方法 裏,  若是  o 參數 實際傳入的是 IntBox 的話, 

那麼, 會 這樣 取出 裏面 的 int 值:

 

Type   type   =   o.GetType () ;

 

if   (  type.IsValueBox  )

{

            Type valueType = type.GetValueType()  ;

 

            if  (  valueType == typeof ( int )  )

            {

                        IntBox   iBox   =   ( IntBox )   o   ;

                        int  i   =   iBox.value  ;          //    取出 int 值

            }

}

 

值類型(int long float double char  結構體 )  在 內存空間 裏 是 不包括  類型信息 的, 只 單純 的 存儲 值, 這是爲了 執行效率  。

可是, 沒有 類型信息 的 運行期 類型轉換 是 不安全 的, 由於 不能 檢查類型,  跟 上面 假設 的 反射 參數 經過  void *  傳入 的 情形 同樣, 會形成 內存 的 錯誤訪問,

可是, ILBC  巧妙 的 避開 了 這一點 。

 

首先, 編譯期 類型轉換, 這個 能夠 由 編譯器 檢查, 這沒有問題 。

運行期 類型轉換, 就像 上面的代碼 ,

 

IntBox   iBox   =   ( IntBox )   o   ;

int  i   =   iBox.value  ;          //    取出 int 值

 

是把  object  o  轉換成  IntBox ,   IntBox  是 對象 , 有 類型信息, 能夠 類型檢查, 因此     IntBox   iBox   =   ( IntBox )   o   ;      是 安全 的 。

這其實就是一個 正常 的 引用類型 的 類型轉換  。

 

轉換爲   IntBox   iBox   後,    iBox.value  是  明確的 int 型,   這就能夠安全的使用了 。

 

那若是 把  o  轉換成  ValueBox  會 怎樣 ?

 

ValueBox   vBox   =   ( ValueBox )   o   ;

int  i   =   vBox.value  ;          //    取出 int 值

 

這樣 編譯時 會 報錯  「不能把 泛型參數 T 類型 的 vBox.value 字段 賦值 給 int 類型 的 i 變量 。」 ,

 

若是 對 vBox.value 轉型, 轉型成 int :

 

ValueBox   vBox   =   ( ValueBox )   o   ;

int  i   =   ( int )  vBox.value  ;          //    取出 int 值

 

這樣 編譯時 會 報錯  「不能把 泛型參數 T 類型 的 vBox.value 字段 轉型爲 int 類型 。」  。

 

我忽然以爲    D#    Dava    還能夠叫    D++     。    哈哈哈哈

 

上面提到 用  ValueBoxAttribute   [ ValueBox ( typeof ( ABox ) ) ]    來 聲明 ABox 做爲 A Struct 的 ValueBox,

實際上這不必,  ILBC 能夠 提供一個 ValueBox 基類, ValueBox<T> 繼承 ValueBox 類, 那麼 ValueType<T>  的 具體類型 也繼承於 ValueBox,

因此, ILBC 只要 判斷 ABox 是不是 ValueBox 的 子類, 就能夠知道 ABox 是否是 ValueBox,

同時, 經過 ValueBox<T>  的 泛型參數 T  能夠知道 value 的 類型 。

 

在 反射調用 方法 的 時候, 若是 傳給 MethodInfo 的 Invoke( object [ ]  args )  的 args 數組 裏 包含了 ValueBox 類型 的 參數,

ILBC 會 取出 ValueBox<T> 的 T value 字段 的 值 傳給 MethodInfo 包含的 方法,

 

那麼, 怎麼從 不一樣的 ValueBox 裏 來 取出 value 字段 的 值 呢? 

好比    IntBox,   ABox,   DateTimeBox  ,

 

這須要在 元數據  ILBC_Type  增長 2 個 字段 : 

 

struct    ILBC_Type

{

              ……

              int    valueOffset  ;       //   value 字段 的 偏移量

              int    valueSize  ;         //   value 字段 的 大小

}

 

對應的 ValueType 的 classLoader 裏 要 增長一段 代碼, 取得 當前類型 的   value 字段 的 偏移量 和 大小, 寫入  當前類型 的 ILBC_Type 結構體 的  valueOffset ,  valueSize  字段 。

 

好比, 以  IntBox 爲例,  IntBox 的 classLoader 裏會增長這樣一段代碼:

 

ILBC_Type  *    type    =     ILBC_gcNew( sizeof ( ILBC_Type ) )  ;

……

type -> valueOffset =  offsetOf ( IntBox, value )  ;      //   offsetOf  是 InnerC 提供的 關鍵字, 用於 取得 結構體 字段 的 偏移量

type -> valueSize =  sizeOf ( IntBox )  ;

 

當 加載 IntBox 類 時, 會 調用 classLoader, 這段代碼 也會執行, 這樣就把  IntBox 的 value 字段 的 偏移量 和 大小 都 記錄到 IntBox 的 元數據 ILBC_Type 中了 。

 

ILBC 的 MethodInfo.Invoke( object [ ] args )   方法 裏的 代碼 是 這樣:

 

ILBC_Reference o  =  object [ 0 ]  ;

……

 

int offset =  o.type  ->  valueOffset  ;       //   value 字段 在  ValueBox  裏的 偏移量

int size =  o.type  ->  valueSize  ;        //   value 字段 在  ValueBox 裏的 大小

 

//      根據  offset 和 size   取出  value 字段 的 值

 

以上是 代碼  。

 

能夠看出, 以上過程 比 在 代碼中   

 

IntBox iBox = new IntBox( 1 );

int i = iBox.value;

 

強類型 直接 取得 value 要  多 2 次 尋址,  會增長一些 性能損耗 。

 

經過上述設計, 程序員 能夠 自由的 定義 ValueBox,  一個 Value 類型 能夠 有 任意多個 ValueType ,

好比 ILBC 基礎庫 提供了  IntBox,  DateTimeBox,  開發者還能夠 本身定義 任意個 int ,  DateTiime 的 ValueBox 。

 

這樣一來, ILBC 的 數據類型 數據結構 的 架構 就 打通了 。

 

還有一個問題, ILBC_Type 是 元數據 ,  因此 每一個程序集 編譯 的 時候 都要  include   struct  ILBC_Type  所在的 頭文件 (.h 文件),

爲何每一個 程序集 都要 引用  ILBC_Type 的 頭文件 ?

由於 ILBC 調度程序 在 加載 Class 時 是 調用 classLoader 返回 ILBC_Type * , 就是說, ILBC_Type 結構體 是在 classLoader 裏 建立 和 構造 的 。

而  classLoader 是 屬於 程序集 的, 是 高級語言 編譯器 編譯 產生的,

 

若是 程序集 和 調度程序 之間 , 或者 程序集 之間 的  ILBC_Type 的 定義 不同, 就會發生錯誤  。

 

什麼是 定義 不同,  好比  ILBC 2.0  的  ILBC_Type  比  ILBC 1.0  增長了一些 字段, 或者 改變 了 字段 的 順序 。

 

這樣, 若是 把 1.0 的 程序集 放到 2.0 的 調度程序(運行時)裏 運行 就會有問題, 或者 2.0 和 1.0 的 程序集 放在一塊兒使用, 也會有問題 。

 

一般, 若是 2.0 增長了 ILBC_Type 的 字段, 那 1.0 的 程序集 放到 2.0 的 調度程序(運行時) 會有問題, 由於 2.0 的 調度程序 可能 越界訪問內存, 由於 1.0 的 ILBC_Type 沒有 2.0 新增 的 字段, 2.0 調度程序 對 1.0 的 ILBC_Type Struct 方法 訪問 新增的 字段 就會 越界 。

若是 2.0 沒有 新增 字段, 可是改變了 C 源代碼 裏 ILBC_Type 字段 的 順序, 那 會 形成 1.0 中 ILBC_Type 的 字段 偏移量 和 2.0 的 字段 偏移量 不一致, 一樣會形成 字段數據 的 錯誤訪問 。

 

因此, 爲了解決這個問題, 須要對 ILBC_Type 也進行 動態連接, 就是 把 當前 調度程序(運行時) 的 各字段 的 偏移量 告訴 各程序集 。

可是 ILBC 不會使用 加載 程序集 和 類 時候 的 動態連接, 而是會用 一段 專門 的 代碼 進行 元數據對象 好比 ILBC_Type 的 動態連接 。

ILBC 調度程序 會 提供 2 個 方法:

 

iint           ILBC_GetTypeSize()        //   返回  ILBC_Type 的 大小(Size)

ILBC_Type  *       ILBC_GetTypeFieldOffset (  fieldName  )           //  返回  ILBC_Type 的 名爲 fieldName 的 字段 的 偏移量

 

程序集 能夠 調用 這  2 個 方法 來 得到 當前 ILBC 調度程序(運行時) 的  ILBC_Type 的 大小(Size) 和 字段偏移量 。

 

這會不會 有點 過分設計 了  ?

相關文章
相關標籤/搜索