ILBC 规范

 

本文是 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    。

相关文章
相关标签/搜索