【翻译】Javac骇客指南

这篇文章英文的原文在:http://scg.unibe.ch/archive/projects/Erni08b.pdf. 做毕业设计报告时,老师要求必须翻译一篇外文,于是很认真的翻译了一下,也算是为开源做一点小贡献。翻译如下:

Javac骇客指南

David Erni and Adrian Kuhn

University of Bern, March 2008

0. 摘要:

这篇文章是介绍修改java编译器的。其中包含对Java编译器的介绍,以及两个例子的实现:一个简单的hello world 和一个重写AST(抽象语法树)的插件。

1. 介绍

随着Java 6的发布,java编译器已经有了开源的版本了。开源的编译器是OpenJDK项目的一部分,可以从Java编译器小组的网站下载 http://www.openjdk.org/groups/compiler/ 。然而就这篇文档的例子来说,任何Java 6的版本都是可用的,因为这些例子并不会重新编译编译器,他们只是扩展编译器的功能。

这篇文章介绍了Java编译器的内在实现。首先我们给出java编译器所包含的编译步骤,然后我们在编写两个例子。两个例子都使用到了编译器里面的插件机制,也就是JSR269所描述的机制。然而,这两个例子却超出了JSR269的范围。把JSR对象和编译器对接,我们实现了AST的重写。我们的例子里面,没有使用assertion(断言)语句,而使用了if-throw语句。

2. Java编译器的内核

这个部分概括了OpenJDK里面的java编译器的编译步骤和对应的注释。这个小节包含了一个简短的介绍。

编译的过程是由定义在com.sun.tools.javac.main里面的Java Compiler类来决定的。当编译器以默认的编译参数编译时,它会执行以下步骤:

a) Parse: 读入一堆*.java源代码,并且把读进来的符号(Token)映射到AST节点上去。

b) Enter: 把类的定义放到符号表(Symbol Table)中去。

c) Process annotations: 可选的。处理编译单元(compilation units)里面所找到的标记(annotation)。

d) Attribute: 为AST添加属性。这一步包含名字解析(name resolution),类型检测(type checking)和常数折叠(constant fold)。

e) Flow: 为前面得到的AST执行流分析(Flow analysis)操作。这个步骤包含赋值(assignment)的检查和可执行性(reachability)的检查。

f) Desugar: 重写AST, 并且把一些复杂的语法转化成一般的语法。

g) Generate: 生成源文件或者类文件。

wps_clip_image-12054_thumb_thumb

2.1 Parse

想要Parse文件,编译器要用到com.sun.tools.javac.parser.*里面的类。作为第一步,词法分析器(lexical analyzer)把输入的字符流(character sequence)映射成一个符号流(token sequence)。然后Parser再把生成的符号流映射成一个抽象语法树(AST)

2.2 Enter

在这个步骤中,编译器会找到当前范围(enclosing scope)中发现的所有的定义(definitions),并且把这些定义注册成符号(symbols)。Enter这个步骤又分为以下两个阶段:

在第一个阶段,编译器会注册所有类的符号,并且把这写符号和相应的范围(scope)联系在一起。实现方法是使用一个Visitor(访问者)类,由上而下的遍历AST,访问所有的类,包括类里面的内部类。Enter给每一个类的符号都添加了一个MemberEnter对象,这个对象是由第二个阶段来调用的

在第二个阶段中,这些类被MemberEnter对象所完成(completed,即完成类的成员变量的Enter)。首先,MemberEnter决定一个类的参数,父类和接口。然后这些符号被添加进了类的范围中。不像前一个步骤,这个步骤是懒惰执行的。类的成员只有在被访问时,才加入类的定义中的。这里的实现,是通过安装一个完成对象(member object)到类的符号中。这些对象可以在需要时调用member-enter

最后,enter把所有的顶层类(top-level classes)放到一个todo-queue中,

2.3 Process Annotations

