逆コンパイルで学ぶGroovyの仕組み

  • 18
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

Groovy が裏で何をやっているのかを、 groovyc で生成した class ファイルをデコンパイルして勉強する。

裏の動きを理解すれば、プログラミングの幅も広がる気がするので。

Hello World

スクリプトをコンパイルすると生成されるクラス

script.groovy
println 'Hello Groovy!!'

これを groovyc でコンパイルして class ファイルを生成する。
次に、 Jad を使ってデコンパイルし、 Java ソースコードを生成する。

すると、 こんなファイル が生成される。

ファイル名と同じ名前のクラスが生成されている。

script.java
public class script extends Script
{
...

Hello World はどこに行ったか

Hello World はどこにいったのかというと、 ここ

script.java
    public Object run()
    {
        CallSite acallsite[] = $getCallSiteArray();
        return acallsite[1].callCurrent(this, "Hello Groovy!!");
    }
  • トップレベルに記述したコードは、全てこの run() メソッドに格納されることになる。
  • つまり、意味はないけどこんな実装をすると、
script.groovy
println 'Hello Groovy!!'

def s = new script()
s.run()
Hello Groovy!!
Hello Groovy!!
Hello Groovy!!
Hello Groovy!!
:
:
        at script.run(script.groovy:4)
        at script$run.call(Unknown Source)
        at script.run(script.groovy:4)
        at script$run.call(Unknown Source)
        at script.run(script.groovy:4)
        at script$run.call(Unknown Source)
  • run() メソッドが無限に呼び出されてプログラムが死亡する。

デフォルトのメソッド定義

