惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
Cyberwarzone
Cyberwarzone
The GitHub Blog
The GitHub Blog
云风的 BLOG
云风的 BLOG
P
Proofpoint News Feed
小众软件
小众软件
Recent Announcements
Recent Announcements
博客园 - 三生石上(FineUI控件)
Security Archives - TechRepublic
Security Archives - TechRepublic
W
WeLiveSecurity
Cloudbric
Cloudbric
博客园 - 司徒正美
美团技术团队
N
News and Events Feed by Topic
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
PCI Perspectives
PCI Perspectives
宝玉的分享
宝玉的分享
H
Help Net Security
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Google DeepMind News
Google DeepMind News
Help Net Security
Help Net Security
Last Week in AI
Last Week in AI
S
Schneier on Security
N
News | PayPal Newsroom
B
Blog RSS Feed
L
LINUX DO - 最新话题
T
Troy Hunt's Blog
S
Secure Thoughts
雷峰网
雷峰网
aimingoo的专栏
aimingoo的专栏
L
Lohrmann on Cybersecurity
G
Google Developers Blog
Microsoft Azure Blog
Microsoft Azure Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
T
Tenable Blog
S
Securelist
L
LangChain Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
I
InfoQ
H
Heimdal Security Blog
Cisco Talos Blog
Cisco Talos Blog
F
Full Disclosure
Y
Y Combinator Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
K
Kaspersky official blog
T
Tailwind CSS Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
阮一峰的网络日志
阮一峰的网络日志
C
Cisco Blogs

侯锐的思考与分享

如何进行站点网络优化 使用Elasticsearch分析腾讯云EO日志 XXL-JOB的部署、搭建与使用 使用NGINX的auth_request进行统一jwt鉴权 使用APISIX解析jwt并获取payload信息 《推荐系统实践》 《上瘾:让用户养成使用习惯的四大产品逻辑》 Kotlin与Java对照手册 ComfyUI的操作与使用 ComfyUI简介 利用whisper为视频自动生成字幕 ffmpeg笔记 如何参与Apache顶级开源项目 使用frp实现内网ssh穿透 安装并使用zsh APISIX的使用 使用ANTLR4格式化JSON ANTLR4从入门到实践 ShardingSphere-JDBC介绍 Maven详细介绍
自己动手实现一个可以运行在JVM上的编程语言
Raymond · 2023-09-07 · via 侯锐的思考与分享

众所周知,JVM虚拟机被设计为可以执行栈式指令的机器。因此任何一个语言只要编译之后得到的字节码符合JVM的标准,就可以在JVM上执行,例如Kotlin、Groovy、Scala、Clojure。

我们自己设计一款语言,并命名为Jinx,它支持类定义、变量定义、变量打印。它的语法解析逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
grammar Jinx;

@header {
package com.nosuchfield.jinx.code;
}

jinx: CLASS ID LEFT_BR classBody RIGHT_BR EOF;
classBody: (variable | print)*;
variable: VARIABLE ID EQUALS value;
print: PRINT ID;
value: STRING | INT | DOUBLE;

LEFT_BR: '{';
RIGHT_BR: '}';
CLASS: 'class';
VARIABLE: 'var';
PRINT: 'print';
EQUALS: '=';
STRING: '"' ('\\"' | ~'"')+ '"';
DOUBLE: [0-9]+ '.' [0-9]+;
INT: [0-9]+;
// 这个ID不能放在前面,不然会被提前解析,导致print等字符串被解析为ID
ID: [a-zA-Z] [a-zA-Z0-9]*;

WS: [\n\r\t ]+ -> skip;

Jinx的最外层是类class,class的内部可以包含变量的定义和打印,变量的值支持字符串、整数和小数。有了ANTLR4的解析逻辑之后,我们就可以处理程序的语法树了,语法树的解析如下

