请在2024 年 Clojure 调查!中分享您的想法。

欢迎!请在关于页面了解有关此工作方式的更多信息。

0
编译器

场景:给定两个文件

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

7 个答案

0
by

评论者:alexmiller

已将以下内容拉入1.7以供考虑。

0
by

评论者:sfnelson

我在我的示例中添加了一个调试标志,该标志会导致打印类型实例哈希码及其类加载器。

编译 dispatch.core deftype => 652433136 (clojure.lang.DynamicClassLoader@23c30a20) defmethod => 652433136 (clojure.lang.DynamicClassLoader@23c30a20) instance => 652433136 (clojure.lang.DynamicClassLoader@23c30a20) dispatch: :pass 编译 dispatch.dispatch deftype => 652433136 (clojure.lang.DynamicClassLoader@23c30a20) defmethod => 652433136 (clojure.lang.DynamicClassLoader@23c30a20) instance => 760357227 (sun.misc.Launcher$AppClassLoader@42a57993) dispatch: :fail

0
by

评论者:bronsa

当在类路径中使用 compile 并且存在一个clj文件和一个class文件时,编译器有一些奇怪的加载规则。

这个错误是由于RT.load加载AOT类文件时重新绑定 ->Ctor以使用AOT deftype实例所引起的。

修复这个问题的方法是在加载时使"已加载库"知道,以避免不必要的/有害的重新加载。

0
by

评论者:bronsa

所附补丁通过记录已加载的内容并仅在必要时加载AOT类来修复此错误

0
by

评论者:alexmiller

原始描述(已被替换)

使用错误类型实例定义类型分派多方法

当使用在类型上分派的多方法时,例如print-dup/print-method,存在aot时在命名空间的第二次加载中传递给{{addMethod}}的类型引用是不正确的。这意味着如果该命名空间已经被另一个文件作为依赖项加载,则当命名空间为aot编译而加载时,第二次加载将生成一个无法正确分派的多方法。

我创建了一个示例仓库
https://github.com/sfnelson/clj-mm-dispatch

要独立重复,创建一个包含deftype和基于类型分派的多方法的命名空间,以及一个需要第一个且在第一个之前按字母顺序排序的第二个命名空间。aot编译这两个命名空间。当类型定义的命名空间通过{{require}}加载时,它会为deftype产生一个类文件。当它第二次加载以进行aot编译时,会将现有的类文件对应的类型提供给defmethod,而不是由加载命名空间构建的新类。这会导致多方法无法正确分派。

在我看来,这个问题似乎与CLJ-979相似:传递给多方法的类型是使用错误的类加载器检索的。这表明它可能比AOT和多方法分派有更广泛的影响。

0
by

评论者:bronsa

我刚刚意识到这个工单是CLJ-1650的重复

0
by
参考: https://clojure.atlassian.net/browse/CLJ-1741 (由alex+import报告)
...