-
Notifications
You must be signed in to change notification settings - Fork 574
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bytecode verification failure when using abstract class and inlined lambda (kotlin) #280
Comments
Thanks, this indeed reproduces the issue. |
This seems to be a tough one, as Kotlin produces some very weird code:
The This code looks so weird that it may be a Kotlin bug. |
OTOH, this code: inline fun function(block: () -> Unit) {
val x = block()
SomeClass(x)
} is compiled to the more reasonable bytecode:
Which works fine with Quasar's instrumentation. |
Interesting! |
I guess -- that's certainly what's happening in this case -- but I haven't taken a close look at other cases of Kotlin compilation. In general, calls to |
For reference: https://youtrack.jetbrains.com/issue/KT-19251 |
See https://youtrack.jetbrains.com/issue/KT-19251 puniverse/quasar#280 Inline function calls (as well as try/catch expressions) in constructor arguments produce bytecode that spills stack, and stores uninitialized objects (created by 'NEW C', but not initialized by 'C.<init>') to local variables. Such bytecode is valid according to the JVM spec, but confuses Quasar (and other bytecode postprocessing tools). In order to avoid that, we apply 'processUnitializedStores' already implemented for coroutines. It moves 'NEW' instructions after the constructor arguments evaluation, producing code like <evaluate constructor arguments> <store constructor arguments to variables> NEW C DUP <load constructor arguments from variables> INVOKESPECIAL C.<init>(...) This transformation changes evaluation order for the expression, because 'NEW C' might cause 'C.<clinit>' invocation, thus we can enable it only in 1.2+. NB some other expressions, such as break/continue in the constructor arguments, also can produce "weird" bytecode: object is created by a 'NEW C' instruction, but later (conditionally) POPped from stack and left uninitialized. This, as we know, also can screw bytecode postprocessing. However, it looks like we can get away with it ATM. Otherwise it looks like we'd have to analyze constructor arguments, see if the evaluation can "jump out", and perform argument linearization in codegen. TODO KT-19251 Fixed Target versions 1.2
See https://youtrack.jetbrains.com/issue/KT-19251 puniverse/quasar#280 Inline function calls (as well as try/catch expressions) in constructor arguments produce bytecode that spills stack, and stores uninitialized objects (created by 'NEW C', but not initialized by 'C.<init>') to local variables. Such bytecode is valid according to the JVM spec, but confuses Quasar (and other bytecode postprocessing tools). In order to avoid that, we apply 'processUnitializedStores' already implemented for coroutines. It moves 'NEW' instructions after the constructor arguments evaluation, producing code like <initialize class C using Class.forName> <evaluate constructor arguments> <store constructor arguments to variables> NEW C DUP <load constructor arguments from variables> INVOKESPECIAL C.<init>(...) NB some other expressions, such as break/continue in the constructor arguments, also can produce "weird" bytecode: object is created by a 'NEW C' instruction, but later (conditionally) POPped from stack and left uninitialized. This, as we know, also can screw bytecode postprocessing. However, it looks like we can get away with it ATM. Otherwise it looks like we'd have to analyze constructor arguments, see if the evaluation can "jump out", and perform argument linearization in codegen.
See https://youtrack.jetbrains.com/issue/KT-19251 puniverse/quasar#280 https://bugs.openjdk.java.net/browse/JDK-8046233 Inline function calls (as well as try/catch expressions) in constructor arguments produce bytecode that spills stack, and stores uninitialized objects (created by 'NEW C', but not initialized by 'C.<init>') to local variables. Such bytecode is valid according to the JVM spec, but confuses Quasar (and other bytecode postprocessing tools), and fails to verify under some (buggy) versions of JDK 8. In order to avoid that, we apply 'processUnitializedStores' already implemented for coroutines. It moves 'NEW' instructions after the constructor arguments evaluation, producing code like <initialize class C using Class.forName> <evaluate constructor arguments> <store constructor arguments to variables> NEW C DUP <load constructor arguments from variables> INVOKESPECIAL C.<init>(...) NB some other expressions, such as break/continue in the constructor arguments, also can produce "weird" bytecode: object is created by a 'NEW C' instruction, but later (conditionally) POPped from stack and left uninitialized. This, as we know, also can screw bytecode postprocessing. However, it looks like we can get away with it ATM. Otherwise it looks like we'd have to analyze constructor arguments, see if the evaluation can "jump out", and perform argument linearization in codegen.
See https://youtrack.jetbrains.com/issue/KT-19251 puniverse/quasar#280 https://bugs.openjdk.java.net/browse/JDK-8046233 Inline function calls (as well as try/catch expressions) in constructor arguments produce bytecode that spills stack, and stores uninitialized objects (created by 'NEW C', but not initialized by 'C.<init>') to local variables. Such bytecode is valid according to the JVM spec, but confuses Quasar (and other bytecode postprocessing tools), and fails to verify under some (buggy) versions of JDK 8. In order to avoid that, we apply 'processUnitializedStores' already implemented for coroutines. It moves 'NEW' instructions after the constructor arguments evaluation, producing code like <initialize class C using Class.forName> <evaluate constructor arguments> <store constructor arguments to variables> NEW C DUP <load constructor arguments from variables> INVOKESPECIAL C.<init>(...) NB some other expressions, such as break/continue in the constructor arguments, also can produce "weird" bytecode: object is created by a 'NEW C' instruction, but later (conditionally) POPped from stack and left uninitialized. This, as we know, also can screw bytecode postprocessing. However, it looks like we can get away with it ATM. Otherwise it looks like we'd have to analyze constructor arguments, see if the evaluation can "jump out", and perform argument linearization in codegen.
I think the way this is going, is that it'll actually be considered a Quasar bug. What Kotlin is doing is - as you observe - technically valid. They have a change to the compiler that can push new closer to invokespecial, but it changes evaluation ordering and is thus in their view not backwards compatible. The issue being that My guess is that for Quasar to be fully JVMS compliant in this edge case will require more assistance from the JVM side to be able to process stacks containing uninitialised classes. Kotlin may change its behaviour in future, and I guess we can opt-in to the required behaviour now, but it'd also be required for our users to set this flag and that ... would be annoying. At the very least, can Quasar perhaps detect this scenario early and bail out with an error message explaining to the user about the Kotlin compiler flag? |
This is as far as I got reducing the test case:
Running the above main produces:
The text was updated successfully, but these errors were encountered: