以下の StringUtil.printHello
がKotlinの拡張関数の仲間になりたそうにこちらを見ています。あなたは仲間にしてあげられますか?
// StringUtil.java public final class StringUtil { public static void printHello(final String target) { System.out.println("Hello, " + target); } }
本記事は以下のAdvent Calendarの記事です。
- 🎄 エムスリー Advent Calendar 2021 | 6日目
- 🎄 Kotlin Advent Calendar 2021 | 6日目
※ 12/6現在まだ空きがあるのでぜひご投稿を!
こんにちは、星川 (@oboenikui) です。皆さんはKotlinで書かれたコードとJavaで書かれたコードの見分けがつくでしょうか?
👩💻「変数の宣言で左側にvalが使われていたらKotlin、型名が使われていたらJavaでしょ?」
👨💻「メソッドの定義でfunが使われていたらほぼ間違いなくKotlinだよ」
︙
そうですね、色々見分けるポイントがあります。
では、コンパイル済みのclassファイルやjarファイルではどうでしょう?
本記事では、KotlinコンパイラがどのようにしてKotlinとJavaでコンパイルされたコードを見分けているのか理解し、冒頭の printHello
メソッドをKotlinに書き換えることなく、以下のKotlinコードとほぼ等価なトップレベル拡張関数として認識させてみます。
// StringExt.kt fun String.printHello() { println("Hello, $this") }
※ 実際の利用が推奨されるものではありません。
デコンパイルして仕様を知る
上記の StringExt.kt
をIntelliJ IDEAの機能でShow Kotlin Bytecode → Decompileしてみましょう。すると以下のようなJavaコードを得られます。
@Metadata( mv = {1, 6, 0}, k = 2, d1 = {"\u0000\f\n\u0000\n\u0002\u0010\u0002\n\u0002\u0010\u000e\n\u0000\u001a\n\u0010\u0000\u001a\u00020\u0001*\u00020\u0002¨\u0006\u0003"}, d2 = {"printHello", "", "", "kotlin-lib"} ) public final class StringExtKt { public static final void printHello(@NotNull String $this$printHello) { Intrinsics.checkNotNullParameter($this$printHello, "$this$printHello"); String var1 = "Hello, " + $this$printHello; System.out.println(var1); } }
ここからわかることについて以下で見ていきます。
トップレベル関数・拡張関数
JavaとKotlinを併用した経験のある方は目にしたことがあるのではないかと思いますが、Kotlinのトップレベル関数は (Kotlinのファイル名)Kt
という名前のクラスのstaticメソッドとしてコンパイルされます。また、拡張関数はレシーバークラスを第一引数に取るメソッドとなります。
ただし、Ktをつけたら即トップレベル関数として認識されるかというとそうではありません。次に@Metadataアノテーションについて見ていきます。
Metadata
Kotlinからコンパイルされたクラスには、@Metadataというアノテーションが付加されます。StringExtKtクラスに付加されている@Metadataアノテーションはこのようになっていますね。
@Metadata( mv = {1, 6, 0}, k = 2, d1 = {"\u0000\f\n\u0000\n\u0002\u0010\u0002\n\u0002\u0010\u000e\n\u0000\u001a\n\u0010\u0000\u001a\u00020\u0001*\u00020\u0002¨\u0006\u0003"}, d2 = {"printHello", "", "", "kotlin-lib"} )
以下ではそれぞれのパラメータについて解説していきます。
metadataVersion (mv)
mv
は metadataVersion
の短縮名で、その名の通りメタデータのバージョンを表します。
上の例では、Kotlin 1.6.0のコンパイラを使用したため、 {1, 6, 0}
という配列が設定されています。
kind (k)
k
は kind
の短縮名で、そのクラスの種類を表します。現在以下の5種類が定義されており、それぞれintの値が割り振られています。
- CLASS_KIND (1)
- FILE_FACADE_KIND (2)
- SYNTHETIC_CLASS_KIND (3)
- MULTI_FILE_CLASS_FACADE_KIND (4)
- MULTI_FILE_CLASS_PART_KIND (5)
※ それぞれの説明は以下のファイルをご覧ください。
今回はFILE_FACADE_KINDというタイプになるので、2が設定されています。
data1 (d1), data2 (d2)
メソッドなどのKotlinにおける定義をProtocolBufferでエンコードされたバイナリとして保存しているのがdata1、それに関連した文字列を保持するのがdata2です。
data1には以下のように文字列テーブルの情報とクラスの情報が含まれます。
data1は
"\u0000\f\n\u0000\n\u0002\u0010\u0002\n\u0002\u0010\u000e\n\u0000\u001a\n\u0010\u0000\u001a\u00020\u0001*\u00020\u0002¨\u0006\u0003"
と設定されていますが、HEX表記で分割すると、
00
⋯ UTF-8でエンコードされていることを表す0C
⋯ 文字列テーブルのサイズが12バイト0A 00 0A 02 10 02 0A 02 10 0E 0A 00
⋯ 文字列テーブル (ProtocolBufferのバイナリ)1A 0A 10 00 1A 02 30 01 2A 02 30 02 A8 06 03
⋯ FileFacadeタイプのデータ (ProtocolBufferのバイナリ)
となります。
文字列テーブルとクラス情報はKotlinのリポジトリとProtocol Bufferのprotocコマンドでデコード可能です。
$ git clone https://github.com/JetBrains/kotlin $ cd kotlin $ echo -ne '\x0A\x00\x0A\x02\x10\x02\x0A\x02\x10\x0E\x0A\x00' | protoc --decode=org.jetbrains.kotlin.metadata.jvm.StringTableTypes core/metadata.jvm/src/jvm_metadata.proto # 文字列テーブルのデコード record { } record { predefined_index: 2 } record { predefined_index: 14 } record { } $ echo -ne '\x1A\x0A\x10\x00\x1A\x02\x30\x01\x2A\x02\x30\x02\xA8\x06\x03' | protoc --decode=org.jetbrains.kotlin.metadata.Package core/metadata.jvm/src/jvm_metadata.proto # FileFacadeデータのデコード function { name: 0 return_type { class_name: 1 } receiver_type { class_name: 2 } } [org.jetbrains.kotlin.metadata.jvm.package_module_name]: 3
まず、文字列テーブルから読んでいきましょう。data2の文字列の配列 ({"printHello", "", "", "kotlin-lib"}
) と、文字列テーブルのrecordは1:1で対応しています。
0, 3番目のrecordは定義が空です。そのため文字列はdata2そのままで、それぞれ"printHello", "kotlin-lib"となります。1, 2番目はrecordを読むと、事前に定義された2番目と14番目の文字列であると読み取れます。この「事前に定義された文字列」というのは以下の文字列のリストのことです。
すなわち、0~3のそれぞれの値は以下のとおりです。
0: "printHello" 1: "kotlin/Unit" 2: "kotlin/String" 3: "kotlin-lib"
これらを基に、FileFacadeデータを読んでいきましょう。
そのまま読むと、このファイルには、名前が0で、戻り値型が1、レシーバータイプが2の関数が含まれること、またパッケージモジュール名が3であることが読み取れます。
これらの数字は上で解明した文字列テーブルのインデックスです。置き換えて読むと、「名前がprintHello
で、戻り値型がkotlin/Unit
、レシーバータイプがkotlin/String
の関数が含まれる、またパッケージモジュール名がkotlin-lib
である」と解釈できます。
kotlin_module
トップレベル関数や拡張関数といった情報の埋め込みは、ここまで説明した話が全てです。実際、クラス名変更とアノテーション付加済みのStringExtKtクラスをコンパイルし、IntelliJ IDEAのデコンパイラ機能で表示すると、Kotlinのコードからコンパイルされたように表示されます。
ただ残念ながらまだコンパイル時にはJavaのstaticメソッドと認識され、拡張関数としての利用はできません。コンパイル時にKotlinのクラスや関数であると認識してもらうにはkotlin_moduleファイルにて対象のクラスが指定されている必要があります。これは通常jarファイルのMETA-INFディレクトリ内に (モジュール名).kotlin_module
という名で配置されます。
バイナリエディタで開いてみると以下のようになっています。
最初の20バイトはバージョン互換性に関する情報です。Kotlinでは通常1以上のメジャーバージョンおよび2以上のマイナーバージョンでコンパイルされたjarファイルは互換性がないものとして振る舞うようです。例えばKotlin 1.4.32のコンパイラはKotlin 1.6.0でコンパイルされたjarファイルと互換性がないです。そのため、上記のように、コンパイラのバージョン番号 (1.6.0) が記録されています。
また、21バイト目以降はProtocol Bufferでエンコードされたバイナリです。その内容をデコードすると以下のようになります。
package_parts { package_fq_name: "com.oboenikui.jatlin.kotlin" short_class_name: "MainKt" } string_table { } qualified_name_table { }
端的に説明すると、これはどのクラスがKotlinでコンパイルされたのかを、モジュール単位で記録するものです。このモジュールには、com.oboenikui.jatlin.kotlin.MainKt
というKotlinからコンパイルされたクラスが含まれていることを意味します。
JavaをKotlin化してみる
さて、今までの情報を基に冒頭のJavaコードをKotlin化してみましょう。
やることは3つです。ちなみにモジュール名は java-lib
としました。
- StringUtilからStringExtKtにリネーム
- @Metadataアノテーションを付加
java-lib.kotlin_metadata
ファイルを作成
Javaファイルの編集
リネームと@Metadataアノテーションを付加します。@Metadataにわたすパラメータは幸いほぼ同じで、異なるのはモジュール名を java-lib
とすることくらいです。
@Metadata( mv = {1, 6, 0}, k = 2, d1 = {"\u0000\f\n\u0000\n\u0002\u0010\u0002\n\u0002\u0010\u000e\n\u0000\u001a\n\u0010\u0000\u001a\u00020\u0001*\u00020\u0002\u00a8\u0006\u0003"}, d2 = {"printHello", "", "", "java-lib"}) public final class StringExtKt { @NotNull public static void printHello(final String $this) { System.out.println("Hello, " + $this); } }
※ 実際のKotlinコードにはnullチェック処理も含まれます。
java-lib.kotlin_metadata
ファイルの作成
パッケージ名とクラス名を修正したファイルをresources/META-INFディレクトリ内に作成します。
jarファイルの生成
あとはjarファイルを生成し、別のKotlinを使ったプロジェクトに読み込ませると正しく認識します。
また、このmain関数を実行すると正常に出力されます🎉
本記事のサンプルコードは以下のリポジトリで公開しています。
We're hiring!
弊社ではAndroidアプリだけでなく、サーバーサイドでもKotlinを広く採用しています。製品コードでJavaコードを魔改造してKotlin化はしていませんが、興味を持たれた方は下記よりお問い合わせください。