大家好,

我发起并创立了一个 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#的更多相关文章

  1. 我发起并创立了一个 C 语言编译器 开源项目 InnerC

    本文是 VMBC / D#  项目 的 系列文章, 有关 VMBC / D# ,  见 <我发起并创立了一个 VMBC 的 子项目 D#>(以下简称 <D#>)  https: ...

  2. 我发起并创立了一个 Javascript 前端库 开源项目 jWebForm

    在线演示地址: ( 在线演示 云平台 由 Kooboo 提供  https://www.kooboo.com/ ) 按钮:      http://iwebform.kgeking.kooboo.si ...

  3. 我发起并创立了一个 EPWA 的 开源项目

    EPWA ,  是  Easy PWA  的 意思, PWA 取自于 Google 的 PWA, EPWA   是一个用   C#  Cef  Html  js  css   开发 桌面程序 的 架构 ...

  4. 我发起并创立了一个 .Net 平台上的 Web 业务系统 基础库 开源项目 WebEasy

    我 强调一点, 程序员 应该对 程序 有 控制感 . 过多的 控制反转 使 程序员 丧失了 对 程序 的 控制感 . 过多的 依赖注入 束缚了 程序员 的 创造力 . 过度复杂的 架构设计 束缚了 程 ...

  5. 我发起了一个 ILBC 的 子项目 ILBC Studio

    ILBC  见 <ILBC 规范>  https://www.cnblogs.com/KSongKing/p/10354824.htm 发起这个项目的原因是, 本来想用 VsCode 来写 ...

  6. DNS Tunneling及相关实现——总之,你发起攻击都需要一个DNS server,下载一些工具作为client发起数据,server收集数据并响应

    摘自:http://www.freebuf.com/sectool/112076.html DNS Tunneling,是隐蔽信道的一种,通过将其他协议封装在DNS协议中传输建立通信.因为在我们的网络 ...

  7. 我发起了一个 ILBC 的 子项目 EScript

    ILBC  见 <ILBC 规范>  https://www.cnblogs.com/KSongKing/p/10354824.html 今天装了个 VsCode , 听说 VsCode ...

  8. java环境搭建(及安装问题“No repository found containing”解决) 并创立第一个java程序

    环境: java8 及 Eclipse java8 配置:http://jingyan.baidu.com/article/e2284b2b5967e7e2e7118d74.html Eclipse ...

  9. ILBC 规范

    本文是 VMBC / D# 项目 的 系列文章, 有关 VMBC / D# , 见 <我发起并创立了一个 VMBC 的 子项目 D#>(以下简称 <D#>) https://w ...

随机推荐

  1. <Java><Multi-thread><Lock interface>

    Overview 介绍java的lock interface. Motivation java拥有像synchronized这样的内置锁,那为什么还需要lock这样的外置锁呢? 首先,性能不是选择sy ...

  2. <顺序访问><随机访问><HDFS>

    Overview 如果你了解过HDFS,至少看过这句话吧: HDFS is a filesystem designed for storing very large files with stream ...

  3. L293 给地球降温

    Countries look at ways to tinker with Earth’s thermostat The idea of cooling the climate with strato ...

  4. logging日志模块,hashlib hash算法相关的库,

    logging: 功能完善的日志模块 import logging #日志的级别 logging.debug("这是个调试信息")#级别10 #常规信息 logging.info( ...

  5. SQL注入之Sqli-labs系列第二十八关(过滤空格、注释符、union select)和第二十八A关

    开始挑战第二十八关(Trick with SELECT & UNION) 第二十八A关(Trick with SELECT & UNION) 0x1看看源代码 (1)与27关一样,只是 ...

  6. Webservice客户端动态调用服务端功能方法

    一.发布WebService服务 方式一:在服务端生成wsdl文件,下方客户端直接引用即可     优点:针对要发布的方法生成一个wsdl文件即可,无需多余配置.   缺点:每次服务端方法发生改变都需 ...

  7. Android内核sys_setresuid() Patch提权(CVE-2012-6422)

    让我们的Android ROOT,多一点套路. 一.简单套路 CVE-2012-6422的漏洞利用代码,展示了另一种提权方法.(见附录) 这也是一个mmap驱动接口校验导致映射任意内核地址的洞.将内核 ...

  8. stack-protector-strong

    Improve protection against stack buffer overflows Much like its predecessor, stack-protector, stack- ...

  9. 一个简单的 IDA f5插件问题分析

    有人提出问题,以下汇编f5结果缺失代码: .text:00000C18 Java_com_a_b_c .text:00000C18 PUSH {R3,LR} .text:00000C1A CMP R2 ...

  10. Django框架之下载以及基本命令

    Django的下载与基本命令 pip3 install Django #下载完成后保存在python解释器下的bin文件目录下 创建一个Django Project #命令行输入: django-ad ...