如果存在标记处理器,并且编译参数里面指定要处理标记,那么这个过程就会处理在某个编译单元里面的标记。JSR269定义了一个接口,可以用来写这种Annotation处理插件。然而,这个接口的功能非常有限,并且不能用Collective Behavior扩展这种语言。主要的限制是JSR269不提供子方法的反射调用。

2.4 Attribute

为Enter阶段生成的所有AST添加属性。应当注意,Attribte可能会需要额外的文件被解析(Parse),通过SourceCompleter加入到符号表中。

大多数的环境相关的分析都是发生在这个阶段的。这些分析包括名称解析,类型检查,常数折叠。这些都是子任务。有些子任务调用下列的一些类,但也可能调用其他的。

l Check:这是用于类型检查的类。当有完成错误(completion error)或者类型错误时,它就会报错。

l Resovle: 这是名字解析的类。如果解析失败,就会报错。

l ConstFold: 这是参数折叠类。常数折叠用于简化在编译时的常数表达式。

l Infer:类参数引用的类。

2.5 Flow

这个阶段会对添加属性后的类,执行数据流的检查。存活性分析(liveness analysis) 检查是否每个语句都可以被执行到。异常分析(Excepetion analysis) 检查是豆每个被抛出的异常都是声明过的,并且这些异常是否都会被捕获。确定行赋值(definite assignment)分析保证每个变量在使用时已经被赋值。而确定性不赋值(definite unassignment)分析保证final变量不会被多次赋值。

2.6 Desugar

除去多余的语法,像内部类,类的常数,assertion断言语句,foreach循环等。

2.7  Generate

这是最终的阶段。这个阶段生成许多源文件或者类文件。到底是生成源文件还是类文件取决于编译选项。

3. 什么是JSR 269

Annotation(标记)是java 5里面引进来的,用于在源代码里面附加元信息(meta-information).Java 6则进一步加强了标记的处理功能,即JSR269. JSR269,即插入式标记处理API,为java编译器添加了一个插件机制。有了JSR269,就有能力为java编译器写一个特定的标记处理器了。

JSR269有两组基本API,一组用于对java语言的建模,一组用于编写标记处理器。这两组API分别存在于javax.lang.model.* 和 javax.annotation.processing里面。JSR269的功能是通过以下的java编译选项来调用的。

-proc:{none,only} 是否执行Annotation处理或者编译

-processor <classes> 指定标记处理器的名字。这个选项将绕过默认的标记处理器查找过程

-processorpath <path> 指定标记处理器的位置

标记处理在javac中时默认开启的。如果要是只想处理标记,而不想编译生成类文件的话,用 –proc:only 选项既即可。

4. 如何用Javac打印出“Hello World!”

在这第一个例子里面,我们些一个简单的标记处理器,用于在编译的时候打印“Hello World!”.我们用编译器的内部消息机制来打印“hello world”。

首先,我们定义如下HelloWorld标记。

public @interface HelloWorld{

}

添加一个Dummy类使用以上的标记

@HelloWorld

public class Dummy{

}

标记处理可能会发生很轮。每一轮处理器只处理特定的一些标记,并且生成的源文件或者类文件,交给下一轮来处理。如果处理器被要求只处理特定的某一轮,那么他也会处理后续的那些次,包括最后一轮,就算最后一轮没有可以处理的标记。处理器可能也会去处理被这个工具生成的文件。

后一个方法处理前一轮生成的标记类型,并且返回是否这些标记会声明。如果返回是True,那么后续的处理器就不会去处理它们。如果返回是false,那么后续处理器会继续处理它们。一个处理器可能总是返回同样的逻辑值,或者是根据选项改变结果。为了要写一个标记处理器,我们用一个子类来继承AbstractProcessor,并且用SupportedAnnotationTyps 和SupportedSourceVersion标记这个子类。这个子类必须要复写这两个方法:

l public synchronized void init(ProcessingEnvironment processingEnv)

l public boolean process(Set<? extends TypeElement> annotations,

RoundEnvironment roundEnv)