flat
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class Loader extends JinxBaseListener {




private final Map<String, ImmutablePair<Integer, Integer>> variables = new HashMap<>();




private final List<Instruction> instructions = new ArrayList<>();

private String className;

@Override
public void enterJinx(JinxParser.JinxContext ctx) {
className = ctx.ID().getText();
}

@Override
public void exitVariable(JinxParser.VariableContext ctx) {

String name = ctx.ID().getText();
JinxParser.ValueContext variable = ctx.value();

String text = variable.getText();

int type = variable.getStart().getType();

int idx = variables.size();


variables.put(name, ImmutablePair.of(idx, type));

instructions.add(new VariableInstruction(idx, type, text));
}

@Override
public void exitPrint(JinxParser.PrintContext ctx) {
String name = ctx.ID().getText();
if (!variables.containsKey(name)) {
System.err.printf("variable %s not exist\n", name);
System.exit(1);
}
int idx = variables.get(name).getLeft();
int type = variables.get(name).getRight();

instructions.add(new PrintInstruction(idx, type));
}

public List<Instruction> getInstructions() {
return instructions;
}

public String getClassName() {
return className;
}

}

在上面的语法树解析中,我们会解析每一个变量的定义语法和打印语法。

变量定义

我们会在定义每个变量的时候记录下变量的类型和索引,并把记录的数据关联到这个变量的名字上。此外,我们还会针对这个变量的类型、索引和值生成JVM保存变量的指令。

变量打印

在打印程序的解析中,我们会先通过变量的名称从关联表中取出变量的类型和索引(如果不存在就报错),之后根据变量的类型和索引创建JVM打印的指令。

上面的语法树解析最终生成了一个指令列表instructions,我们接下来根据这个指令列表生成JVM所需要的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private byte[] generateBytecode(List<Instruction> instructions, String className) {
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
classWriter.visit(V1_8, ACC_PUBLIC + ACC_SUPER, className, null, "java/lang/Object", null);

MethodVisitor methodVisitor = classWriter.visitMethod(ACC_PUBLIC + ACC_STATIC, "main",
"([Ljava/lang/String;)V", null, null);
for (Instruction instruction : instructions) {
instruction.apply(methodVisitor);
}
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(0, 0);
methodVisitor.visitEnd();
classWriter.visitEnd();
return classWriter.toByteArray();
}

如上我们根据指令和类名使用ASM生成了字节码数据,它生成了一个包含main方法的类,并且把我们的指令放在main方法中。每个指令都调用了其apply方法,接下来我们具体看一下变量定义和变量打印的apply方法是如何实现的。

变量定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void apply(MethodVisitor mv) {
switch (type) {
case JinxLexer.DOUBLE -> {
double val = Double.parseDouble(value);

mv.visitLdcInsn(val);

mv.visitVarInsn(DSTORE, idx);
}
case JinxLexer.INT -> {
int val = Integer.parseInt(value);
mv.visitLdcInsn(val);
mv.visitVarInsn(ISTORE, idx);
}
case JinxLexer.STRING -> {
mv.visitLdcInsn(Utils.removeFirstAndLastChar(value));
mv.visitVarInsn(ASTORE, idx);
}
}

变量的定义很简单,都是先把变量的值从常量池取出,然后推到操作数栈的顶部。之后从操作数栈顶取数据,根据变量的idx把变量保存到局部变量表的指定索引位置。区别在于浮点型的保存指令是DSTORE,整型是ISTORE,字符串是ASTORE

变量打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void apply(MethodVisitor mv) {
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
switch (type) {
case JinxLexer.INT -> {
mv.visitVarInsn(ILOAD, idx);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
}
case JinxLexer.DOUBLE -> {
mv.visitVarInsn(DLOAD, idx);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(D)V", false);
}
case JinxLexer.STRING -> {
mv.visitVarInsn(ALOAD, idx);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}
}

变量的打印会先使用System.out变量,之后从局部变量表中根据变量的idx取出变量的值,然后执行println方法,入参分别为整型、浮点型和字符串。

有了以上这些指令,我们就可以正常生成字节码了,我们进行语法分析生成instructions,并使用instructions最终生成字节码文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void compile0(String file) throws IOException {

JinxLexer lexer = new JinxLexer(CharStreams.fromFileName(file));
CommonTokenStream tokens = new CommonTokenStream(lexer);

JinxParser parser = new JinxParser(tokens);
parser.removeErrorListeners();
parser.addErrorListener(new ErrorHandler());
ParseTree tree = parser.jinx();

ParseTreeWalker parseTreeWalker = new ParseTreeWalker();
Loader loader = new Loader();
parseTreeWalker.walk(loader, tree);

List<Instruction> instructions = loader.getInstructions();

String className = loader.getClassName();
String classFile = Paths.get(new File(file).getParent(), className + ".class").toString();
writeByteArrayToFile(classFile, generateBytecode(instructions, className));
}

上面代码的最后一行就是根据指令列表和类名生成字节码,并把字节码保存到文件中。我们创建一个源代码

class Test {
    var name = "Mike"
    var salary = 2370
    print name
    print salary
    var number = 1.1
    print number
}

使用编译器解析如上代码并最终生成一个字节码文件Test.class,运行这个字节码文件可以打印出变量的值

$ java Test
Mike
2370
1.1

我们也可以查看字节码的信息如下

$ javap -verbose Test
Classfile /src/main/resources/jinx/Test.class
Last modified Jan 3, 2023; size 342 bytes
MD5 checksum fff7d9ac9c044299ffd5a6194c452502
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Utf8               Test
#2 = Class              #1             // Test
#3 = Utf8               java/lang/Object
#4 = Class              #3             // java/lang/Object
#5 = Utf8               main
#6 = Utf8               ([Ljava/lang/String;)V
#7 = Utf8               Mike
#8 = String             #7             // Mike
#9 = Integer            2370
#10 = Utf8               java/lang/System
#11 = Class              #10            // java/lang/System
#12 = Utf8               out
#13 = Utf8               Ljava/io/PrintStream;
#14 = NameAndType        #12:#13        // out:Ljava/io/PrintStream;
#15 = Fieldref           #11.#14        // java/lang/System.out:Ljava/io/PrintStream;
#16 = Utf8               java/io/PrintStream
#17 = Class              #16            // java/io/PrintStream
#18 = Utf8               println
#19 = Utf8               (Ljava/lang/String;)V
#20 = NameAndType        #18:#19        // println:(Ljava/lang/String;)V
#21 = Methodref          #17.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
#22 = Utf8               (I)V
#23 = NameAndType        #18:#22        // println:(I)V
#24 = Methodref          #17.#23        // java/io/PrintStream.println:(I)V
#25 = Double             1.1d
#27 = Utf8               (D)V
#28 = NameAndType        #18:#27        // println:(D)V
#29 = Methodref          #17.#28        // java/io/PrintStream.println:(D)V
#30 = Utf8               Code
{
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
    stack=3, locals=4, args_size=1
        0: ldc           #8                  // String Mike
        2: astore_0
        3: ldc           #9                  // int 2370
        5: istore_1
        6: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
        9: aload_0
        10: invokevirtual #21                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
        16: iload_1
        17: invokevirtual #24                 // Method java/io/PrintStream.println:(I)V
        20: ldc2_w        #25                 // double 1.1d
        23: dstore_2
        24: getstatic     #15                 // Field java/lang/System.out:Ljava/io/PrintStream;
        27: dload_2
        28: invokevirtual #29                 // Method java/io/PrintStream.println:(D)V
        31: return
}

参考

ANTLR4表达式
Java代码
Java ASM系列
Enkel-JVM-language

本文链接: https://www.nosuchfield.com/2023/09/06/Implementing-a-Programming-Language-Running-on-the-JVM/