Bug 57238 - proceed on interface fails with ClassCastException
Summary: proceed on interface fails with ClassCastException
Status: REOPENED
Alias: None
Product: AspectJ
Classification: Tools
Component: Compiler (show other bugs)
Version: unspecified   Edit
Hardware: PC Windows XP
: P5 normal (vote)
Target Milestone: ---   Edit
Assignee: Andrew Clement CLA
QA Contact:
URL:
Whiteboard:
Keywords:
Depends on:
Blocks:
 
Reported: 2004-04-02 09:48 EST by Andrew Clement CLA
Modified: 2009-08-30 02:49 EDT (History)
2 users (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Andrew Clement CLA 2004-04-02 09:48:26 EST
I have the following program:

aspect Simple {

  pointcut interesting(A instance): execution(* getString(..)) && this
(instance);

  Object around(A instance) : interesting(instance) {
    System.err.println("around advice");
    C c = new C();
    return proceed(c);
  }

  public static void main(String[] argv) {
    B b = new B();
    C c = new C();

    System.out.println(b.getString());
    System.out.println(c.getString());
  }

}

interface A {
  public String getString();
}

class B implements A {
  String s = "hello from B";

  public String getString() { return s; }
}

class C implements A {
  String s = "hello from C";

  public String getString() { return s; }
}

It compiles but it won't run.  I (we) were thinking that because the around 
advice signature takes an 'A' then we could proceed with an instance of any 
type implementing A.  In the case above although the advice matches on the 
execution of a method on B - we attempt to create a 'C' (which also implements 
A) and proceed with that.  The class cast exception is because some of the 
extracted methods in the generated code have parameters of type 'B' rather 
than 'A'.  Here is the relevant code from a decompiled B.class:

class B implements A {
    B() { s = "hello from B"; }

    public String getString() {
        return (String)getString_aroundBody1$advice(this, Simple.aspectOf(), 
this, null);
    }

    static final String getString_aroundBody0(B b) {
        return b.s;
    }

    static final Object getString_aroundBody1$advice(Simple this, A instance, 
AroundClosure ajc_aroundClosure, C c) {
        System.err.println("around advice");
        C c1 = new C();
        C c2 = c;
        C c3 = c1;
        return getString_aroundBody0((B)c3);
    }

    String s;
}

The classcastexception comes out at the point we try and cast C to a B ready 
for consumption by getString_aroundBody0(B b).  

The code that does this seems to be BcelShadow.createMethodGen().

Should this even work?  Should we be constructing getString_aroundBody0() 
taking an A rather than a B?
Comment 1 Andrew Clement CLA 2004-04-02 10:01:15 EST
Ignore the decompiled code - the decompilers seem to have trouble consuming 
the generated around advice method - the signature is all wrong ?!?  This 
doesn't mean there isn't a problem, just that its gonna be hard to investigate!
Comment 2 Andrew Clement CLA 2004-04-02 11:44:48 EST
Adrian and I have been discussing what to do...

It seems the getString() in our example is extracted into a method called 
something like:

static String getString_adviceBody0(B b) {...}

And it is invoked from the advice method with: 

getString_adviceBody0((B)c);

============
In fact when proceeding on C we'd like something like this:

The method is extracted to

    static String getString_adviceBody<UNIQUE_ACROSS_TYPES_ID>(A a) {...}

and the advice calls

    c.getString_adviceBody<UNIQUE_ACROSS_TYPES_ID>(c);

Notice the extracted form of the method now takes the same parameter as was 
indicated in the around advice declaration (i.e. A, the common interface type)

Notice that now, although the extracted method is static, we invoke it on 'c' 
which allows us to in fact invoke the variant that exists in the C class 
rather than the one in the B class.  If we call it in this way there is no 
need to type cast the argument to the extracted method.

Finally, because just using a counter on the end of extracted method names 
could cause problems in the case of two pieces of advice applying to one file 
and only one piece applying to another - we use a name suffix that relates to 
the around advice declaration.

In our example, this would mean that at the point the extracted method is 
called, it is called in 'C' rather than 'B' and so returns the right result.

of course, Adrian and I are not experts on compilers or type binding so we 
haven't a clue if any of this would work ;)
Comment 3 Adrian Colyer CLA 2004-04-02 14:46:48 EST
There are a number of problems with the scheme just described. Chief of which is 
that there is no virtual dispatch for static methods. 

So the method:
 String getString_adviceBody<UNIQUE_ACROSS_TYPES_ID>(A a) {...}
would have to be non-static.

Secondly, every implementor of the interface would need to be woven to add the
getString_adviceBody method (since you don't know which implementor will be 
dispatched to in the proceed). Some of these implementors may not even be 
matched by the pointcut associated with the around advice - so what would 
trigger their munging? 

It therefore seems that the dispatch has to happen to the actual orginal method 
as declared on the interface (getString() in this case), and the around advice 
body would then need to be smart enough to know not to reapply itself the second 
time round. But this is a big change, and can it be done in a way that doesn't 
hurt performance for all the other uses of around advice? And what happens in 
the case when the target implementor is itself the recipient of other advice - 
do all the precedence rules work out ok??

Note also that although it doesn't give a class cast exception at runtime, 
proceeding with a subtype of the type receiving the advice (e.g. if you change 
the sample program to make C extend B) gives the wrong results as B's 
implementation of getString() will execute rather than C's. 

The programming and semantics guide are kind of silent on exactly what should 
happen when you start changing a variable in a proceed argument list that is 
bound to the target() (or this()) at a join point. It is a very powerful and 
useful thing to be able to do. Since I can't see an easy implementation right 
now, it's tempting to document an implementation limitation that you can only 
proceed with the same concrete type - but that cuts out at least one important 
use case that I can't easily see another way of implementing: namely using 
around advice to delegate execution of all methods in an interface to another 
implementor, without resorting to reflection or listing each method in the 
interface individually. If I can control all the call sites, I might be able to 
get close with dynamic proxies (http://java.sun.com/j2se/1.
3/docs/guide/reflection/proxy.html), but if I need to advise executions...

bring on the gurus....
Comment 4 Jim Hugunin CLA 2004-04-04 21:50:05 EDT
This code is behaving according to the type model for around advice that I 
believe Erik and I both have in our heads.  However, I can't find this 
documented anywhere in the language spec, so I'm cc'ing Erik to keep me honest.

Joinpoint shadows have a static type signature that is independent of any 
advice on them.  So, in your example, the execution joinpoint on B has the 
static signature
  this and target: B
  args: ()

This static signature is used a lot for optimizing matching and residues.  The 
one place that it can be visible is with proceed and the value returned from 
around.

The rule for proceed is that you must call it with arguments that correspond 
to the static type of the corresponding joinpoint shadow or you'll get a 
runtime type error.  Having this rule makes it a lot easier to think about 
complicated cases.  For example, what happens if A doesn't contain a getString 
method?  What if your advice was on this(instance) && get(* s)?  Should it 
still be possible to substitute a C for a B?  Where would the control-flow 
pick up in that case?

The fact that you can write this code and you get runtime type errors instead 
of compile-time errors is the one known hole in AspectJ's type system.  I've 
heard there are papers showing it's not possible to do any better, but all I 
know is this is the best we could come up with at the time.

There are several small ways to improve the error messages from this situation 
that are worth doing as time permits.  One is to modify the casts that are 
inserted to instead check the type and throw a special 
IncompatibleClassForProceed exception.  Another important change is to add an 
xlint warning for cases where such casts are inserted, and to detect cases 
like this one where the cast is guaranteed to fail and signal them more 
aggressively.

For the particular example you show here you can solve this problem by 
modifying your advice to read:

  Object around(A instance) : interesting(instance) {
    System.err.println("around advice");
    C c = new C();
    return c.getString();
  }

Obviously this isn't a completely general solution if you have too many 
wildcards in play.

The current static type of joinpoint shadows is a relatively simple place to 
be.  It might be possible to come up with a more aggressive type model for 
proceed that enables this kind of change, as well as reducing the 
corresponding restrictions on the values returned from around advice.  
However, I don't see how that could be done in time for 1.2.  
Comment 5 Adrian Colyer CLA 2005-03-22 08:49:13 EST
No plans to change the current implementation at the moment...
Comment 6 Eclipse Webmaster CLA 2009-08-30 02:49:11 EDT
LATER/REMIND bugs are being automatically reopened as P5 because the LATER and REMIND resolutions are deprecated.