这两个方法都是在标记处理过程中被java编译器调用的。第一个方法用来初始化插件,只被调用一次。而第二个方法每一轮标记处理都会被调用,并且在所有处理都结束后还会调用一次。

我们的简单的HelloWorldProcessors是这样生成的:

import javax.annotation.processing.*;

import javax.lang.model.SourceVersion;

import javax.lang.model.element.TypeElement;

import javax.tools.Diagnostic;

@SupportedAnnotationTypes("HelloWorld")

@SupportedSourceVersion(SourceVersion.RELEASE_6)

public class HelloWorldProcessor extends AbstractProcessor {

@Override

public synchronized void init(ProcessingEnvironment processingEnv) {

super.init(processingEnv);

}

@Override

public boolean process(Set<? extends TypeElement> annotations,

RoundEnvironment roundEnv) {

if (!roundEnv.processingOver()) {

processingEnv.getMessager().printMessage(

Diagnostic.Kind.NOTE, "Hello Worlds!");

}

return true;

}

}

第八行注册了HelloWorld的标记处理器。也就是说,当标记出现是,就会有一系列的程序被自动调用。第九行设置了标记所支持的源代码版本。

第12到15行复写了初始化方法, 目前为止,我们只是调用父类的方法。

第17到24行复写了处理方法。这个方法是由一些列被标记的程序元素来调用的。这个方法在每一轮处理时,都会调用,并且在最后会多出一轮,用于对空集合的元素的处理。这样,我们可以由一个简单的if语句,使得最后多出的那一轮什么事情都不做。在其他轮中,我们只打印一个hello world消息。我们不用System.out.print,二十使用编译器的消息框架来打印一个消息(note类型的)。其他可能的类型是警告(warning)或者错误(error)。

这个方法返回true,如果你想要声明元素已经被处理过了。

要运行这个例子,执行:

javac HelloWorldProcessor.java

javac -processor HelloWorldProcessor *.java

这个应该会输出:

Note: Hello World!

5. 如何巧妙利用JSR269来重写AST

在这个例子中,我们深入到编译器自身的实现细节中去。我们利用JSR269做一些超出它本身的事情—重写AST。这个处理器会把每一个Assertion语句替换成一个throw语句。也就是说,每当有以下语句出现时

assert cond: detail;

会被替换成:

If(!cond) throw new AssertionError(detail);

后面的这个语句不会生成assert的字节码,而是生成一个普通的if语句,带有一个throw重句。结果就算你的虚拟机没有激活assertions功能时,assertions的检查还是会被执行。这个功能对各种库是非常有用的,因为你写库的时候,是没有办法控制用户的VM设置的。

再次,我们还是先继承AbstractProcessor。然而,这次我们不会针对某一个特殊的标记,而是用“*”这个符号来表示对所有的源代码都调用处理器。

@SupportedAnnotationTypes("*")

@SupportedSourceVersion(SourceVersion.RELEASE_6)

public class ForceAssertions extends AbstractProcessor {

}

初始化方法如下:

30 private int tally;

31 private Trees trees;

32 private TreeMaker make;

33 private Name.Table names;

34

35 @Override

36 public synchronized void init(ProcessingEnvironment env) {

37      super.init(env);

38      trees = Trees.instance(env);

39     Context context = ((JavacProcessingEnvironment)

env).getContext();

40      make = TreeMaker.instance(context);

41      names = Name.Table.instance(context);

42      tally = 0;

43 }

我们使用处理环境(ProcessingEnvironment)来获得对编译器一些组件的引用。在编译器里面,在每次调用编译器时都会有一个处理环境(ProcessingEnvironment)。在编译器中,我们使用Component.instance(context)来获得对组件的引用。

我们使用的组件如下:

l Trees – JSR269的一个工具类,用于联系程序元素和树节点。比如,对于一个方法元素,我们可以获得这个元素对应的AST树节点。

