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

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

ILBC 规范 2的更多相关文章

  1. ILBC 规范

    本文是 VMBC / D# 项目 的 系列文章, 有关 VMBC / D# , 见 <我发起并创立了一个 VMBC 的 子项目 D#>(以下简称 <D#>) https://w ...

  2. 我发起了一个 ILBC 的 子项目 ILBC Studio

    ILBC  见 <ILBC 规范>  https://www.cnblogs.com/KSongKing/p/10354824.htm 发起这个项目的原因是, 本来想用 VsCode 来写 ...

  3. 我发起了一个 ILBC 的 子项目 EScript

    ILBC  见 <ILBC 规范>  https://www.cnblogs.com/KSongKing/p/10354824.html 今天装了个 VsCode , 听说 VsCode ...

  4. ILBC 运行时 (ILBC Runtime) 架构

    本文是 VMBC / D# 项目 的 系列文章, 有关 VMBC / D# , 见 <我发起并创立了一个 VMBC 的 子项目 D#>(以下简称 <D#>)  https:// ...

  5. 我发起了一个 用 C# 写 的 浏览器 开源项目 HtmlCore

    我之前还发起过一个 项目, 名字也叫 HtmlCore, 见 <我发起了一个 .Net 开源 跨平台 GUI (界面开发框架)项目 HtmlCore>  https://www.cnblo ...

  6. 自己写一个 Hash 表

    项目地址:  https://github.com/kelin-xycs/HashTableLib 为什么会想要自己写一个 Hash 表, 以前也想过 Hash 表 的 原理, 觉得很神奇, 不过最近 ...

  7. 我发起并创立了一个 VMBC 的 子项目 D#

    大家好, 我发起并创立了一个 VMBC 的 子项目 D#  . 有关 VMBC ,  请参考 <我发起了一个 用 C 语言 作为 中间语言 的 编译器 项目 VMBC>     https ...

  8. iOS代码规范(OC和Swift)

    下面说下iOS的代码规范问题,如果大家觉得还不错,可以直接用到项目中,有不同意见 可以在下面讨论下. 相信很多人工作中最烦的就是代码不规范,命名不规范,曾经见过一个VC里有3个按钮被命名为button ...

  9. node.js学习(三)简单的node程序&&模块简单使用&&commonJS规范&&深入理解模块原理

    一.一个简单的node程序 1.新建一个txt文件 2.修改后缀 修改之后会弹出这个,点击"是" 3.运行test.js 源文件 使用node.js运行之后的. 如果该路径下没有该 ...

随机推荐

  1. Linux ssh下实现免密码登录

    1.Linux 生成密钥 ssh-keygen -t rsa 进入“.ssh”会生成以下几个文件 id_rsa : 生成的私钥文件 id_rsa.pub : 生成的公钥文件 know_hosts : ...

  2. .NET Core 管道过滤器扩展

    if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseBrowserLink(); } else { app.UseEx ...

  3. windows常见软件库

    1.护卫神软件库 http://soft.huweishen.com/special/1.html 2.护卫神windows资料库 http://v.huweishen.com/ 3.国超软件下载 h ...

  4. Openstack中查看虚拟机console log的几种方法

    Openstack中有时候虚拟机启动不正常,这时可以通过查看虚拟机console log能得到一些有用的信息. 有这些方法可以查看或获取虚拟机console log: 1)openstack控制台图形 ...

  5. 7.2 GRASP原则二:信息专家 Information Expert

    2.GRASP原则二:信息专家 Information Expert  What is a general principle of assigning responsibility to obje ...

  6. 构建工具build tools

    构建工具是从源代码自动创建可执行应用程序的程序(例如.apk for android app).构建包括将代码编译,链接和打包成可用或可执行的形式. 基本上,构建自动化是脚本或自动化软件开发人员在日常 ...

  7. mongoose中connect()、createConnection()和connection的区别和作用

    转文:原文 1 mongoose简介 在使用mongodb数据库开发项目中,nodejs环境下可能会使用到mongoose模块连接并操作mongodb数据库.mongoose模块相当于Java中的数据 ...

  8. C++使用: C++中map的基本操作和用法

    在阅读SSD代码中发现作者使用了C++中的map方法,因此搜索该关联式容器的使用方法,在这里一并总结. 一.Map 簡介 Map是STL的一個容器,它提供一對一的hash. 第一個可以稱為關鍵字(ke ...

  9. Mac系统在Pycharm中切换解释器

    1. 2. 3. 4. 5.

  10. SqlServer表中添加新字段

    表中添加新字段ALTER TABLE 表名 ADD 字段名 VARCHAR(20) NULL 表中添加自增idalter table lianxi add id int primary key IDE ...