背景
前几年主要在搞"编译优化"相关的事情(热修框架、字节码hook框架、字节码优化之类的)不过其实严格来说做的这些跟"编译"没啥关系,只是熟悉Java字节码以及对asm、javassist这类库的api比较熟而已,只是叫"编译优化"听起来高大上一些😂
当时也想着去写一个真正的"编译器",看了一点"龙书"、"编译器设计"之类比较有名的书籍,发现连实现一个词法分析器似乎都很困难,直接就劝退了。
后来因为业务需要,开发了一个javac的插件:javac 编译器插件在代码迁移中的简单应用,插件本身实现比较简单,当时搞这个的时候顺带看了下javac的代码,似乎并不算太复杂。于是就仿照他也写个自己的Java编译器,实现了部分的语法,不过没有坚持写下去,部分原因是我当时的实现方案无法实现apt之类的机制。
虽然只实现了很小的一部分,但对我而言收益还是不小的,比如:
- 这是我实现的第一个手写的词法分析器,语法分析器,算是迈出了编译器开发的第一步
- 此外还让我发现了一个多年来都未曾注意到的Java语法:java里面返回多维数组的时候,数组的维度可以分别写在函数声明的左右两边,维度是两边之和,所以下面的三个声明是等效的,都是返回一个二维数组,虽然知道这个没啥价值,但若不是尝试写这个小编译器我可能一直不会知道这一点。
java
private static int[][] f() {...}
private static int[] f()[] {...}
private static int f()[][] {...}
近期在搞一些稳定性相关的事情,有些时候需要通过hook来定位或者修复问题,不免要看看虚拟机的代码,不考虑各种优化,就解释执行而言似乎也不是特别复杂,又临近五一假期,就想着实现个动态类型的编程语言,这样编译器、虚拟机开发就都能体验一遍,于是就有了这个项目--charon
charon
目前实现的特性如下:
- 支持的类型:bool、long、double、string、function、class
- 函数可以赋值给变量、类的字段、作为参数或者返回值(method跟function不同,不是first-class类型,不能赋值给变量,类字段,也不能作为函数、方法的参数或者返回值)
- 支持常见的语言结构,比如:if-elseif-else,while-break-continue
- 支持定义类、方法,方法中可以通过'this'访问当前实例的字段
- 支持简单的
ffi
机制,用于实现charon
做不到的事情,比如打印输出: __print, __println
- 类EBNF的语法描述可以参考:grammar.txt
- 字节码格式&字节码指令可以参考: bytecode-format.txt
具体的语法&语意后面预计会在"语法介绍&语法分析器实现"的文章中提一下,比如在charon中类似于rust:变量是可以重定义的,赋值是语句而不是表达式,if的'then'是block而不是一般的语句等。
代码仓库&构建
代码仓库:github.com/0x264/charo...
构建:
- 需要先安装rust,如果之前未安装过,可以先按rust官网的指示去安装一下
- clone一下代码仓库
- cd 到 charon 项目根目录
- 执行:
cargo build --release
执行完上面的步骤后,会在target/release
目录下生成3个可执行程序:
charonc
: charon 的编译器,会将代码编译成字节码,文件后缀:.charonbc
,字节码格式类似于Java字节码charonp
: 反汇编charonc
生成的字节码文件.charonbc
,功能类似于javapcharon
: 虚拟机可执行程序,可以传入charon
源代码,也可以传入charonc
编译生成的字节码
代码示例
工程根目录下有个examples
,里面有一些示例代码可以参考。
下面用charon
构建一棵二叉树,并进行中序遍历作为一个例子:
ini
#!/usr/bin/env charon
// 调用createBinaryTree创建一棵二叉树,然后通过inOrder进行中序遍历
inOrder(createBinaryTree());
class Node {}
// 10
// / \
// 6 14
// / \ / \
// 4 8 12 16
//
func createBinaryTree() {
var left = Node();
left.value = 4;
var right = Node();
right.value = 8;
var l = Node();
l.value = 6;
l.left = left;
l.right = right;
var left = Node();
left.value = 12;
var right = Node();
right.value = 16;
var r = Node();
r.value = 14;
r.left = left;
r.right = right;
var root = Node();
root.value = 10;
root.left = l;
root.right = r;
return root;
}
func inOrder(root) {
if (root.left) {
inOrder(root.left);
}
__println(root.value);
if (root.right) {
inOrder(root.right);
}
}
上面的代码示例可以有如下几种方法执行:
-
直接调用虚拟机执行:
charon binary-tree.charon
-
先编译成字节码,再交由虚拟机执行
charonc binary-tree.charon
charon binary-tree.charonbc
-
charon
是支持shebang
,因此可以给源码文件加上可执行权限,然后直接执行chmod +x binary-tree.charon
./binary-tree.charon
另外从上面的代码可以看到:
- 函数不必提前声明即可使用
- 变量可以重定义
- 跟很多动态语言一样类的字段不必在类体中定义
- 对于未赋值的类字段,读取时将得到null
- 像null之类的值在作为bool表达式时会隐式转换为false
- ...
后续计划
-
后面计划整理几篇相关的文档以期加深对编译器&虚拟机相关技术的理解,预计会补充以下几篇文章:
- 词法介绍&词法分析器的实现
- 语法介绍&语法分析器的实现
- 语意分析、字节码设计&字节码生成(charon是基于栈的,也会和基于寄存器的实现简单对比下)
- 虚拟机的实现(比如栈帧的布局,对于method 'this'的支持实现和java不太相同,以及ffi的支持,后面会简单讨论一下)
- 编译器&虚拟机中的错误处理(编译过程中的行号、列号信息处理、关于错误恢复的讨论,运行时的栈溢出检测等)
-
虽然是个玩具项目,但开发他还是学到了一些东西:比如之前我一直以为熟悉Java字节码,但对于像max_locals这样的信息是否必要,以及虚拟机如何使用?虚拟机在调用方法,以及调用外部方法的时候如何进行栈帧布局的?等等在此之前我其实并不清楚。不过这个语言还是太小了,而且我也不会真的去用,所以即使加上gc等机制可能也没有test case去测,如果后面有时间的话可能会考虑学习写个简单的jvm😂
受制于能力和时间限制,原本想支持的一些特性,比如类的自定义构造函数等等没来得及搞,原本想支持一个简单的gc的,也没来得及做,并且测试也很少😂,若有大佬发现bug或者有建议,希望帮忙指正~