-
Notifications
You must be signed in to change notification settings - Fork 24.9k
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
Fix Painless Lambdas for Java 9 #24070
Conversation
and delegate method can be resolved at compile-time.
@@ -63,7 +63,7 @@ | |||
} | |||
|
|||
/** | |||
* A secure class loader used to define Painless scripts. | |||
* A secure class loader used to defineScript Painless scripts. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
leftover?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accidental refactor.
} | ||
|
||
/** | ||
* Runs the two-pass compiler to generate a Painless script. | ||
* @param <T> the type of the script | ||
* @param loader The ClassLoader used to define the script. | ||
* @param loader The ClassLoader used to defineScript the script. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
leftover?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accidental refactor.
java.lang.reflect.Constructor<? extends PainlessScript> constructor = | ||
clazz.getConstructor(String.class, String.class, BitSet.class); | ||
|
||
return iface.cast(constructor.newInstance(name, source, root.getStatements())); | ||
} catch (Exception exception) { // Catch everything to let the user know this is something caused internally. | ||
throw new IllegalStateException("An internal error occurred attempting to define the script [" + name + "].", exception); | ||
throw new IllegalStateException("An internal error occurred attempting to defineScript the script [" + name + "].", exception); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
leftover?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accidental refactor.
* <p> | ||
* Once you have created one of these, you have "everything you need" to call LambdaMetaFactory | ||
* either statically from bytecode with invokedynamic, or at runtime from Java. | ||
* Once you have created one of these, you have "everything you need" to call LambdaBootstrap |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{@link LambdaBootstrap}
? The advantage of the @link
is that IDEs will automatically fix the comment if you rename the class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added.
* lambda functions and method references within Painless. The code generation | ||
* used here is based upon the following article: | ||
* http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html | ||
* However, it is a simplified version as Painless has no concept of generics. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nor serialization which the link goes into a lot of depth about.
Also, one of the key reasons that they took that strategy for java code is that the compiler doesn't know what runtime it is going to be run under. invokedynamic future proofs the generated code. We don't have that concern. So I expect we can make different choices there too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Chatted with @jdconrad - we have a different reason for needing invokedynamic than java does - we don't know the target functional interface until runtime sometimes. If you invoke a method on a def
object that takes a lambda then we must delay building the class. For simplicity, we always delay.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a note about serialization.
* this.arg$0 = arg$0; | ||
* } | ||
* | ||
* public static $$Lambda0 get$Lambda(List arg$0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reading this from top to bottom, I'm curious why we need this instead of calling the ctor directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To invoke a constructor the class would have to be newed and duped onto the stack. The caller doesn't know what the generated class is, so it calls a factorymethod that can create the class instead.
|
||
/** | ||
* A counter used to generate a unique name | ||
* for each lambda function/reference class. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is probably worth leaving a comment that this is done in the child classloader so this is a per-script counter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like that's not the case as when I test with two independent scripts the counter is incremented both times rather than starting at 1 for both of them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, there is only a single LambdaBootstrap so there is only one counter. I'd prefer one per script so I could look at the counter and know how many lambdas we've generated but I don't think that is worth holding up the PR for.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not worth complicating this to have to reload this class on a per-script basis. If you really want know the lambda count during debugging you can run one script at a time in a debugger. Plus, you also already know the answer since you generate exactly one lambda factory method per lambda per target type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, it isn't worth holding the PR for. I'd like it for debugging issues in running systems rather than the debugger. But it isn't a big deal.
* @throws LambdaConversionException Thrown when an illegal type conversion occurs at link time | ||
*/ | ||
public static CallSite lambdaBootstrap( | ||
Lookup lookup, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you indent this one extra time so it doesn't look like method body?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@nik9000 Ready for another round -- think I responded to all comments. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't say I really understand all the subtleties but I think this makes sense.
I think it'd be worth testing what happens when you blow PIC and this falls back to megamorphic. I'm not sure if this code is worse or better than LambdaMetaFactory in that case but I'd love to understand it.
return defineClass(name, bytes, 0, bytes.length, CODESOURCE).asSubclass(PainlessScript.class); | ||
} | ||
|
||
Class<?> defineLambda(String name, byte[] bytes) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think about javadocs for it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
owner = WriterConstants.AUGMENTATION_TYPE.getInternalName(); | ||
|
||
if ("<init>".equals(delegateMethod.name)) { | ||
delegateInvokeType = Opcodes.H_NEWINVOKESPECIAL; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd either statically import all of these opcodes or none of them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes! Fixed.
@@ -33,7 +33,7 @@ | |||
/** | |||
* Represents a method call. | |||
*/ | |||
final class PSubCallInvoke extends AExpression { | |||
final class PSubCallInvoke extends AExpression { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leftover.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
* {@code | ||
* List list1 = new ArrayList(); " | ||
* list1.add(2); " | ||
* List1 list2 = new ArrayList(); " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/List1/List/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
|
||
/** | ||
* A counter used to generate a unique name | ||
* for each lambda function/reference class. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, there is only a single LambdaBootstrap so there is only one counter. I'd prefer one per script so I could look at the counter and know how many lambdas we've generated but I don't think that is worth holding up the PR for.
* of either a lot more code or requiring many {@link Definition.Type}s to be looked | ||
* up at link-time. | ||
*/ | ||
public static CallSite delegateBootstrap(Lookup lookup, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you move this method up with the other public method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Going to leave this one as the methods are in order of which ones will be called first.
I don't understand this -- "I think it'd be worth testing what happens when you blow PIC and this falls back to megamorphic. I'm not sure if this code is worse or better than LambdaMetaFactory in that case but I'd love to understand it." Can you please explain what you mean here? Lambdas don't use this PIC at all except indirectly through another def type. A direct lambda cannot be the target type ever. For that def type to even fall back to megamorphic requires the target type to change I believe at least 6 times. (I'm not even sure this is possible using lambdas given our current supported whitelist.) If a user has a def value representing a lambda it's simply thought of as the functional interface type. I also know the fallback is tested for other types already. Maybe it's interesting in an academic sense, but it's really not practically necessary at this time. |
Sure. I'm debugging some issues someone pinged me about where our stack traces aren't good when we fall back to the megamorphic case so I'm probably overthinking this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's get this in so we aren't busted in Java 9.
@nik9000 Is there an issue up for the stack traces? I'm curious to see what the difference in stack traces looks like. Also, thanks for the review! I will make sure to respond to the rest of the comments before merging. |
I'm playing with it now. Someone pinged my privately. I'm still trying to reproduce. |
So yes, there is an issue with stack traces but it isn't unexpected. Basically the PIC and MIC end up in stacktraces when |
Replaces LambdaMetaFactory with LambdaBootstrap, a custom solution for lambdas in Painless using a design similar to LambdaMetaFactory, but allows for custom adaptation of types which recent changes to LambdaMetaFactory no longer allowed.
@nik9000 Thank you for the review. |
Thanks for diving into LambdaMetaFactory! |
I figured out that the special handling of public void testCtorWithParams() {
assertArrayEquals(new Object[] { "foo", "bar" },
(Object[]) exec("List l = new ArrayList(); l.add('foo'); l.add('bar'); " +
"Stream stream = l.stream().map(StringBuilder::new);" +
"return stream.map(Object::toString).toArray()"));
} This code fails because the script returns a I have a fix ready... |
In addition, the static factory as replacement for the constructor of the lambda ( |
I opened PR #24406 to fix both issues. All tests pass for me with Java 8 and Java 9. |
I have seen some discussions initiated by @nik9000 and @jdconrad about the useless stack frames we see in the stack trace on exceptions. Since Java 8u60, the whole Lambda class is hidden from all stack traces (so user only sees a direct call in the stack trace to the lambda/function reference, but no wrapper classes). The trick is to mark methods that you want to hide (because they are completely synthetic and have no useful meaning to users reading the stack). This would also solve the problem with the ever-incrementing class name counter. This is the change in Java 8u60: http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/rev/f7dd864a52ea We can do the same (after #24406 is merged): iface.visitAnnotation("Ljava/lang/invoke/LambdaForm$Hidden;", true); The problem is that this runtime annotation is internal only (OpenJDK specific). So I'd only add the annotation to the interface method and the ctor proxy only if |
I tried to hide the stack frames like done above. Unfortunately it does not seem to work if the classes are not anonymous (via Unsafe). During that I found some smaller things, like the doPrivileged around getting the ctor: This is not needed, as it is public in our case. I can provide a PR later. I'd also change LambdaConversionException like the original LambdaMetaFactory on problems instead of IllegalStateException (which is wrong). |
As hiding the stack frames did not work, I made the counter "per-script". The trick was to put an I don't like the IllegalStateExceptions in LambdaBootstrap instead of LambdaConversionExceptions. We should also change that. OpenJDKs bootstrap throws LambdaConversionException on every problem while creating the lambda class. |
Finally I also changed the codesource parameter in defineClass to be consistent in my PR. The lambdas are now running in same unprivileged code source. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I solved the mentioned issues in my other PR #24406
// A new instance of the requested type will be created and the | ||
// constructor with no parameters will be called. | ||
// Example: String::new | ||
if (delegateInvokeType == H_NEWINVOKESPECIAL) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we sure here that we never have arguments for ctor references? If no (e.g., painless does not support ::new with args) we should assert this and throw exception!
* @return A Class object. | ||
*/ | ||
Class<?> defineLambda(String name, byte[] bytes) { | ||
return defineClass(name, bytes, 0, bytes.length); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not here also use CODESOURCE
for safety? I know the lambda class on its own cannot do any bad stuff, but I'd still make it use the same codesource!
JDK9 has made changes to LambdaMetaFactory that prevent Painless lambdas from being able to do proper adaptation of types from the interface method to the delegate method even using a bridge. In particular boxed types are no longer widened appropriately, and Object can no longer be adapted to primitive types. Though, the former is likely a bug based on the spec, whether the latter is allowed according to spec is somewhat ambiguous.
This fix adds a LambdaBootstrap class that replaces LambdaMetaFactory in Painless using the same methodology that LambdaMetaFactory was based on (http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html). This allows us to do whatever adaptations we believe are necessary without having to worry about future changes/bugs in LambdaMetaFactory.
This is related to #23473.