阅读有一定数量的代码时,首先要做的就是把握代码目录以及文件的构成。这一节将对本书制作的C Ь 编译器cbc 的代码构成进行说明。 cbc 的代码树 cbc 采用Java 标准的目录结构,即将作者的域名倒序,将倒序后的域名作为包(package)名的前缀,按层次排列。比如,笔者的个人主页的域名是loveruby.net,则包名以net.loveruby 开头,接着是程序的名称cflat,其下面排列着cbc 所用的包。代码的目录结构如图2.1 所示。 图2.1 cbc 中包的层次 从asm 到utils 的11 个目录,各自对应着同名的包。也就是说,cbc 有11 个包,所有cbc 的类都属于这11 个包中的某一个。cbc 不直接在net.loveruby 和net.loveruby.cflat 下面放置类。 cbc 的包 cbc 的包的内容如表2.1 所示(省略了包名的前缀net.loveruby.cflat)。表2.1 cbc 中的包 图 在这些包之中,asm、ast、entity、ir、type 这5 个包可以归结为数据相关(被操作)的类。另一方面,compiler、parser、sysdep、sysdep.x86 这4 个包可以归结为处理相关(进行操作的一方)的类。 把握代码整体结构时最重要的包是compiler 包,其中基本收录了cbc 编译器前端的所有内容。例如,编译器程序的入口函数main 就定义在compiler 包的Compiler 类中。 compiler 包中的类群 我们先来看一下compiler 包中的类。compiler 包中主要的类如表2.2 所示。 表2.2 compiler 包中主要的类 图 Compiler 类是统管cbc 的整体处理的类。编译器的入口函数main 也在Compiler 类中定义。 从Visitor 类到TypeResolver 类都是语义分析相关的类。关于这些类的作用将在第9章详细说明。 最后,IRGenerator 是将抽象语法树转化为中间代码的类,详情请参考第11 章。 main 函数的实现 在本章最后,我们一起来大概地看一下Compiler 类的代码。Compiler 类中main 函数的代码如代码清单2.3 所示。 代码清单2.3 Compiler#main(compiler/Compiler.java) static final public String ProgramName = "cbc"; static final public String Version = "1.0.0"; static public void main(String[] args) { new Compiler(ProgramName).commandMain(args); } private final ErrorHandler errorHandler; public Compiler(String programName) { this.errorHandler = new ErrorHandler(programName); } main 函数中,通过new Compiler(ProgramName) 生成Compiler 对象,将命令行参数args 传递给commandMain 函数并执行。ProgramName 是字符串常量"cbc"。 Compiler 类的构造函数中,新建ErrorHandler 对象并将其设为Compiler 的成员。之后,在输出错误或警告消息时使用该对象。 commandMain 函数的实现 接着来看一下负责cbc 主要处理的commandMain 函数(代码清单2.4)。原本的代码中包含较多异常处理的内容,比较繁琐,因此这里只列举主要部分。 代码清单2.4 Compiler#commandMain 的主要部分(compiler/Compiler.java) public void commandMain(String[] args) { Options opts = Options.parse(args); List<SourceFile> srcs = opts.sourceFiles(); build(srcs, opts); } commandMain 函数中,首先用Options 类的parse 函数来解析命令行参数args,并取得SourceFile 对象的列表(list)。一个SourceFile 对象对应一份源代码。实际的build 部分,是由build 函数来完成的。 Options 对象中的成员如表2.3 所示。 表2.3 Options 对象的成员 图 Options 对象中还定义有其他成员和函数,因为只和代码生成器、汇编器、链接器相关,所以等介绍上述模块时再进行说明。 Java5 泛型 可能有些读者对List<SourceFile> 这样的表达式还比较陌生,所以这里解释一下。 List<SourceFile> 表示“ 成员的类型为SourceFile 的列表”, 简单地说就是“SourceFile 对象的列表”。到J2SE 1.4 为止,还不可以指定List、Set 等集合中元素对象的类型。从Java 5 开始,才可以通过集合类名< 成员类名> 来指定元素成员的类型。 通过采用这种写法,Java 编译器就知道元素的类型,在取出元素对象时就不需要进行类型转换了。 这种能够对任意类型进行共通处理的功能称为泛型。在Java5 新增的功能中,泛型使用起来尤其方便,是不可缺少的一项功能。 build 函数的实现 我们继续看负责build 代码的build 函数,其代码大概如代码清单2.5 所示。 代码清单2.5 Compiler#build 的主要部分(compiler/Compiler.java) public void build(List<SourceFile> srcs, Options opts) throws CompileException { for (SourceFile src : srcs) { compile(src.path(), opts.asmFileNameOf(src), opts); assemble(src.path(), opts.objFileNameOf(src), opts); } link(opts); } 首先,用foreach 语句(稍候讲解)将SourceFile 对象逐个取出,并交由compile 函数进行编译。compile 函数是对单个C Ь 文件进行编译,并生成汇编文件的函数。 接着,调用assemble 函数来运行汇编器,将汇编文件转换为目标文件。 最后,使用link 函数将所有的对象文件和程序库链接。 可见上述代码和第1 章中叙述的build 的过程是完全对应的。 Java 5 的foreach 语句 这里介绍一下Java 5 中新增的foreach 语句。foreach 语句, 在写代码时也可以写成“for...”,但通常叫作foreach 语句。 foreach 语句是反复使用Iterator 对象的语句的省略形式。例如,在build 函数中有如下foreach 语句。 for (SourceFile src : srcs) { compile(src.path(), opts.asmFileNameOf(src), opts); assemble(src.path(), opts.objFileNameOf(src), opts); } 这个foreach 语句等同于下面的代码。 Iterator<SourceFile> it = srcs.iterator(); while (it.hasNext()) { SourceFile src = it.next(); compile(src.path(), opts.asmFileNameOf(src), opts); assemble(src.path(), opts.objFileNameOf(src), opts); } 通过使用foreach 语句,遍历列表等容器的代码会变得非常简洁,因此本书中将尽量使用foreach 语句。 compile 函数的实现 最后我们来看一下负责编译的compiler 函数的代码。剩余的assemble 函数和link 函数将在本书的第4 部分进行说明。 compiler 函数中也有用于在各阶段处理结束后停止处理的代码等,多余的部分比较多,所以这里将处理的主要部分提取出来,如代码清单2.6 所示。 代码清单2.6 Compiler#compiler 的主要部分(compiler/Compiler.java) public void compile(String srcPath, String destPath, Options opts) throws CompileException { AST ast = parseFile(srcPath, opts); TypeTable types = opts.typeTable(); AST sem = semanticAnalyze(ast, types, opts); IR ir = new IRGenerator(errorHandler).generate(sem, types); String asm = generateAssembly(ir, opts); writeFile(destPath, asm); } 首先,调用parseFile 函数对代码进行解析,得到的返回值为AST 对象(抽象语法树)。 再调用semanticAnalyze 函数对AST 对象进行语义分析,完成抽象语法树的生成。接着,调用IRGenerator 类的generate 函数生成IR 对象(中间代码)。至此就是编译器前端处理的代码。 之后,调用generateAssembly 函数生成汇编语言的代码,并通过writteFile 函数写入文件。这样汇编代码的文件就生成了。 从下一章开始,我们将进入语法分析的环节。