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

你们好,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 语法分析器》            写做中  。

相关文章
相关标签/搜索