zoukankan      html  css  js  c++  java
  • 我发起并创立了一个 VMBC 的 子项目 D#

    大家好,

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

     

    有关 VMBC ,  请参考 《我发起了一个 用 C 语言 作为 中间语言 的 编译器 项目 VMBC》     https://www.cnblogs.com/KSongKing/p/9628981.html ,

    和 《漫谈 编译原理》  https://www.cnblogs.com/KSongKing/p/9683831.html    。

     

    D# ,  就是一个 简单版 的 C#  。

     

    下面说一下 D#  项目 的 大概规划 :

     

    第 1 期,  实现 new 对象 的 机制,  GC,  堆  。      (我做)

    第 2 期,  实现 对象 的 函数(方法) 调用  。           (后人做)

    第 3 期,  实现 元数据,  简单的  IL 层 基础架构  。      (后人做)

    第 4 期,  实现 简单类型,  如 int, long, float, double  等 。   (后人做)

    第 5 期,  实现 简单的 表达式 和 语句,  如   变量声明,  加减乘除,  if else,  for 循环  等 。  (后人做)

    第 6 期,  实现 D# 代码 翻译为  C 语言 中间代码  。      (后人做)

    第 7 期,  实现 将  C 语言 代码 编译 为 本地代码 。      (后人做)

    第 8 期,  各种 高级 语法特性 逐渐 加入 。      (后人做)

    第 9 期,  各种   完善发展   ……                (后人做)

     

    我们来 具体 看一下 每一期 怎么做 :

    第 1 期,  对象 的 new 机制,  就是用  malloc()  在 内存 里 申请一段 内存, 内存的 大小(Size) 是 对象 里 所有字段 的 Size 宗和, 可以用 C 语言的 sizeof() 根据 字段类型 取得 字段占用的 内存长度,  加起来 就是 对象 占用的 内存长度 。

     

    GC,  D# 的 GC 和 C# 有一点不同,  C# 的 GC 会 做 2 件事 : 

    1  回收 对象 占用的 内存

    2  整理 堆 里的 碎片空间

     

    D# 只有 第 1 点, 没有 第 2 点 。  就是说 D# 只 回收 对象占用的 内存,  但不进行 碎片整理 。

    C#  GC   进行 碎片整理 需要 移动对象, 然后 修改 指向 这个对象 的 引用,   引用 是一个 结构体, 里面 包含了 一个指针, 指向 对象 的 地址,  对象 被移动后, 地址 发生了 改变, 所以 引用 里的 这个指针 也需要 修改 。

    其实 不做 碎片管理 的 主要原因 是 碎片整理 的 工作 很复杂,  我懒得写了 。 ^^

    碎片 整理 主要是 解决 碎片 占用了 地址空间 和 内存空间 的 问题,  以及 碎片 增多时 堆 分配 效率变低 的 问题 。

    当然还有 碎片 占用了 操作系统 虚拟内存 页 的 问题 。

     

    首先, 关于 碎片占用 地址空间 的问题,  现在 是 64 位 操作系统,  地址空间 可以达到  16 EB,  不用担心 地址空间 用完 。 

    内存空间 的 问题,   现在 固态硬盘 已经普及, 内存 也 越来越大,  固态硬盘 可以 让 操作系统 虚拟内存 很快, 再加上 内存 也 越来越大, 所以 也不用担心 内存空间 不够 的 问题 。

    碎片 增多时 堆分配 效率变低 的 问题,  我们打算自己实现一个 堆算法,  下面会 介绍 。

    碎片 占用了 操作系统 虚拟内存 页 的 问题 是指 碎片 占用了 较多 的 页, 导致 操作系统 虚拟内存 可能 频繁 的 载入载出 页,  这样 效率 会降低 。

    这个问题  其实 和 碎片 占用 内存空间 的 问题一样,  固态硬盘 可以 让 操作系统 虚拟内存 很快, 内存 也 越来越大,  所以 基本上 也可以 忽略 。

     

    另一方面, GC 整理碎片 移动对象 本身 就是一个 工作量 比较大 的 工作,  且 移动对象 时 需要 挂起 所有 线程 。

    所以,  碎片整理 也是 有利有弊 的 。

     

    D#  GC  去掉了  整理碎片 的 部分,  也可以说是 “空间换时间” 的做法, 

    另外,  D#  GC 工作时 不用 挂起 应用程序 线程,  可以 和 应用程序 线程 正常的 并发 运行 。

    相对于 C#,   实时性 也会 好一些 。

     

    为什么 要 自己实现一个 堆 呢?

    因为 C / C++ 的 堆 分配(malloc() , new) 是 有点 “昂贵” 的 操作,

    C / C++ 是 “静态语言”, 没有 GC 来 整理碎片, 所以 就需要有一个 “精巧” 的 分配算法,

    在 申请一块内存(malloc() , new) 的 时候, 需要 寻找 和 申请的 内存块 大小(size) 最接近 的 空闲空间,

    当内存出现大量碎片,或者几乎用到 100% 内存时, 分配 的 效率会降低, 就是说 分配操作 可能 会 花费 比较长 的 时间 。

    见 《C++:在堆上创建对象,还是在栈上?》  https://blog.csdn.net/qq_33485434/article/details/81735148

    原文是这样:

    首先,在堆上创建对象需要追踪内存的可用区域。这个算法是由操作系统提供,通常不会是常量时间的。当内存出现大量碎片,或者几乎用到 100% 内存时,这个过程会变得更久。


     

    而 对于   java , C#   这样的语言来说,  new 操作 是 常规操作,  时间复杂度 应该 接近 O(1) 。

    事实上  java ,  C#  的  new 操作 时间复杂度 可能就是 O(1),  因为有 GC 在 整理碎片,  所以 new 只需要从 最大的 空闲空间 分配一块内存 就可以 。

     

    所以 D# 也需要 设计一种 O(1) 的 堆算法 。

    D# 的 堆 算法 也会沿用 “空间换时间” 的 思路,  new 直接从 最大的 空闲空间 分配 指定 size 的 内存块, 由 另外一个 线程 定时 或 不定时 对 空闲空间 排序,

    比如 现在 在 堆 里 有 10 个 空闲空间, 这个 线程 会对 这 10 个 空闲空间 排序, 把 最大的 空闲空间 放在 最前面,

    这样 new 只要在 最大的 空闲空间 里 分配内存块 就可以了 。

    这样 new 的 时间复杂度 就是 O(1) 。

     

    这个对 空闲空间 排序 的 线程 可以是 GC 线程, 或者说, 对 空闲空间 排序 的 工作 可以放在 GC 线程 里 。

     

    当然, 这样对 内存空间 的 利用率 不是最高的,  但上面说了,  空间 相对廉价, 这里是 “用 空间换时间” 。

     

    这个 堆 算法 还有一个 特点 就是 简单, 简单 有什么用 呢?

    作为一个 IL 层, 虽然 C / C++  提供了 堆 算法,  但是自己还是有可能自己实现一个 堆, 至少 要有这个 储备力量, 

    上面这个 算法 的好处是, 因为 简单, 所以 把 研发成本 降低了, 包括 升级维护 的 成本 也降低了 。  哈哈哈 。

     

    我可不希望 后来人 学习 VMBC 的 时候, 看到 一堆 天书 一样的 代码,

    我不觉得 像 研究 九阴真经 一样 去 研究 Linux 内核 这样 的 事 是一个 好事 。  ^^

     

    接下来, 我再论证一下 GC 存在的 合理性, 这样 第 1 期 的 部分 就结束了 。

    过去有 观点 认为, GC 影响了 语言 的 实时性(比如 java, C#),  但如果从 另外一个角度 来看, 应用程序 运行在 操作系统 上, 也会 切换回 系统进程, 系统进程 负责 进程调度 虚拟内存 IO  等 工作, 总的来说, 是 对 系统资源 的 管理 。

    GC 也可以看作是 应用程序 这个 “小系统” 里 对 系统资源 管理 的 工作, 所以 GC 是一个 合理 的 并发, GC 是合理的 。

     

    第 2 期,  实现 对象 的 函数(方法) 调用, 这很简单, 就是 调用 函数, 给 函数 增加一个 参数, 这个 参数 作为 第一个参数, 这个参数 就是 this 指针, 把 对象 自己的 地址 传进去 就可以了 。

     

    第 3 期,  实现 元数据,  简单的  IL 层 基础架构 。 简单的 IL 层 基础架构 主要 就是 元数据 架构 。

    元数据 就是 一堆 结构体, 声明一堆 静态变量 来 保存这些 结构体 就可以了 。  不过 考虑到 元数据 是 可以 动态加载 的, 这样 可以 用 D# 自身的 new 对象 机制 来实现 。  只要 声明一个 静态变量 作为 元数据树 的 根 就可以了 。

    元数据 实际上 也 包含了 第 2 期 的 内容,  元数据 会 保存 对象 的 方法(函数) 的 指针, 这还涉及到 IL 层 的 动态链接,

    就跟 C# 一样, 比如 用 D# 写了 1 个 .exe 和 1 个 .dll,  用 .exe 调用 .dll , 涉及到一个 IL 层 的 动态链接 。

    C# 或者 .Net  是 完全 基于 元数据 的 语言 和 IL 平台,  java 应该也是这样,  java 刚出现时, 逐类编译, 也就是说, 每个类 编译 为 一个 class 文件, class 文件 是 最小单位 的 动态链接库, 可以 动态加载 class 文件, 这个 特性, 在 java 刚出现的时代, 是 “很突出” 的 ,  也是 区别于 C / C ++ 的 “动态特性” 。

    这个 特性 在 今天 看来 可能 已经 习以为常,  不过在 当时,  这个特性 可以用来 实现 “组件化” 、“热插拔” 的 开发, 比如 Jsp 容器, 利用 动态加载 class 文件 的 特性, 可以实现 动态 增加 jsp 文件,  在 web 目录下 新增一个 jsp 文件,一个 新网页 就上线了 。  当然 也可以 动态 修改 jsp 文件 。

     

    第 4 期,  实现 简单类型,  如 int, long, float, double  等 。

    C 语言 里本来就有 int, long, float, double,  但是在 C# 里, 这些 简单类型 都是 结构体, 结构体 里 除了 值 以外, 可能还有 类型信息 之类的 。

    总之 会有一些 封装 。

    D# 也一样, 用 结构体 把 C 语言 的 int, long, float, double  包装一下 就可以了 。

     

    第 5 期,  实现 简单的 表达式 和 语句,  如   变量声明,  加减乘除,  if else,  for 循环  等 。

    这些 也 不难, 上面说了, 值类型 会 包装成 结构体, 那么 变量声明 就是 C 语言 里 相应 的 结构体 声明,

    比如 int  对应的 结构体 是   IntStruct,  那么,  D# 里   int i;     对应的  C 语言 代码 就是    IntStruct  i;    ,

    严格的讲,  应该是

    IntStruct  i;

    i.val = 0;

     

    应该是 类似 上面这样的 代码,  因为 C 语言 里   IntStruct i;    这样不会对 i 初始化,  i.val  的 值 是 随机的 。

    按照 C# 语法, int i;  ,   i 的 值 是 默认值 0  。

     

    也可以用  IntStruct i = IntStruct();       通过  IntStruct  的 构造函数 来 初始化 。

     

    我在 网上 查了 这方面的文章, 可以看看这篇 《c++的struct的初始化》  https://blog.csdn.net/rush_mj/article/details/79753259   。

     

    加减乘除,   if else,  for 循环  基本上 可以直接用  C 语言 的 。

     

    第 6 期,  实现 D# 代码 翻译为  C 语言 中间代码  。

    在 第 6 期 以前,  都还没有涉及 语法分析 的 内容,  都是 在 设计, 用  C 语言 怎样 来 描述 和 实现 IL 层,  具体 会用  C 语言 写一些 demo 代码 。

    第 6 期 会通过 语法分析 把 D# 代码 翻译为 C 语言 中间代码 。

    具体的做法是,

    通过 语法分析, 把 D# 代码 转换为 表达式树,  表达式 是 对象,  表达式树 是 一棵 对象树,

    转换为 表达式树 以后, 我们就可以进行  类型检查  等 检查,  以及 语法糖 转换工作,

    然后 让 表达式 生成 目标代码,  对于 一棵 表达式树,  就是 递归生成 目标代码,

    一份 D# 代码文件, 可以 解析为 一棵 表达式树,   这棵 表达式树 递归 生成 的 目标代码 就是 这份 D# 代码 对应的 C 语言 目标代码 。

     

    关于 语法分析,  可以参考 《SelectDataTable》  https://www.cnblogs.com/KSongKing/p/9683831.html    。

     

    第 7 期,  实现 将  C 语言 代码 编译 为 本地代码 。

    这一期 并不需要 我们自己 去 实现 一个 C 编译器, 我们只要和 一个 现有的 C 编译器 连接起来 就可以了 。

     

    第 8 期,  各种 高级 语法特性 逐渐 加入 。

    基本原理 就 上面 那些了,   按照 基本原理 来加入 各种 特性 就可以 。

    不过 别把 太多 C# 的 “高级特性” 加进来, 

    C# 已经变得 越来越复杂,  正好 乘此机会,  复杂的 不需要的 特性 就 不用 加进来了 。

     

    C# 的 “高级特性” 增加了 很多复杂,   也增加了 很多 研发成本 。

    刚好 我们 不要 这些 特性,   我们的 研发成本 也降低了 。

     

    第 9 期,  各种   完善发展   ……

    语法特性, 优化, IDE, 库(Lib),  向 各个 操作系统 平台 移植   ……

    好了,  说的 有点远 。

    优化 是一个 重点, 比如 生成的 C 语言 中间代码 的 效率, IL 层 架构 对 效率 的 影响, 等等, 这些是 重要的 评估 。

    就像 C / C++ 的 目标 是 执行效率,  我认为 D# 的 目标 也是 执行效率 。

     

    D#  提供了  对象 和 GC, 

    对象 提供 了 封装抽象 的 程序设计 的 语法支持,

    GC  提供了  简洁安全 的 内存机制,

    这是 D# 为 开发者 提供的 编写 简洁安全 的 代码 的 基础,  是 D# 的 基本目标 。

     

    在此 基础上,  就是 尽可能 的   提升执行效率 。

     

    还可以看看 《漫谈 C++ 虚函数 的 实现原理》  https://www.cnblogs.com/KSongKing/p/9680632.html    。

     

    上文中提到 IL 层 的 动态链接, 这是个问题,  也是个 课题 。

    在 C# 中, IL 层 的 动态链接 是 JIT 编译器 完成的 。

    对于  D#,  可以这样来 动态链接,  假设    A.exe  会调用  B.dll,  那么 在 把 A 的 D# 代码 编译成 C 语言 目标代码 的 时候, 会声明一个 全局变量 数组, 这个 全局变量 数组 作为 “动态链接接口表”,   接口表 会保存 A 中调用到 B 的 所有 构造函数 和 方法 的 地址, 但是在 编译 的时候 还不知道 这些 构造函数 和 方法 的 地址(在 运行时 才知道),  所以 这些 地址 都 预留 为 空(0),  就是说 这个 接口表 在编译时 是 为 运行时 预留的,  具体的 函数地址 要在 运行时 填入 。

    在 运行时, JIT 编译器(内核是个 C 编译器)  加载 B.dll,  将 B.dll 中的 C 语言 中间代码 编译为 本地代码, 然后 将 编译后的 各个函数 的 地址 传给 A, 填入 A 的 “动态链接接口表”,

    A 中调用 B 的 函数的 地方在 编译 时 会处理为 到 接口表 中 指定 的 位置 获得 实际要调用的 函数地址, 然后根据这个 函数地址 调用函数 。

    这有点像 虚函数 的 调用 。

     

    接口表 中 为什么 要 保存 构造函数 呢?  因为如果要 创建 B 中定义的 类 的 对象, 就需要 调用 构造函数 。

    其实 接口表 除了 构造函数,  还要保存 对象 的 大小(Size),  创建对象 的 时候, 先根据 Size 在 堆 里 分配空间, 再 调用 构造函数 初始化 。

     

    B.dll       JIT 编译 完成时,   需要把  本地代码 中 各函数 的 地址 传给 A,  对于 C# 来说, 这些是 JIT 编译器 统一做的, 没有 gap,

    但是 对于 D# 来说, 如果我们不想 修改 C 编译器, 那么 就有 gap,

    这需要 在 B.dll 的 C 语言 中间代码 里 加上一个 可以作为 本地代码 动态链接 的 函数(比如 win32 的 动态链接库 函数),  通过这个函数, 来把 B 的 元数据 传给 A, 比如 JIT 编译后 本地代码 中 各个函数 的 地址,

    这样 A 通过调用 B 的 这个函数, 获取 元数据,  把 元数据 填入 接口表 。

     

    上面说的 win32 动态链接库 函数 是 通过  extern "C"  和   dllexport  关键字 导出 的 方法, 比如:

    extern "C"
    {
                _declspec(dllexport) void foo();
    }

     

    这是 导出了一个  foo()  方法  。

     

    这种方法 就是       纯方法, 纯 C 方法,  不涉及对象, 更和 Com 什么的无关,  干脆利落,  是 方法 中的 极品 。

    这种方法 也 再次 体现了  C 语言 是  “高级汇编语言”  的   特点,

    你可以用   C 语言 做 任何事 。

    爽,  非常爽 。

     

    IL 层 动态链接      和     本地代码库 动态链接         的 区别 是:

    IL 层 动态链接 的 2 个 dll 是 用 同样的语言 写的(比如 D# 的 dll 是 C 语言 写的), 又是 同一个 编译器 编译成 本地代码 的,  2 个 dll 编译后 的 本地代码 的 寄存器 和 堆栈 模型 相同, 只要知道 函数地址, 就可以 相互调用 函数 。   其实 就跟 把  A.exe  和  B.dll  里包含的 C 文件全部放在一起编译 的 效果 是一样的 。

    本地代码库 动态链接 的 话,  2 个 dll  可能是用 不同的语言 写的, 也可能是 不同的编译器 编译的,  2 个 dll 的 寄存器 和 堆栈 模型 可能 不相同, 需要 按照 操作系统 定义 的 规范 调用 。

    在 上文提到的  《漫谈 编译原理》  中, 也 简单的讨论了 链接 原理 。

     

    这个道理 搞通了,          D#    要搞成   JIT  也是可以的 。

    事实上 也 应该 搞成 JIT,   不搞成 JIT 估计没人用 。

    JIT  还真不是 跨平台 的 问题,

    我想起了,  C++   写了 3 行代码,   就需要一个 几十 MB 的 “Visual Studio 2012 for C++ Distribute Package”   ,

    看到这些,  就知道是 怎么回事 了 。

     

    经过 上面的 讨论, 一些 细节 就 更清楚了 。

    D#   编译产生的  dll,   实际上是个 压缩文件,  解压一看,   里面是 一些 .c 文件 或者 .h 文件,  相当于是一个 C 语言 项目 。

    这样是不是 很容易 被 反编译 ?

    实际上 不存在 反编译,   直接打开看就行了  。  ^^

    如果怕被 反编译 的话,  可以把 C 代码 里的 回车 换行 空格 去掉, 这样 字符 都 密密麻麻 的 排在一起,

    再把 变量名 和 函数名 混淆一下 。

    感觉好像   javascript    ……

     

    如果跟 Chrome V8  引擎 相比,    VMBC / D#   确实像    javascript  。

     

    try catch 可以自己做, 也可以 用 C++ 的,  但我建议 自己做,

    因为 VMBC 是   Virtual Machine Base on C,    不是   Virtual Machine Base on C++     。

     

    try catch      可能会用到     goto  语句 。

     

    昨天网友提起  C 语言 的 编译速度 相对 IL 较低, 因为 C 语言 是 文本分析, IL 是 确定格式 的 二进制数据,

    我之前也想过这个问题,   我还想过 像 .Net Gac 一样搞一个 本地代码程序集 缓存, 这样, 运行一个 D# 程序时, 可以先 用 Hash 检查一下    C 中间代码程序集 文件 是否 和 之前的一样, 如果一样就 直接运行 缓存里的 本地代码程序集 就可以 。

     

    由这个问题, 又想到了,  D# 应该支持 静态编译(AOT),  这也是  C 语言 的 优势 。

    D# 应该 支持 JIT 和 AOT,   JIT 和 AOT 可以 混合使用 。

    比如, 一个 D# 的 程序, 里面一些 模块 是 AOT 编译好的, 一些 模块 是  JIT  在 运行时 编译的 。

    为此,  我们提出一个    ILBC    的 概念,   ILBC 是   Intermediate Language  Base on C     的 意思 。

    ILBC    不是一个 语言,  而是一个 规范 。

    ILBC    是 指导  C 语言 如何构建 IL 层 的 规范, 以及 支持 这个 规范 的 一组 库(Lib) 。

     

    ILBC 规范草案 大概是这样 :

    ILBC 程序集 可以提供 2 个 C 函数 接口,

    1  ILBC_Main(),  这是 程序集 的 入口点,  和 C# 里的 Main() 是一样的, 

    2  ILBC_Link() ,  这就是 上面 讨论的 IL 层 的 动态链接 的 接口,  这个 函数 返回 程序集 的 元数据, 其它 ILBC 程序集 获得 元数据后,可以 根据 元数据 调用 这个 程序集 里的 类 和 方法 。 元数据 里 的 内容 主要是 类 的 大小(Size)、 构造函数地址 、 成员函数地址 。

        哎?   不过说到这里,  如果要访问 另外一个 程序集 里的 类 的 公有字段 怎么办 ?   嘿嘿嘿,  

        比如 A.dll 要 访问 B.dll 里的 Person 类的 name 字段,  这需要在 把 A 项目 的 D# 代码 编译成 A.dll 时 从 B.dll 的 元数据 里 知道 name 字段 在 Person 类 里的 偏移量, 这样就可以把 这个 偏移量 编译到 A.dll 里,  A.dll 里 访问 Person 类 name 字段 的 代码 会被 处理成    *( person    +   name 的 偏移量 )  ,    person 是 Person 对象 的 指针 。

        这是 在把 D# 代码 编译成 A.dll 的 时候 根据 B.dll 里的 元数据 来做的工作, 这不是 动态链接, 那算不算 “静态链接” ?   因为 字段 的访问 的 处理 比较简单, “链接” 包含的 工作 可能 更复杂一些,  当然, 你要把 字段 的 处理 叫做 链接 也可以, 怎么叫都可以 。

        那 函数调用 能不能 也 这样处理 ?

        访问字段 的 时候, 是 对象指针  +  字段偏移量, 

        函数 则是 编译器 编译 为 本地代码,  函数 的 本地代码 的 入口地址 是 编译器 决定的,  需要 编译器 把  C 中间代码 编译 为 本地代码 后才知道, 所以 函数 需要 动态链接 。

     

    从上面的讨论我们也看到,  ILBC 程序集 会有一个  .dat 文件(数据文件), 用来存放 可以 静态知道 的 元数据, 比如 类 字段 方法,类的大小(Size), 字段的偏移量(Offset) 。  元数据 的 作用 是     类型检查    和     根据 偏移量 生成 访问字段 的 C 中间代码  。

    元数据 里的 类的大小(Size) 和  字段偏移量  是   D# 编译器  计算 出来的,  这需要 D# 编译器 知道 各种 基础类型(int, long, float, double, char  等) 在  C 语言 里的 占用空间大小(Size),   这是  D# 编译器 的 参数,   需要 根据 操作系统平台 和  C 编译器 来 设定 。

    类(Class) 在 ILBC 里 是用  C 语言 的   结构体(Struct) 来表示,  结构体 由 基础类型 和 结构体 组成,  所以 只要 知道了 基础类型 的 Size,  就可以 计算出 结构体 的 Size, 当然 也就知道了 类 的 Size 和 字段偏移量  。

    但有一个 问题 是,  D# 编译器 对 字段 的 处理顺序 和  C 编译器 是否一样 ?   如果不一样, 那  D#  把  name 字段 放在 age 之前,  C 编译器 把 age 字段 放在 name 字段 之前,  那计算出来的 字段偏移量 就不一样了,  就错误了 。    这就 呵呵 了  。

     

    不过  C 编译器 好像是 按照 源代码 里 写的 字段顺序 来 编译 的,  这个可以查证确认一下 。

    比如,  有一个 结构体  Person ,

     

    struct Person

    {

               char[8]    name;

               int      age;

    }

     

    那么, 编译后的结果 应该是  Person  的 Size 是  12 个 byte,  前 8 个 byte 用来 存储  char[8]  name;   ,   后 4 个 字节 用来 存储  int  age;   , (假设 int 是 32 位整数)  。

    如果是这样, 那就没问题了 。  D# 编译器  和  C 编译器   都 按照 源代码 里 书写 的 顺序 来 编译字段 。

    C#  好像也沿袭了这样的做法,  在 反射 里 用   type.GetFields()   方法 返回  Field List,   Field 的 顺序 好像 就是 跟 源代码 里 书写的顺序 一样的 。

    而且在  C#  和 非托管代码 的 交互中(P / Invoke),  C# 里 定义一个 字段名 字段顺序 和  C 里的 Struct 一样的 Struct,  好像也直接可以传给 C 函数用,  比如有一个 C 函数 的 参数 是 struct Person, 在 C# 里 定义一个 和  C 里的 Person 一样的 Struct 可以直接传过去用 。

     

    我们来看一下  方法 的 动态链接 的 具体过程:

    假设  A 项目 里 会调用到 B.dll  的  Person 类 的 方法,   Person 类 有  Sing()  和   Smile()   2 个 方法,  D# 代码 是这样:

     

    public class Person

    {

                public Sing()

                {

                            //    do something

                }

     

                public Smile()

                {

                            //    do something

                }

    }

     

    那么 A 项目 里 调用 这 2 个 方法 的  C 中间代码  是:

     

    Person *       person    ;          //  Person 对象 指针

     

    ……

     

    ilbc_B_MethodList [ 0 ]  ( person );            //  调用  Sing()   方法

    ilbc_B_MethodList [ 1 ]  ( person );            //  调用  Smile()   方法

     

    大家注意, 这里有一个    ilbc_B_MethodList  ,     这是  A 项目 的 D# 代码 编译 生成的  C 中间代码 里的 一个 全局变量:

     

    uint     ilbc_B_MethodList ;

     

    是一个   uint  变量  。

    uint 变量 可以 保存 指针,  ilbc_B_MethodList  实际上 是一个 指针,  表示一个 数组 的 首地址 。

    这个数组 就是 B.dll 的 函数表 。  函数表 用来 保存 B.dll 里  所有类 的 所有方法 的 地址(函数指针),  D# 编译器 在 编译 B 项目 的 时候 会给  每个类的每个方法  编一个 序号 。

    编号规则 还是 跟 编译器  对 源代码 的  语法分析 过程 有关,  基本上 可能还是 跟 书写顺序 有关,  不过 不管 这个 编号规则 如何,  这都没有关系 。

    总之   D# 编译器  会给  所有方法 都 编一个号(Seq No),   每个方法 的 编号 是多少, 这些信息 会 记录在  B.dll   的 元数据 里(metadata.dat),

    D# 编译器 在 编译 A 项目 时, 会根据 A 引用的 B.dll 里的 元数据 知道 B.dll 里的 方法 的 序号,

    这样,  D# 编译器 就可以 把 调用  Sing()  方法 的 代码 处理成 上述的 代码:

     

    ilbc_B_MethodList [ 0 ]  ();            //  调用  Sing()   方法

     

    注意,   ilbc_B_MethodList [ 0 ]   里的  “0”   就是  Sing()  方法 的 序号,  通过 这个 序号 作为    ilbc_B_MethodList  数组 的 下标(index), 可以取得  Sing()  方法 的 函数地址(函数指针),   然后 就可以 调用   Sing()   方法 了 。

     

    上文说了,   ilbc_B_MethodList   表示 B.dll 的 函数表 的 首地址,

    那么,   B.dll  的 函数表 从哪里来 ?

    函数表  是在  加载  B.dll  时生成的 。

    运行时 会把  B.dll   编译为 本地代码  并加载到内存,  然后 调用 上文定义的    ILBC_Link()    函数,

    ILBC_Link()    函数 会 生成 函数表,   并 返回 函数表 的 首地址  。

    ILBC_Link()    函数 的 代码 是这样的:

     

    uint      ilbc_MethodList    [  2  ]  ;          //  这是一个 全局变量

     

    uint      ILBC_Link()

    {

               ilbc_MethodList  [  0  ]   =    &   ilbc_Method_Person_Sing   ;

               ilbc_MethodList  [  1  ]   =    &   ilbc_Method_Person_Smile   ;

     

               return       ilbc_MethodList     ;

    }

     

    void       ilbc_Method_Person_Sing   ( thisPtr )

    {

               //         do something

    }

     

    void       ilbc_Method_Person_Smile   ( thisPtr )

    {

               //         do something

    }

     

    uint      ilbc_MethodList    [  2  ]  ;      就是  B.dll  的 函数表,  这是一个 全局变量  。

    里面的 数组长度  “2”  表示  B.dll  里 有 2 个方法,  现在 B.dll 里只有 1 个 类 Person,  Person 类 有 2 个方法, 所以 整个 B.dll  只有 2 个方法  。

    如果 B.dll 有 多个类, 每个类有 若干个 方法,  那 D# 编译器 会 先对 类 排序, 再对 类里的方法 排序,  总之 会给 每个 方法 一个 序号  。

     

    uint      ILBC_Link()      函数     的 逻辑 就是 根据  方法 的 序号 把  方法 的 函数地址 填入 ilbc_MethodList  数组 对应的 位置,

    再返回    ilbc_MethodList   数组 的 首地址  。

     

    也就是     先 生成 函数表,   再 返回 函数表 首地址  。

    上文说了,  运行时 加载 B.dll  的 过程 是,  先把 B.dll 编译成 本地代码, 加载到 内存,  再调用   ILBC_Link()   函数,  这样 B 的 本地代码 函数表 就生成了 。

    然后 运行时 会把   ILBC_Link()  函数   返回 的 函数表 首地址  赋值给   A   的  ilbc_B_MethodList  ,  这样 A 就可以 调用 B 的 方法了 。

     

    因为 函数 是 动态链接 的, 函数表 里 函数 的 顺序 是 由 D# 编译器 决定的, 所以 和  C 编译器 无关, 不需要像 字段 那样 考虑  C 编译器 对 函数 的 处理顺序  。

     

    以上就是  ILBC  的 草案 。   还会 陆续补充 。

     

    IL 层 动态链接 是 ILBC 的  一个 基础架构  。

     

    ILBC  的 一大特点 是 同时支持  AOT 和 JIT ,   AOT  和  JIT  可以混合使用,  也可以 纯 AOT,   或者 纯 JIT 。

     

    我查了一下,   “最小的 C 语言编译器”,  查到 一个  Tiny C,  可以看下 这篇文章  《TCC(Tiny C Compiler)介绍》  http://www.cnblogs.com/xumaojun/p/8544083.html    ,

    还查到一篇 文章 《让你用C语言实现简单的编译器,新手也能写》  https://blog.csdn.net/qq_42167135/article/details/80246557   ,

    他们 还有个 群,   我打算去加一加 。

     

    还查到一篇 文章 《手把手教你做一个 C 语言编译器:设计》  https://www.jianshu.com/p/99d597debbc2   ,

     

    看了一下他们的文章,   主要是 我 对 汇编 和 操作系统 环境 不熟, 不然 我也可以写一个 小巧 的  C 语言编译器 。

     

    ILBC  会 自带 运行时,  如果是 纯 AOT,  那么 运行时 里 不用 带  C 语言编译器,  这样 运行时 就可以 小一些 。

    如果 运行时 不包含 庞大的 类库,  又不包含 C 语言编译器,  那么 运行时 会很小 。

     

    我建议   ILBC  不要用 在 操作系统 上 安装 运行时 的 方式,  而是 每个 应用程序 随身携带 运行时,

    ILBC 采用 简单的 、即插即用 的 方式,  引用到的  ILBC 程序集 放在 同一个 目录下 就可以找到 。

    程序集 不需要 安装,  也不需要 注册 。

     

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

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

    这有  2 个 原因:

    1   GC 需要运行在一个 独立的 线程里,  GC 负责 内存回收 和 空闲空间排序 。  所以 D# 需要有一个 线程 的 架构 。

    2   D# 的 堆 算法 是 不严格的 、松散的, 需要运行在 虚拟内存 广大的 地址空间 和 存储空间 下,  不适合 用于 物理内存 。

    所以,  D#  的 适用场景 是 在 进程调度 虚拟内存 文件系统 的 基础上 。

    为什么 和 文件系统 有关系 ?

    因为 虚拟内存 会用到 文件系统, 所以  ~  。

     

    D#  /  ILBC    的 目标 是    跨平台  跨设备 。

     

    后面会把  进一步 的 设计 放在 系列文章 里, 文章列表 如下:

     

    《我发起并创立了一个 C 语言编译器 开源项目 InnerC》  https://www.cnblogs.com/KSongKing/p/10352273.html  

    《ILBC 运行时 (ILBC Runtime) 架构》  https://www.cnblogs.com/KSongKing/p/10352402.html  

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

    《堆 和 GC》               写作中  。

    《InnerC 语法分析器》            写作中  。

     

     

     

     

  • 相关阅读:
    Delete 语句带有子查询的sql优化
    标量子查询SQL改写
    自定义函数导致的sql性能问题
    Oracle 11G RAC For ASM 利用RMAN COPY进行存储迁移
    WPF 如何控制右键菜单ContextMenu的弹出
    将字符串以用二进制流的形式读入XML文件
    WPF 将数据源绑定到TreeView控件出现界面卡死的情况
    WPF如何实现TreeView节点重命名
    Azure一个Cloud Service支持多个公网地址
    Azure上部署Barracuda WAF集群 --- 2
  • 原文地址:https://www.cnblogs.com/KSongKing/p/10348190.html
Copyright © 2011-2022 走看看