Files
kotlin-fork/libraries/kotlinx-metadata/jvm/ReadMe.md
T
Leonid Startsev 36703ff9ae Implement strict and lenient modes for Kotlin metadata reading
In strict mode, an exception will be thrown when inconsistent metadata is encountered. In lenient mode, the reader will attempt to handle the inconsistent metadata by ignoring certain inconsistencies. This is a solution to a problem of reading metadata 'from the future' that is not allowed by default, but desired in certain cases. See updated ReadMe for details.

Also fix problem with Strict Semantics flag.

#KT-57922 Fixed
#KT-59441 Fixed
2023-12-01 17:43:11 +00:00

9.1 KiB
Raw Blame History

kotlinx-metadata-jvm

This library provides an API to read and modify metadata of binary files generated by the Kotlin/JVM compiler, namely .class and .kotlin_module files.

Usage

To use this library in your project, add a dependency on org.jetbrains.kotlinx:kotlinx-metadata-jvm:$kotlinx_metadata_version (where kotlinx_metadata_version is the version of the library).

Example usage in Maven:

<project>
    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlinx</groupId>
            <artifactId>kotlinx-metadata-jvm</artifactId>
            <version>${kotlinx_metadata_version}</version>
        </dependency>
    </dependencies>
    ...
</project>

Example usage in Gradle:

repositories {
    mavenCentral()
}

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-metadata-jvm:$kotlinx_metadata_version"
}

Overview

The entry point for reading the Kotlin metadata of a .class file is KotlinClassMetadata.readStrict. The data it takes is the kotlin.Metadata annotation on the class file generated by the Kotlin compiler. Obtain the kotlin.Metadata annotation reflectively or construct it from binary representation (e.g. by reading classfile with org.objectweb.asm.ClassReader), and then use KotlinClassMetadata.readStrict to obtain the correct instance of the class metadata.

val metadataAnnotation = Metadata(
    // pass arguments here
)
val metadata = KotlinClassMetadata.readStrict(metadataAnnotation)

There are other methods of reading metadata, but readStrict is a preferred one. See the differences in working with different versions section.

KotlinClassMetadata is a sealed class, with subclasses representing all the different kinds of classes generated by the Kotlin compiler. Unless you are sure that you are reading a class of a specific kind and can do a simple cast, a when is a good choice to handle all the possibilities:

when (metadata) {
    is KotlinClassMetadata.Class -> ...
    is KotlinClassMetadata.FileFacade -> ...
    is KotlinClassMetadata.SyntheticClass -> ...
    is KotlinClassMetadata.MultiFileClassFacade -> ...
    is KotlinClassMetadata.MultiFileClassPart -> ...
    is KotlinClassMetadata.Unknown -> ...
}

Let us assume we have obtained an instance of KotlinClassMetadata.Class; other kinds of classes are handled similarly, except some of them have metadata in a slightly different form. The main way to make sense of the underlying metadata is to access the kmClass property, which returns an instance of KmClass (Km is a shorthand for “Kotlin metadata”):

val klass = metadata.kmClass
println(klass.functions.map { it.name })
println(klass.properties.map { it.name })

Please refer to MetadataSmokeTest.listInlineFunctions for an example where all inline functions are read from the class metadata along with their JVM signatures.

Attributes

Most of the Km nodes (KmClass, KmFunction, KmType, and so on) have a set of extension properties that allow to get and set various attributes. Most of these attributes are boolean values, but some of them, such as visibility, are represented by enum classes. For example, you can check function visibility and presence of suspend modifier with corresponding extension properties:

val function: KmFunction = ...
if (function.visibility == Visibility.PUBLIC) {
    println("function ${function.name} is public")
}
if (function.isSuspend) {
    println("function ${function.name} has the 'suspend' modifier")
}

Writing metadata

To create metadata of a Kotlin class file from scratch, construct an instance of KmClass/KmPackage/KmLambda, fill it with the data, and call corresponding KotlinClassMetadata.write function. Resulting kotlin.Metadata annotation can be written to a class file.

When using metadata writers from Kotlin source code, it is very convenient to use Kotlin scoping functions such as apply to reduce boilerplate:

// Writing metadata of a class
val klass = KmClass().apply {
    // Setting the name and the modifiers of the class.
    name = "MyClass"
    visibility = Visibility.PUBLIC

    // Adding one public primary constructor
    constructors += KmConstructor().apply {
        visibility = Visibility.PUBLIC
        isSecondary = false
        // Setting the JVM signature (for example, to be used by kotlin-reflect)
        signature = JvmMethodSignature("<init>", "()V")
    }

    ...
}

Then, you can encode a resulting KmClass to an annotation.

val annotation = KotlinClassMetadata.writeClass(klass)

// Write annotation directly or use annotation.kind, annotation.data1, annotation.data2, etc.


Please refer to [`MetadataSmokeTest.produceKotlinClassFile`](test/kotlinx/metadata/test/MetadataSmokeTest.kt) for an example where metadata of a simple Kotlin class is created,
and then the class file is produced with ASM and loaded by Kotlin reflection.

## Module metadata

Similarly to how `KotlinClassMetadata` is used to read/write metadata of Kotlin `.class` files, [`KotlinModuleMetadata`](src/kotlinx/metadata/jvm/KotlinModuleMetadata.kt)
is the entry point for reading/writing `.kotlin_module` files. Use `KotlinModuleMetadata.read` or `KotlinModuleMetadata.write` in very much the same fashion as with the class files.
The only difference is that the source for the reader (and the result of the writer) is a simple byte array, not the structured data loaded from `kotlin.Metadata`:

```kotlin
// Read the module metadata
val bytes = File("META-INF/main.kotlin_module").readBytes()
val metadata = KotlinModuleMetadata.read(bytes)
val module = metadata.kmModule
...

// Write the module metadata
val bytes = KotlinModuleMetadata.write(module)
File("META-INF/main.kotlin_module").writeBytes(bytes)

Working with different versions

Short guide

There are two methods to read metadata:

readStrict(): This method allows you to read the metadata strictly, meaning it will throw an exception if the metadata version is greater than what kotlinx-metadata-jvm understands. It's suitable when your tooling can't tolerate reading potentially incomplete or incorrect information due to version differences. It's also the only method that allows metadata transformation and KotlinClassMetadata.write subsequent calls.

readLenient(): This method allows you to read the metadata leniently. If the metadata version is higher than what kotlinx-metadata-jvm can interpret, it may ignore parts of the metadata it doesn't understand but it won't throw an exception. Its more suitable when your tooling needs to read metadata of possibly newer Kotlin versions and can handle incomplete data, because it is interested only in part of it (e.g. visibility of declarations) Metadata read in lenient mode can not be written back.

Detailed explanation

Kotlin compiler and its features evolve over time, and so its metadata format. Metadata format version is equal to the Kotlin compiler version. As you might guess, evolving metadata format usually involves adding new fields for new Kotlin language features. Therefore, some problems may occur when you're reading new metadata with an older version of Kotlin compiler or kotlinx-metadata-jvm library.

By default, the Kotlin compiler (and similar, kotlinx-metadata-jvm library) have forward compatibility for versions not higher than current + 1. It means that Kotlin compiler 2.1 can read metadata from Kotlin compiler 2.2, but not 2.3. The same is true for KotlinClassMetadata.readStrict() method: it will throw an exception if you try to read metadata with version higher than COMPATIBLE_METADATA_VERSION + 1. Such restriction comes from the fact that higher metadata versions (e.g. 2.3) might have some unknown fields that we skip during reading; therefore, if we write transformed metadata back, missing some fields may result in corrupted metadata that is no longer valid for version 2.3.

However, there are a lot of use-cases for metadata introspection alone, without further transformations — for example, binary-compatibility-validator which is interested only in visibility and modality of declarations. For such use-cases it seems over restrictive to prohibit reading newer metadata versions (and therefore, requiring authors to do frequent updates of kotlinx-metadata-jvm dependency), so there is a relaxed version of the reading method: KotlinClassMetadata.readLenient(). It is a best-effort reading method that will potentially skip all unknown fields, but still provide some access to metadata. Keep in mind that this method has limitations:

  1. Metadata returned by this method can not be written back, because we are not sure if it is still valid format for newer versions. It is intended for introspection alone.
  2. While some unknown fields are skipped, we cannot guarantee that metadata is not changed in the other unpredictable ways in the future. readLenient() tries its best, but still may throw a decoding exception if metadata cannot be read at all.