2
2

More than 5 years have passed since last update.

Dexファイルにinvoke-polymorphicとinvoke-customが現れることを確認する

Last updated at Posted at 2017-03-23

Dexファイルフォーマットのバージョン038で、invoke-polymorphicinvoke-customが追加されました。

Note: Support for version 038 of the format was added in the Android 8.0 release. Version 038 added new bytecodes (invoke-polymorphic and invoke-custom) and data for method handles.

これらがDexファイルに現れることを確認します。

以下に、コンパイル対象のコードを示します。

app/src/main/java/com/example/myapplication/Main.java
package com.example.myapplication;

import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.function.BiFunction;

public class Main {

    public static void main(String[] args) throws Throwable {
        System.out.println(MethodHandles.lookup()
                                   .findStatic(Main.class,
                                               "add",
                                               MethodType.methodType(int.class, int.class, int.class))
                                   .invoke(2, 3));
    }

    static int add(int a, int b) {
        BiFunction<Integer, Integer, Integer> f = (i0, i1) -> i0 + i1;
        return f.apply(a, b);
    }

}

Dexファイルがどうなるかを確認するためだけのコードです。
Android上で動作させることは考えていません。

build.gradleはそれぞれ以下のとおりです。

build.gradle
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.4.0-alpha3'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
app/build.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion 'android-O'
    buildToolsVersion '26.0.0-rc1'
    defaultConfig {
        jackOptions {
            enabled = true
        }
        applicationId "com.example.myapplication"
        minSdkVersion 'O'
        versionCode 1
        versionName '1.0'
    }
    compileOptions {
        targetCompatibility JavaVersion.VERSION_1_8
        sourceCompatibility JavaVersion.VERSION_1_8
    }
}

compileSdkVersionandroid-Oに、minSdkVersionOにしています。
また、ラムダ式を使用しているので、Jackを有効にしています。

ビルドします。

$ ./gradlew clean assembleDebug
:clean
:app:clean
:app:preBuild UP-TO-DATE
:app:resolveDebugDependencies
:app:preDebugBuild
:app:compileDebugNdk NO-SOURCE
:app:checkDebugManifest
:app:prepareDebugDependencies
:app:compileDebugAidl
:app:compileDebugRenderscript
:app:generateDebugBuildConfig
:app:generateDebugResValues
:app:handleDebugMicroApk
:app:generateDebugResources
:app:mergeDebugResources
:app:processDebugManifest
:app:processDebugResources
:app:generateDebugSources
:app:transformClassesWithPreJackPackagedLibrariesForDebug
:app:transformClassesWithPreJackRuntimeLibrariesForDebug
:app:processDebugJavaRes NO-SOURCE
:app:transformResourcesWithMergeJavaResForDebug
:app:transformJackAndJavaSourcesWithJackCompileForDebug
/home/tmura/AndroidStudioProjects/MyApplication2/app/src/main/java/com/example/myapplication/Main.java:10:28-14:48: Call to a method with a polymorphic signature requires at least an android provisional level o-b1
:app:transformJackAndJavaSourcesWithJackCompileForDebug FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:transformJackAndJavaSourcesWithJackCompileForDebug'.
> com.android.build.api.transform.TransformException: com.android.builder.core.JackToolchain$ToolchainException: Jack compilation exception

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

Total time: 3.683 secs

Call to a method with a polymorphic signature requires at least an android provisional level o-b1とのことで、レベルを設定する必要がありそうです。

jack.jarのオプションを確認します。

$ java -jar $ANDROID_HOME/build-tools/26.0.0-rc1/jack.jar --help-properties

...

jack.android.min-api-level:
     Minimum Android API level compatibility (default is '1')
     a released API level as an integer belonging to [1 .. 9223372036854775807] or a provisional API level as {o-b1,o-b2} (case insensitive)
          o-b1: Dex file 0x37 with invoke-poly
          o-b2: Dex file 0x38 with invoke-{poly,custom}

...

jack.android.min-api-levelというプロパティにo-b1またはo-b2を指定すれば良さそうです。

app/build.gradleにて、上記オプションを設定します。

app/build.gradle
android {
    defaultConfig {
        jackOptions {
            enabled = true
            additionalParameters 'jack.android.min-api-level': 'o-b1'
        }
    }
}

まずは、o-b1を試します。

ビルドします。

