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 '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) 不要将编译路径放在类路径上(这违反了当前的预期,但可以避免加载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投票

评论由:alexmiller发表

将内容合并到1.7版本供考虑。

0投票

评论由:sfnelson发表

我在示例中增加了调试标志,它会导致类型实例hashcodes及其类加载器被打印。

编译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实例。

修复此问题的方法是在负载时使“已加载库”知情,以避免不必要的/有害的重新加载。

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 提交)
...