invokespecial of default methods in unrelated interfaces
Daniel Heidinga
Daniel_Heidinga at ca.ibm.com
Thu Jun 20 19:34:14 PDT 2013
Regarding the invokespecial to an unrelated interface, I agree this should
be illegal. At a minimum, it should be required that the interface
targeted by the invokespecial must be compatible with the invoker as this
prevents invoking methods in unrelated interfaces.
While thinking about this, I've also taken a look at how the VM can prevent
"level skipping" while preserving both binary compatibility and programmer
intent. The picture is not pretty.
After reading both your and Dan's emails, there is a subtle difference in
the terms both of you are using:
* "immediate supertype of the invoker" - Immediate supertype is not defined
by the JVM spec but intuitively, a superinterface ("I") is not an immediate
supertype if it is also a parent of another superinterface (J, J extends
I).
* "direct superinterface" (Dan's term) - This is defined by the JVM spec
4.1 to be any interface declared in the classfile's interfaces[]. Using
this definition, but I and J would be considered direct superfaces if they
were declared in the interfaces[]. Using this definition means that
redundant interface declarations are suddenly meaningful information.
Starting with this simple example:
interface J { default void m() {...}}
interface I { default void m() {...}}
interface K extends I, J { default void m() { I.super.m(); }
K is a valid class that resolves the conflicting defenders by choosing to
dispatch to I.m().
If J is changed to extend I, a binary compatible change, then the question
becomes "is I still a direct / immediate superinteface?" and a valid target
for invokespecial?
If I is not a valid target because it allows K to skip J.m(), then K will
throw an AbstractMethodError and violate the binary compatibility
requirements.
Enforcing "no level skipping" can also break programs if new defender
implementations are added, which decreases our ability to evolve
interfaces.
interface A { default void m() {...}}
interface B { }
interface C { }
interface D extends B, C { default void m() { C.super.m(); }
Adding a new default implementation of m() to B will also result in
AbstraceMethodError as D's super send would skip the new B.m().
If resolution started at the targeted class 'C', as stated in the current
335 spec, then this would continue to call A.m() matching the intent at the
time the code was written. Unfortunately, this makes 'C' a "bypass
interface" which allows level skipping by using dummy subinterfaces.
(Note, a clever programmer can always target any particular interface's
default implementation by adding a "bypass interface" that implements the
required method using a super send to the actual desired interface - this
is just an abuse of the standard way for users to fix defender conflicts.)
This seems to require the VM to ignore level skipping. Unfortunately,
there's holes here with respect to programmer intent because the targeted
interface must be named:
interface S { default void m() { } }
class T implements S { }
class U extends T implements S { void m() { S.super.m(); } }
The programmer than refines T to target a interface V:
interface V extends S { default void m() {...} }
class T implements V { }
And now class U will throw an AbstractMethodError. This is the simple
case, a single inheritance path, that might argue for some kind of
interface analog to the ACC_SUPER search rules.
If the goal is to preserve binary compatibility - that pre-existing
binaries continue to link without error whenever possible - then preventing
level skipping in the VM is impossible. The following is a resolution
algorithm for invokespecial that attempts to limit level skipping while
preserving binary compatibility:
* Perform interface resolution using the set of interfaces implemented by
the invoker of the invokespecial as defined in the JSR 335 JVM spec
5.4.3.4.
* Examine the set of candidate methods:
** If there is only 1 method, then resolution succeeds with that
method (This assumes any super implementation meets the required contract)
** If there are more than 1 method, remove any method that is not
provided by the the interface named in the invokespecial or subclass of it.
*** If this results in a single method, resolution succeeds
with that method.
* Otherwise, redo method resolution starting at the interface named in the
invokespecial and succeed or fail with the result of this resolution.
This uses two-passes to limit level skipping in cases when possible, such
as the first example in this email, while preserving binary compatibility
when the first pass would otherwise of failed.
--Dan
> Well, I wouldn't expect C code to be invoked in any case, but a crash is
> probably not a good result.
>
> I believe the intent was to have invokespecial reject an invocation if
> the named class was not an immediate supertype of the invoker. There
> may also be additional restrictions that the compiler imposes, designed
> to prevent "level skipping" usages, that may or may not need to also be
> enforced by the VM.
>
> On 6/13/2013 1:31 PM, Daniel Heidinga wrote:
> > We've put together a defender method test case using invokespecial and
> > we'd like some confirmation of our spec interpretation. The testcase
is:
> >
> > interface A {
> > default void m(){ System.out.println("A"); };
> > }
> > interface B {
> > default void m(){ System.out.println("B"); };
> > interface C {
> > default void m(){ System.out.println("C"); };
> > }
> > class D implements A, B {
> > void m() {
> > C.super.m(); // Note, D does not implement C
> > }
> > public static void main(String[] args) {
> > new D().m();
> > }
> > }
> >
> > While javac won't compile this, it is legal bytecode (generated with
ASM
> > because class D is unrelated to interface C). Executing 'new D().m()'
> > unfortunately crashes Hotspot b93.
> >
> > The lambda changes to the JVM spec for invokespecial indicate 'The
named
> > method is resolved (5.4.3.3)'. 5.4.3.3 states "Method resolution
> > attempts to look up the referenced method in C and its superclasses".
> > As C clearly has an implementation of m(), resolution should succeed
> > and C.m() should be called. (As an aside, there is an issue with
> > invokespecial referencing 5.4.3.3 for interface sends: "If C is an
> > interface, method resolution throws an IncompatibleClassChangeError.")
> >
> > Does anyone disagree with this interpretation of the spec?
> >
> > --Dan
> >
>
More information about the lambda-spec-observers
mailing list