$ ./gradlew clean assembleDebug
:clean
:app:clean
:app:preBuild UP-TO-DATE
:app:resolveDebugDependencies
:app:preDebugBuild
:app:compileDebugNdk NO-SOURCE
:app:checkDebugManifest
:app:prepareDebugDependencies
:app:compileDebugAidl
:app:compileDebugRenderscript
:app:generateDebugBuildConfig
:app:generateDebugResValues
:app:handleDebugMicroApk
:app:generateDebugResources
:app:mergeDebugResources
:app:processDebugManifest
:app:processDebugResources
:app:generateDebugSources
:app:transformClassesWithPreJackPackagedLibrariesForDebug
:app:transformClassesWithPreJackRuntimeLibrariesForDebug
:app:processDebugJavaRes NO-SOURCE
:app:transformResourcesWithMergeJavaResForDebug
:app:transformJackAndJavaSourcesWithJackCompileForDebug
:app:compileDebugSources
:app:mergeDebugShaders
:app:compileDebugShaders
:app:generateDebugAssets
:app:mergeDebugAssets
:app:transformJackWithJackDexerForDebug
:app:mergeDebugJniLibFolders
:app:transformNativeLibsWithMergeJniLibsForDebug
:app:validateSigningDebug
:app:packageDebug
:app:assembleDebug

BUILD SUCCESSFUL

Total time: 4.636 secs

dexdumpします。

$ $ANDROID_HOME/build-tools/26.0.0-rc1/dexdump -d -h app/build/intermediates/transforms/jackDexer/debug/folders/1000/1f/main/classes.dex
Processing 'app/build/intermediates/transforms/jackDexer/debug/folders/1000/1f/main/classes.dex'...
Opened 'app/build/intermediates/transforms/jackDexer/debug/folders/1000/1f/main/classes.dex', DEX version '037'

...

