请在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 项目中更为常见。文件按字母顺序编译。在这种情况下,首先编译 dispatch.core,然后编译 dispatch.dispatch。

原因

(compile 'dispatch.core)
- 递归编译 dispatch.dispatch
- 将.class文件写入到 compile-path(它在类路径上)
- 断言通过

(compile 'dispatch.dispatch)
- 由于之前的编译,通过appclassloader加载了dispatch.dispatch__init
- ->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) 不要将编译路径放在类路径上(这违反了当前期望,但可以避免加载dispatch__init)

(set! *compile-path* "foo") (compile 'dispatch.core) (compile 'dispatch.dispatch)

目前通过Leiningen设置起来有些困难。

4) 使用独立的Clojure运行时编译每个文件 - 避免在DCL中使用缓存的类字面量。

现在在Leiningen或其它方式下实现可能太麻烦了。

5) 使编译非递归。这类似于CLJ-322,这是一个另一只“大茶壶”(can of worms)。但这也可能是我们应该前往的方向。

筛选: 我认为提出的补丁不是一个好主意 - 它只是掩盖了症状,而没有解决根本原因。我认为我们需要重新评估与编译路径 (#3) 和传递性 (CLJ-322) (#5) 的编译方式,但我觉得我们应该在1.7之后进行这些工作 - Alex

另请参阅:CLJ-1650

7 个答案

0

由alexmiller发表的评论:

将此内容拉入1.7版进行考虑。

0

由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

由bronsa发表的评论:

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

此错误发生是因为RT.load将加载AOT类文件,并通过将->Ctor重新绑定到使用AOT deftype实例来解决。

修复此问题的方法将是让load“已加载库”知道,以避免不必要的或有害的重新加载。

0

由bronsa发表的评论:

附带的补丁通过跟踪已加载的内容并仅在有必要时加载AOT类来解决此错误

0

由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

由bronsa发表的评论:

我意识到这个工单与 CLJ-1650 重复

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