ILBC 规范 2

接上篇 《ILBC 规范》  https://www.cnblogs.com/KSongKing/p/10354824.html  ,

 

ILBC    的 目标 是    跨平台  跨设备 。

 

D# / ILBC  可以 编写 操作系统 内核 层 以上的 各种应用, 

其实 除了 进程调度 虚拟内存 文件系统  外,  其它 的 内核 模块 可以用  D#  编写, 比如 Socket 。

 

D# / ILBC  的 设计目标 是 保持简单, 比如  D#  支持  Lambda 表达式,  但是  LinQ  应该 由 库 来 支持,  与 语言 无关 。

另一方面,  ILBC  不打算 发展一个 庞大 的 优化体系 。   C++ ,  .Net / C#  的 优化体系 已经 庞大复杂 到 成为 大公司 也很难 承受 之重 了 。

我们不会这么干 。

ILBC 认为   “简单 就是 优化”  。

 

保持 简单设计 和 模块化,  模块化 会 带来一些 性能损耗,  这些 性能损耗 是 合理 的 。

 

保持 简单设计 和 模块化,  对于  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) 和 字段偏移量 。

 

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

 

 

 

 

posted on 2019-02-26 20:40  凯特琳  阅读(351)  评论(0编辑  收藏  举报

导航