情景:给定两个文件
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 项目中更为常见。文件按字母顺序编译,在本例中,先编译 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,而不是磁盘上的版本
在 1.6 中,RT.classForName() 不会检查动态类加载器缓存,所以像实例一样从磁盘加载 T。此更改在 CLJ-979 中进行,以支持其他重定义和 AOT 混合使用。
方法
1) 按反向依赖顺序编译以避免重复编译。
可以在第一个例子中交换编译顺序或在 project.clj 中指定顺序
:aot [dispatch.dispatch dispatch.core]
这是一个短期解决方案。
2) 将 deftype 放入与它使用的命名空间不同的命名空间中,以便在第二次编译时不会被重新定义。这是一个短期解决方案。
3) 不要将 compile-path 放在类路径上(这违反了当前期望,但避免加载 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