• 关于具有二进制兼容性的应用程序二进制接口 GCJ 的说明
    时间:2008-11-23   作者:佚名   出处:互联网

    许多 Java 应用程序假定它们将只会由一个 Java 虚拟机解释,而不是由一个像 GCJ 一样与时俱进的编译器进行本地编译的。这些 Java 应用程序可能假设自己很难在没有经过重大源代码修改情况下进行本地编译的运行环境中运行。显然,在读取字节码流并使用 ClassLoader.defineClass() 方法把它们载入为类的时候,这些应用程序含有它们自己的代码。

    可以看到那是一个怎样的问题,因为没有字节码流是用于一个本地编译了的类的。如果本地编译一个使用 defaineClass() 的程序,当你要结束一个类,这个类尝试为可能不曾以字节码存在的别的类读取一个字节码流,这是无法实现的。更进一步,当你本地编译一个类,指望它载入其他类的时候,过去默认的是相当于只把它作为一个本地编译了的类给找出来。

    考虑到这个问题,GCJ 4 有一个新的编译模式,称为间接发布,它使用新的具有二进制兼容性的应用程序二进制接口。如果你喜欢,你可以把旧的编译模式当作“直接发布”。详情请参阅 ftp://gcc.gnu.org/pub/gcc/summit/2004/GCJ%20New%20ABI.pdf

    简而言之,为了完成它的工作,GCJ 使用 gcj-dbtool(参阅 gcj-dbtool(1)),它产生一个稍后由 GCJ 使用的“.db”文件。想法是,首先使用 -findirect-dispatch 选项对你的 .jar 或 .class 文件(你打算本地编译的应用程序依赖这些文件)进行本地编译。然后使用 gcj-dbtool 把放在一个 .db 数据库文件中的关于那些本地编译了的类的信息存储起来。

    在你正在构建的程序的本地编译过程中,当 GCJ 看到 defineClass() 准备被调用的时候,就把已经通过了的字节码跟共享库匹配(使用 .db 文件中的映射表),这些共享库包含你已经本地编译了的类。

    从一个类到在 .db 文件中的编译形式的映射,映射为一个类的签名(一个加密校验和)到一个共享库。你可以把它看成像使用一种缓存的 JIT 一样使用 GCJ。

    还有其他关于具有二进制兼容性的应用程序二进制接口的重要细节,这些细节用于确定本地代码遵循某些二进制兼容性规则。这对于让本地代码在处理 .class 文件时得以正确执行是个不错的辅助作用。 :)

    大部分典型的不使用自定义类加载器(classloaders)的 Java 应用程序不必为此担心。具有二进制兼容性的应用程序二进制接口可以为你正在不依赖 gcj-dbtool 地构建中的代码启用(通过把 -findirect-dispatch 选项传给 gcj )而生成一个二进制程序,它应该不理会对 libgcj 和其他依赖的类库的修改而继续工作(参阅 gcj(1))。另一方面,如果你使用旧的“C++ 应用程序二进制接口”风格(即不使用 -findirect-dispatch 选项)编译一个应用程序,它会在对所依赖类库的公共 API 的发生任何修改时马上中断 —— 就像 C++ 一样。在过去,正如你可以想象的,这是一个重大的限制并且是更广泛应用 GCJ 的一个障碍。

    在启用了二进制兼容性的本地编译构建完成以后,对于外界来说,原始的应用程序仍然做着它以前一直在做的事 —— 例如,和往常一样从一些没有本地编译过的 .jar 文件中载入字节码。然而,defineClass() 过去一直在做的调用过程现在已经被改进了;它们首先尝试找出所寻找的类的一个本地编译形式(很可能现在在一个共享库中),如果没有找到本地编译了的类的话,则回去解释 .class 文件(很可能在一个 .jar 文件中)。

    如果你希望能从本地代码中调用解释代码,你需要在构建本地代码时使用 -findirect-dispatch 选项。如果不使用间接发布,你的代码将不能够在需要时载入字节码形式的类。如果在代码中你从来没有提及过“new ThatClassIWant();”的话,你可以私下实现这种需求,手动加载类,并使用工厂模式,但那会很快变得单调乏味。

    从解释代码中调用本地代码会工作得很好,无需关心应用程序二进制接口。对解释代码使用间接发布在定义上是缺省的。没错,为本地编译而使用 -findirect-dispatch 选项应该是缺省的,但仍未如此 —— 大部分是因为它还没完善。主要存在的障碍如下:

    • CNI,因为 C++ 编译器仍不明白具有二进制兼容性的应用程序二进制接口
    • 并且当被使用于源代码编译时(即从 .java 到 .class),程序缺陷(bug(s))会影响 -findirect-dispatch 选项。

    当然, 目标是只要问题一解决就马上把二进制兼容性作为应用程序二进制接口的缺省的特性。

    还有一个来自具有二进制兼容性的应用程序二进制接口的性能打击。Bryce McKinlay 完成的测试表明这与多数应用程序关系不大(<= 10%),但若让它拥有更多数据会比较好。

    编译 JAR

    重要:有些时候当 -findirect-dispatch 选项直接从 Java 源文件编译为本地代码的时候并不总是可以工作的。那种情况仍未完全实现(在 gcj 4.0.x 时),且不被支持。唯一被支持的带有 -findirect-dispatch 选项的编译方式就是在这里说明了的方式。

    第一步是在应用程序中编译所有 JAR 文件。例如:

    gcj -shared -findirect-dispatch -Wl,-Bsymbolic -fjni -fPIC myapp.jar -o myapp.jar.so


    这将编译所有包含了的类。注意,没有要求设置类路径(classpath);通过具有二进制兼容性的应用程序二进制接口,所有类在运行时被链接。当使用 -findirect-dispatch 选项时当前必须使用 -Wl,-Bsymbolic 选项。

    设置数据库

    首先创建一个数据库。

    gcj-dbtool -n myapp.db


    现在,添加所有编译了的 jar 文件到数据库,例如:

    gcj-dbtool -a myapp.db myapp.jar myapp.jar.so

    运行

    现在可以运行你的应用程序了。用 gij 启动它,正如你用 Java 命令启动它一样。然而,还要把 gij 指向你创建的 .db 文件:

    gij --cp myapp.jar -Dgnu.gcj.precompiled.db.path=myapp.db org.package.ClassName etc


    注意:应用程序的 jar 文件仍然需要在 classpath 中。

    便利方法

    你有一堆 JAR 文件而又有点懒于手工完成所有的二进制兼容性编译工作吗?这里有一份脚本,遍历进入每个目录,用 GCJ 处理每个 JAR 文件并把新的库添加到数据库文件中。

    #!/bin/sh
    # ${1} is the name of a GCJ database file
    gcj-dbtool -n ${1}

    for JAR_FILE in `find -iname "*.jar"`
    do
            echo "Compiling ${JAR_FILE} to native"
            gcj -shared -findirect-dispatch -Wl,-Bsymbolic -fjni -fPIC -o ${JAR_FILE}.so ${JAR_FILE}
            gcj-dbtool -a ${1} ${JAR_FILE} ${JAR_FILE}.so
    done


    以数据库文件的名字调用这份脚本并稍等片刻。如果所有事情都完成了,这将会给你生成所有的本地库以及一个满载的数据库,这些库和数据库现在可以用于使用 GCJ 来运行应用程序了。

    范例

    对一些现实的例子感兴趣吗?进入 《Classpath 用例》 页面。在那里你可以看到构建本地的 Eclipse 是多么容易。

    故障纠纷

    1. 我如何区分代码是运行于解释模式还是本地模式呢?

    在 gdb 下运行应用程序,在你的代码中设置一个断点。设置在被解释的代码中的断点会完全不能工作(然而,某些断点可能因为 gdb 的其他程序缺陷而工作失效,因此不要依赖它作为一种迹象),并且被解释的代码将以 _Jv_InterpMethod::run 或类似的形式出现在 gdb 的后台跟踪中。

    作为一种选择,在 gij 4.0 中有一个奇怪现象可以利用。gij 中的栈跟踪将显示若干行“(Unknown Source)”,就是正在被解释的代码,而显示库名称的就是被编译的代码。
    注意:这个“(Unknown Source)”的奇怪现象将在 4.1 中去除。

    2. 即使按照上面的说明去做,我的代码仍运行在解释模式中。为什么呢?

    在 gcc 4.0 中,.jar.so 文件由于某些原因而载入失败时不会有没有错误报告 —— 它们只是默默地被忽略。这个策略在以后可能会改变。

    对于代码不能正确运行于解释模式有两个很大可能的原因:

    a)你忘记了使用 JNI(Java 本地接口)的 -fjni 编译器选项编译代码。这种情况下虚拟机将在每当它尝试载入与那些库相关的类的时候,重复尝试载入链接库并失败。(链接库失败是因为它尝试以 CNI 形式链接,那是失败的。)

    或者

    b)你的映射文件(例如 class.db)过时了,或者没有在 gnu.gcj.compiled.db.path 中。

    3. 当我在 gdb 下运行应用程序时,gdb 把我的库一次又一次地载入很多次。

    你大概忘记了在编译时指定 -fjni 选项。请参阅上面 2.a 部分的内容。

    相关文章

    《如何使用 GCJ 进行二进制兼容性编译》
    《Classpath 用例》

    附录:

    原文的 Web 文档网址:
    http://gcc.gnu.org/wiki/How%20to%20BC%20compile%20with%20GCJ

    网友留言/评论

    我要留言/评论