言いたいことはそれだけか

KotlinとかAndroidとかが好きです。調べたことをメモします。٩( 'ω' )و

Kotlin 1.1.2-5くらいからSAM変換がちょっと変わってcrashが起きた件

Kotlinのバージョンあげたら既存コードがcrashしたのでちょっと調べてみたメモ。

何が起こったのか

kotlin のversionを1.0.5-3から1.1.2-5にあげたら既存コードがcrashするようになった。具体的には、こんな感じのIllegalStateExceptionが吐かれる。

java.lang.IllegalStateException: invoke(...) must not be null

何が悪かったか

Kotlin側に渡すJavaで定義したFunction0のオブジェクトでinvoke()をoverrideした際にreturn nullしていたのが悪かった。Javaでいうvoidに当たるUnitのインスタンスをreturnすることで落ちなくなる。

        new SamConversionExperimentalKt().invokeFunctionFromKotlin(new Function0<Unit>() {
            @Override
            public Unit invoke() {
                doSomething();
                return Unit.INSTANCE; // return nullするとcrashになるよ
            }
        });

Android Studio(v2.3)だとinvoke()メソッドを補完するときにreturn nullで補完してくれるからいちいち手で書き換えないといけない_:(´ཀ`」 ∠):
Javaでいうvoidはkotlinのnull許容・非許容と離れた概念だからどっち返しても大丈夫でしょ、とか思ってたら思わぬところで痛い目をみる。

で、問題はなんだったの?

実はこのcrashが発生するのにいくつか条件があって、

  1. JavaでFunctionN系のinvoke()メソッドをreturn nullしてoverrideする
  2. 1で定義したインスタンスをkotlinのコードに渡す
  3. 2のkotlinのコードからさらにjavaのコードに渡して、invoke()ではなくSAM変換後のメソッドとして実行する

3がちょっとなんて言ったらわからないのでコードを交えて説明。

適当なAndroidのコードで試したものです。まずはFunction0型のインスタンスをkotlinのコードに渡します。

public class SamConversionExperimental {

    public void invokeFunction() {
        new SamConversionExperimentalKt().invokeFunctionFromKotlin(new Function0<Unit>() {
            @Override
            public Unit invoke() {
                doSomething();
                return Unit.INSTANCE; // return nullするとcrashになるよ
            }
        });
    }

    private void doSomething() {

    }
}

その後、受け取ったkotlinコード側でSAM変換がかかるような感じで実行してやればcrash. 手元だとRunnable#run() が走るようにしてcrashさせた。
(Runnableは単一メソッドrun()だけを持つインターフェイス

    fun invokeFunctionFromKotlin(function: () -> Unit) =
        Handler(Looper.getMainLooper()).post(function)

SAM変換変わったね?

エラー文から察するに、この辺りのチェックコードがversion上がって入ってきたっぽい。 github.com

    public static void checkExpressionValueIsNotNull(Object value, String expression) {
        if (value == null) {
            throw sanitizeStackTrace(new IllegalStateException(expression + " must not be null"));
        }
    }

で、このメソッドがどこから呼ばれているかというと、この辺り。

kotlin/RedundantNullCheckMethodTransformer.kt at a5620454fa2fef926b4ca35b95fdb46a44506211 · JetBrains/kotlin · GitHub

        private fun analyzeTypesAndRemoveDeadCode(): Map<AbstractInsnNode, Type> {
            val insns = methodNode.instructions.toArray()
            val frames = analyze(internalClassName, methodNode, OptimizationBasicInterpreter())

            val checkedReferenceTypes = HashMap<AbstractInsnNode, Type>()
            for (i in insns.indices) {
                val insn = insns[i]
                val frame = frames[i]
                if (insn.isInstanceOfOrNullCheck()) {
                    checkedReferenceTypes[insn] = frame?.top()?.type ?: continue
                }
                else if (insn.isCheckParameterIsNotNull() || insn.isCheckExpressionValueIsNotNull()) { // ここにチェックが入っている
                    checkedReferenceTypes[insn] = frame?.peek(1)?.type ?: continue
                }
            }

            val dceResult = DeadCodeEliminationMethodTransformer().removeDeadCodeByFrames(methodNode, frames)
            if (dceResult.hasRemovedAnything()) {
                changes = true
            }

            return checkedReferenceTypes
        }

なんかこの辺りのcommitでcheck増えてそうな気配を感じるのでこれかなぁ

github.com