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) 和 字段偏移量 。

 

这会不会 有点 过分设计 了  ?

相关文章
相关标签/搜索