- About Scala
- Documentation
- Code Examples
- Software
- Scala Developers
traits, binary compatibility, and the protection racket
Thu, 2011-12-22, 07:34
Consider the perversion taking place in the following tale.
trait A { private[this] val bippy = 5 def dingus = bippy * bippy } class B extends A
trait A has a single public method and a private[this] immutable fieldwhich does not escape and which the language prohibits anything outsideof A from accessing. Nevertheless, we generate not only a public getter, but a public setter, thus making the name of the private[this] field acentral piece of A's binary compatibility story. May as well be a publicvar for all the insulation A has from its implementation.
Now class B comes along and extends A. It receives the private field andthe public getter and setter. To populate the field, B's constructorcalls into A$class, which turns around and calls the public setter on B, the need for which is now apparent. To implement the one interfacemethod which A intentionally exposed, we again send B packing to A$classto perform the actual operation. Having left the warm confines of B where we could have accessed the field directly, we must resort to usingthe public getter which also is trying to justify its existence.
I understand why this machinery is in place and that (at least given the current encoding) it's necessary to support access which is scala-legalbut jvm-illegal. But I can't see why it can't be done more selectively,with the especially interesting case being the private field which never hurt a fly. Separate compilation can hardly be banking on the presenceof private fields which can't be separately accessed.
Optimally: interface A { def dingus(): Int } class B extends A { private int bippy = 5 def dingus = bippy * bippy }
In actual fact:
interface A { def dingus(): Int def A$$bippy(): Int def A$_setter_$A$$bippy_$eq(x: Int): Unit } class B extends A { private int A$$bippy = 5 def A$$bippy(): Int = A$$bippy def A$_setter_$A$$bippy_$eq(x: Int): Unit = A$$bippy = x def dingus(): Int = A$class.dingus(this) } class A$class { def dingus(x: A) = x.A$$bippy() * x.A$$bippy() }
Bytecode:
// interface Apublic abstract int A$$bippy();public abstract void A$_setter_$A$$bippy_$eq(int);public abstract int dingus();
// class Bprivate final int A$$bippy;
public int A$$bippy(); 0: aload_0 1: getfield #11; //Field A$$bippy:I 4: ireturn
public void A$_setter_$A$$bippy_$eq(int); 0: aload_0 1: iload_1 2: putfield #11; //Field A$$bippy:I 5: return
public int dingus(); 0: aload_0 1: invokestatic #19; //Method A$class.dingus:(LA;)I 4: ireturn
// Implementation class A$class:public static int dingus(A); 0: aload_0 1: invokeinterface #12, 1; //InterfaceMethod A.A$$bippy:()I 6: aload_0 7: invokeinterface #12, 1; //InterfaceMethod A.A$$bippy:()I 12: imul 13: ireturn
public static void $init$(A); 0: aload_0 1: iconst_5 2: invokeinterface #26, 2; //InterfaceMethod A.A$_setter_$A$$bippy_$eq:(I)V 7: return
trait A { private[this] val bippy = 5 def dingus = bippy * bippy } class B extends A
trait A has a single public method and a private[this] immutable fieldwhich does not escape and which the language prohibits anything outsideof A from accessing. Nevertheless, we generate not only a public getter, but a public setter, thus making the name of the private[this] field acentral piece of A's binary compatibility story. May as well be a publicvar for all the insulation A has from its implementation.
Now class B comes along and extends A. It receives the private field andthe public getter and setter. To populate the field, B's constructorcalls into A$class, which turns around and calls the public setter on B, the need for which is now apparent. To implement the one interfacemethod which A intentionally exposed, we again send B packing to A$classto perform the actual operation. Having left the warm confines of B where we could have accessed the field directly, we must resort to usingthe public getter which also is trying to justify its existence.
I understand why this machinery is in place and that (at least given the current encoding) it's necessary to support access which is scala-legalbut jvm-illegal. But I can't see why it can't be done more selectively,with the especially interesting case being the private field which never hurt a fly. Separate compilation can hardly be banking on the presenceof private fields which can't be separately accessed.
Optimally: interface A { def dingus(): Int } class B extends A { private int bippy = 5 def dingus = bippy * bippy }
In actual fact:
interface A { def dingus(): Int def A$$bippy(): Int def A$_setter_$A$$bippy_$eq(x: Int): Unit } class B extends A { private int A$$bippy = 5 def A$$bippy(): Int = A$$bippy def A$_setter_$A$$bippy_$eq(x: Int): Unit = A$$bippy = x def dingus(): Int = A$class.dingus(this) } class A$class { def dingus(x: A) = x.A$$bippy() * x.A$$bippy() }
Bytecode:
// interface Apublic abstract int A$$bippy();public abstract void A$_setter_$A$$bippy_$eq(int);public abstract int dingus();
// class Bprivate final int A$$bippy;
public int A$$bippy(); 0: aload_0 1: getfield #11; //Field A$$bippy:I 4: ireturn
public void A$_setter_$A$$bippy_$eq(int); 0: aload_0 1: iload_1 2: putfield #11; //Field A$$bippy:I 5: return
public int dingus(); 0: aload_0 1: invokestatic #19; //Method A$class.dingus:(LA;)I 4: ireturn
// Implementation class A$class:public static int dingus(A); 0: aload_0 1: invokeinterface #12, 1; //InterfaceMethod A.A$$bippy:()I 6: aload_0 7: invokeinterface #12, 1; //InterfaceMethod A.A$$bippy:()I 12: imul 13: ireturn
public static void $init$(A); 0: aload_0 1: iconst_5 2: invokeinterface #26, 2; //InterfaceMethod A.A$_setter_$A$$bippy_$eq:(I)V 7: return
Fri, 2011-12-23, 02:11
#2
Re: traits, binary compatibility, and the protection racket
On Thu, Dec 22, 2011 at 10:35 AM, Geoff Reedy <geoff@programmer-monk.net> wrote:
I think it can make a difference.
Oh, I didn't say it couldn't make a difference, I said that separate compilation shouldn't be banking on it. I think there are all kinds of hijinx you can bring out with selective recompilation and private members.
I think anything where different semantics arise for A based on implementation details of the trait encoding should be a guarantee-free zone.
On Wed, Dec 21, 2011 at 10:34:38PM -0800, Paul Phillips said
> I understand why this machinery is in place and that (at least given the
> current encoding) it's necessary to support access which is scala-legal
> but jvm-illegal. But I can't see why it can't be done more selectively,
> with the especially interesting case being the private field which never
> hurt a fly. Separate compilation can hardly be banking on the presence
> of private fields which can't be separately accessed.
I think it can make a difference. Say A was in some library xyz that has
two different versions
Version 1:
trait A {
private[this] val bippy = 5
def dingus = bippy * bippy
}
Version 2:
trait A {
private[this] val bippy = 10
def dingus = bippy * bippy
}
and B was in some app using xyz
and we also had
object C {
def main(args: Array[String]) { println(new B.dingus) }
}
As it stands now, with no recompilation of the app inbetween
% scala -cp xyz-1.jar:app.jar C
25
and
% scala -cp xyz-2.jar:app.jar C
100
> Optimally:
>
> interface A {
> def dingus(): Int
> }
> class B extends A {
> private int bippy = 5
> def dingus = bippy * bippy
> }
Then we would have, with no recompilation of the app inbetween
% scala -cp xyz-1.jar:app.jar C
25
and
% scala -cp xyz-2.jar:app.jar C
25