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

欢迎!请查看 关于 页面以了解更多关于该功能的信息。

+52
Clojure
关闭

Android ART 对字节码进行编译时验证,并在任何使用锁定宏的地方失败。错误看起来像 CLJ-1829 中看到的那样(在这种情况下,clojure.core.server/stop-server 调用了锁定宏)

10-16 14:49:26.801 2008-2008/? E/AndroidRuntime: java.lang.VerifyError: Rejecting class clojure.core.server$stop_server because it failed compile-time verification (declaration of 'clojure.core.server$stop_server' appears in /data/app/com.clojure_on_android-1/base.apk)

原因: 根据Android问题讨论(https://code.google.com/p/android/issues/detail?id=80823),这似乎是因为JVM规范中更严格执行了“结构锁定”条款(https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.11.10)。

似乎在 locking 中的 monitor-enter、monitor-exit 和 try/finally 块的组合创建了 ART 会标记为不平衡 monitorenter/monitorexit 字节码路径。特别是,monitorenter 和 monitorexit 本身可以抛出异常(在 null 锁定对象上)。Java 字节码对这些情况进行了一些复杂的异常表处理,但这些处理(afaict)无法在不修改 Clojure 编译器的情况下完成。

方法: 一个可能的方法是在 Java 的同步块上下文中调用传入的身体,这样就能避免麻烦的字节码问题,但是对性能的影响尚不清楚。

补丁: clj-1472-3.patch

也参见: 将字节码与 Java synchronized 块进行比较表明存在许多差异
https://gist.github.com/AdamClements/2ae6c4919964b71eb470

审核者: Alex Miller - 我认为这是一种可行的方案,可以解决问题,而且由于使用频率不高,我对它成为性能问题的担忧不大。我将指出我认为另一种处理方法是使 锁定 成为一个特殊形式,并带有编译器支持,但我不确定这样做是否值得,所以我会让 Rich 决定。

已关闭,备注: 已修复

41 答案

0
by
 
最佳答案

针对此问题的修复已在 1.10.2-alpha1 版本中发布。

0
by

评论由:adamclements

在使用了一段时间后,我发现将此代码移出 try 块会导致 nREPL 运行中断。

查看字节码,clojure.tools.nrepl.middleware.session/session-out 以及其他一些地方的 locking 代码的 monitorenter 竟然在不同的方法定义中,我们现在还收到了 JVM IllegalMonitorStateException 以及 ART 验证错误。

0
by

评论由:jafingerhut

Adam,我无法评论你的补丁是否有趣,但的确是事实,如果没有签署贡献者协议,任何补丁都无法提交到 Clojure,你可以在https://clojure.org/contributing在线完成。

0
by

评论由:adamclements

上传了新的补丁(并签署了贡献者协议)。这个补丁通过了 JVM 和 ART 字节码验证,围绕监视器退出的额外 try/catch 是可选的(无论是带它还是不带它都可以通过验证),但在 java 版本中,如果监视器退出无限重试,将会显示出死锁,而且不会在监视器退出时捕获错误,如果将来在某时发生未确定的监视器进入,可能会失败,无法显示实际的错误。

这看起来并不太美观,但如果没有更细粒度的对生成的字节码的控制,这可能是我能做到的最好。

0
回答

评论由:adamclements

刚刚与Lollipop进行了测试,这个补丁可能不再足够。

正在与ART团队取得联系,看看他们是否能提供更多线索,并验证它是否可以在AOSP当前主分支上工作

0
回答

评论由:adamclements

在AOSP项目中提交了一个错误,希望他们可以说明这是否是我们的问题,如果是的话,我们应该如何修复它。

https://code.google.com/p/android/issues/detail?id=80823

0
回答

评论由:adamclements

我已经上传了一个锁定宏的替代实现(0001-CLJ-1472-Locking-macro-without-explicit-monitor-ente.patch),它稍微有点欺骗性——实际上同步块是在Java中实现的,并保证了兼容性。这是以牺牲微小的额外间接导引为代价的,命名和位置可能可以更好。

但它确实修复了错误,并且可以在所有版本的android、android + art和 JVM上工作。这种方法是否可以接受?

0
回答

评论者:hiredman

我还没有看到任何证据表明Clojure生成的字节码以某种方式违反了JVM规范,所以我怀疑问题是Clojure需要JVM来运行,而Android并不提供JVM,只是如果你不偏离常规路径,它看起来就像一个JVM。

0
回答

评论者:hiredman

根据https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.11.10中的结构化锁定术语,(locking nil)可能生成违反结构化锁定的运行时行为的字节码。这个问题的第一个补丁会导致编译器在不同的方法中发出monitorenter/exit指令,这显然违反了结构化锁定。

0

评论由:adamclements

是的,第一个补丁绝对是不正确的,我留下它是为了给对话提供一些上下文,但可能最好是将其移除以保持清晰。

对于任何一个想跟随这个话题但又不想反编译和观察字节码的人来说,这里有一个包含Java同步块与Clojure锁定区别的gisthttps://gist.github.com/AdamClements/2ae6c4919964b71eb470

虽然我觉得很难找出与规范的不同之处,但我可以看出Java版本的区别,如果有什么不同,Clojure版本看起来比Java版本更接近规范中描述的内容!

如果有人在这方面的知识比我更丰富,并能参与AOSP bug(https://code.google.com/p/android/issues/detail?id=80823),那么这可能可以作为安卓的bug来解决,因为这个问题过于关注Java实现而不是JVM规范,也许他们能找到Clojure实现中的一些问题。我已经上传了原始的Clojure行为,并请他们解释为什么它会失败。

0

评论由:adamclements

ART团队对我们认为违反的内容的回应是

"结构化锁定"一节包含以下内容

“(链接: ...) 实现 (链接: ...) 允许但不强制执行以下两条规则,以确保结构化锁定。(链接: ...)"
ART当前在验证时强制执行这两条规则,包括

“在方法调用过程中,任何时间点T对M的退出监控器的次数不得超过自方法调用以来在M上对T执行的监控器进入次数。”

“不对。”
“不对。”
“不对。”

0

评论由:adamclements

例如,如果指令https://gist.github.com/AdamClements/2ae6c4919964b71eb470#file-test_locks-class-L24或下一行的monitor-enter本身失败,它难道不能结束在finally子句中并尝试释放锁,尽管它从未被捕获过吗?

我认为这违反了你链接的JVM规范中的结构化锁定规则。

0

评论者:hiredman

0
by

评论由:adamclements

是的,我不应该把它弄混了带补丁的版本。现在,Gist和当前上传的版本都使用了Vanilla Clojure的锁定宏。

我认为问题来自异常表和覆盖的指令。如果第24行可以抛出异常,那么在运行时你可能会遇到monitor-exit,但从未遇到过monitor-enter。

0
by

评论者:gshayban

根据Marcus Lagergren JRockit和Hotspot都将锁定释放放在不同的方法中。也许Android有不同的(错误的)解释?

...