我发起并创立了一个 VMBC 的 子项目 D#
Posted 凯特琳
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了我发起并创立了一个 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 语法分析器》 写作中 。
以上是关于我发起并创立了一个 VMBC 的 子项目 D#的主要内容,如果未能解决你的问题,请参考以下文章
我发起并创立了一个 Javascript 前端库 开源项目 jWebForm
我发起了一个 分布式爬虫调度 的 开源项目 DSpiders