【kotlinx-metadata】Java Constructor/MethodからKmConstructor/KmFunctionを取得する

# やること
`kotlin-reflect`では、`Java` `Constructor`/`Method`から`KFunction`への変換が提供されています。
一方、`kotlinx-metadata-jvm`では`KmConstructor`/`KmFunction`への変換が提供されていません[^hsk]。

[^hsk]: 正確には、探した限りでは見つけられませんでした。

そこで、この記事では`Java` `Constructor`/`Method`から`KmConstructor`/`KmFunction`を取得する方法を解説します。

# やり方
`Jvm`ではメソッド名と引数の型の並びが一致すればメソッドを特定できるため、`KmConstructor`/`KmFunction`から得られる情報を用いてこれを判定する方法について解説します。

## Constructor/Methodからの引数情報文字列（JVMデスクリプタ）の取得
次の章で紹介するコードでは、以下に示す`Executable.getParametersJvmDescHead`関数を使って解説を行います。

```kotlin
import java.lang.reflect.Executable

val PRIMITIVE_CLASS_TO_DESC = mapOf(
    Byte::class.javaPrimitiveType to 'B',
    Char::class.javaPrimitiveType to 'C',
    Double::class.javaPrimitiveType to 'D',
    Float::class.javaPrimitiveType to 'F',
    Int::class.javaPrimitiveType to 'I',
    Long::class.javaPrimitiveType to 'J',
    Short::class.javaPrimitiveType to 'S',
    Boolean::class.javaPrimitiveType to 'Z',
    Void::class.javaPrimitiveType to 'V'
)

fun Class<*>.toDescriptor(): String = when {
    isPrimitive -> PRIMITIVE_CLASS_TO_DESC.getValue(this).toString()
    isArray -> "[${componentType.toDescriptor()}"
    else -> "L$name;".replace('.', '/')
}

// Executable(Constructor/Methodの親クラス)の戻り値の情報を除いたメソッドデスクリプタ文字列
fun Executable.getParametersJvmDescHead() = parameterTypes
    .joinToString(prefix = "(", separator = "", postfix = ")") { it.toDescriptor() }
```

### Executable.getParametersJvmDescHead関数の解説
`KmConstructor`/`KmFunction`から取得できる関数名と引数（と戻り値）の型情報は[`JvmMethodSignature`という型](https://github.com/JetBrains/kotlin/blob/92d200e093c693b3c06e53a39e0b0973b84c7ec5/compiler/frontend.java/src/org/jetbrains/kotlin/resolve/jvm/jvmSignature/JvmMethodSignature.java)に格納されています。
この型には以下2つのフィールドが有ります。

- `name`: 関数名
- `desc`: `JVM`デスクリプタ（引数と戻り値の型情報の文字列）

`desc`は、`(${引数の型のデスクリプタ形式表現を区切り文字無しで並べたもの})${戻り値の型のデスクリプタ形式表現}`という形式になります[^constructor]。
例えば、`(Short, Int?): Long`というような引数・戻り値の関数に対しては`(SLjava/lang/Integer;)J`という文字列となります。
`S` -> `Short`、`Ljava/lang/Integer;` -> `Int?`、`J` -> `Long`と対応しています。
`Class`とデスクリプタ形式表現の対応に関しては[こちらの資料](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3)をご覧下さい。

[^constructor]: `Java Constructor`に関しては例外的に戻り値の型情報が全て`V`（= `Void`）になっていました。

`desc`の中身は文字列であるため、そのままでは`Java` `Constructor`/`Method`の引数の型情報と比較することができません。
この比較をできるようにするため、`Java` `Constructor`/`Method`の引数の型情報を`JVM`デスクリプタ形式に変換するのが`Executable.getParametersJvmDescHead`関数の役割です。
この関数では戻り値の型情報について省略していますが、これは名前と引数の型情報さえあれば特定には十分なためです。

## 取得処理
先ほど定義した`Executable.getParametersJvmDescHead`関数を用い、`Java` `Constructor`/`Method`から`KmConstructor`/`KmFunction`をそれぞれ取得する処理のサンプルです（`Class` -> `KmClass`の変換処理は省略しています）。
`Constructor`に関しては`name`が必ず`<init>`になるため比較を省略しています。

```kotlin
import kotlinx.metadata.KmClass
import kotlinx.metadata.KmConstructor
import kotlinx.metadata.KmFunction
import kotlinx.metadata.jvm.JvmMethodSignature
import kotlinx.metadata.jvm.signature
import java.lang.reflect.Constructor
import java.lang.reflect.Method

/* 静的に定義した型でsignatureが取得できないパターンは分からなかったため強制アンラップしている */

// コンストラクタに関する取得処理
fun Constructor<*>.kmConstructor(): KmConstructor {
    val kmClass: KmClass = this.declaringClass.toKmClass()
    return kmClass.constructors.first { it.signature!!.desc.startsWith(this.getParametersJvmDescHead()) }
}

// メソッドに関する取得処理
fun Method.kmFunction(): KmFunction {
    val kmClass: KmClass = this.declaringClass.toKmClass()

    return kmClass.functions.first {
        val signature: JvmMethodSignature = it.signature!!
        signature.name == this.name && signature.desc.startsWith(this.getParametersJvmDescHead())
    }
}
```

これを用いて、以下に定義する`TargetClass`の`TargetMarker`が付与されたコンストラクタ/メソッドについて一連の取得処理を行うサンプルを示します。

```kotlin:処理対象クラス
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FUNCTION)
annotation class TargetMarker

class TargetClass(val l1: Long, val l2: Long?) {
    @TargetMarker constructor(s1: Short, i2: Int?) : this(s1.toLong(), i2?.toLong())

    fun plus(ol1: Long?, ol2: Long) = l1 + (ol1 ?: 0) + (l2 ?: 0) + ol2
    @TargetMarker fun plus(s: Short, i: Int?) = l1 + s + (l2 ?: 0) + (i ?: 0)
}
```

```kotlin:実際の取得処理例
val targetClazz: Class<*> = TargetClass::class.java

val targetConstructor: Constructor<*> =
    targetClazz.constructors.first { it.annotations.any { ann -> ann is TargetMarker } }
val kmConstructor: KmConstructor = targetConstructor.kmConstructor()
// 取得したKmConstructorのパラメータ名が、TargetMarkerを付与したコンストラクタのパラメータ名と一致するか確認する
// -> s1, i2
println(kmConstructor.valueParameters.joinToString { it.name })

val targetMethod: Method =
    targetClazz.declaredMethods.first { it.annotations.any { ann -> ann is TargetMarker } }
val kmFunction: KmFunction = targetMethod.kmFunction()
// -> s, i
println(kmFunction.valueParameters.joinToString { it.name })
```

### 補足
`value class`(`inline class`)を用いた場合、`Kotlin`上の型定義や引数とコンパイル結果が食い違うことが有りますが、今回紹介した方法では正常に`KmConstructor`/`KmFunction`を取得できます。
これは、比較に用いている`JvmMethodSignature`が保持しているのはコンパイル結果の情報だからです。

# 参考にさせて頂いた内容
`MoshiX/moshi-metadata-reflect`より、以下のコード・コメントを参考にさせて頂きました。

- [MoshiX/JvmDescriptors\.kt at main · ZacSweers/MoshiX](https://github.com/ZacSweers/MoshiX/blob/main/moshi-metadata-reflect/src/main/kotlin/dev/zacsweers/moshix/reflect/JvmDescriptors.kt#L21-L40)
- [MoshiX/JvmDescriptorsTest\.kt at main · ZacSweers/MoshiX](https://github.com/ZacSweers/MoshiX/blob/main/moshi-metadata-reflect/src/test/kotlin/dev/zacsweers/moshix/reflect/JvmDescriptorsTest.kt)

https://github.com/ZacSweers/MoshiX/tree/main/moshi-metadata-reflect