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

这个场景在 leiningen 项目中更常见,尤其是设置有 {{:aot :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,而不是磁盘上的版本

在 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 中的内容相符,这是一个另一个麻烦事。但我们可能正是要走向的方向。

筛选:我相信提出的补丁不是一个好主意 - 它只是掩盖了症状而没有解决根本原因。我认为我们需要重新评估编译如何与 compile-path(#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实例。

解决方案是将load修改为对已加载库有所了解,以避免不必要的/有害的重载。

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

评论者:bronsa

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

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