ILBC 规范

 

本文是 VMBC / D# 项目 的 系列文章,

 

有关 VMBC / D# , 见 《我发起并创立了一个 VMBC 的 子项目 D#》(以下简称 《D#》) https://www.cnblogs.com/KSongKing/p/10348190.html  。

 

ILBC 系列文章 收录在 《ILBC 白皮书》   https://www.cnblogs.com/KSongKing/p/11070978.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 表》  这篇文章已经写好了, 见    https://www.cnblogs.com/KSongKing/p/10425152.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》  https://www.cnblogs.com/KSongKing/p/10440001.html    。

 

 

 

posted on 2019-02-07 14:58  凯特琳  阅读(979)  评论(0编辑  收藏  举报

导航