场景:给定两个文件
src/dispatch/core.clj
(ns dispatch.core (:require [dispatch.dispatch]))
src/dispatch/dispatch.clj
(ns dispatch.dispatch) (deftype T []) (def t (->T)) (println "T = (class t):" (= T (class t)))
首先编译 core,然后编译 dispatch
java -cp src:target/classes:clojure.jar -Dclojure.compile.path=target/classes clojure.main user=> (compile 'dispatch.core) T = (class t): true dispatch.core user=> (compile 'dispatch.dispatch) T = (class t): false ;; 预期 true dispatch.dispatch
此场景在带有 {{:aot :all}} 的 leiningen 项目中更为常见。文件按照 :all 编译,按字母顺序排列。在这种情况下,将先编译 dispatch.core,然后是 dispatch.dispatch。
原因
(compile 'dispatch.core)
- 递归编译 dispatch.dispatch
- 将 .class 文件写入 编译路径(该路径在类路径上)
- 断言通过
(compile 'dispatch.dispatch)
- 由于之前的编译,dispatch.dispatch__init 是通过 appclassloader 加载的
- ->T 构造函数将使用新的字节码来实例化一个 T 实例 - 这使用 appclassloader,从磁盘上的编译后的 T 加载
- 然而,T 类字面量是通过 RT.classForName 解析的,该函数检查动态类加载器缓存,因此使用旧的运行时版本 T,而不是磁盘上的版本
In 1.6,RT.classForName() 没有检查动态类加载器缓存,所以像实例一样从磁盘加载 T。这已在 CLJ-979 中更改,以支持其他重定义和 AOT 混合使用。
方法
1) 按反向依赖顺序编译,以避免编译两次。
或者在第一个示例中交换编译的顺序或指定项目.clj 中的顺序
:aot [dispatch.dispatch dispatch.core]
这是一个短期解决方案。
2) 将 deftype 移到与它使用的不同命名空间,这样它就不会在第二次编译时被重新定义。这也是另一个短期解决方案。
3) 不要将编译路径放在类路径上(这违反了当前的期望,但可以避免加载 dispatch__init)
(set! *compile-path* "foo") (compile 'dispatch.core) (compile 'dispatch.dispatch)
目前通过 Leiningen 设置起来并不容易。
4) 使用独立的 Clojure 运行时编译每个文件 - 避免在 DCL 中使用缓存的类字面量。
现在在 Leiningen 或其他方式下做起来可能太麻烦了。
5) 使编译非传递性的。这与 CLJ-322 的区域相似,还有一个问题。但也可能是我们应该前进的方向。
筛选: 我认为提出的补丁不是一个好主意 - 它只是掩盖了症状而未解决根本原因。我认为我们需要重新评估编译路径( #3 )和传递性( CLJ-322)( #5)与编译方式的关系,但我认为我们应该在1.7之后这样做 - Alex
另请参阅:CLJ-1650