本文是 VMBC / D# 项目 的 系列文章,html
有关 VMBC / D# , 见 《我发起并创立了一个 VMBC 的 子项目 D#》(如下简称 《D#》) http://www.javashuo.com/article/p-zziqptgy-s.html 。java
ILBC 系列文章 收录在 《ILBC 白皮书》 http://www.javashuo.com/article/p-bsuuysoc-bo.html 。算法
ILBC 规范:编程
加载程序集:数组
ILBC 程序集 有 2 种, 安全
1 Byte Code 程序集, 扩展名 为 .ilb, 表示 “ILBC Byte Code” 。性能优化
2 Native Code 程序集, 扩展名 遵循 操做系统 定义的 动态连接库 规范, 好比 Windows 上就是 .dll 文件,闭包
Native Code 程序集 就是 操做系统 定义的 动态连接库 。架构
假设 操做系统 是 Windows, 程序集 名字 是 A, 加载 A 的 过程 是:并发
在 当前目录 下 先查找 A.ilb, 若存在 则 JIT 编译 A.ilb 为 本地代码 A.dll, 加载 A.dll,
若找不到 A.ilb, 则找 A.dll, 若存在 则 加载 A.dll 。
加载 本地库 A.dll 的 方式 遵循 操做系统 定义的 动态连接 规范 。
JIT 编译 A.ilb 为 本地代码 并 加载 的 过程 能够在 内存中 完成, 不必定要 生成 文件 A.dll (若是 技术上 能够实现 在 内存 中加载的话)。
高级语言(D#) 编译 的 过程:
高级语言(D#) 编译 有 2 种方式,
1 AOT, 高级语言(D#) 编译器 先根据 高级语言(D#) 源代码 生成 C 语言 中间代码, 再由 InnerC (InnerC to Byte Code) 编译为表达式树, 再由 InnerC(Byte Code to Native Code) 把 表达式树 生成为 Native Code 。 Native Code 是一个 本地库, 好比 .dll 。
2 JIT , 高级语言(D#) 编译器 先根据 高级语言(D#) 源代码 生成 C 语言 中间代码, 再由 InnerC (InnerC to Byte Code) 编译为表达式树, 把 表达式树 序列化 获得 Byte Code, 将 Byte Code 保存为 ilb 文件 即 获得 Byte Code 程序集(.ilb) 。
.ilb 在 运行的时候 由 ILBC 运行时 的 InnerC (Byte Code to Native Code) 把 Byte Code 反序列化 为 表达式树, 再把 表达式树 编译为 Native Code 。
把 Native Code 程序集 加载到 应用程序 后, ILBC 运行时 会 调用 程序集 的 ILBC_Load() 函数, ILBC_Load() 会 建立一个 ILBC_Assembly 结构体, 并返回这个 结构体 的 指针, ILBC_Assembly 结构体 包含了 程序集 的 元数据 信息, 相似 .Net / C# 中 的 System.Reflection.Assembly 。
元数据 就是 一堆 结构体(Struct), 这些 Struct 及 ILBC_Load() 函数 的 代码是由 高级语言(D#)编译器 生成, 代码以下:
struct ILBC_Assembly
{
ILBC_ClassLoader classLoaderList [ n ] ; // n 是 程序集 中 Class 的 数量, 由 高级语言(D#) 编译器 在 编译时 指定
// classLoader 包含了 加载 Class 的 函数 的 函数指针 (保存在 load 字段 里)
// 每一个 Class 有一个 classLoader,
// classLoaderList 是 保存 classLoader 的 数组,
// 在 ILBC 运行时 加载 Class 时 会调用 classLoader.load 保存的 函数指针 指向 的 函数, 具体内容见下文
// Class 加载完成获得的 Type 对象 保存在 type 字段 里
}
struct ILBC_ClassLoader
{
char * className ; // Class 名字
void * load ; // 加载 Class 的 函数 的 函数指针
ILBC_Type * type = 0 ; // 加载 Class 完成后把 Type 对象 保存在这里
}
struct ILBC_Type
{
char * name ; // Class 名字
int size ; // Class 占用的 空间大小(字节数)
ILBC_Field fieldList [ n ] ; // n 是 Class 中 Field 的 数量, 由 高级语言(D#) 编译器 在 编译时 指定
int fieldCount ; // C 语言数组 的 长度 须要 本身记录
ILBC_Method methodList [ n ] ; // n 是 Class 中 Method 的 数量, 由 高级语言(D#) 编译器 在 编译时 指定
int methodCount ; // C 语言数组 的 长度 须要 本身记录
}
struct ILBC_Field
{
char name [ n ] = "字段名" ; // n 应和 字段名字符串 的 字节数 相等, n 由 高级语言(D#) 编译器 在 编译时 指定
int size; // 字段 占用的 字节数
int offset; // 字段 相对于 ILBC_Field 结构体 的 首地址 的 偏移量
// ILBC_Type * type ;
char * type ; // type 不能 声明为 ILBC_Type 或者 ILBC_Type * 类型, 由于会形成 Type 和 Field 之间的 循环引用,
// 因此先声明为 char * (字符串), 保存 Type 的名字, 经过 GetFieldType() 之类 的 方法 来返回 Type 对象,
// Type 对象 就至关于 这里的 ILBC_Type 或者 ILBC_Type * 。
}
struct ILBC_Method
{
char name [ n ] = "方法名"; // n 应和 方法名字符串 的 字节数 相等, n 由 高级语言(D#) 编译器 在 编译时 指定
ILBC_Argument * argList [ n ] ; // n 是 方法 中 参数 的 数量, 由 高级语言(D#) 编译器 在 编译时 指定
Type * returnValue ; // 返回值 类型
void * funcPtr ; // Method 对应的 函数指针
}
struct ILBC_Argument
{
char name [ n ] = "参数名"; // n 应和 参数名字符串 的 字节数 相等, n 由 高级语言(D#) 编译器 在 编译时 指定
ILBC_Type * type; // 参数类型
}
看到这里, 是否是跟 C# 反射里的 AssemblyInfo, Type, FieldInfo, MethodInfo 很像 ?
是的, ILBC 也要支持 完整的 元数据 架构, 元数据 用于 动态连接 和 反射 。
接下来 是 ILBC_Load() 相关的 代码:
假设 程序集 名字 是 B, 包含了 Person 类 和 Animal 类 2 个 类, Person 类 有 2 个字段 name, age, 有 2 个方法 Sing(0, Smile() ,
void * ILBC_ClassLoaderList_B [ 2 ] ; // 数组长度 2 表示 B 程序集 包含了 2 个 类
ILBC_Assembly * ILBC_Load()
{
ILBC_Assembly * assembly = ILBC_gcNew( sizeof ( ILBC_Assembly ) ) ;
assembly.classLoaderList [ 0 ].className = "Person" ;
assembly.classLoaderList [ 0 ].load = & ILBC_LoadClass_B_Person ;
assembly.classLoaderList [ 1 ].className = "Animal" ;
assembly.classLoaderList [ 1 ].load = & ILBC_LoadClass_B_Animal ;
return assembly ;
}
ILBC_Type * ILBC_LoadClass_B_Person()
{
ILBC_Type * type = ILBC_gcNew ( sizeof ( ILBC_Type ) );
// ILBC_gcNew( ) 是 ILBC 提供的一个 库函数, 用于 在 堆 里申请一块空间, 这里是在 堆 里 建立一个 ILBC_Type 结构体
type.name = "Person";
type.size = 8; // Class 占用的 空间大小(字节数), name 字段是 char * 类型, 假设 指针 是 32 位 地址, 占用 4 个 字节, age 是 int 类型, 假设是 32 位整数, 占用 4 个字节, 那么 Class 的 占用字节数 就是 4 + 4 = 8, 即 size = 8; , size 是由 编译器 计算决定的
type.fieldList [ 0 ].name = "name";
type.fieldList [ 0 ].size = // String 是 引用类型, 因此这里是 引用 的 Size
type.fieldList [ 0 ].type = "String"; // 假设 基础库 提供了 String 类型
type.fieldList [ 1 ].name = "age";
type.fieldList [ 1 ].size = 4; // 假设 int 是 32 位 整数类型
type.fieldList [ 1 ].type = "Int32"; // 假设 int 是 32 位 整数类型, 且 基础库 提供的 32 位 整数类型 是 Int32
type.methodList [ 0 ].name = "Sing";
// 由于 Sing() 方法 没有 参数, 因此 argList [ 0 ] 长度为 0, 不用 初始化
type.methodList [ 0 ].funcPtr = & ILBC_Class_B_Sing; // ILBC_Class_B_Sing 是 Sing() 方法 对应的 函数, 由 编译器 生成
type.methodList [ 1 ].name = "Smile";
// 由于 Smile() 方法 没有 参数, 因此 argList [ 0 ] 长度为 0, 不用 初始化
type.methodList [ 1 ].funcPtr = & ILBC_Class_B_Smile; // ILBC_Class_B_Smile 是 Smile() 方法 对应的 函数, 由 编译器 生成
return type;
}
ILBC_LoadClass_B_Animal() 函数 和 ILBC_LoadClass_B_Person() 函数 相似 。
当 程序 中 第一次 用到 程序集 时, ILBC 运行时(调度程序) 才会 加载 程序集,
第一次 用到 程序集 是指 第一次 用到 程序集 里的 类,
第一次 用到 类 是指 第一次 建立对象( new 类() ) 或者 第一次 调用静态方法( 类.静态方法() ) 、 第一次 访问静态字段( 类.静态字段 ) 这 3 种状况 。
类 也是 在 第一次 用到 时 加载,
固然, 第一次 加载 程序集 是 必定会 加载一个 类, 但 其它 的 类 会在 用到 时 才加载 。
加载类 完成时 会调用 类 的 静态构造函数 。
调度程序 加载完 程序集 后, 会把 程序集 的 ILBC_Load() 返回的 ILBC_Assembly 结构体 的 指针 保存到一个 名字是 ILBC_AssemblyList 的 链表 里,
新加载 的 程序集 的 ILBC_Assembly 结构体 的 指针 会 追加到 这个 链表 里 。
ILBC_AssemblyList 是 调度程序 里 的 一个 全局变量:
ILBC_LinkedList * ILBC_AssemblyList ;
ILBC_LinkedList 是一个 链表 实现, ILBC_LinkedList 自己是一个 结构体, 定义见下文, 再配合一些 向链表追加元素 、删除元素 等函数 就是 一个 链表 实现, 函数 的 部分 略 。
struct ILBC_LinkedList
{
ILBC_LinkedListNode * first ; // 链表 头指针
ILBC_LinkedListNode * last ; // 链表 尾指针
}
struct ILBC_LinkedListNode
{
ILBC_LinkedListNode * before ; // 上一个 节点
ILBC_LinkedListNode * next ; // 下一个 节点
void * element ; // 节点包含的元素, 就是 实际存放 的 数据
}
假设 有 A 、B 2 个 程序集, A 引用了 B,
B 中 包含 Class Person, Person 有 构造函数 Person() { } , 那么, A 中 new Person() 的 代码 会被 编译成:
void * ILBC_Class_Person_Constructor = 0 ; // 这是 A 里的 全局变量, 表示 Person 的 构造函数 的 函数指针, 0 表示 空指针, 也表示 未初始化
……
// 代码 中 调用 Person 类 构造函数 的 代码
// ILBC_Class_Person 是 高级语言(D#) 编译器 生成的 表示 Person 类 的 Struct, 包含了 Person 类 的 字段
if ( ! ILBC_ifClassInit_Person )
{
ILBC_Init_Linked_Class_Person() ; // 初始化 Person 类
}
// ILBC_Linked_ClassSize_Person 是一个 全局变量, 表示 Person 类 占用的 空间大小(字节数)
void * person = ILBC_gcNew( ILBC_Linked_ClassSize_Person );
// Person 类 初始化 后, 构造函数 指针 ILBC_Linked_Class_Person_Constructor 就被 初始化 了(填入了 Person 构造函数 的 地址), 就能够调用了
ILBC_Linked_Class_Person_Constructor ( person ); // 调用 Person 类 构造函数, 把 person 结构体 指针 传给 构造函数 进行 初始化
调用 Person 类的 静态字段 和 静态方法 的 代码 和 上面 相似, 只须要把 最后一句 代码 换成:
字段类型 变量 = * ILBC_Linked_Class_Person_静态字段名 ; // 访问 静态字段
ILBC_Linked_Class_Person_静态函数名 ( 参数列表 ) ; // 调用 静态函数
ILBC_ifClassInit_Person 是一个 全局变量, 表示 Person 类 是否 已经 初始化, 定义以下:
char ILBC_ifClassInit_Person = 0 ;
B 程序集 的 Person 类 在 A 程序集 里的 “初始化” 是指 完成了 Person 类 在 A 里的 连接工做, 初始化 完成后, A 的 代码 就能够 访问 Person 类 了 。
访问 Person 类 包括 建立对象(new Person() )、调用函数 、访问字段 。
连接工做 包括
类连接, 向 A 里定义好的 保存 Person 类 的 占用空间大小(Size (字节数)) 的 全局变量 写入 类 的 占用空间大小(Size (字节数)),
字段连接 是 向 A 里定义好的 保存 Person 类的 各个字段的偏移量 的 变量 写入 字段的偏移量,
函数连接 是 向 A 里定义好的 保存 Person 类 的 各个方法 的 函数地址(函数指针) 的 变量 写入 函数地址, 包括 构造函数 和 成员函数 。
ILBC_Linked_Class_Person_Constructor 是 一个 全局变量, 表示 Person 类 的 构造函数 的 函数指针,定义以下:
void * ILBC_Linked_Class_Person_Constructor ;
ILBC_Init_Linked_Class_Person () 的 代码以下:
ILBC_Init_Linked_Class_Person ()
{
lock ( ILBC_ifClassInit_Person )
{
if ( ! ILBC_ifClassInit_Person )
{
ILBC_Type * type = ILBC_Runtime_GetType( "B", "Person" ) ; // 参数 "B" 表示 程序集 名字, "Person" 表示 类 名
ILBC_Linked_ClassSize_Person = type.size ;
// ILBC_Linked_Class_Person_name 是 保存 Person 类 name 字段 偏移量 的 全局变量, 由 编译器 生成, 值 须要在 加载类 的 时候 初始化, 也就是 下面的 代码 里 初始化
// ILBC_Linked_ClassFieldType_Person_name 是 保存 Person 类 name 字段 类型(类型名字) 的 常量, 由 编译器 生成, 值 由 编译器 给出, 值 就是 name 字段 的 类型 的 名字
ILBC_Init_Linked_Class_Field( & ILBC_Linked_Class_Person_name, ILBC_Linked_ClassFieldType_Person_name, "name", type ); // 初始化 name 字段 的 偏移量
ILBC_Init_Linked_Class_Field( & ILBC_Linked_Class_Person_age, ILBC_Linked_ClassFieldType_Person_age, "age", type ); // 初始化 age 字段 的 偏移量
// 若是有 静态字段, 也是 一样的 初始化, 不过 静态字段 应该 不是 初始化 偏移量, 而是 直接 是 地址,
// 静态字段 的 指针变量 好比 “变量类型 * ILBC_Linked_Class_Person_静态字段名 ;”
ILBC_Init_Linked_Class_Person_Constructor( type ); // 初始化 构造函数 的 函数指针
ILBC_Init_Linked_Class_Method( & ILBC_Linked_Class_Person_Sing, "Sing", type ); // 初始化 Sing() 函数 的 函数指针
ILBC_Init_Linked_Class_Method( & ILBC_Linked_Class_Person_Smile "Smile", type ); // 初始化 Smile() 函数 的 函数指针
// 若是有 静态方法, 也是 一样的 初始化, 静态方法 的 指针变量 好比 “void * ILBC_Init_Linked_Class_Person_静态方法名 ;”
ILBC_ifClassInit_Person = 1 ;
}
}
}
void ILBC_Init_Linked_Class_Field( int * fieldOffsetVar, char * fieldType, char * name, ILBC_Type * type )
{
for (int i = 0; i<type.fieldCount; i++)
{
ILBC_Field * field = & type.fieldList [ i ];
if ( field.name == name ) // 这句代码是 伪码 , 意思是 判断 2 个字符串 是否相等
{
// 咱们这里 判断 类型 是否 相同 是 不严格的, 只是 判断 了 名字
// 这里 涉及到 类型检查 和 类型安全, 详细讨论 见 文章 最后 总结 部分
if ( field.type ! = fieldType ) // 这句代码是 伪码 , 意思是 判断 2 个字符串 是否相等
throw new Exception ( "名字为 " + name + " 的 字段 的 类型 与 引用 的 元数据 里的 类型 不符 。" ); // 这句代码 是 伪码, 应该是 函数 增长一个 errorCode 参数, 经过 errorCode 参数返回异常
* fieldOffsetVar = field -> offset;
return ;
}
}
throw new Exception( "找不到名字是 " + name + " 的 字段 。" ); // 这句代码 是 伪码, 应该是 函数 增长一个 errorCode 参数, 经过 errorCode 参数返回异常
}
void ILBC_Init_Linked_Class_Method ( void * funcPtrVar, char * name, ILBC_Type * type )
{
for (int i = 0; i<type.methodCount; i++)
{
ILBC_Method * method = & type.methodList [ i ];
if ( method.name == name ) // 这句代码是 伪码 , 意思是 判断 2 个字符串 是否相等
{
* funcPtrVar = method -> funcPtr;
return ;
}
}
throw new Exception( "找不到名字是 " + name + " 的 方法 。" ); // 这句代码 也是 伪码, 应该是 函数 增长一个 errorCode 参数, 经过 errorCode 参数返回异常
}
相关的 全局变量 / 常量 总结以下:
char ILBC_ifClassInit_Person = 0 ; // Person 类 是否 已 初始化
int ILBC_Linked_ClassSize_Person ; // Person 类 占用的 空间大小(字节数), 值 由 编译器 在 编译 A 项目时 根据 B 的 元数据 给出
int ILBC_Linked_Class_Person_name ; // Person 类 name 字段 的 偏移量
int ILBC_Linked_Class_Person_age ; // Person 类 age 字段 的 偏移量
const char * ILBC_Linked_ClassFieldType_Person_name ; // Person 类 name 字段 的 类型(类型名字)
const char * ILBC_Linked_ClassFieldType_Person_age ; // Person 类 age 字段 的 类型(类型名字)
void * ILBC_Linked_Class_Person_Constructor ; // Person 类 的 构造函数 函数指针
void * ILBC_Linked_Class_Person_Sing ; // Person 类 的 Sing 方法 函数指针
void * ILBC_Linked_Class_Person_Smile ; // Person 类 的 Smile 方法 函数指针
看到这里, 你们可能会问, 若是 构造函数 和 方法 有 重载 怎么办 ?
确实 有这个问题, 这个 须要 再做 进一步 的 细化设计, 如今 先 略过 。
ILBC_Runtime_GetType() 函数 的 定义以下:
ILBC_Type * ILBC_Runtime_GetType( char * assemblyName, char * typeName )
{
先在 ILBC_AssemblyList 中查找 名字 是 assemblyName 的 程序集 是否已存在,
若是 不存在, 就先 加载 程序集,
加载程序集 的 过程 上文 中 提过, 就是 先把 程序集 加载 到 应用程序, 再调用 程序集 的 ILBC_Load() 函数, 返回一个 ILBC_Assembly 结构体 的 指针,
调度程序 把 这个 结构体 指针 保存 到 ILBC_AssemblyList 这个 链表 里 。
找到 程序集 后, 再在 assembly.classLoaderList 里 找 名字 是 className 的 classLoader,
找到 classLoader 之后, 看 classLoader.type 字段 是不是 空指针(0), 若是是, 就说明 Class 尚未 加载,
就 加载 Class, 加载 Class 获得的 Type 对象 就存放在 classLoader.type 字段 里 。
加载 Class 的 过程 上文中 讲述过, 假设 加载 B 程序集 的 Person 对象,
就是调用 B 程序集 里的 ILBC_LoadClass_B_Person() 函数, 该 函数 加载 Person 类, 并返回 表示 Person 类 的 Type 对象 的 ILBC_Type 结构体 的 指针 。
调用 类 的 静态构造函数 ************* 这里 加个 着重号, 类 加载 完成后 调用 类 的 静态构造函数
返回 ILBC_Type 结构体 的 指针 。
}
访问 Person 对象 的 字段 的 代码 是:
void * person ;
……
char * name = * ( person + ILBC_Linked_Class_Person_name ) ;
int age = * ( person + ILBC_Linked_Class_Person_age ) ;
调用 Person 对象 的 方法 的 代码 是:
void * person ;
ILBC_Linked_Class_Person_Sing ( person ) ; // 调用 Sing() 方法, person 参数 是 this 指针
ILBC_Linked_Class_Person_Smile ( person ) ; // 调用 Smile() 方法, person 参数 是 this 指针
总结一下:
ILBC 的 连接 是 相似 .Net / C# 的 动态连接,
ILBC 的 连接 以 程序集 为 单位, 采用 延迟加载(Lazy Load) 的方式, 只有用到 程序集 的时候才加载, “用到” 是指 第一次 用到 程序集 里的 类(Class) 。
将 程序集 加载 到 应用程序 之后, 对 程序集 里的 类(Class) 也采用 延迟加载(Lazy Load) 的方式,
第一次 用到 类 的 时候才会 初始化 类 的 连接表, 连接表 初始化 完成后, 就 能够 调用 类 了, 包括 建立对象,访问 字段 和 方法 。
连接表 不是 一个 “表”, 而是 一堆 全局变量 / 常量, 就是 上文 中 列举出的 全局变量 / 常量, 这里再列举出来看看:
char ILBC_ifClassInit_Person = 0 ; // Person 类 是否 已 初始化
int ILBC_Linked_ClassSize_Person ; // Person 类 占用的 空间大小(字节数), 值 由 编译器 在 编译 A 项目时 根据 B 的 元数据 给出
int ILBC_Linked_Class_Person_name ; // Person 类 name 字段 的 偏移量
int ILBC_Linked_Class_Person_age ; // Person 类 age 字段 的 偏移量
const char * ILBC_Linked_ClassFieldType_Person_name ; // Person 类 name 字段 的 类型(类型名字)
const char * ILBC_Linked_ClassFieldType_Person_age ; // Person 类 age 字段 的 类型(类型名字)
void * ILBC_Linked_Class_Person_Constructor ; // Person 类 的 构造函数 函数指针
void * ILBC_Linked_Class_Person_Sing ; // Person 类 的 Sing 方法 函数指针
void * ILBC_Linked_Class_Person_Smile ; // Person 类 的 Smile 方法 函数指针
这些 全局变量 是 A 里 定义 的, 是 A 里 引用 B 的 连接表 。
注意, Class 的 加载 是 在 ILBC 运行时 里 进行的, 一个 Class 的 加载 对于 整个 应用程序 只进行一次,
Class 的 连接表 初始化(Init) 是 和 程序集 相关的, 假设有 A 、B 、C 3 个 程序集 引用了 D 程序集,
那么 当 A 用到 D 的时候, 会 初始化 A 里 引用 D 的 连接表,
当 B 用到 D 的时候, 会 初始化 B 里 引用 D 的 连接表,
当 C 用到 D 的时候, 会 初始化 C 里 引用 D 的 连接表 。
连接表 是 属于 程序集 的, 假设 A 引用了 B C D, 那么 A 里 会有 B C D 的 连接表,
也就是说 上面的 全局变量 会在 A 里 声明 3 组, 分别 对应 B C D 程序集 。
说到这里, 咱们会发现, 上面的 全局变量 的 命名 没有 包含 程序集 的 名字, 好比 ILBC_Linked_Class_Person_name,
这个 表示 Person 类 的 name 字段 的 偏移量,
可是 并无 表示出 Person 类 是 哪个 程序集 的 。
因此, 应该 给 变量 增长一个 分隔符(链接符) 来 分隔(链接) 各项信息,
咱们规定, InnerC 应支持 在 变量名 里 使用 "<>" 字符串, 这样可使用 "<>" 来 分隔(链接) 各项信息 。
注意, 是 "<>" 字符串, 不是 "<", 也不是 ">" , 也不是 "< …… >" ,
好比, a<>b 这个 变量名 是 合法的, a<b 是 不合法 的, a>b 是 不合法的, a<b>c 这个变量名 也是 不合法的 。
ILBC_Linked_Class_Person_name 能够 这样 来 表示:
ILBC_Linked<>B<>Person<>name , 这表示 连接(引用) 的 B 程序集 的 Person 类 的 name 字段 的 偏移量
"<>" 字符串 在 D# 里 是 不能用于 程序集 名字空间 类 字段 方法 的 名字 的, 因此能够在 C 中间语言 里 用在 变量名 里 做为 分隔符(链接符) 。
ILBC 运行时 调度程序 应提供 如下 函数:
ILBC_Type * ILBC_Runtime_GetType( char * assemblyName, char * typeName )
该函数用于 返回 指定的 程序集名 的 程序集 中 指定的 类名 的 类 的 Type 对象
ILBC_Type 是 调度程序 中 定义的 结构体, 为了能让 程序集 访问, 须要 高级语言(D#)编译器 引用 调度程序 发布 的 头文件(.h 文件),
这个 头文件 咱们 能够命名为 ILBC_Runtime.h , 里面 会 包含 ILBC_Assembly 、ILBC_ClassLoader 、ILBC_Type 、ILBC_Field 、ILBC_Method 、ILBC_Argument 等 结构体 定义 。
void * ILBC_Runtime_heapNew ( int size )
该函数用于 从 堆 里 分配 一块 指定大小 的 内存块, 参数 size 是 内存块 大小(字节数) 。 返回值 是 内存块 指针 。
ILBC 运行时 本身实现了一个 堆 和 GC 。
固然 对应的 还会有一个 void ILBC_Runtime_heapFree ( void * ptr, int size ) 函数,
C 语言 里的 void free(void *ptr); 是没有 size 参数的, So 。
没事, 这个能够保留讨论 。
ILBC 程序集 应提供 如下 函数:
ILBC_Assembly * ILBC_Load()
该函数 在 ILBC 运行时 调度程序 加载 程序集 时 调用, 负责 程序集 的 初始化 工做,
包括 建立一个 ILBC_Assembly 结构体, 并 初始化 ILBC_Assembly 结构体 的 classLoaderList 字段, 能够参考 上文 代码 。
ILBC 运行时 调度程序 接收到 程序集 的 ILBC_Load() 函数 返回的 ILBC_Assembly 结构体 指针 后, 会 将 该指针 保存到 ILBC_AssemblyList 中,
ILBC_Assembly 是 调度程序 里的一个 全局变量, 是一个 链表 。
说到 链表, 调度程序 里 保存 Assembly 的 列表 ILBC_AssemblyList 是 链表,
Assembly 里 保存 Type 的 列表 classLoaderList 是 数组,
Type 里 保存 Field 、Method 的 列表 fieldList, methodList 也是 数组,
而 上文 中 根据 名字 查找 Field 、Method 的算法是 遍历 数组, 查找 Assembly 、Type 的部分虽然没有直接用代码写出来, 但应该是 遍历 链表 / 数组 。
从 性能优化 的 角度 来看, 根据 名字 查找 成员(Assembly, Type, Field, Method 等) 应该 优化 为 查找 Hash 表,
这个 优化 关系 到 加载 程序集 和 类 的 效率, 也是 反射 的 效率 。
动态连接 程序集, 加载 程序集 和 类, 就是一个 反射 的 过程 。
相传 .Net 2.0 对 反射 性能 进行了优化, 使得 反射 性能 获得了 明显的 提高, 大概 也是 加入了 Hash 表 吧 ! 哈哈哈 。
而 .Net 对 反射 进行了 优化, 理论上 自己 就是 提高了 动态连接 程序集 、加载 程序集 和 类 的 效率, 也就是 提高了 .Net 运行 应用程序 的 效率 。
在 .Net / C# 里, Hash 表 可使用 Dictionary, 但在 IL 里, 估计 得 本身写一个 。
不过 这也是一件 好玩的事情,
我接下来 会 写一篇 文章 《本身写一个 Hash 表》 。
《本身写一个 Hash 表》 这篇文章已经写好了, 见 http://www.javashuo.com/article/p-ervzhqtj-dk.html 。
调度程序 的 ILBC_Runtime_GetType() 、 ILBC_Runtime_heapNew() 、 ILBC_Runtime_heapFree() 和 程序集 的 ILBC_Link() 这 4 个 函数 是 操做系统 动态连接库 规范 定义 的 动态连接库 导出函数 。
这么考虑 主要是 以前 并未打算 本身实现一个 C 编译器,
但 如今 既然 咱们要本身 实现一个 C 编译器(InnerC), 那么 这些就 不成问题了,
这 4 个 函数 能够 用 咱们本身 定义的 规则 来 访问 。
好比, 咱们能够 定义 在 调度程序 的 开头 的 一段字节 来 保存 ILBC_Runtime_GetType() 、 ILBC_Runtime_heapNew() 、 ILBC_Runtime_heapFree() 这 3 个 函数 的 地址, 在 程序集 的 开头 的 一段字节 来 保存 ILBC_Link() 函数 的 地址 。
这样, 调度程序 和 程序集 之间 就能够经过 函数指针 来 调用 接口函数, 速度很快 。
但 若是要这样的话, 调度程序 和 程序集 应该是 同构 的, 同构 是指 同一种语言 、同一个编译器 编译 产生的 本地代码 。
因此, 调度程序 也应该是 用 InnerC 编写 和 编译 生成的 。
这么一来, InnerC 的 地位 就 很重要了 。 ^^
InnerC 是 ILBC 的 基础 。
不过 这样一来, InnerC 可能也须要 支持 结构体, 否则 很差写 。 呵呵 。
这样的话, ILBC 本地代码 程序集 就 不须要 是 操做系统 定义的 动态连接库, 而是 按照 ILBC 规范 编译成的 本地代码, 咱们能够把 这种 按照 ILBC 规范 编译成的 本地代码 程序集 的 扩展名 命名为 “.iln”, 表示 “ILBC Native Code” 。
关于 泛型, 忽然想到, 泛型 纯粹 是 编译期 检查, 除此之外 什么 都 不用作, 顶多为 每一个 泛型类型 生成一个 具体类型, 经过 具体类型 能够获取 泛型参数类型 就能够了 。
但 泛型 确实能 提升性能, 由于 泛型 不须要 运行期类型转换(Cast),
运行期 类型转换 就是 一堆 if else ,
咱们能够看看 编译后 生成的代码,
源代码:
B b = new B();
A a = (A) b ;
编译后的代码:
B b = new B();
A a;
Type aType = typeof(A) ;
Type bType = typeof(B);
if ( aType == bType )
a.ptr = b.ptr ; // 这句是 伪码, 表示 b 引用 的 指针值 赋给 a 引用
else if ( aType 是 bType 的 父类)
a.ptr = b.ptr ;
else if ( 其它 转型 规则 )
a.ptr = b.ptr ; // 或者 其它 转型方式, 好比 拆箱装箱
else
throw new CastException( "没法将 " + bType + " 的 对象 转换为 " + aType + " 。" ) ;
而 泛型 是这样:
List<string> strList = new List<string>();
strList [ 0 ] = "aa" ;
string s = strList [ 0 ];
编译后的代码:
List<string> strList = new List<string>();
strList [ 0 ] = "aa" ;
string s;
s.ptr = strList [ 0 ].ptr; // 指针 直接 赋值
由于 编译期 已经作过 类型检查, 因此 引用 的 指针直接赋值, 因此 泛型 没有 性能损耗 。
固然, JIT 编译器 须要为 泛型类型 生成 具体类型, 使得 泛型类型 能够按照 CLR 的 规则 “是一个 正常的 类型”, 经过 具体类型 能够获取 泛型参数类型 。
泛型类型? 具体类型? 泛型参数类型?
有点绕 。
假设有 class A<T> ,
那么, A<T> 叫 泛型类型,
A<string> 叫 具体类型,
T , 叫 泛型参数类型, 好比 A<string> 的 泛型参数类型 是 string 。
对于 ILBC, 具体类型 能够在 C 中间代码 里 生成 。
再来看看 基础类型,
基础类型 包括 值类型 、数组 、String,
ILBC 会 内置实现 基础类型,
值类型 包括 int, long, float, double, char 等, 这些 类型 在 C 语言 里 都有 对应的类型, 可是为了实现 “一切皆对象”, 即 全部类型, 包括 值类型 和 引用类型 都从 object 继承 这个 架构, 还须要 对 C 语言 里的 int, long, float, double, char 等 作一个包装, 用一个 结构体(Struct) 来把 int, long, float, double, char 等 包起来 。
包起来之后, 为了提升执行效率, 编译器 还须要 对 代码 进行一些 优化, 对于 栈 里 分配 的 int, long, float, double, char 等 的 加减乘除 等 运算 就 直接用 C 语言 的 int, long, float, double, char 等 的 加减乘除 等 运算, 即 不用 结构体 包起来, 而是 直接编译为 C 语言 里 的 int, long, float, double, char 等 。
而 对于
void Foo( object o )
{
Type t = o.GetType() ;
}
这样的代码, 由于 参数 o 多是 任意类型, 因此 传给 参数 o 的 int 类型 就 应该是 包装过的 int, 也就是 一个 结构体, 好比:
struct Int32
{
int val ; // 值
string typeName ; // 类型名字, 或者 广义的来讲, 这个 字段 表示 类型信息
}
Object 的 GetType() 方法 经过 这个 字段 返回 Type 对象 。
而 对于 typeof(int) 则 能够在 编译器 编译为 Hard Code 返回 Int32 的 Type 对象 。
又好比 对于 Convert.ChangeType( object o, Type t ) 方法,
假设 参数 o 要传一个 int 类型的话, 也须要 传 包装过的 int 类型, 也就是 上文 定义的 struct Int32 。
因此, InnerC 的 InnerC to Byte Code 模块, 除了 语法分析器, 又增长了一个模块, 优化器 。
So ……
语法分析器 产生表达式对象树 后, 把 表达式树 传给 优化器, 优化器 能够 阅读 表达式树, 发现能够优化 的 地方 能够修改 表达式树,
修改后的 表达式树 就是 优化后的 表达式树, 再 传给 Byte Code to Native Code, 编译为 本地代码 。
能够把 优化后 的 表达式树 再 逆向为 C 代码, 这样就能够 看到 优化后 的 C 中间代码 。
InnerC 的 InnerC to Byte Code 能够提供 逆向 的 功能 。
再来看 结构体(Struct),
D# / ILBC 不打算 提供 结构体, 由于 结构体 没什么用 。 ^^
提供 结构体 会让 ILBC 的 设计 变得 复杂, 增长了 研发成本 。
固然 结构体 使用 栈空间, 减小了 堆 管理 和 GC 的 工做, 可是 从 线程 的角度来看, 栈 比较大的话 线程切换 的 性能消耗 可能 也 比较大 。 看你怎么看了 ~ 。
出于 动态连接 的 要求, .Net / C# 的 结构体 应该不是 在 编译期 静态分配内存空间 的, 而是 在 运行期 分配空间, 由于 结构体 保存 在 栈 里, 因此 是 动态分配 栈 空间 。
因此, .Net / C# 里 建立 结构体 也是用 new 关键字 。
D# / ILBC 的 DateTime 类型 是一个 引用类型(Class), 是一个 能够用 D# 写的 普通的 引用类型(Class) 。
.Net / C# 的 DateTime 是 值类型, 我估计 .Net / C# 如今 想把 DateTime 改为 Class, 可是 改不过来了 。 哈哈哈哈。
如 上文所述, D# / ILBC 提供 的 基础类型 是 基础类型 值类型 、数组 、String, 值类型 包括 int, long, float, double, char 等,
基础类型 由 D# / ILBC 内置实现 。
其它类型 由 D# 编写, 包括 DateTime 及 基础库 里的 各类类型 。
说到 基础库, 就会想到 和 本地代码 的 交互性, 就是 访问 本地代码,
在 .Net / C# 里, 托管代码 和 本地代码 之间 的 交互 使用 P / Invoke ,
对于 D# / ILBC, 会提供这样一些接口:
1 指针
2 申请一段 非托管内存, 非托管内存 不会由 GC 回收, 须要 手动回收
3 回收一段 非托管内存
有了 这 3 个 接口, 基本上就够了, 能够 访问 非托管代码 了 。
非托管内存 和 托管内存 同属一个堆, 只是 GC 不会回收 非托管内存 。
再来看 类型检查 和 类型安全,
上文中 初始化 连接表 的 字段偏移量 时 会对 字段类型 进行 检查, A 程序集 在 运行期 连接 的 B 程序集 的 Person 类 的 字段类型 应该 和 A 程序集 在 编译期 引用 的 B 程序集 的 Person 类 的 类型一致, 不然 认为 类型不匹配, 不容许连接, 也就是 不容许 使用 如今 的 Person 类 。
为何要进行 类型检查 ?
若是 类型不匹配, 会发生 访问了不应访问的内存 的 错误, 这种 错误 难以排查, 产生的 结果 是 意想不到 的,
这也是 java, .Net 这类 虚拟机(运行时) 出现 要 解决的 问题 吧 !
java, .Net 这类 虚拟机(运行时) 经过 运行期 类型检查 来 实现 类型安全, 避免 类型错误 致使 访问了错误的内存 。
.Net / C# 对 类型 的 检查 是 严格准确 的, 全部类型 最终会 归结到 基础类型(值类型 数组 String),
而 基础类型 都是 .Net 内置类型, 是 强名称 的, 能够 严格 的 检查,
推而广之, .Net 基础库 都是 强名称 的, 能够 准确 的 检查 类型,
对于 开发人员 本身编写 的 类, 也能够 根据 字段 逐一校验, 实际加载 的 程序集 的 类 的 字段 应包含 大于等于 编译时 引用的 程序集 的 类 的 字段, 字段 名字 和 类型 必须 匹配, 好比 编译时 引用 的 Person 类 的 name 字段 是 String 类, 那么 运行期 加载的 B 的 Person 类 也应该要有 name 字段, 且 类型 应该是 String, 不然 认为 类型 不匹配 。
咱们 上文 对 字段 类型 的 检查 是 不严格 的, 只是 检查 类型 的 名字 。
应该注意的是, 强名称 类型检查 不表明 内存安全, 强名称 只是 验证 程序集(类) 的 身份, 可是 类 若是 自己 存在 Bug, 也会发生 访问了 自身对象 之外 的 内存 的 问题 。
可是, 因为 数组 做为 基础类型 提供, 数组 中 会判断 “索引 是否 超出 数组界限”, 因此, 开发者 写的 代码 通常 应该不会发生 访问内存越界(访问了 自身对象 之外 的 内存) 的 问题 。
固然 这仅限于 托管代码, 对于 非托管代码, 由于 指针 的 存在, 因此有可能发生 访问内存越界 的 问题 。
.Net / C# 解决 这个问题的作法是, 把 指针 用 IntPtr 类型 封装起来, 不容许修改, 只是做为一个 常量数值 传递 。
另外一方面, 若是 Class Size(类占用的空间大小(Size)) 、 字段偏移量 、 方法的函数地址 这 3 项 元数据 都是 动态连接 的话,
类型检查 其实 也没什么 好查的 。 ^^
由于 这 3 项 元数据 都是 来源于 同一个 类, 是 自洽 的, 若是发生了 访问内存越界 的问题, 是 类 自身代码 的 逻辑问题 。
强名称 检查 是 验证 程序集(类) 的 身份 。
为何要 动态连接 Class Size(类占用的空间大小(Size)) 、 字段偏移量 ?
这是为了 兼容性, 好比, B 程序集 的 Person 类 如今有 name, age 2 个 字段, 后来又加了一个 favour 字段, 这样就改变了 Class Size,
name, age 的 偏移量 也可能会发生改变,
可是 应该 让 原来 引用了 B 程序集 的 应用程序 能 继续 正常 使用 Person 类,
因此 须要 动态连接 Class Size 和 字段偏移量 。
考虑到 软件 被 攻击 和 破解 的 风险, 能够考虑 加入 像 .Net / C# 同样的 强名称程序集 的 功能 。
不过若是 是 AOT 编译 的话, 即便没有 强名称, 要 破解 也没有那么容易, 由于 AOT 编译 生成的是 本地代码 。 ^^
咱们上面说 程序集 和 类型 的 名字, 好比 调用 ILBC_Runtime_GetType( "B", "Person" ) 函数 返回 Person 的 ILBC_Type 结构体 指针,
"B" 是 程序集 名字, "Person" 是 类 名,
这段代码 是 举例, 咱们给 程序集 名字 和 类型 的 名字 下一个 定义:
程序集 名字 是 程序集 文件 的 文件名(不包含 扩展名),
类型 的 全名(Full Name) 是 “名字空间.类名”, 这个 和 C# 同样 。
假设 名字空间 是 “B”, 则 Person 类 的 全名 是 “B.Person”,
上文 调用 ILBC_Runtime_GetType( "B", "Person" ) 函数 的 类名 应该是 类 的 全名 “B.Person” 。
若是 D# / ILBC 支持 强名称 程序集, 则 对于 强名称 程序集, Full Name 中 还会包含 强名称 版本信息, 能够认为 和 .Net / C# 同样 。
咱们再详细说明一下 高级语言(D#)编译 的 过程,
高级语言(D#) 编译 会生成 2 个文件,
1 元数据 文件,
2 程序集 文件
上文中 没有 交代 元数据 文件,
元数据 文件 保存了 程序集 的 元数据 信息, 包括 类, 类的字段(字段名 、字段类型), 方法(方法签名),
高级语言(D#) 编译器 能够 根据 元数据 知道 程序集 有 哪些成员(类, 类的字段, 类的方法),
这样能够用于 开发时 的 智能提示, 以及 编译时 的 类型检查 。
最重要 的 是 高级语言(D#) 编译器 须要 根据 元数据 生成 程序集 中 加载 Class 的 代码,
加载 Class 的 代码 就是 上文中的 ILBC_Type * ILBC_LoadClass_B_Person() 函数 ,
这个 函数 就是 “Class Loader”, 是 保存在 ILBC_Assembly 结构体 的 classLoaderList 字段中,
classLoaderList 是 一个 数组, 元素 是 ILBC_ClassLoader 结构体, ILBC_ClassLoader 结构体 的 load 字段 就是 保存 “Class Loader” 函数 的 函数指针 的 字段 。
程序集 文件 多是 Byte Code 程序集, 也多是 本地代码 程序集,
若是是 JIT 编译方式, 就是 Byte Code 程序集,
若是是 AOT 编译方式, 就是 本地代码 程序集,
高级语言(D#) 编译器 编译时 只须要 元数据 文件, 不须要 程序集 文件,
应用程序 运行的时候 只须要 程序集 文件, 不须要 元数据 文件 。
元数据 文件 就像是 C 语言 的 头文件 。
因此, ILBC 涉及的 文件 会有 这么几种:
1 元数据 文件
2 C 中间代码 文件, 这个 不是 必需 的, 可是 做为 调试 研究 学习, 能够生成出来 。
3 Byte Code 程序集 文件,
4 本地代码 程序集 文件,
咱们 能够 对 这 4 种 文件 命名 扩展名:
1 元数据 文件, 扩展名 “.ild”, 表示 “ILBC Meta Data”,
2 C 中间代码 文件, 扩展名 “.ilc”, 表示 “ILBC C Code”,
3 Byte Code 程序集 文件, 扩展名 “.ilb”, 表示 “ILBC Byte Code”,
4 本地代码 程序集 文件, 扩展名 “.iln”, 表示 “ILBC Native Code”,
好的, ILBC 规范 暂时 就写这么多 ,
接下来的 计划 是 堆 、 GC 、 InnerC 语法分析器 。
有 网友 提出 不须要 沿袭 传统的 面向对象 方式, 而是能够用和 Rust 类似的方式,
我下面 写一段代码 把这种方式 描述一下:
class C1
{
int f1;
string f2;
}
void M1( C1 this )
{
……
}
void M2( C1 this)
{
……
}
这就是 C1 类 的 定义, 方法 定义在 外面, 相似 C# 的 扩展方法,
这至关于 传统的 面向对象 里 C1 类 有 2 个 方法(M1(), M2()),
咱们在 定义 一个 C2 类, 让 C2 “继承” C1 类:
class C2 : C1
{
}
再把 M1() 的 定义 改一下:
void M1( C2 C1 this )
{
……
}
this 参数 的 类型 加入了 C2, 由 C2 C1 共同做为 this 参数 的 类型,
这样 C2 就 继承 了 C1 的 M1() 方法,,, 注意 只 继承了 M1() 方法, 没有 继承 M2() 方法 。
C2 能够 添加 本身 的 字段, 也能够 多继承, 固然 若是 “父类” 之间有 重名 的 字段, 就 不能 同时继承 有 重名 字段 的 父类 。
C2 也能够 添加 本身 的 方法, 事实上 这也不能 说是 本身 的 方法, 这个 方法 不只仅 能在 “父子” 类 之间 共享,
也能在 “毫无关系” 的 类 之间 共享, 只要 方法 内 对 this 引用 的 字段 在 类 里 存在就行 。
这种 作法 确实 挺 呵呵 的, 但也 很爽 。
这种作法 我称之为 “静态绑定”, 由于 和 Javascript 的 “动态绑定” 类似, 只不过 这是 在 编译期 进行的, 因此叫 “静态绑定” 。
同时, 从 编译期 “静态” 的 角度, 又和 泛型 很像 。
网友 说 这种作法 “只须要 结构体 和 扩展方法 就行, 不须要 类 。” ,
确实, 就是这样, 只要有 结构体 和 扩展方法 就能够 。
说的 直 一点, 只要有 结构体 和 函数 就能够 。
我要 呵呵 了, 这算是 面向过程 -> 面向对象 -> 面向过程 么 ?
通过后来的 讨论 和 思考, D# 仍是不打算这样作, D# 的 目标 是 实现一个 经典 的 简洁 的 面向对象 语言 。
D# 会 支持 简洁 的 面向对象 和 函数式 。
简洁 的 面向对象 包括 单继承 、接口 、抽象类 / 抽象方法 / 虚方法,
函数式 是 闭包 。
不过, 关于 上述 的 “静态绑定” 的 作法, 却是 讨论清楚 了, “绑定” 有 3 种:
1 静态绑定, 在 编译期 为 每一个 绑定 生成一份 方法(函数) 代码, 每一份 函数 代码 逻辑相同, 区别是 访问 对象 字段 的 偏移量 。
2 静态绑定, 方法(函数) 只有一份, 但在 编译期 为 每一个 绑定 生成一段 绑定代码, 绑定代码 的 逻辑 是 把 对象 字段 的 偏移量 转换为 函数 里 对应的 偏移量 。
3 动态绑定, 在 运行期 为 绑定 生成 绑定代码 。
关于 堆 和 GC, 个人 想法 是这样:
GC 根据 2 张 表 来 回收 对象(内存),
1 引用表
2 对象表
这 2 张表 其实是 链表,
每次 new 对象 的 时候, 会把 对象 添加 到 对象表 里,
每次 给 引用 赋值 的 时候, 会把 引用 添加 到 引用表 里,
每次 引用 超出 做用域, 或者 引用 被赋值 为 null 时, 会 将 引用 从 引用表 里 删除, 固然 这段代码 是 编译器 生成的 。
这样, GC 回收 对象(内存) 的 时候, 就 先 扫描 引用表, 对 引用表 里 的 引用 指向 的 对象, 在 对象表 里 作一个标记, 表示 这个 对象 还在使用,
扫描完 引用表 后, 扫描 对象表, 若是 对象 未被标记 还在使用, 就表示 已经没有 引用 在 指向 对象, 能够 回收对象 。
而 要 在 每次 给 引用 赋值 的 时候 把 引用 添加到 引用表, 须要 lock 引用表, 把 对象 添加到 对象表 也须要 lock 对象表 。
lock 会 带来 性能损耗, 经过 测试 能够看到, C# 中 lock 的 时间 花费 大约 是 new 的 3 倍 (new 应该要 查找 和 修改 堆表, 因此 应该 也有 lock),
执行次数 比较小时, 小于 3, 好比 10 万次,
执行次数 比较大时, 大于 3, 好比 1 亿次,
因此, 看起来, C# 的 new 的 lock 的 效率 比 lock 关键字 的 lock 的 效率 高,
或者说, 若是 咱们 用 上述 的 架构, 给 引用 赋值 时 把 引用 添加到 引用表, 使用 lock 关键字 来 实现 lock,
这样 对 性能 的 影响 很大, 只要 想一想 给 引用 赋值 的 性能花费 比 new 还大 就 知道 了,
从 测试结果 上来看, new 的 执行 应该是 指令级 的, 大概在 5 个 指令 之内 就能够完成,
对于 .Net / C# 这样有 GC 的 语言, 应该 只须要 从 剩余空间 中 分配 内存块 就能够, 不须要 像 C / C++ 那样 用 树操做 查找 最接近 要 分配 的 内存块 大小 的 空闲空间,
再加上 lock 的 时间, 所有加起来 大概 在 5 个 指令 之内,
lock 大概 占 2 个 指令, 开始 lock 占 1 个 指令, 结束 lock 占 1 个 指令,
固然 这些 是 估算 。
因此 能够看出来, .Net / C# 的 new 操做 对 堆表 的 lock 是 指令级 的, 不是调用 操做系统 的 lock 原语,
这样 的 目的 是 让 new 的 操做 很快, 接近 O(1),
对于 ILBC 而言, 若是 采用 给 引用 赋值 时 修改 引用表, new 对象 时 修改 对象表,
那么, 修改 引用表 和 对象表 的 操做 也应该 接近 O(1), 就是 像 .Net / C# 的 new 同样, 这样才有足够的效率 。
这就是说, 修改 引用表 和 对象表 的 lock 也要像 .Net / C# 的 new 对 堆表 的 lock 同样, 是 指令级 的 。
这就须要 咱们 本身 来 实现一个 lock, 而不是使用 操做系统 的 lock 原语 。
怎么来 实现 本身的 一个 lock ?
根据 网上 查阅 的 结果, 光从 软件 层面 是 不行 的, 光从 C 语言 层面 也不行, 须要 硬件 的 支持 和 汇编 编程 。
能够参考 《聊聊C++中的原子操做》 https://baijiahao.baidu.com/s?id=1609585581486387645&wfr=spider&for=pc ,
《java并发中的原子变量和原子操做以及CAS介绍》 https://blog.csdn.net/wxw520zdh/article/details/53731146 ,
文中提到 “CAS …… 虽然看似复杂,但倒是 Java 5 并发机制优于原有锁机制的根本。” ,
而 CAS 是 经过 CPU 提供的 CMPXCHG 指令 支持, 能够参考 《cpu cmpxchg 指令理解 (CAS)》 https://blog.csdn.net/xiuye2015/article/details/53406432 ,
因此 咱们能够 用 CMPXCHG 指令 来实现 lock , 原理 是 这样:
在 内存 里用一个 字 来 存储 lock 标志(flag), 若是 是 64 位 处理器, 则 字长 是 64, 即 8 个 字节(Byte),
简化起见, 咱们 就 不 考虑 32 位 处理器 了, 只 考虑 64 位 处理器 。
当要 lock 时, 用 CMPXCHG 指令 比较 flag 是否 等于 0, 若是相等 则 将 当前线程 ID 复制到 flag, 这表示 当前线程 得到了 锁, 接着执行 锁 里 要执行 的 操做 就行 。
若是 不等于 0, 则 CMPXCHG 指令 会把 当前 flag 的 值 复制到 指定 的 寄存器 里, 检查 寄存器 里 的 flag 值 是否 是 当前线程 ID, 若是 是, 表示 在 当前线程 的 锁 范围内, 接着执行 锁 里 要 执行 的 操做 就行 。
若是 flag 值 不等于 当前线程 ID, 表示 当前锁 由 别的 线程 占有, 则 当前线程 挂起, 挂起前 会把 指令计数器 再次指向 上述 检查锁 的 指令, 下次 恢复运行 时, 会 从新执行 上述 检查锁 的 操做 。
咱们能够用 多个 字 来表示 多个 lock, 好比 用 一个字 表示 引用表 lock, 一个字 表示 对象表 lock, 一个字 表示 堆表 lock, 等等 。
固然, 为了提升效率, 对象表 lock 和 堆表 lock 大概 能够 合为一个 lock, 由于 修改 对象表 和 堆表 都 发生在 new 操做 的 时候, 能够把 new 操做 做为一个 原子操做, 只用 一个 lock, 这样, new 操做 包含的 2 个步骤 修改 对象表 和 修改 堆表 都在 一个 lock 里 进行 。
这种作法 相比 操做系统 的 lock 原语, 可能更简单, 可是 功能 也 相对局限, 好比 不能支持 嵌套 lock, 以及 必须 预先 为 每一种 lock 分配一个 字, 而 操做系统 lock 是 能够 动态 lock 的, 好比 C# 中 只要 调用 Monitor.Enter() 方法 就能够 开始 lock, 一般 咱们 是用 lock 关键字, 这在 编译期 被 编译器 处理为 Monitor.Enter() 和 Monitor.Exit() 方法对, 可是 若是 在 运行期 调用 Monitor.Enter() 方法, 也是 能够 开始 lock 的 。
操做系统 的 lock 可能 是 利用了 虚拟内存, 或者说 存储管理部件, 只须要 在 存储管理 的 锁表 里 设置 要锁定 的 地址, 存储管理 部件 会判断 是否容许 访问 该地址 。
设置 锁表 的 原理 是, 在 锁表 里 设置 当前线程 ID 和 要锁定的地址, 若是 相同 的 线程 ID + 锁定地址 已经 存在, 则 设置失败, 设置失败 则 线程挂起, 等下次 恢复运行 时 再接着设置 。
设置成功 则 表示 当前线程 得到 对 指定地址 的 锁, 存储管理部件 将 只容许 当前线程 访问 指定地址, 不容许 其它线程 访问 指定地址 。
事实上, 咱们 用 CMPXCHG 指令 的 作法 也能够 实现 和 操做系统 相似 的 效果, 包括 动态的锁定 任意 的 对象(不须要 预先 分配字), 也 支持 嵌套 lock,
这须要 在 object 类(全部 引用类型 的 基类) 里 加入一个 lock 字段, 当咱们 lock 某个 对象 时, 会先看 lock 字段 是否等于 0, 若是 等于 0, 则 写入 当前线程号, 这样 就 得到了 对 该 对象 的 锁, 若是 不等于 0, 则 比较 是否等于 当前 线程 ID, 若是 等于, 表示 对象 被 当前对象 锁定, 因而接着执行 锁定 里 的 操做, 若是 不等, 表示 对象 被 其它线程 锁定, 则 当前线程 挂起, 等下次 恢复运行 时, 重复上述过程 。
这个过程 和 上面叙述的 利用 CMPXCHG 指令 实现 锁 的 过程 是同样的, 但不用 预先 分配 字, 用 object 的 lock 字段 做为 这个 “字” 就能够 。
判断 object 的 lock 字段 是否 等于 0, 若 等于 则 写入 当前 线程号, 返回 true, 不然 lock 字段不变, 返回 false, 这个操做是 “原子操做”, 这个 原子操做 就是 CMPXCHG 指令 实现的 。
但 用 咱们的 作法 有一个条件, 就是 须要在 全部 (可能 并发) 访问 对象 的 地方 都 加上 lock,
而 操做系统 的 锁 则 没必要需, 操做系统 因为是利用 虚拟内存(存储管理部件) 实现的, 因此 在 代码 的 a 处 加了 lock, b 处 不加 lock, 但 a 处 锁定 对象, 则 b 处 将不能访问 。
虽然如此, 咱们在 使用 操做系统 lock 的 时候, 一般 也会在 a 处 和 b 处 都 加上 lock, 这是为了 设计意图 的 须要, 咱们 须要 a 和 b 严格的 同步(互斥)通讯, 就 须要 给 a 处 和 b 处 都 加上 lock 。
我把 咱们 的 作法 称为 “IL Lock” , 用 关键字 illock 表示,
把 操做系统 的 lock 称为 “System Lock”, 用 关键字 syslock 表示,
在 D# 中, 使用 IL Lock 能够这样写:
illock ( obj )
{
……
}
使用 System Lock 能够这样写:
syslock ( obj )
{
……
}
理论上, 咱们能够提倡 使用 IL Lock, 这样能够 得到 比 System Lock 更高 的 性能 。 ^^
好的, 堆 和 GC 的 部分 基本 理清 了, 接下来 会开始 InnerC 语法分析器 。
到 目前为止, InnerC 在 ILBC 的 地位 变得重要, InnerC 会是 ILBC 的 内核模块 。
InnerC 支持 基础类型(int, long, float, double, char), if else, for, while, 函数, 指针, 数组, 结构体,
InnerC 不保证 支持 Ansi C 的 所有标准,
InnerC 还会有一些 新的 特性:
1 对 void * 类型 的 函数指针 不检查 函数签名, 能够调用任意的参数列表 和 返回任意的返回值, 固然调用了 不匹配 的 参数列表 就 会发生 错误, 可能致使 程序 崩溃, 这个 特性 是用在 C 中间代码 里, 不建议 开发人员 使用 。
对于 声明了 函数签名 的 函数指针, 仍然 会 检查 调用的参数列表 及 返回值 是否 符合 函数签名(指针类型), 开发人员 应使用 这种方式, 保证 安全性 。
2 为了便于实现一些 动态特性 和 对 本地代码 访问 的 灵活性, InnerC 支持 用 函数指针 调用 动态的参数列表, 参数列表 是 一个 数组, 相似 .Net / C# 的 反射, 把 参数 放在 数组 里 传给 MethodInfo.Invoke( object[] args ) 方法 。
初步构想 能够 增长一个 invoke 关键字, 能够用于 函数指针 的 函数调用, 好比:
void * funcPtr ;
void * args ;
……
( * funcPtr ) ( invoke args ) ; // 调用 funcPtr 指向 的 函数, 参数列表 是 args
3 新增 casif 关键字 以 支持 casif 语句 。
casif 语句 相似 if 语句, 但 判断条件 是 经过 CMPXCHG 指令 实现的 CAS 原子操做, CAS 全称 “Compare and Swap” 。
casif 语句 格式 以下:
casif ( 参数1, 参数2, 参数3 )
{
语句块 1
}
else
{
语句块 2
}
参数1 是一个 变量 或者 常量, 参数2 是 一个 指针, 参数3 是 一个 变量 或者 常量,
当 参数1 和 参数2 指向 的 值 相等 时, 把 参数3 的 值 复制到 参数2 指向 的 存储单元, 并认为 判断条件 成立, 执行 语句块 1 。
不然 认为 判断条件 不成立, 执行 语句块 2 。
其实 上面说的 用 CMPXCHG 指令 实现 IL Lock 的 作法 还有一点问题, 其实 不须要 向 对象 的 lock 字段 写入 当前线程 ID, 只要 写入 1 就能够, 1 表示 对象 被 锁定, 0 表示 对象 未被锁定 。
这样 逻辑 就 更 简化了 。
对 引用表 对象表 堆表 的 lock 都会 统一使用 IL Lock 。
暂时先写到这里, ILBC 目前计划 发展 2 门 高级语言, D# 和 c3 , c3 由 一位 网友 提出, 参考《c3 语言草案》 https://note.youdao.com/ynoteshare1/index.html?id=bec52576b45ec0d918a95f75db0ea68e&type=note#/ 。
内容有点多, 因此后面的内容放到了 《ILBC 规范 2》 http://www.javashuo.com/article/p-uqmiarbb-g.html 。