Skip to content
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

painless: Fix method references to ctor with the new LambdaBootstrap and cleanup code #24406

Merged
merged 10 commits into from
May 1, 2017

Conversation

uschindler
Copy link
Contributor

This PR fixes #24070 (comment):

I figured out that the special handling of H_NEWINVOKESPECIAL is broken. The following test fails:

    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 Object[] consisting only of 2 empty Strings. The reason for this is: StringBuilder::new points to the StringBuilder(CharSequence) ctor. The code in LambdaBootstrap ignores all parameters and calls the default ctor of StringBuilder. The result is mapped back to a String afterwards and is of course empty.

To fix this I changed the LamdaBootstrap to create a static proxy/delegator method around the ctor (delegate$ctor) with the same parameters as the ctor, returning the newly created instance (somehow similar like the factory for the captured lambda - which I removed, see below). When generating the INVOKEDYNAMIC to our type-adapting callsite, we change the H_NEWINVOKESPECIAL to a static method call to our proxy. I tried to fix this without using a static method, but although the JLS/class file format allows H_NEWINVOKESPECIAL for a handle, when passing this as a Handle (in constant pool) to INVOKEDYNAMIC, it causes a class format error. I am not sure if this is a bug in ASM or if INVOKEDYNAMIC does not support this. It has nothing to do with the fact of a newinstance bytecode may be needed - no it's not for MethodHandles!!!

