This library provides an API to read and modify binary metadata in files generated by the Kotlin/JVM compiler, namely .class
(in a form of @kotlin.Metadata
annotation) and .kotlin_module
files.
Starting with Kotlin 2.0, the kotlin-metadata-jvm library is promoted to stable and is a part of the Kotlin distribution now. Therefore, it has the same version as Kotlin compiler and standard library, and has org.jetbrains.kotlin
coordinates.
To use this library in your project, add a dependency on org.jetbrains.kotlin:kotlin-metadata-jvm:$kotlin_version
.
Example usage in Gradle:
repositories { mavenCentral() } dependencies { implementation("org.jetbrains.kotlin:kotlin-metadata-jvm:$kotlin_version") }
Example usage in Maven:
<project> <dependencies> <dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-metadata-jvm</artifactId> <version>${kotlin_version}</version> </dependency> </dependencies> ... </project>
To use older pre-1.0 versions of the library, use
org.jetbrains.kotlinx:kotlinx-metadata-jvm
coordinates. Migration guide to stable versions is available here.
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.
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") }
If you transform some classes produced by Kotlin compiler (e.g., change visibilities or strip methods), then you will likely need to transform Kotlin metadata as well. Process of doing that is relatively simple due to the fact that every property inside KotlinClassMetadata
and Km nodes is mutable; you can rewrite/edit desired parts in-place. After doing desired changes, the KotlinClassMetadata.write
member method can give you the new kotlin.Metadata
instance to put into transformed classfiles:
val classMetadata: KotlinClassMetadata.Class = KotlinClassMetadata.readStrict(oldAnnotation) as KotlinClassMetadata.Class classMetadata.kmClass.functions.removeIf { it.visibility == Visibility.PRIVATE } val newAnnotation: Metadata = classMetadata.write() // store newAnnotation with ASM, etc.
It is recommended to retain the original instance of KotlinClassMetadata.*
because it preserves version
and flags
from the original metadata.
To simplify the transformation process even more, there is an utility method KotlinClassMetadata.transform
that performs reading and writing for you:
val oldAnnotation: Metadata = ... val newAnnotation: Metadata = KotlinClassMetadata.transform(oldAnnotation) { metadata -> when(metadata) { is KotlinClassMetadata.Class -> metadata.kmClass.functions.removeIf { it.visibility == Visibility.PRIVATE } is KotlinClassMetadata.FileFacade -> metadata.kmPackage.functions.removeIf { it.visibility == Visibility.PRIVATE } else -> { /* no-op */ } } // at the end of the lambda, metadata is written automatically. }
To create metadata of a Kotlin class file from scratch, first, construct an instance of KmClass
/KmPackage
/KmLambda
, and fill it with the data. 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 put a resulting KmClass
/KmPackage
/KmLambda
into an appropriate container and write it. Pay attention to the metadata version. Usually, JvmMetadataVersion.LATEST_STABLE_SUPPORTED
is a good choice, but you may want to write a specific version you obtained from existing Kotlin classfiles in your project. You also have to set up the flags
. For description of all the available flags, see Metadata.extraInt. 0
(no flags) may be a good choice, but in case you already have some ground truth files in your project, you may want to copy flags from them.
val classMetadata = KotlinClassMetadata.Class(klass, JvmMetadataVersion.LATEST_STABLE_SUPPORTED, 0) val newAnnotation: Metadata = classMetadata.write() // Write annotation directly or use annotation.kind, annotation.data1, annotation.data2, etc.
Please refer to MetadataSmokeTest.produceKotlinClassFile
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.
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 kotlin-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 kotlin-metadata-jvm can interpret, it may ignore parts of the metadata it doesn't understand. It’s 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). Keep in mind that this method will still throw an exception if metadata is changed in an unpredictable way. Metadata read in lenient mode can not be written back.
Kotlin compiler and its features evolve over time, and so does its metadata format. Metadata format version is equal to the Kotlin compiler version. Naturally, evolving the metadata format may result in various changes, from adding new information to changing metadata format entirely for the new Kotlin language features. Therefore, some problems may occur when you're reading new metadata with an older version of Kotlin compiler or kotlin-metadata-jvm library.
By default, the Kotlin/JVM compiler (and similar, kotlin-metadata-jvm library) has 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 JvmMetadataVersion.LATEST_STABLE_SUPPORTED
+ 1. Such restriction comes from the fact that higher metadata versions (e.g. 2.3) might have some unexpected format that can be read incorrectly; therefore, if we write transformed metadata back, this can 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 overly restrictive to prohibit reading newer metadata versions (and therefore, requiring authors to do frequent updates of kotlin-metadata-jvm dependency), so there is a relaxed version of the reading method: KotlinClassMetadata.readLenient()
. It is a best-effort reading method that will try to build metadata based on the format we currently know, and provide some access to it. Keep in mind that this method has limitations:
readLenient()
tries its best, but still can throw a decoding exception or even return incorrect result.Similarly to how KotlinClassMetadata
is used to read/write metadata of Kotlin .class
files, KotlinModuleMetadata
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
:
// Read the module metadata val bytes = File("META-INF/main.kotlin_module").readBytes() val moduleMetadata = KotlinModuleMetadata.read(bytes) val module = moduleMetadata.kmModule ... // Write the module metadata val bytes = moduleMetadata.write() File("META-INF/main.kotlin_module").writeBytes(bytes)