000634:                                        |[000634] com.example.myapplication.Main.main:([Ljava/lang/String;)V
000644: 6200 0a00                              |0000: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream; // field@000a
000648: 7100 1400 0000                         |0002: invoke-static {}, Ljava/lang/invoke/MethodHandles;.lookup:()Ljava/lang/invoke/MethodHandles$Lookup; // method@0014
00064e: 0c01                                   |0005: move-result-object v1
000650: 1c02 0300                              |0006: const-class v2, Lcom/example/myapplication/Main; // type@0003
000654: 1b03 3900 0000                         |0008: const-string/jumbo v3, "add" // string@00000039
00065a: 6204 0900                              |000b: sget-object v4, Ljava/lang/Integer;.TYPE:Ljava/lang/Class; // field@0009
00065e: 6205 0900                              |000d: sget-object v5, Ljava/lang/Integer;.TYPE:Ljava/lang/Class; // field@0009
000662: 1216                                   |000f: const/4 v6, #int 1 // #1
000664: 2366 1b00                              |0010: new-array v6, v6, [Ljava/lang/Class; // type@001b
000668: 6207 0900                              |0012: sget-object v7, Ljava/lang/Integer;.TYPE:Ljava/lang/Class; // field@0009
00066c: 1208                                   |0014: const/4 v8, #int 0 // #0
00066e: 4d07 0608                              |0015: aput-object v7, v6, v8
000672: 7130 1500 5406                         |0017: invoke-static {v4, v5, v6}, Ljava/lang/invoke/MethodType;.methodType:(Ljava/lang/Class;Ljava/lang/Class;[Ljava/lang/Class;)Ljava/lang/invoke/MethodType; // method@0015
000678: 0c04                                   |001a: move-result-object v4
00067a: 6e40 1300 2143                         |001b: invoke-virtual {v1, v2, v3, v4}, Ljava/lang/invoke/MethodHandles$Lookup;.findStatic:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle; // method@0013
000680: 0c01                                   |001e: move-result-object v1
000682: 1222                                   |001f: const/4 v2, #int 2 // #2
000684: 1233                                   |0020: const/4 v3, #int 3 // #3
000686: fa30 1200 2103 0400                    |0021: invoke-polymorphic  {v1, v2, v3}, Ljava/lang/invoke/MethodHandle;.invoke:([Ljava/lang/Object;)Ljava/lang/Object;, (II)Ljava/lang/Object; // method@0012, proto@0004
00068e: 0c01                                   |0025: move-result-object v1
000690: 6e20 0d00 1000                         |0026: invoke-virtual {v0, v1}, Ljava/io/PrintStream;.println:(Ljava/lang/Object;)V // method@000d
000696: 0e00                                   |0029: return-void

...

Dexファイルのバージョンは037です。
MethodHandler#invoke()の箇所(下から4行目)でinvoke-polymorphicが出現しています。
上記で確認したオプションの説明通りです。

次に、o-b2を試します。

app/build.gradle
android {
    defaultConfig {
        jackOptions {
            enabled = true
            additionalParameters 'jack.android.min-api-level': 'o-b2'
        }
    }
}

ビルドして、確認してみます。

$ ./gradlew clean assembleDebug --quiet
$ $ANDROID_HOME/build-tools/26.0.0-rc1/dexdump -d -h app/build/intermediates/transforms/jackDexer/debug/folders/1000/1f/main/classes.dex
Processing 'app/build/intermediates/transforms/jackDexer/debug/folders/1000/1f/main/classes.dex'...
Opened 'app/build/intermediates/transforms/jackDexer/debug/folders/1000/1f/main/classes.dex', DEX version '038'

...

000634:                                        |[000634] com.example.myapplication.Main.main:([Ljava/lang/String;)V
000644: 6200 0a00                              |0000: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream; // field@000a
000648: 7100 1400 0000                         |0002: invoke-static {}, Ljava/lang/invoke/MethodHandles;.lookup:()Ljava/lang/invoke/MethodHandles$Lookup; // method@0014
00064e: 0c01                                   |0005: move-result-object v1
000650: 1c02 0300                              |0006: const-class v2, Lcom/example/myapplication/Main; // type@0003
000654: 1b03 3900 0000                         |0008: const-string/jumbo v3, "add" // string@00000039
00065a: 6204 0900                              |000b: sget-object v4, Ljava/lang/Integer;.TYPE:Ljava/lang/Class; // field@0009
00065e: 6205 0900                              |000d: sget-object v5, Ljava/lang/Integer;.TYPE:Ljava/lang/Class; // field@0009
000662: 1216                                   |000f: const/4 v6, #int 1 // #1
000664: 2366 1b00                              |0010: new-array v6, v6, [Ljava/lang/Class; // type@001b
000668: 6207 0900                              |0012: sget-object v7, Ljava/lang/Integer;.TYPE:Ljava/lang/Class; // field@0009
00066c: 1208                                   |0014: const/4 v8, #int 0 // #0
00066e: 4d07 0608                              |0015: aput-object v7, v6, v8
000672: 7130 1500 5406                         |0017: invoke-static {v4, v5, v6}, Ljava/lang/invoke/MethodType;.methodType:(Ljava/lang/Class;Ljava/lang/Class;[Ljava/lang/Class;)Ljava/lang/invoke/MethodType; // method@0015
000678: 0c04                                   |001a: move-result-object v4
00067a: 6e40 1300 2143                         |001b: invoke-virtual {v1, v2, v3, v4}, Ljava/lang/invoke/MethodHandles$Lookup;.findStatic:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle; // method@0013
000680: 0c01                                   |001e: move-result-object v1
000682: 1222                                   |001f: const/4 v2, #int 2 // #2
000684: 1233                                   |0020: const/4 v3, #int 3 // #3
000686: fa30 1200 2103 0400                    |0021: invoke-polymorphic  {v1, v2, v3}, Ljava/lang/invoke/MethodHandle;.invoke:([Ljava/lang/Object;)Ljava/lang/Object;, (II)Ljava/lang/Object; // method@0012, proto@0004
00068e: 0c01                                   |0025: move-result-object v1
000690: 6e20 0d00 1000                         |0026: invoke-virtual {v0, v1}, Ljava/io/PrintStream;.println:(Ljava/lang/Object;)V // method@000d
000696: 0e00                                   |0029: return-void

...

Dexファイルのバージョンは038です。
下から4行目にinvoke-polymorphicが出現しているのはオプションの説明通りですが、一方、invoke-customは見つかりませんでした。

$ $ANDROID_HOME/build-tools/26.0.0-rc1/dexdump -d -h app/build/intermediates/transforms/jackDexer/debug/folders/1000/1f/main/classes.dex \
> | grep -i invoke-custom | wc -l
0

JavaのクラスファイルではCallSiteが現れているので、invoke-customが出現するかと思いましたが、そうはなっていませんでした。

$ ./gradlew clean compileDebugJavaWithJavac --quiet
$ javap -v -private app/build/intermediates/classes/debug/com/example/myapplication/Main.class

...

  static int add(int, int);
    descriptor: (II)I
    flags: ACC_STATIC
    Code:
      stack=3, locals=3, args_size=2
         0: invokedynamic #12,  0             // InvokeDynamic #0:apply:()Ljava/util/function/BiFunction;
         5: astore_2
         6: aload_2
         7: iload_0
         8: invokestatic  #13                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        11: iload_1
        12: invokestatic  #13                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        15: invokeinterface #14,  3           // InterfaceMethod java/util/function/BiFunction.apply:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
        20: checkcast     #15                 // class java/lang/Integer
        23: invokevirtual #16                 // Method java/lang/Integer.intValue:()I
        26: ireturn
      LineNumberTable:
        line 18: 0
        line 19: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      27     0     a   I
            0      27     1     b   I
            6      21     2     f   Ljava/util/function/BiFunction;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            6      21     2     f   Ljava/util/function/BiFunction<Ljava/lang/Integer;Ljava/lang/Integer;Ljava/lang/Integer;>;

...

SourceFile: "Main.java"
InnerClasses:
     public static final #81= #57 of #50; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #64 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #65 (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
      #66 invokestatic com/example/myapplication/Main.lambda$add$0:(Ljava/lang/Integer;Ljava/lang/Integer;)Ljava/lang/Integer;
      #67 (Ljava/lang/Integer;Ljava/lang/Integer;)Ljava/lang/Integer;

JackのRopBuilderVisitor.javaを見ると、メソッド呼び出しがinvoke-customかどうかをInvokeCustomHelper#isInvokeCustom(JAbstractMethodCall)で判断しています。
InvokeCustomHelper.javaを見ると、メソッドにCalledByInvokeCustomアノテーションが付与されていれば、invoke-customと判断しているようです。

jack/src/com/android/jack/backend/dex/invokecustom/InvokeCustomHelper.java
public static boolean isInvokeCustom(@Nonnull JAbstractMethodCall call) {
  Collection<JMethod> methods = call.getMethodIdWide().getMethods();
  Iterator<JMethod> methodsIt = methods.iterator();
  boolean isInvokeCustom = false;
  while (methodsIt.hasNext()) {
    JMethod method = methodsIt.next();
    if (getInvokeCustomCallsite(method) != null) {
      isInvokeCustom = true;
      break;
    }
  }

  assert !isInvokeCustom || methods.size() == 1;

  return isInvokeCustom;
}

@CheckForNull
public static JAnnotation getInvokeCustomCallsite(@Nonnull JMethod method) {
  for (JAnnotation annotation : method.getAnnotations()) {
    if (annotation.getType().getName().equals("CalledByInvokeCustom")) {
      return annotation;
    }
  }
  return null;
}

CalledByInvokeCustomjack-testsディレクトリ配下にあるので、これを使ってみます。

$ git clone https://android.googlesource.com/toolchain/jack
$ (cd jack; git checkout -b ub-jack origin/ub-jack)
$ (cd jack/jack-tests/src; cp -p --parent com/android/jack/annotations/*.java ../../../app/src/main/java/)

さらに、Main.javaを以下のように書きかえます。

app/src/main/java/com/example/myapplication/Main.java
package com.example.myapplication;

import com.android.jack.annotations.CalledByInvokeCustom;
import com.android.jack.annotations.LinkerMethodHandle;
import com.android.jack.annotations.MethodHandleKind;

import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.function.BiFunction;

public class Main {

    public static void main(String[] args) throws Throwable {
        System.out.println(MethodHandles.lookup()
                                   .findStatic(Main.class,
                                               "add",
                                               MethodType.methodType(int.class, int.class, int.class))
                                   .invoke(2, 3));
    }

    static int add(int a, int b) {
        BiFunction<Integer, Integer, Integer> f = new BiFunction<Integer, Integer, Integer>() {
            @Override
            public Integer apply(Integer integer, Integer integer2) {
                return add0(a, b);
            }
        };
        return f.apply(a, b);
    }

    @CalledByInvokeCustom(
            invokeMethodHandle = @LinkerMethodHandle(kind = MethodHandleKind.INVOKE_STATIC,
                                                     enclosingType = Main.class,
                                                     name = "linkerMethod",
                                                     argumentTypes = {MethodHandles.Lookup.class, String.class, MethodType.class}),
            name = "add0",
            returnType = Integer.class,
            argumentTypes = {Integer.class, Integer.class})
    static Integer add0(Integer a, Integer b) {
        return a + b;
    }

    private static CallSite linkerMethod(MethodHandles.Lookup caller, String name, MethodType methodType)
            throws NoSuchMethodException, IllegalAccessException {
        return new ConstantCallSite(caller.findStatic(caller.lookupClass(), name, methodType));
    }

}

ビルドして、確認してみます。

$ ./gradlew clean assembleDebug --quiet
$ $ANDROID_HOME/build-tools/26.0.0-rc1/dexdump -d -h app/build/intermediates/transforms/jackDexer/debug/folders/1000/1f/main/classes.dex
Processing 'app/build/intermediates/transforms/jackDexer/debug/folders/1000/1f/main/classes.dex'...
Opened 'app/build/intermediates/transforms/jackDexer/debug/folders/1000/1f/main/classes.dex', DEX version '038'

...

000c18:                                        |[000c18] com.example.myapplication.Main.1.apply:(Ljava/lang/Integer;Ljava/lang/Integer;)Ljava/lang/Integer;
000c28: 5220 0e00                              |0000: iget v0, v2, Lcom/example/myapplication/Main$1;.val$a:I // field@000e
000c2c: 7110 3000 0000                         |0002: invoke-static {v0}, Ljava/lang/Integer;.valueOf:(I)Ljava/lang/Integer; // method@0030
000c32: 0c00                                   |0005: move-result-object v0
000c34: 5221 0f00                              |0006: iget v1, v2, Lcom/example/myapplication/Main$1;.val$b:I // field@000f
000c38: 7110 3000 0100                         |0008: invoke-static {v1}, Ljava/lang/Integer;.valueOf:(I)Ljava/lang/Integer; // method@0030
000c3e: 0c01                                   |000b: move-result-object v1
000c40: fc20 0000 1000                         |000c: invoke-custom {v0, v1}, call_site@0000
000c46: 0c00                                   |000f: move-result-object v0
000c48: 1100                                   |0010: return-object v0

...

invoke-customが現れました(下から3行目)。

将来的にはラムダ式の呼び出しはinvoke-customになるのでしょうか?
Jackは非推奨になるらしいので、dx側でinvokedynamicinvoke-customに変換するようになるのかもしれません。

ちなみに、jack.jarにはjack.lambda.anonymousというプロパティがあるので

jack.lambda.anonymous:
     Enable lambda support with an anonymous class (default is 'true')
     {true,yes,on,1,false,no,off,0} (case insensitive)

これをfalseにしてみたのですが、残念ながらエラーになってしまいました。

app/build.gradle
android {
    defaultConfig {
        jackOptions {
            enabled = true
            additionalParameters 'jack.android.min-api-level': 'o-b2'
            additionalParameters 'jack.lambda.anonymous': 'false'
        }
    }
}
$ ./gradlew clean assembleDebug --stacktrace

...

:app:transformJackAndJavaSourcesWithJackCompileForDebug FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:transformJackAndJavaSourcesWithJackCompileForDebug'.
> com.android.sched.scheduler.RunnerProcessException: Error during 'CodeItemBuilder' runner on 'static int com.example.myapplication.Main.add(int a, int b) (/home/tmura/AndroidStudioProjects/MyApplication2/app/src/main/java/com/example/myapplication/Main.java:17.5-20.5)': Unexpected error during visit: com.android.jack.ir.ast.JLambda at "/home/tmura/AndroidStudioProjects/MyApplication2/app/src/main/java/com/example/myapplication/Main.java:18.51-18.69"

* Try:
Run with --info or --debug option to get more log output.

* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':app:transformJackAndJavaSourcesWithJackCompileForDebug'.

...

Caused by: java.lang.AssertionError
        at com.android.jack.backend.dex.rop.RopBuilderVisitor$AssignBuilderVisitor.visit(RopBuilderVisitor.java:224)
        at com.android.jack.ir.ast.JLambda.traverse(JLambda.java:108)
        at com.android.jack.ir.ast.JVisitor.accept(JVisitor.java:51)
        ... 10 more

エラー元を見ると、無条件でthrow new AssertionError()しているので、現状このオプションをfalseにすることはできないようです。

以上です。

参考

Dalvik bytecode
https://source.android.com/devices/tech/dalvik/dalvik-bytecode.html

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2