l TreeMaker – 编译器的内部组件,是用于创建树节点的工厂类。工厂类里面方法的命名方式跟Javac源代码里面的方法是统一的。

l Name.Table – 另一个编译器的内部组件。Name类是编译器内部字符串的一个抽象。为了提高效率,Javac使用了哈希字符串。

请注意,在第39行,我们把处理环境(ProcessingEnvironment)强制转换成了编译器的内部类型。

最后,我们把一个计数器初始化成0.这个计数器是用来记录发生替换的数量。

处理方法如下:

45 @Override

46 public boolean process(Set<? extends TypeElement> annotations,

RoundEnvironment roundEnv) {

47    if (!roundEnv.processingOver()) {

48       Set<? extends Element> elements = roundEnv.getRootElements();

49       for (Element each : elements) {

50       if (each.getKind() == ElementKind.CLASS) {

51          JCTree tree = (JCTree) trees.getTree(each);

52          TreeTranslator visitor = new Inliner();

53          tree.accept(visitor);

54       }

55   }

56    } else

57    processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,

tally + " assertions inlined.");

58    return false;

59 }

我们遍历所有的程序元素,为每一个类都重写AST。在第51行,我们把JSR269的树节点转换成编译器内部的树节点。这两种树节点的不同之处在于,JSR269节点是停留在方法层的(即方法method是最基本的元素,不会再细分下去),而内部的AST节点,是所有元素(包括方法以下的)都可以访问的。我们要访问每一个语句,所以需要访问到AST的所有节点。

树的转换是通过继承TreeTranslator来完成的,TreeTranslator本身是继承自TreeVisitor的。这些类都不是JSR269的一部分。所以,从这里开始,我们所写的所有代码都是在编译器内部工作的。

在第57行,是else部分,用于报告处理过的assertion语句数量。这个语句只有在最后一轮处理才会执行。

Inliner这个类实现了AST重写。Inliner继承了TreeTranslator,并且是标记处理器的一个内部类。注意,TreeTranslator本身是不会转换任何节点的。

private class Inliner extends TreeTranslator {

}

为了转换assertion语句,我们需要复写默认的TreeTranslator.visitAssert (JCAssert) 方法,如下所示:

63 @Override

64 public void visitAssert(JCAssert tree) {

65    super.visitAssert(tree);

66    JCStatement newNode = makeIfThrowException(tree);

67    result = newNode;

68    tally++;

69 }

正在转换的节点会被当做参数传入到方法中。在第67行,转换的结果,通过赋值给变量TreeTranslator.result而返回。

按照惯例,一个转换方法应该这样生成:

l 调用父类的转换方法,以确保转换可以被应用到自己点上面去。

l 执行真正的转换

l 把转换结果赋值给TreeTranslator.result。结果的类型不一定要和传进来的参数的类型一样。相反,只要java编译器允许,我们可以返回任何类型的节点。这里TreeTranslator本身没有限制类型,但是如果返回了错误的类型,那么就很有在后续过程中产生灾难性后果。

我们写一个私有函数来实现转换,makeIfThrowException:

71 private JCStatement makeIfThrowException(JCAssert node) {

72 // make: if (!(condition) throw new AssertionError(detail);

73 List<JCExpression> args = node.getDetail() == null

74 ? List.<JCExpression> nil()

75 : List.of(node.detail);

76 JCExpression expr = make.NewClass(

77 null,

78 null,

79 make.Ident(names.fromString("AssertionError")),

80 args,

81 null);

82 return make.If(

83 make.Unary(JCTree.NOT, node.cond),

84 make.Throw(expr),

85 null);

86 }

这个方法传入一个assertion语句,返回一个if语句。我们可以这样做,事因为不管是assertion还是if,他们都是语句(statement),所以在java的语法中是等价的。Java中没有明文规定,禁止用if语句来代替assertion语句。

makeIfThrowException是用于AST重写的方法。我们使用TreeMaker来创建新的树节点。如果有这样的一个表达式:

assert cond:detail;

我们就可以替换成下面的形式:

If(!cond) throw new AssertionErrror(detal);

在第73到75行,我们考虑到了detail被省略的情况。在76到81行,我们创建了一个AST节点,这个节点的作用是创建AssertionError。在第79行,我们使用Name.Table来把字符串“AssertionError”变成编译器内部的字符串。在80行,我们再传入73到75行创建的参数args。第77,78和81行传入了null值,因为这个节点既没有外部实例,也没有类型参数,也不是在匿名类内部。

在第83行,我们对assertion的条件做了一个Not操作。84行,我们创建了一个throw表达式,最后,在82到85行,我们把所有的东西都放到了if语句中。

注意:List类是java编译器中另外一个令人印象深刻的实现。编译器用了它自己的数据类型来实现List,而不是使用java集合框架(Java Collection Framework)。List和Pair数据类的实现,都用到了Lisp语言里面所谓的cons。Pairs是这样实现的:

public class Pair<A, B> {

public final A fst;

public final B snd;

public Pair(A fst, B snd) {

this.fst = fst;

this.snd = snd;

}

...

}

而List是这样实现的:

public class List<A> extends AbstractCollection<A> implements

java.util.List<A> {

public A head;

public List<A> tail;

public List(A head, List<A> tail) {

this.tail = tail;

this.head = head;

}

...

}

并且有许多静态的方法,可以很方便的创建List:

l List.nil()

l List.of(A)

l List.of(A,A)

l List.of(A,A,A)

l List.of(A,A,A,A...)

Pair也是一样:

l Pair.of(A,B)

同样,非传统的命名方式也带来了更漂亮的代码

不像传统java中用的代码:

List list = new List();

list.add(a);

list.add(b);

list.add(c);

而现在只需要写:

List.of(a, b, c);

5.1 运行AST重写

为了展示AST重写,我们使用:

1 public class Example {

2

3 public static void main(String[] args) {

4     String str = null;

5     assert str != null : "Must not be null";

6 }

7

8 }

并且执行:

javac ForceAssertions.java

javac -processor ForceAssertions Example.java

就会产生这样的输出:

Note: 1 assertions inlined

现在,我们我们我们禁用assertion,再执行例子:

java -disableassertions Example

得到:

Exception in thread "main" java.lang.AssertionError: Must not be null at Example.main(Example.java:1)

利用编译器的选项 –printsource,我们甚至可以得到重写过后的AST,并且以Java源代码的方式显示出来。要注意的是,我们必须重定向输出,否者原来的源文件会被覆盖了。

执行:

javac -processor ForceAssertions -printsource -d gen Example.java

产生结果:

1 public class Example {

2

3 public Example() {

4     super();

5 }

6

7 public static void main(String[] args) {

8     String str = null;

9     if (!(str != null)) throw new AssertionError("Must not be null");

10 }

11 }

可以发现,第9行已经被重写过了,第3到5行加入了一个默认的构造函数。

5.2 如何把标记处理器注册成服务

Java提供了一个注册服务的机制。如果一个标记处理器被注册成了一个服务,编译器就会自动的去找到这个标记处理器。注册的方法是,在classpath中找到一个叫META-INF/services的文件夹,然后放入一个javax.annotation.processing.Processor的文件。文件格式是很明显的,就是要包含要注册的标记处理器的完整名称。每个名字都要占单独的一行。

5.3 进一步的阅读

Erni在他的本科毕业设计中描述了一个更复杂的编译器修改。他不是依赖JSR269,而是直接在编译过程中的几个点进行直接修改。

参考

[1] David Erni. JAG - a Prototype for Collective Behavior in Java. Bachelors

Thesis, University of Bern. March 2008.

[2] Joseph D. Darcy. JSR-000269 Pluggable Annotation Processing API,

December 2006. http://jcp.org/en/jsr/detail?id=269.

