Hi Mandy,
On 4/3/20 11:32 PM, Mandy Chung wrote:
Hi Peter,
I added a JBS comment [1] to describe this special case trying to put
the story together (let me know if it needs more explanation). More
comment inline below.
Thanks, this helps in establishing the historical context.
On 4/3/20 4:40 AM, Peter Levart wrote:
Ok, I think I found one such use-case. In the following example:
package test;
public class LambdaTest {
protected void m() {
}
}
package test.sub;
public class LambdaTestSub extends test.LambdaTest {
public void test() {
Runnable r = this::m;
r.run();
}
}
Yes.
This is specific for binary compatibility. the invocation of a
protected method inherited from its supertype in a different package.
The lambda proxy is in the same package as the target class
(`test.sub` in the example above) but it has no access to
`test.LambdaTest::m`.
...when compiled with JDK 14 javac. In this case the implClass is
test.sub.LambdaTestSub while implInfo is "invokeVirtual
test.LambdaTest.m:()void" and the method is not public.
In JDK 14, a lambda proxy `test.sub.LambdaTestSub$Lambda$$1234` is VM
anonymous class which has a special powerful access as if the host
class. This proxy class, even though it's not an instance of
`test.LambdaTest`, can invoke the protected`test.LambdaTest.m:()void`
member.
Right, the VM anonymous class "inherits" all access rights from the host
class, which in above example is the subclass of test.LambdaTes.
Anyway, the name of the proxy class is derived from the targetClass
(and therefore shares the same package with targetClass) which is
caller's lookup class. Is targetClass always the same as implClass in
the invokeVirtual/invokeInterface case?
implMethod is the direct method handle describing the implementation
method resolved by the VM. So it depends.
In the above example, it's `test.LambdaTest.m:()void` and implClass is
test.LambdaTest.
Here I think, you are not quite right. First I need to clarify that we
are talking about the case where the method reference in above example
is not converted to lambda by javac, so the proxy class needs to invoke
the superclass method directly (without the help of lambda bridge). I
did an experiment and compiled the example with JDK 13 javac, where the
patch for (JDK-8234729) is not applied yet. What I get from this
compilation is the following metafactory bootstrap method invocation:
0: #35 REF_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:
#42 ()V
#43 REF_invokeVirtual test/LambdaTest.m:()V
#42 ()V
The #43 is the implMethod method handle and it is the following:
#43 = MethodHandle 5:#44 // REF_invokeVirtual
test/LambdaTest.m:()V
#44 = Methodref #2.#45 // test/LambdaTest.m:()V
#45 = NameAndType #46:#6 // m:()V
#46 = Utf8 m
#2 = Class #4 // test/LambdaTest
#4 = Utf8 test/LambdaTest
*BUT* the class that looks up this MH from the constant pool is the
subclass (test.sub.LambdaTestSub) which is equivalent to the following
programmatic lookup:
var mh = MethodHandles.lookup().findVirtual(LambdaTest.class,
"m", MethodType.methodType(void.class));
System.out.println(mh.type());
and this results in a method handle of the following type:
(LambdaTestSub)void
which is correct since the method handle *MUST* check that the passed in
instance (this) is of type LambdaTestSub or subtype or else the
"protected" access would be violated.
And since the ref type is REF_invokeVirtual, the
AbstractValidatingLambdaMetafactory assigns the following to the implClass:
this.implMethodType = implMethod.type();
this.implInfo = caller.revealDirect(implMethod);
switch (implInfo.getReferenceKind()) {
case REF_invokeVirtual:
case REF_invokeInterface:
this.implClass = implMethodType.parameterType(0);
...which makes the implClass be LambdaTestSub in this case. Which is
correct again since we want InnerClassLambdaMetafactory to decide that
this is the special case for proxy to invoke the method via method handle:
useImplMethodHandle =
!implClass.getPackageName().equals(implInfo.getDeclaringClass().getPackageName())
&&
!Modifier.isPublic(implInfo.getModifiers());
If implClass was test.LambdaTest as you said above, this condition would
evaluate to false, since implInfo is "invokeVirtual
test.LambdaTest.m:()void" in above case.
So everything is OK, but my original question was the following: The
name of the generated proxy class is derived from the targetClass which
is the caller's lookup class. In this example the caller is
LambdaTestSub and this is the same as implClass in this case. Would
those two classes always be the same in the case where the evaluation of
the above `useImplMethodHandle` boolean matters? I mean, the decision
about whether to base the proxy invocation strategy on method handle or
direct bytecode invocation is based on one class (implClass), but the
actual package of the proxy class which effectively influences the
bytecode invocation rights is taken from another class (targetClass).
On one hand the package of the proxy class has to be the same as
targetClass if the proxy wants to be the nestmate of the targetClass
(for example to have private access to the members of the nest). But
OTOH it needs to be the same package also with implClass so that the
above decision of the proxy invocation strategy is correct. I have a
feeling that for protected virtual methods, this is true, because the
type of the 0-argument of such method handle is always the lookup class.
But am I right?
What do you think of another alternative to invoking the super protected
method in other package: What if the LMF would decide to base the name
of the proxy class on the implInfo.getDeclaringClass() in such case? It
would not have to be a nestmate of any class, just in the same package
with the method's declaring class. Consequently it would be in the same
module as the declaring class and loaded by the same class loader.
Therefore it would have to be WEAKLY referenced from the class loader.
And the Lookup instance passed to bootstrap LMF method would not be
enough for defining such class. But LMF could obtain special powers
since it is JDK internal class...
Well, I don't know for myself. Is this situation rare enough so that
invoking via method handle is not a drawback? It only happens when
running JDK 13- compiled classes with JDK 15+ and in addition the method
reference must point to protected method in a distant class.
The targetClass is test.sub.LambdaTestSub which is *NOT* the same as
implClass. That's why we change if they are in the same package.
It can be invoking a method in targetClass or a method in another
class in the same package with package access, then implClass may or
may not be the same as targetClass.
I also noticed that JDK 15 patched javac transforms method reference
in above code into a lambda method. But looking at the javac changes
in the patch, I don't quite see where this distinction between JDK 14
and patched JDK 15 javac comes from.
javac has been changed in JDK 14 to synthesize a bridge method if it's
a method reference to access a protected member in a remote supertype
(JDK-8234729).
Ah, that was the reason I haven't seen the change in your patch. I used
the JDK 13 javac and thought it would be the same as JDK 14 javac. My
mistake.
So this answers the question below...
Regards, Peter
BTW, the new tests relevant to this scenario are under
test/jdk/java/lang/invoke/lambda/superProtectedMethod.
From the changes to method
com.sun.tools.javac.comp.LambdaToMethod.LambdaAnalyzerPreprocessor.ReferenceTranslationContext#needsConversionToLambda:
final boolean needsConversionToLambda() {
return interfaceParameterIsIntersectionOrUnionType() ||
isSuper ||
needsVarArgsConversion() ||
isArrayOp() ||
# isPrivateInOtherClass() ||
isProtectedInSuperClassOfEnclosingClassInOtherPackage() ||
!receiverAccessible() ||
(tree.getMode() == ReferenceMode.NEW &&
tree.kind != ReferenceKind.ARRAY_CTOR &&
(tree.sym.owner.isLocal() ||
tree.sym.owner.isInner()));
}
...I would draw the conclusion that conversion to lambda is performed
in less cases not in more.
Jan and Srikanath may be able to explain this further.
Hm.
Regards, Peter
On 4/3/20 11:11 AM, Peter Levart wrote:
Hi Mandy,
Good work.
I'm trying to find out which language use-case is covered by the
InnerClassLambdaMetafactory needing to inject method handle into the
generated proxy class to be invoked instead of proxy class directly
invoking the method:
useImplMethodHandle =
!implClass.getPackageName().equals(implInfo.getDeclaringClass().getPackageName())
&&
!Modifier.isPublic(implInfo.getModifiers());
If I check what implClass and implInfo get resolved to in
AbstractValidatingLambdaMetafactory, there are several cases:
this.implInfo = caller.revealDirect(implMethod);
switch (implInfo.getReferenceKind()) {
case REF_invokeVirtual:
case REF_invokeInterface:
this.implClass = implMethodType.parameterType(0);
// reference kind reported by implInfo may not match
implMethodType's first param
// Example: implMethodType is (Cloneable)String,
implInfo is for Object.toString
this.implKind = implClass.isInterface() ?
REF_invokeInterface : REF_invokeVirtual;
this.implIsInstanceMethod = true;
break;
case REF_invokeSpecial:
// JDK-8172817: should use referenced class here,
but we don't know what it was
this.implClass = implInfo.getDeclaringClass();
this.implIsInstanceMethod = true;
// Classes compiled prior to dynamic nestmate
support invokes a private instance
// method with REF_invokeSpecial.
//
// invokespecial should only be used to invoke
private nestmate constructors.
// The lambda proxy class will be defined as a
nestmate of targetClass.
// If the method to be invoked is an instance method
of targetClass, then
// convert to use invokevirtual or invokeinterface.
if (targetClass == implClass &&
!implInfo.getName().equals("<init>")) {
this.implKind = implClass.isInterface() ?
REF_invokeInterface : REF_invokeVirtual;
} else {
this.implKind = REF_invokeSpecial;
}
break;
case REF_invokeStatic:
case REF_newInvokeSpecial:
// JDK-8172817: should use referenced class here for
invokestatic, but we don't know what it was
this.implClass = implInfo.getDeclaringClass();
this.implKind = implInfo.getReferenceKind();
this.implIsInstanceMethod = false;
break;
default:
throw new
LambdaConversionException(String.format("Unsupported MethodHandle
kind: %s", implInfo));
}
For majority of cases (REF_invokeSpecial, REF_invokeStatic,
REF_newInvokeSpecial) the this.implClass =
implInfo.getDeclaringClass();
Only REF_invokeVirtual and REF_invokeInterface are possible
kandidates, right?
So when does the implMethod type of parameter 0 differ from the
declaring class of the MethodHandleInfo cracked from that same
implMethod and in addition those two types leave in different packages?
This is concerning the instance method and so parameter 0 is what it
wants to look at.
Regards, Peter
Hope this helps.
Mandy
[1]
https://bugs.openjdk.java.net/browse/JDK-8239384?focusedCommentId=14328369&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-14328369