-
2007-10-10
编译与解释
如果我们想将程序设计语言编写的源代码运行起来,通常情况下,我们有两条路可以走:解释和编译。
计算机能够认识的只是01串,所以,我们编写的源代码要想真正启到应有的作用,必须经过转换,转换成一种可执行的格式,然后,由专门的执行引擎将它运行。
解释,通常是将源代码解析为一个中间形式(比如抽象语法树,AST),然后,经由一个软件的执行引擎执行这个中间形式,产生对应的结果。这种做法的好处就是可以把执行逻辑独立处理,无须为每个平台编写不同的代码,所以,这种做法的可移植性很好,于是,它成了不少程序设计语言最初的选择。不过,随之而来的问题就是这会降低执行速度,毕竟,这个软件执行引擎的性能无法与硬件相比。所以,以硬件作为执行引擎的“编译”会让程序拥有更佳的性能。
采用编译的方式,我们可以将代码编译成可以由硬件直接执行的二进制代码。因为不同硬件和不同操作系统的二进制格式是不同的,所以,如果希望语言得到广泛应用,那便需要针对不同平台实现平台相关的编译器后端,理论上说,这不是一件不能完成的任务,但这意味着巨大的工作量。虽然编译语言的数量不在少数,但事实上,真正能够跨越各种软硬平台的编译语言似乎只有C。
虚拟机的出现让人们找到了在软硬件执行引擎之间的一个折衷。一方面,编译器只要生成针对虚拟机的代码,而不必为各种各样的软硬件平台费心,另一方面,虚拟机不断的优化可以让程序在不受编译器的影响下越跑越快。所以,许多程序设计语言走上了这条路,比如Java,比如Python。这些语言大多采用的是自行设计的虚拟机,但这个做法虽然可以充分的将语言特性与虚拟机结合起来,但无疑也意味着大量的重复工作。Parrot希望成为一个集大成者,为众多动态语言搭建一个共同的平台,只是不知何年何月才能实现它宏大的目标。
Java虚拟机(JVM)的普及为这个问题提供了另外一种选择。因为主流软硬件平台上基本都有自己的JVM实现,所以,只要生成针对JVM的代码——字节码,便意味着可以运行在大多数平台上。当然,虚拟机也是一种软件实现,所以,性能上也会有一些损失。不过,从Java平台的广泛应用也证明了,这样的损失在实践中是可以接受的。随着JVM技术上的不断进步,性能损失越来越小。再者,作为一种为静态语言设计的平台,目前,JVM自身并不支持动态语言特性,所以,要想让动态语言语言运行在上面,需要额外做一些工作,搭建一套支撑动态语言的结构。不过,随着一些动态语言逐渐被移植到JVM上,人们也意识到了这个问题上,开始考虑在JVM中增加动态语言的支持。
除了执行性能之外,编译带来的好处还在于保护源代码。因为解释方式通常不会保留的其中间形式,所以,产品发布意味着将源代码也发布出去。对于需要保护知识产权的公司和个人而言,这是他们所不愿意看到的。通过编译的方式,我们将源代码转成了二进制,这样,源代码可以得到有效保护。当然,破解二进制也是有可能的,不过,那就是另外的故事了。 -
2007-09-19
管窥Ruby——类的变量(更新版)
《管窥Ruby——类的变量》写在去年,写成之后便更新了一次,因为最初的描述存在一些偏差。即便如此,jxb8901依然指出了其中的一些不足。最近,dennis-zane再次提出了这个问题。回过头来仔细品味,确实有些地方写得不是很到位,索性把它重新写过。
管窥Ruby——类的变量
变量和方法是面向对象难以割舍的两个重要组成部分。在《管窥Ruby——类的方法》中,我们谈到方法,沿着这条路继续,我们再来看看类中的变量。
开始之前,我们还是要再次回顾RClass的定义:
struct RClass {
struct RBasic basic;
struct st_table *iv_tbl;
struct st_table *m_tbl;
VALUE super;
};
(ruby.h)
如果你看过《管窥Ruby——类的方法》,了解了方法存储方式,变量的存储方式便也一目了然了,同样的st_table,意味着同样的处理方式。
不得不承认的一点是,在讨论类的方法时,我故意忽略了一个事实:方法分为类的方法和实例的方法两种。如果对其它语言实现有些许了解,我们知道,这两种方法差别仅仅是this(C++或Java的说法),到了底层时,这个差别可以视为无物,可以统一存放。关于这点,有兴趣可以参考一下《深入Java虚拟机》,它的第五章讲解了虚拟机内方法的表现形式。在Ruby中,类的方法和实例的方法并不是存放在一起的,这里定义的实际上是实例方法,而类的方法是定义在类的Singleton类中。
遇到变量时,我们会碰到同样的问题:类的变量和实例变量是无法统一管理的。因为类的变量只有唯一的一份,而实例变量则是每个实例都有一份。所以,RClass存放的并不是真正实例变量。代码是说明问题的最好方式,下面这段代码说明了如何设置实例变量:
VALUE
rb_ivar_set(obj, id, val)
VALUE obj;
ID id;
VALUE val;
{
if (!OBJ_TAINTED(obj) && rb_safe_level() >= 4)
rb_raise(rb_eSecurityError, "Insecure: can't modify instance variable");
if (OBJ_FROZEN(obj)) rb_error_frozen("object");
switch (TYPE(obj)) {
case T_OBJECT:
case T_CLASS:
case T_MODULE:
if (!ROBJECT(obj)->iv_tbl) ROBJECT(obj)->iv_tbl = st_init_numtable();
st_insert(ROBJECT(obj)->iv_tbl, id, val);
break;
default:
generic_ivar_set(obj, id, val);
break;
}
return val;
},
(variable.c)
抛开前面那些复杂的东西,直接来看switch语句后面的内容。这里存在两种情况,对于存在T_OBJECT、T_CLASS和T_MODULE标记的,变量会写入iv_tbl,而其余的情况则转交 generic_ivar_set处理。iv_tbl是“实例变量表(instance value table)”的缩写,不过,如果当前对象是个类对象,这个“实例”变量实际上就是类变量,所以,这个名字多少有些名不符实。
除此之外,还有一个generic_ivar_set。下面是generic_ivar_set的实现:
static void
generic_ivar_set(obj, id, val)
VALUE obj;
ID id;
VALUE val;
{
st_table *tbl;
if (rb_special_const_p(obj)) {
special_generic_ivar = 1;
}
if (!generic_iv_tbl) {
generic_iv_tbl = st_init_numtable();
}
if (!st_lookup(generic_iv_tbl, obj, (st_data_t *)&tbl)) {
FL_SET(obj, FL_EXIVAR);
tbl = st_init_numtable();
st_add_direct(generic_iv_tbl, obj, (st_data_t)tbl);
st_add_direct(tbl, id, val);
return;
}
st_insert(tbl, id, val);
}
(variable.c)
同样忽略一些非主干的部分,我们看到,这段代码先在一个generic_iv_tbl中进行查找,用作查找键值的是对象实例(obj),而目标同样是一个st_table。得到这个表之后,利用变量名做键值将值插入到表中。我们便不难分析在这种做法中实例变量的存储方式。存在一个全局的实例变量表,走到这里的实例都在其中拥有一席之地,而实例变量存储在实例对应的表中。Ruby通过这样一个二级结构,解决了这些实例变量存储的问题。
了解过基本的做法之后,随之而来的一个问题就是,这些代码都会在什么情况下起作用。
代码已经说得很明白了,只有在有那几个标记的情况下,才会直接调用iv_tbl。T_CLASS和T_MODULE都很好理解,那什么情况下会有T_OBJECT呢?在C Ruby中,在Ruby层次上定义的类,生成的实例都是会标有T_OBJECT(参见《管窥Ruby——Allocator》)。所以,所有由Ruby层次上生成的对象都会走到这里来。
那generic_ivar_set呢?除了那几个标志外的其他部分都会走到这里。除此之外的标志表示什么呢?读一下代码我们便不难发现,几乎就是builtin的几个类,比如数组、字符串、正则表达式等等。那为什么这些builtin类没有一个iv_tbl。对于这些builtin而言,它们真正的实例变量都是以C的形式给出,所以,额外存在一个iv_tbl实际上是一种空间上的浪费。虽然不是常态,但我们依然可以为这些builtin类添加自己的实例成员。为了保持Ruby的动态特性,这才有了generic_ivar_set的存在。
-
2007-07-30
用Annotation标记Ruby方法
JRuby最近在讨论是否要支持Java 5。
JRuby邮件列表的讨论
http://www.nabble.com/Moving-to-Java-5--tf4131923.html
InfoQ报道
http://www.infoq.com/news/2007/07/jruby-java5-move
http://www.infoq.com/cn/news/2007/07/jruby-java5-move
XRuby起步就是从Java 5开始的,所以,不存在这个问题。在他们还在为此争论的时候,受到Charles Nutter最开始那封邮件的启发,我已经完成了Annotation标记Java代码和Ruby代码绑定的第一个版本。
下面是一个例子:
@RubyLevelClass(name="ClassFactory")
public class ClassFactoryValue extends RubyValue {
...
@RubyLevelMethod(name="test", type=MethodType.NO_ARG)
public RubyValue test() {
return RubyConstant.QNIL;
}
}
首先,用@RubyLevelClass标记出这个Java类对应着一个Ruby层次上的类,其名称为ClassFactory。然后,用@RubyLevelMethod标记出一个Java方法是对应着Ruby的方法,它的Ruby名称为test,而且它是无参数的。
我们可以这样利用这段代码:
RubyClass klass = RubyTypeFactory.getClass(ClassFactoryValue.class);
通过RubyTypeFactory,我们可以生成ClassFactoryValue将Java层次和Ruby层次对应起来的代码,生成代码大致如下:
public class ClassFactoryValue$ClassBuilder implements RubyClassBuilder {
public RubyClass createRubyClass() {
RubyClass rubyclass = RubyAPI.defineClass("ClassFactory", RubyRuntime.ObjectClass);
MethodFactory methodfactory = MethodFactory.createMethodFactory(ClassFactoryValue.class);
rubyclass.defineMethod("test", methodfactory.getMethod("test", MethodType.NO_ARG));
return rubyclass;
}
}
这里用之前介绍过的生成方法Wrapper的MethodFactory去辅助代码生成,简化了编写。
XRuby本身为了生成bytecode已经做了大量的代码生成,这里只是把代码生成更多的用在了其他的部分。把这里的Annotation更广泛的用在XRuby中,会让代码看上去更干净。 -
2007-07-08
生成方法的Wrapper
在《管窥Ruby——方法定义》中,我们曾经讨论过在Java中实现方法定义时,因为Java语言的限制,我们不得不为方法加上了一个Wrapper,让它可以满足接口定义的需要。事实上,XRuby一直就是这么做的,通常Wrapper放在com.xruby.runtime.builtin这个包中,而诸如Array、Hash、String之类的具体实现放在com.xruby.runtime.value这个包中。初涉XRuby的人,常常会被这两个包搞得晕头转向。到底方法的具体实现应该放在哪里,大多是根据感觉来定义的。
在那篇blog中,我还讨论了JRuby的实现,不过,那里面的讨论并不是特别的完善。事实上,除了利用reflection,其中还有一种方式,通过代码生成动态产生这个Wrapper。我将这个实现借鉴到了XRuby中。通过采用这种生成Wrapper的方式,我们就可以在实际编写的代码中,忽略掉Wrapper部分的实现,而将原来令人混淆的builtin和value包统一起来。
下面是Array的clear方法原来的实现:
class Array_clear extends RubyNoArgMethod {
protected RubyValue run(RubyValue receiver, RubyBlock block) {
RubyArray value = (RubyArray) receiver;
return value.clear();
}
}
在这里,其实只要将receiver转型,然后调用它的方法就好。实际上,生成的代码只是做这样简单的工作就好,当然,根据具体的方法还会略有些差别。关于如何使用ASM进行代码生成,我在几篇blog中都进行了介绍,这里就不介绍生成代码的实现了。下面是在代码中如何使用这个方法。
MethodFactory factory = MethodFactory.createMethodFactory(RubyArray.class);
c.defineMethod("clear", factory.getMethod("clear", 0));
在这个方法的实现过程中,还有一些比较有趣的点。首先,并不是每次都需要调用getMethod都要动态生成一个类,因为一个类一旦已经加载,就没有必要再次进行加载,即便强行加载,class loader也是会抱怨的。所以,在创建类之前,我们需要尝试加载一下这个,如果加载成功,便省去了再次生成的麻烦。再有,每次都去创建这个Wapper实际上也没有什么必要,一次生成之后,保存起来就可以了。如果我们把它放在class path中,那么我们尝试加载就会成功,所以,刚才提到的尝试加载还有这样一层含义。
作为builtin实现,我们还是希望这些生成类是可以放在我们最终发布的jar文件中,所以,我们通常的做法,是在打包之前,先用XRuby做一次最简单的执行,让所有的Wrapper生成出来,这样,打包的时候就可以将它们都加入其中。
具体的做法可以参考XRuby中MethodFactory和build.xml。 -
2007-06-30
Javac背后的故事——创建对象
要让Java这个面向“对象”的世界正常运作,创建对象就是一项不可或缺的操作。
public class NewMain {
public static void main(String[] args) {
new Object();
}
}
用javap反编译上面的代码,我们可以得到下面的指令,这里省去了javac暗中创建的构造函数。
public class NewMain extends java.lang.Object{
...
public static void main(java.lang.String[]);
Code:
0: new #3; //class java/lang/Object
3: invokespecial #8; //Method java/lang/Object."<init>":()V
6: return
}
从这段代码中,我们可以清晰的看出创建对象(new)和调用构造函数(invokespecial)两个过程。关于这个问题,我在《对象的生命》中曾经进行过讨论。
既然javac将一个new的动作被解释为两条指令,那在JVM的层面上,我们当然就可以将它们分开。下面是一段没什么实际用途的代码,只是证明这个观点可行性。
public class NewGenerator {
public static void main(String[] args) throws Exception {
String className = "New";
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cw.visit(Opcodes.V1_2, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null);
Method m = Method.getMethod("void main (String[])");
GeneratorAdapter mg = new GeneratorAdapter(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, m, null, null, cw);
mg.newInstance(Type.getType(Object.class));
Label label = mg.newLabel();
mg.ifNonNull(label);
mg.mark(label);
mg.getStatic(Type.getType(System.class), "out", Type.getType(PrintStream.class));
mg.push("new object is not null");
mg.invokeVirtual(Type.getType(PrintStream.class), Method.getMethod("void println(java.lang.String)"));
mg.pop();
mg.returnValue();
mg.endMethod();
cw.visitEnd();
OutputStream os = null;
try {
os = new FileOutputStream(className + ".class");
os.write(cw.toByteArray());
} finally {
if (os != null) {
os.close();
}
}
}
}
这段代码生成的类是能够运行的,有兴趣的可以自己试一下。这段代码的作用是new出一个对象之后,如果这个对象非空的话,就会产生一个输出:
new object is not null
当然,如果尝试用这个对象做一些其它的操作,会有错误等待着我们,因为这个对象并不是一个完整的对象。在JVM规范中有相关的解释:new指令并不能完整创建出一个新的对象,直到对未初始化的对象调用了实例初始化方法才会完成实例的创建。这段代码也正好符合《对象的生命》中的解释,它只是负责做出申请内存。当然,在JVM中,它的实际工作要略多一些,如果这个对象的类没有加载,就会加载相应的类。







