你们好,javascript
我发起并创立了一个 VMBC 的 子项目 D# 。html
有关 VMBC , 请参考 《我发起了一个 用 C 语言 做为 中间语言 的 编译器 项目 VMBC》 http://www.javashuo.com/article/p-djxdwein-hp.html ,java
和 《漫谈 编译原理》 http://www.javashuo.com/article/p-fhvkvvhp-hk.html 。c++
D# , 就是一个 简单版 的 C# 。web
下面说一下 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++ 虚函数 的 实现原理》 http://www.javashuo.com/article/p-wtpwxdib-du.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》 http://www.javashuo.com/article/p-gpskqdni-bo.html
《ILBC 运行时 (ILBC Runtime) 架构》 http://www.javashuo.com/article/p-vvybjngc-be.html
《ILBC 规范》 http://www.javashuo.com/article/p-hsmtjoox-s.html
《堆 和 GC》 写做中 。
《InnerC 语法分析器》 写做中 。