  • acallsite[1].callCurrent(this, "Hello Groovy!!") このコードのその後を追いかけて行くと、最終的に org.codehaus.groovy.runtime.DefaultGroovyMethods というクラスの println() メソッドにたどり着く。
  • クラスに定義されていないメソッドが呼ばれた際、 Groovy は最終的にこの DefaultGroovyMethods のメソッドを呼ぶようになっている。
DefaultGroovyMethods.println()
    public static void println(Object self, Object value) {
        // we won't get here if we are a PrintWriter
        if (self instanceof Writer) {
            final PrintWriter pw = new GroovyPrintWriter((Writer) self);
            pw.println(value);
        } else {
            System.out.println(InvokerHelper.toString(value));
        }
    }

run() という関数は定義できない

また、 run() という関数を定義すると、以下のようにコンパイルエラーが発生する。

def run() {
    println 'run'
}
実行結果
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:
script.groovy: 1: The method public java.lang.Object run()  { ... } is a duplicate of the one declared for this script's body code
. At [1:1]  @ line 1, column 1.
   def run() {
   ^
  • 独自に宣言した run() 関数と、 Groovy が生成する run() メソッドが重複してしまうため、コンパイルエラーが発生する。

変数の宣言

def a = 1
int b = 2
def c = 100000000000
def d = 1.0
def e = 'string'
def f = true
    public Object run()
    {
        CallSite acallsite[] = $getCallSiteArray();
        Object a = Integer.valueOf(1);
        Object _tmp = a;
        int b = 2;
        int _tmp1 = b;
        Object c = Long.valueOf(0x174876e800L);
        Object _tmp2 = c;
        Object d = new BigDecimal("1.0");
        Object _tmp3 = d;
        Object e = "string";
        Object _tmp4 = e;
        Object f = Boolean.valueOf(true);
        return f;
    }
  • def で定義した変数は、全て Object 型で定義されている。
  • 型を明示した場合は、その型が使用されている。
  • 数値は、値が小さければ Integer 、あふれる場合は適宜 Long が使われている。
  • 実数は BigDecimal が使用される。

関数の定義

func('hoge')

def func(param) {
    println param
}
    public Object run()
    {
        CallSite acallsite[] = $getCallSiteArray();
        return acallsite[1].callCurrent(this, "hoge");
    }

    public Object func(Object param)
    {
        CallSite acallsite[] = $getCallSiteArray();
        return acallsite[2].callCurrent(this, param);
    }

...

    private static void $createCallSiteArray_1(String as[])
    {
        as[0] = "runScript";
        as[1] = "func";
        as[2] = "println";
    }
  • acallsite[1].callCurrent() は、最終的に func() メソッドを呼び出している。
    • acallsite[] には関数や変数を実行・参照するための CallSite オブジェクトが入っている。
    • 配列の各インデックスに、どの関数(変数)が入っているかは、 $createCallSiteArray_1() というメソッドを見ると分かる。
  • トップレベルに定義した関数は、スクリプトのクラス(script)のメソッドとして定義される。

色々な関数の呼び出し方

ということは、こんな書き方もできる。

this.func('this.func()')
new script().func('new script().func()')

def func(param) {
    println param
}
実行結果
this.func()
new script().func()

トップレベルで型付きで宣言した変数は、関数からは参照できない

また、この挙動から以下のような実装は動作しないことが簡単に理解できる。

int i=10

func()

def func() {
    println 'i=' + i
}
    public Object run()
    {
        CallSite acallsite[] = $getCallSiteArray();
        int i = 10;
        int _tmp = i;
        if(__$stMC || BytecodeInterface8.disabledStandardMetaClass())
            return acallsite[1].callCurrent(this);
        else
            return func();
    }

    public Object func()
    {
        CallSite acallsite[] = $getCallSiteArray();
        return acallsite[2].callCurrent(this, acallsite[3].call("i=", acallsite[4].callGroovyObjectGetProperty(this)));
    }
実行結果
Caught: groovy.lang.MissingPropertyException: No such property: i for class: script
groovy.lang.MissingPropertyException: No such property: i for class: script
        at script.func(script.groovy:6)
        at script.run(script.groovy:3)
  • 変数 irun() メソッドのローカル変数なので、 func() メソッドから参照することはできない。
  • そのため、実行時に MissingPropertyException が発生する。
  • JavaScript のクロージャを経験していると、ハマりやすいポイントです。

型を指定せずに宣言した変数

i=10

func()

def func() {
    println 'i=' + i
}
    public Object run()
    {
        CallSite acallsite[] = $getCallSiteArray();
        byte byte0 = 10;
        ScriptBytecodeAdapter.setGroovyObjectProperty(Integer.valueOf(byte0), script, this, "i");
        byte _tmp = byte0;
        if(__$stMC || BytecodeInterface8.disabledStandardMetaClass())
            return acallsite[1].callCurrent(this);
        else
            return func();
    }

    public Object func()
    {
        CallSite acallsite[] = $getCallSiteArray();
        return acallsite[2].callCurrent(this, acallsite[3].call("i=", acallsite[4].callGroovyObjectGetProperty(this)));
    }

...

    private static void $createCallSiteArray_1(String as[])
    {
        as[0] = "runScript";
        as[1] = "func";
        as[2] = "println";
        as[3] = "plus";
        as[4] = "i";
    }
  • def などを使わずにトップレベルで変数を宣言すると、 ScriptBytecodeAdapter.setGroovyObjectProperty() というメソッドを使って値が保存されている。
  • setGroovyObjectProperty() の実装は以下のようになっている。
ScriptBytecodeAdapter.setGroovyObjectProperty()
    public static void setGroovyObjectProperty(Object messageArgument, Class senderClass, GroovyObject receiver, String messageName) throws Throwable {
        try {
            receiver.setProperty(messageName, messageArgument);
        } catch (GroovyRuntimeException gre) {
            throw unwrap(gre);
        }
    }
  • receiver には this、すなわち script クラスのインスタンスが渡されている。
  • つまり、型無しで宣言した変数は、その場所における this が指すオブジェクトのプロパティとして設定されることになる。
  • 関数内ではプロパティから値を取得しているので、トップレベルで宣言された変数に参照できるようになっている。
実験
i = 10

func()

def func() {
    println 'this.i = ' + this.i
}
実行結果
this.i = 10

クラスを宣言する

new Hoge()

def class Hoge {
}
  • 逆コンパイルすると、 Hoge クラスだけを定義したソースファイルが script とは別に出力される。
  • 全文は長いので こちら を参照。
Hoge.java
public class Hoge
    implements GroovyObject
{
...
}
  • script.groovy 内で定義しているが、 script クラスのインナークラスとして出力されるわけではない。

スクリプト内で定義したクラス内では、トップレベルで宣言した型付きの変数を参照できない

def i = 10

new Hoge().method()

def class Hoge {
    def method () {
        println 'i = ' + i
    }
}
実行結果
Caught: groovy.lang.MissingPropertyException: No such property: i for class: Hoge
groovy.lang.MissingPropertyException: No such property: i for class: Hoge
        at Hoge.method(script.groovy:7)
        at Hoge$method.call(Unknown Source)
        at script.run(script.groovy:3)
  • Hoge クラスは別クラスとして定義されるので、当然 script クラスの run() メソッド内に定義されるローカル変数にはアクセスできない。

スクリプト内で定義したクラス内では、トップレベルで宣言した型無しの変数も参照できない

i = 10

new Hoge().method()

def class Hoge {
    def method () {
        println 'i = ' + i
    }
}
実行結果
Caught: groovy.lang.MissingPropertyException: No such property: i for class: Hoge
groovy.lang.MissingPropertyException: No such property: i for class: Hoge
        at Hoge.method(script.groovy:7)
        at Hoge$method.call(Unknown Source)
        at script.run(script.groovy:3)
  • 関数の時は、型なしで定義すればトップレベルの変数を参照できた。
  • しかし、クラスの場合は、それでも参照ができない。
  • 理由は以下の通り。
script.groovy
i = 10 // ←この i は、 script インスタンスのプロパティになる

new Hoge().method()

def class Hoge {
    def method () {
        println 'i = ' + i // ←ここで参照しようとしている i は、 Hoge インスタンスのプロパティを参照しようとしている
    }
}

クロージャ

def closure = { it ->
    println 'it=' + it
}

closure('test')

長いので全体は こちら

一部抜粋
    public Object run()
    {
        CallSite acallsite[] = $getCallSiteArray();
        class _run_closure1 extends Closure
            implements GeneratedClosure
        {

            public Object doCall(Object it)
            {
                CallSite acallsite1[] = $getCallSiteArray();
                return acallsite1[0].callCurrent(this, acallsite1[1].call("it=", it));
            }

            ...

            private static void $createCallSiteArray_1(String as[])
            {
                as[0] = "println";
                as[1] = "plus";
            }

            ...
        }

        Object closure = new _run_closure1(this, this);
        Object _tmp = closure;
        return acallsite[1].call(closure, "test");
    }

...

    private static void $createCallSiteArray_1(String as[])
    {
        as[0] = "runScript";
        as[1] = "call";
    }
  • Closure を継承したローカルなクラスが定義されている。
    • (メソッドの途中でクラスが定義できるなんて、初めて知った...)
  • クロージャの処理は、作成されたクラスの doCall() メソッド内にまとめられている。

クロージャーなら、トップレベルで型付きで宣言した変数を参照できる

int i = 10

def closure = {
    println 'i = ' + i
}
    public Object run()
    {
        CallSite acallsite[] = $getCallSiteArray();
        Reference i = new Reference(Integer.valueOf(10));
        Reference _tmp = i;
        class _run_closure1 extends Closure
            implements GeneratedClosure
        {

            public Object doCall(Object it)
            {
                CallSite acallsite1[] = $getCallSiteArray();
                return acallsite1[0].callCurrent(this, acallsite1[1].call("i = ", i.get()));
            }

            ...
        }

        Object closure = new _run_closure1(this, this, i);
        return closure;
    }
  • トップレベルに型付きで宣言した変数は run() メソッドのローカル変数になるため、トップレベルで定義した関数からは参照できなかった。
  • しかし、クロージャは run() メソッドの内部で定義されるので、トップレベルで型付きで宣言された変数を参照できる。
  • 変数が Reference に格納されているのは、おそらくクロージャ内から値の変更ができるようにするためだと思う。
    • Java でコードを書いてみるとわかるが、ローカルクラス内で、外部で宣言された変数を参照する場合、その変数は final (変更不可)である必要がある。

クロージャ内で参照する this

def closure = {
    println this
}
    public Object run()
    {
        CallSite acallsite[] = $getCallSiteArray();
        class _run_closure1 extends Closure
            implements GeneratedClosure
        {

            public Object doCall(Object it)
            {
                CallSite acallsite1[] = $getCallSiteArray();
                return acallsite1[0].callCurrent(this, getThisObject());
            }

            ...
        }

        Object closure = new _run_closure1(this, this);
        return closure;
    }
  • this の参照は、 getThisObject() というメソッドに置き換えられている。
  • getThisObject() は、親クラスの Closure クラスに定義されている。
Closure
    private Object delegate;
    private Object owner;
    private Object thisObject;
    ...

    public Closure(Object owner, Object thisObject) {
        this.owner = owner;
        this.delegate = owner;
        this.thisObject = thisObject;

        ...
    }

    ...

    public Object getThisObject(){
        return thisObject;
    }
  • thisObject は、コンストラクタの第二引数で渡されたオブジェクトが設定されている。
  • クロージャのコンストラクタの第二引数には、その場所における this オブジェクトが渡される。
  • つまり、クロージャ内で参照する this は、クロージャを定義した場所で this を参照したときのモノと同じになる。
def closure = {
    println 'this.class=' + this.class
}

def class Hoge {

    def method(closure) {
        closure()
    }
}

new Hoge().method(closure);
実行結果
this.class=class script
  • クロージャ自体は Hoge クラス内部で実行されている。
  • しかし、クロージャ内で参照した this は、クロージャが宣言された場所における this、すなわち、 script クラスのインスタンスを参照している。

演算子オーバーロード

'left' << 'shift'
'plus' + 'plus'
'index'[1]
    public Object run()
    {
        CallSite acallsite[] = $getCallSiteArray();
        acallsite[1].call("left", "shift");
        acallsite[2].call("plus", "plus");
        return acallsite[3].call("index", Integer.valueOf(1));
    }

...

    private static void $createCallSiteArray_1(String as[])
    {
        as[0] = "runScript";
        as[1] = "leftShift";
        as[2] = "plus";
        as[3] = "getAt";
    }
  • それぞれ、 CallSite を使ったメソッドの呼び出しに置き換えられている。

真偽値の判定

boolean b = 'truth'
    public Object run()
    {
        CallSite acallsite[] = $getCallSiteArray();
        boolean b = DefaultTypeTransformation.booleanUnbox("truth");
        return Boolean.valueOf(b);
    }
  • Groovy は、型が boolean でなくても、その値から良しなに boolean に読み替えて真偽を判定してくれる。
  • その仕組は、 DefaultTypeTransformation.booleanUnbox() によって実行されている。
DefaultTypeTransformation
    public static boolean booleanUnbox(Object value) {
        return castToBoolean(value);
    }

...

    public static boolean castToBoolean(Object object) {
        // null is always false
        if (object == null) {
            return false;
        }

        // equality check is enough and faster than instanceof check, no need to check superclasses since Boolean is final
        if (object.getClass() == Boolean.class) {   
            return ((Boolean)object).booleanValue();
        }

        // if the object is not null and no Boolean, try to call an asBoolean() method on the object
        return (Boolean)InvokerHelper.invokeMethod(object, "asBoolean", InvokerHelper.EMPTY_ARGS);
    }
  • null なら常に false
  • Boolean 型なら、単純にその boolean 値。
  • それ以外の場合は、対象オブジェクトの asBoolean() メソッドを実行して、その結果を返している。
  • デフォルトの asBoolean() メソッドは、 DefaultGroovyMethods クラスに定義されている。

Groovy 独自の演算子

Spread Operator

'abcd'*.getAt(0)
    public Object run()
    {
        CallSite acallsite[] = $getCallSiteArray();
        return ScriptBytecodeAdapter.invokeMethodNSpreadSafe(script, "abcd", "getAt", new Object[] {
            Integer.valueOf(0)
        });
    }
  • ScriptBytecodeAdapter.invokeMethodNSpreadSafe() というメソッドに置き換えられている。
  • 当該メソッドを見に行くと、以下のような実装になっている。
ScriptBytecodeAdapter.invokeMethodNSpreadSafe()
    public static Object invokeMethodNSpreadSafe(Class senderClass, Object receiver, String messageName, Object[] messageArguments) throws Throwable {
        if (receiver == null) return null;
        List answer = new ArrayList();
        for (Iterator it = InvokerHelper.asIterator(receiver); it.hasNext();) {
            answer.add(invokeMethodNSafe(senderClass, it.next(), messageName, messageArguments));
        }
        return answer;
    }
  • receiver (ここでは、文字列 "abcd")を Iterator に変換し、それぞれの要素に対してメソッドを実行している(invokeMethodNSafe())。
  • 結果は ArrayList に格納し、返却している。

Elvis Operator

'Elvis' ?: 'Operator'
    public Object run()
    {
        CallSite acallsite[] = $getCallSiteArray();
        String s;
        return DefaultTypeTransformation.booleanUnbox(s = "Elvis") ? s : "Operator";
    }
  • これは単純で、そのまま三項演算子に置き換えられている。

Safe Navigation Operator

'str'?.a?.b
    public Object run()
    {
        CallSite acallsite[] = $getCallSiteArray();
        return acallsite[1].callGetPropertySafe(acallsite[2].callGetPropertySafe("str"));
    }

...

    private static void $createCallSiteArray_1(String as[])
    {
        as[0] = "runScript";
        as[1] = "b";
        as[2] = "a";
    }
  • CallSitecallGetPropertySafe() メソッドを呼び出している。
  • CallSite 自体はインターフェースで、実装は org.codehaus.groovy.runtime.callsite.AbstractCallSite にある。
AbstractCallSite.callGetPropertySafe()
    public final Object callGetPropertySafe(Object receiver) throws Throwable {
        if (receiver == null)
            return null;
        else
            return callGetProperty(receiver);
    }
  • receiver(プロパティを取得しようとしているオブジェクト)が null の場合は、そのまま null を返し、そうでない場合にだけプロパティの取得を試みている。

比較演算子

'abc' == 'def'
    public Object run()
    {
        CallSite acallsite[] = $getCallSiteArray();
        if(!BytecodeInterface8.isOrigZ() || __$stMC || BytecodeInterface8.disabledStandardMetaClass())
            return Boolean.valueOf(ScriptBytecodeAdapter.compareEqual("abc", "def"));
        else
            return Boolean.valueOf(ScriptBytecodeAdapter.compareEqual("abc", "def"));
    }
  • == の比較は、 ScriptBytecodeAdapter.compareEqual() に置き換えられる。
  • この実装を辿って行くと org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformationcompareEqual() メソッドに行き着く。
DefaultTypeTransformation.compareEqual()
    public static boolean compareEqual(Object left, Object right) {
        if (left == right) return true;
        if (left == null || right == null) return false;
        if (left instanceof Comparable) {
            return compareToWithEqualityCheck(left, right, true) == 0;
        }
        // handle arrays on both sides as special case for efficiency
        Class leftClass = left.getClass();
        Class rightClass = right.getClass();
        if (leftClass.isArray() && rightClass.isArray()) {
            return compareArrayEqual(left, right);
        }
        if (leftClass.isArray() && leftClass.getComponentType().isPrimitive()) {
            left = primitiveArrayToList(left);
        }
        if (rightClass.isArray() && rightClass.getComponentType().isPrimitive()) {
            right = primitiveArrayToList(right);
        }
        if (left instanceof Object[] && right instanceof List) {
            return DefaultGroovyMethods.equals((Object[]) left, (List) right);
        }
        if (left instanceof List && right instanceof Object[]) {
            return DefaultGroovyMethods.equals((List) left, (Object[]) right);
        }
        if (left instanceof List && right instanceof List) {
            return DefaultGroovyMethods.equals((List) left, (List) right);
        }
        if (left instanceof Map.Entry && right instanceof Map.Entry) {
            Object k1 = ((Map.Entry)left).getKey();
            Object k2 = ((Map.Entry)right).getKey();
            if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                Object v1 = ((Map.Entry)left).getValue();
                Object v2 = ((Map.Entry)right).getValue();
                if (v1 == v2 || (v1 != null && DefaultTypeTransformation.compareEqual(v1, v2)))
                    return true;
            }
            return false;
        }
        return ((Boolean) InvokerHelper.invokeMethod(left, "equals", right)).booleanValue();
    }
  • 比較する値が配列か、List か Map かなどを調べて、比較の仕方を切り替えている。
  • 単純に equals() メソッドを呼んでいるのではなく、結構複雑な処理をしている。

GString

def str = "string"

"str = ${str}"
    public Object run()
    {
        CallSite acallsite[] = $getCallSiteArray();
        Object str = "string";
        Object _tmp = str;
        return new GStringImpl(new Object[] {
            str
        }, new String[] {
            "str = ", ""
        });
    }
  • 単純にダブルクォーテーションで括っただけの文字列は、普通に文字列リテラルとして置き換えられる。
  • ${...} の置換対象文字列があれば、 GStringImpl のインスタンスが生成される。
    • (ダブルクォーテーション文字列だと常に GString のインスタンスが生成されて、シングルクォーテーションの文字列よりコストが大きいのかなと思っていた)
    • (でも、 GString にすべきかどうか判断する必要があるから、やっぱりシングルクォーテーション文字列の方がコストは小さい?)
    • (GString にすべきかどうかの判定はコンパイル時に行うから、プリコンパイルしてから使うのであれば、どちらも一緒か)

ヒアドキュメント

def str = '''
          hear
          document
          '''
    public Object run()
    {
        CallSite acallsite[] = $getCallSiteArray();
        Object str = "\n          hear\n          document\n          ";
        return str;
    }
  • 単純に、改行コード入りの文字列に置き換えられている。

参考