In addition, the static factory as replacement for the constructor of the lambda (get$lambda) is not needed. If you use lookup.findConstructor() everything works as it should. So I also removed it. @jdconrad said (#24070 (comment)) that its needed, but it isn't, because lookup.findConstructor() returns a MethodHandle that also does newinstance bytecode.

Documentation was updated accordingly.

The final TODO is to check why the direct MH in constant pool leads to a ClassFormatError. I will look into lambdas created by javac, maybe I see something that I was missing!

…od references to ctors. Also remove the get$lambda factory.
@elasticmachine
Copy link
Collaborator

Since this is a community submitted pull request, a Jenkins build has not been kicked off automatically. Can an Elastic organization member please verify the contents of this patch and then kick off a build manually?

…nternal class names (uses ASM Type class instead)
@uschindler
Copy link
Contributor Author

I cleaned up the code a bit more and removed most of the IMHO "bad" conversions between internal/slashed class names and binary class names. Instead I propagated the ASM Type, whcih also makes it type safe. This also removes duplicated method calls.

@uschindler
Copy link
Contributor Author

uschindler commented Apr 29, 2017

I checked the bytecode of a javac-compiled class similar to the failing test and figured out with javap:

BootstrapMethods:
  0: #72 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #74 (Ljava/lang/Object;)Ljava/lang/Object;
      #80 newinvokespecial java/lang/StringBuilder."<init>":(Ljava/lang/CharSequence;)V
      #82 (Ljava/lang/CharSequence;)Ljava/lang/StringBuilder;

As we see the original LambdaMetaFactory takes the last parameter with the "real signature" (#82) of the MethodHandle. The Handle in #80 is exactly as I would expected the ctor to be referred to. In fact LambdaMetaFactory does adapt this like we do: It converts the NEWINVOKESPECIAL into a sequence of a newinstance followed by an invokespecial of the ctor using the void return type.

My code does exactly the same type of adaption (in a static helper method calling the constructor after creating the instance). In our case we just use a static helper method, so we can delegate the whole thing easily to delegateBootstrap(). For all other invocation types we do not need any special adaption.

@uschindler
Copy link
Contributor Author

uschindler commented Apr 29, 2017

I think this is ready now. I moved some code around and it looks elegant to me! 🥇

@uschindler uschindler force-pushed the bugs/fixCtorReference branch from b02bb65 to c42dfad Compare April 29, 2017 22:00
@uschindler uschindler force-pushed the bugs/fixCtorReference branch from c42dfad to ab898aa Compare April 29, 2017 22:03
@uschindler uschindler changed the title painless: Fix method references to ctor with new LambdaBootstrap painless: Fix method references to ctor with the new LambdaBootstrap and cleanup code Apr 29, 2017
@uschindler uschindler force-pushed the bugs/fixCtorReference branch from d8fbf97 to b547fa9 Compare April 30, 2017 09:17
@uschindler
Copy link
Contributor Author

I also moved the lambda counter to the classloader so it is per script (makes debugging easier and results more consistent). I also removed the doPrivileged for the constructor, as its not needed (OpenJDK code does it, because it uses setAccessible, because lambda class is package private).

@uschindler
Copy link
Contributor Author

I also changed the code source of the lambda classes to be the same unprivileged as the script itsself.

@jdconrad
Copy link
Contributor

jdconrad commented May 1, 2017

@uschindler I can't thank you enough for fixing all these things! I really appreciate you going through all my code again :). I'll get this checked in today or tomorrow.

On the constructors with arguments -- just a really poor assumption on my part that all constructors using ::new would have no parameters, I'm not sure why I was thinking that way.
On the counter -- you provided a much better design, I just hadn't thought of doing it this way.
On the CODESOURCE -- same answer as just above.
On the factory and lookup.findStatic -- I think I was trying to imitate what LambdaMetaFactory was doing, but I was just wrong about not being able to call the constructor directly. I guess my question here is, is there a reason that LambdaMetaFactory didn't use lookup.findConstructor here?

@uschindler
Copy link
Contributor Author

On the constructors with arguments -- just a really poor assumption on my part that all constructors using ::new would have no parameters, I'm not sure why I was thinking that way.

No problem. There was also no test about this!

Anyhow, the idea you had and how to use invokedynamic to call asType() to do the type adaption is really phantastic! Incredible cool! Congrats!

I spent nights to think about how to do this without lots of code redoing the same thing like asType! And on top, this causes no slowdown, as the invokedyanmic is done once and then reused (its a constant callsite).

On the factory and lookup.findStatic -- I think I was trying to imitate what LambdaMetaFactory was doing, but I was just wrong about not being able to call the constructor directly. I guess my question here is, is there a reason that LambdaMetaFactory didn't use lookup.findConstructor here?

Very simple reason, they can't do otherwise! The original LambdaMetaFactory has some bootstrapping issues. Because of this it does a lot of stuff only using internal APIs only and doesn't touch public stuff (which is not yet initialized during JVM bootstrapping). In addition, this has less security checks than the public APIs, which prevents them from doing doPrivileged everywhere! E.g. it uses a limited, private-only lookup instance (implLookup, which is a early stage lookup without any security features) and it also creates method handles using that (e.g. the one to the static factory). It also uses Unsafe to define anonymous classes! Because of this it also has to handle security in a very different way, so never ever copy the code from inside the original LambdaMetaFactory. The reason for all this is: While bootstrapping the JVM, they already use lambdas and so there is some chicken and egg problems. Not everything is initialized, so the public APIs may break. Because of this they limit internally to private APIs. And the ctor features need more bootstrapping. Our code uses public APIs and so it can directly return a MethodHandle to call the lambda's class' ctor (internally this calls "newinstance" followed up by an "invokespecial", like your static method). In general, it is a bad idea to look at internal OpenJDK implementations and try to reimplement them in "user code" (it should also not done because of GPL-related issues!). I know that you copypasted some code, because you added the no-op doPrivileged with the same try-catch-structure as the GPLed code. The patch here removes all this stuff that somebody could use to argue that ES was copying GPL code!

@jdconrad
Copy link
Contributor

jdconrad commented May 1, 2017

@uschindler Thank you for the explanation here, I think it's clear that I don't deal with the internal APIs in this way very often :) so the chicken and egg problems related to bootstrapping never really occurred to me until you pointed it out. Also I greatly appreciate the help in avoiding any licensing issues.

I'm also happy you approved of using invokedynamic to get access to asType for adaptation. I was worried this wasn't a good way to do the adaptation, but I really didn't want to re-write adaptation code.

@uschindler
Copy link
Contributor Author

uschindler commented May 1, 2017

Hi, another reason why the original JDK8 code used a static factory was that ctor MethodHandles were not direct. Since Java 8u40 the new LambdaForms made this obsolete, too: the constructor MHs are direct and are as effective as a calls to static methods. The subclass DirectMethodHandle.Constructor just injects an Unsafe.allocateInstance() before invokespecial to the original constructor (identical to the static factory). So the static factory is just a relic from earlier days where performance was an issue.

@jdconrad
Copy link
Contributor

jdconrad commented May 1, 2017

@elasticmachine test this please

Split up a line in LambdaBootstrap that is over 140 characters long.
@jdconrad
Copy link
Contributor

jdconrad commented May 1, 2017

@elasticmachine test this please

@uschindler
Copy link
Contributor Author

Does Gradle not show too long lines when running tests? I thought it did in the past...!

@jdconrad
Copy link
Contributor

jdconrad commented May 1, 2017

gradle check should check all the style as far as I know. When I ran locally gradle check failed for me, but gradle test passed. A lot of this stuff has changed over the past year or two, so it's definitely possible that gradle test used to check line length as well. I blame @rjernst :)

@uschindler
Copy link
Contributor Author

uschindler commented May 1, 2017

Ok, will check next time.

BTW: I hate line length limitations. The code is much less readable to me with enforced breaks. 🤐

@jdconrad
Copy link
Contributor

jdconrad commented May 1, 2017

No worries! And I agree with you on the length limits, but it's not up to me.

@jdconrad jdconrad merged commit e88d54b into elastic:master May 1, 2017
jdconrad pushed a commit that referenced this pull request May 1, 2017
…and cleanup code (#24406)

* Fix wrong delegation to constructors when compiling lambdas with method references to ctors. Also remove the get$lambda factory.
* Cleanup code and remove unneeded transformations between binary and internal class names (uses ASM Type class instead)
* Cleanup Exception handling
* Simplification by moving the type adaption to the outside
* Remove STATIC access flag from our Lambda class (not required and also officially not allowed)
* Move the lambda counter to the classloader, so we have a per-script lambda ID
* Change Codesource of generated lambdas to be consistent
@jdconrad
Copy link
Contributor

jdconrad commented May 1, 2017

Thanks again @uschindler for all the help with this! I greatly appreciate your expertise in getting this all working correctly. This was committed to both master and 5.x

@uschindler uschindler deleted the bugs/fixCtorReference branch May 1, 2017 23:38
@uschindler uschindler restored the bugs/fixCtorReference branch May 1, 2017 23:41
@uschindler
Copy link
Contributor Author

OK. All fine :-) Thanks for committing. I opened another PR for adding two Java 9 related tests 2 days ago! #24405

@jdconrad
Copy link
Contributor

jdconrad commented May 1, 2017

Oops, sorry I missed those. I'll take a look right now.

@uschindler uschindler deleted the bugs/fixCtorReference branch May 2, 2017 16:26
jasontedor added a commit to jasontedor/elasticsearch that referenced this pull request May 2, 2017
* master: (27 commits)
  Check index sorting with no replica since we cannot ensure that the replica index is ready when forceMerge is called. Closes elastic#24416
  Docs: correct indentation on callout
  Build that java api docs from a test (elastic#24354)
  Move RemoteClusterService into TransportService (elastic#24424)
  Fix license header in WildflyIT.java
  Try not to lose stacktraces (elastic#24426)
  [DOCS] Update XPack Reference URL for 5.4 (elastic#24425)
  Painless: Add tests to check for existence and correct detection of the special Java 9 optimizations: Indified String concat and MethodHandles#ArrayLengthHelper() (elastic#24405)
  Extract a common base class to allow services to listen to remote cluster config updates (elastic#24367)
  Adds check to snapshot repository incompatible-snapshots blob to delete a pre-existing one before attempting to overwrite it.
  Added docs for batched_reduce_size
  Fixes checkstyle errors
  Allow scripted metric agg to access `_score` (elastic#24295)
  [Test] Add unit tests for HDR/TDigest PercentilesAggregators (elastic#24245)
  Fix FieldCaps documentation
  Upgrade to JUnit 4.12 (elastic#23877)
  Set available processors for Netty
  Painless: Fix method references to ctor with the new LambdaBootstrap and cleanup code (elastic#24406)
  Doc test: use propery regex for file size
  [DOCS] Tweak doc test to sync_flush
  ...
@clintongormley clintongormley added :Core/Infra/Scripting Scripting abstractions, Painless, and Mustache and removed :Plugin Lang Painless labels Feb 14, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
>bug :Core/Infra/Scripting Scripting abstractions, Painless, and Mustache v5.5.0 v6.0.0-alpha1
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants