Announcing kotlinx-metadata-jvm library for reading/modifying metadata of Kotlin/JVM class files

@xdenss Could you please describe process of removing @Metadata annotation with R8?
Just added in gradle.properties entries to support R8 android.enableR8=true.
Also tried with android.enableR8.fullMode=true, but metadata annotation is still visible in decompiled code.

kotlinx-metadata-jvm 0.1.0 has been published on Bintray and will be available in Maven Central soon.

The highlight of this release is the new value-based API (KT-26602) which is now the preferred way to work with the metadata instead of visitors. Take a look at an updated ReadMe and/or an example test case to learn more.

For example, here’s an excerpt from that test case that creates class metadata from scratch:

    val klass = KmClass().apply {
        name = "Hello"
        flags = flagsOf(Flag.IS_PUBLIC)
        constructors += KmConstructor(flagsOf(Flag.IS_PUBLIC, Flag.Constructor.IS_PRIMARY)).apply {
            signature = JvmMethodSignature("<init>", "()V")
        }
        functions += KmFunction(flagsOf(Flag.IS_PUBLIC, Flag.Function.IS_DECLARATION), "hello").apply {
            returnType = KmType(flagsOf()).apply {
                classifier = KmClassifier.Class("kotlin/String")
            }
            signature = JvmMethodSignature("hello", "()Ljava/lang/String;")
        }
    }
1 Like

@udalov the new API is great, but I was wondering why the approach of all simple classes with mutable variables was taken rather than making them immutable data classes? Taking that same example snippet

val klass = KmClass(
    name = "Hello",
    flags = flagsOf(Flag.IS_PUBLIC),
    constructors = listOf(
        KmConstructor(
            flags = flagsOf(Flag.IS_PUBLIC, Flag.Constructor.IS_PRIMARY),
            signature = JvmMethodSignature("<init>", "()V")
        )
    ),
    functions = listOf(
        KmFunction(
            flags = flagsOf(Flag.IS_PUBLIC, Flag.Function.IS_DECLARATION),
            name = "hello",
            returnType = KmType(
                flags = flagsOf(),
                classifier = KmClassifier.Class("kotlin/String")
            )
            signature = JvmMethodSignature("hello", "()Ljava/lang/String;")
        )
}

Later mutating could be done safely with data class copy functions instead then

A separate question - would you be open to a PR with a non-ServiceLoader initialization option? As the library is not stable yet, it’s not safe to ship as a direct dependency yet in its current form. I’ve been testing out shading it here, but have had enough issues with getting service rewriting to work correctly that I’m hoping you’d be open to a non-service solution instead.

One more separate question (sorry if this the wrong place, since this isn’t a separate kotlinx repo I can’t file these as separate issues)

Is it possible to distinguish between receiver types and non-receiver types? For example: I don’t currently see a way to differentiate between String.() -> Unit and (String) -> Unit as they’re both just Function1<String, Unit> as far as metadata is concerned

Hi! Sorry for a late response.
To reproduce the R8 issue I implemented Kotlin class with reflection.
For example:

This example works fine with Proguard but crashed with R8 with access exception. Without Metadata there is no information about getters for vals.
In R8 sources I found line where metadata is used to collect additional info for desugaring (and maybe obfuscation) and then just remove it. There was following code (at least month ago):
snippet from src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java

@hzsweers Thanks for the feedback!

I was wondering why the approach of all simple classes with mutable variables was taken rather than making them immutable data classes?

One of the obstacles is the fact that we plan to extend the library to other platforms in the future, thus the API of common classes cannot refer to JVM specifics (such as signature). Currently all JVM extensions are done via Kotlin extension properties and this design seems to scale well in the presence of other platforms.

Another is performance. I agree that data classes seem nicer, but not that much nicer to justify the additional cost of all the allocations caused by mutations, and all the extra equals/hashCode/toString/component methods in the bytecode. We’re still aiming for kotlinx-metadata-jvm to be used in tools such as bytecode obfuscators and optimizers that are supposed to work fast on big codebases.

A separate question - would you be open to a PR with a non-ServiceLoader initialization option?

Yes, although I’m wondering what exactly do you have in mind. ServiceLoader seems to be the preferred way of simple DI (especially in light of the Java module system) and it should be handled properly by the Shadow plugin. In any case, let’s not discuss the details here – if you have an idea, please send a PR and I’ll take a look!

Is it possible to distinguish between receiver types and non-receiver types? For example: I don’t currently see a way to differentiate between String.() -> Unit and (String) -> Unit as they’re both just Function1<String, Unit> as far as metadata is concerned

The way this is handled in the language is by a type annotation ExtensionFunctionType. So the type String.() -> Unit is actually the same as @ExtensionFunctionType (String) -> Unit. You can find type annotations via the KmType.annotations extension property.