A 在OSX下面安装Java 6

默认情况下,当前的OSX只是内置 了Java 5.0,为了安装Java6,从以下地址下载安装文件http://www.apple.com/support/downloads/javaformacosx105update1.html. 这个更新需要Mac OS X 10.5.2或者是更新的,而且要64位基于Intel的Mac。这个更新不会替换已经存在的J2SE 5.0安装,或者是改变Java的默认版本。

B ForceAssertions.java的全部源代码

1 import java.util.Set;

2

3 import javax.annotation.processing.AbstractProcessor;

4 import javax.annotation.processing.ProcessingEnvironment;

5 import javax.annotation.processing.RoundEnvironment;

6 import javax.annotation.processing.SupportedAnnotationTypes;

7 import javax.annotation.processing.SupportedSourceVersion;

8 import javax.lang.model.SourceVersion;

9 import javax.lang.model.element.Element;

10 import javax.lang.model.element.ElementKind;

11 import javax.lang.model.element.TypeElement;

12 import javax.tools.Diagnostic;

13

14 import com.sun.source.util.Trees;

15 import com.sun.tools.javac.processing.JavacProcessingEnvironment;

16 import com.sun.tools.javac.tree.JCTree;

17 import com.sun.tools.javac.tree.TreeMaker;

18 import com.sun.tools.javac.tree.TreeTranslator;

19 import com.sun.tools.javac.tree.JCTree.JCAssert;

20 import com.sun.tools.javac.tree.JCTree.JCExpression;

21 import com.sun.tools.javac.tree.JCTree.JCStatement;

22 import com.sun.tools.javac.util.Context;

23 import com.sun.tools.javac.util.List;

24 import com.sun.tools.javac.util.Name;

25

26 @SupportedAnnotationTypes("*")

27 @SupportedSourceVersion(SourceVersion.RELEASE_6)

28 public class ForceAssertions extends AbstractProcessor {

29

30 private int tally;

31 private Trees trees;

32 private TreeMaker make;

33 private Name.Table names;

34

35 @Override

36 public synchronized void init(ProcessingEnvironment env) {

37     super.init(env);

38     trees = Trees.instance(env);

39     Context context = ((JavacProcessingEnvironment)

env).getContext();

40    make = TreeMaker.instance(context);

41     names = Name.Table.instance(context);

42     tally = 0;

43 }

44

45 @Override

46 public boolean process(Set<? extends TypeElement> annotations,

RoundEnvironment roundEnv) {

47     if (!roundEnv.processingOver()) {

48     Set<? extends Element> elements =

roundEnv.getRootElements();

49     for (Element each : elements) {

50         if (each.getKind() == ElementKind.CLASS) {

51         JCTree tree = (JCTree) trees.getTree(each);

52         TreeTranslator visitor = new Inliner();

53         tree.accept(visitor);

54         }

55     }

56 } else

57     processingEnv.getMessager().printMessage(

Diagnostic.Kind.NOTE, tally + " assertions

inlined.");

58     return false;

59 }

60

61 private class Inliner extends TreeTranslator {

62

63 @Override

64 public void visitAssert(JCAssert tree) {

65      super.visitAssert(tree);

66      JCStatement newNode = makeIfThrowException(tree);

67      result = newNode;

68      tally++;

69 }

70

71 private JCStatement makeIfThrowException(JCAssert node) {

72 // make: if (!(condition) throw new AssertionError(detail);

73     List<JCExpression> args = node.getDetail() == null

74                      ? List.<JCExpression> nil()

75                      : List.of(node.detail);

76     JCExpression expr = make.NewClass(

77     null,

78     null,

79     make.Ident(names.fromString("AssertionError")),

80     args,

81     null);

82     return make.If(

83            make.Unary(JCTree.NOT, node.cond),

84            make.Throw(expr),

85            null);

86 }

87

88 }

89

90 }

91

转载于:https://my.oschina.net/superpdm/blog/129715

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值