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.
To use this library in your project, add the kotlinx repository at https://kotlin.bintray.com/kotlinx, and 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> <repositories> <repository> <id>bintray-kotlin-kotlinx</id> <name>bintray</name> <url>https://kotlin.bintray.com/kotlinx</url> </repository> </repositories> ... </project>
Example usage in Gradle:
repositories { mavenCentral() maven { url "https://kotlin.bintray.com/kotlinx/" } } dependencies { compile "org.jetbrains.kotlinx:kotlinx-metadata-jvm:$kotlinx_metadata_version" }
The entry point for reading the Kotlin metadata of a .class
file is KotlinClassMetadata.read
. The data it takes is encapsulated in KotlinClassHeader
which is basically what is written in the kotlin.Metadata
annotation on the class file generated by the Kotlin compiler. Construct KotlinClassHeader
by reading the values from kotlin.Metadata
reflectively or from some other resource, and then use KotlinClassMetadata.read
to obtain the correct instance of the class metadata. (Note that loading values of kotlin.Metadata
reflectively is only possible from Java sources until Kotlin 1.3, because this annotation is internal in the standard library, see KT-23602.)
val header = KotlinClassHeader( ... /* pass Metadata.k, Metadata.d1, Metadata.d2, etc as arguments ... */ ) val metadata = KotlinClassMetadata.read(header)
KotlinClassMetadata
is a sealed class, with subclasses representing all the different kinds of classes generated by the Kotlin compiler. Unless you‘re sure that you’re 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‘s assume we’ve 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 invoke accept
, passing an instance of KmClassVisitor
to handle the incoming information (Km
is a shorthand for “Kotlin metadata”):
metadata.accept(object : KmClassVisitor() { override fun visitFunction(flags: Flags, name: String): KmFunctionVisitor? { // This will be called for each function in the class. "name" is the // function name, and "flags" represent modifier flags (see below) ... // Return an instance of KmFunctionVisitor for more details, // or null if this function is of no interest } })
Please refer to MetadataSmokeTest.listInlineFunctions
for an example where all inline functions are read from the class metadata along with their JVM signatures.
Numerous visit*
methods take the parameter named flags
. These flags represent modifiers or other boolean attributes of a declaration or a type. To check if a certain flag is present, call one of the flags in Flag
on the given integer value. The set of applicable flags is documented in each visit*
method. For example, for functions, this is common declaration flags (visibility, modality) plus Flag.Function
flags:
override fun visitFunction(flags: Flags, name: String): KmFunctionVisitor? { if (Flag.IS_PUBLIC(flags)) { println("function $name is public") } if (Flag.Function.IS_SUSPEND(flags)) { println("function $name has the 'suspend' modifier") } ... }
Certain information in the metadata of Kotlin .class
files is JVM-only, just like certain information in the .meta.js
files on Kotlin/JS is JS-only. To retain the possibility to release Kotlin/JS- (and later, Kotlin/Native-) reading metadata library, we‘ve extracted most of the API of this library to a platform-independent kotlinx-metadata
(here platform-independent means not that it’s agnostic to the platform it‘s compiled to, but that it’s agnostic to the platform it allows to read Kotlin metadata from), and kotlinx-metadata-jvm
is a small addition with JVM-only data.
To read platform-specific (in this case, JVM-specific) data, each visitor that has that data declares a visitExtensions
method, taking the extension type and returning the visitor of that type, capable of reading platform-specific data. The intended way to implement visitExtensions
for JVM is to check if the given extension type is of the needed JVM extension visitor and return a new instance of that visitor, or return null otherwise. Each JVM extension visitor has its type declared in the TYPE
variable in the companion object. For example, to read JVM extensions on the property:
override fun visitExtensions(type: KmExtensionType): KmPropertyExtensionVisitor? { // If these are JVM property extensions, read them by returning a visitor if (type == JvmPropertyExtensionVisitor.TYPE) { return object : JvmPropertyExtensionVisitor() { // Read JVM property extensions ... } } // If these are extensions of some other type, ignore them return null }
To create metadata of a Kotlin class file from scratch, use one of the Writer
classes declared in KotlinClassMetadata
's subclasses. Writers of relevant classes inherit from the corresponding Km*Visitor
classes. Invoke the corresponding visit*
methods successively on the writer to add declarations (do not forget to call visitEnd
where applicable!), and call write
in the end to produce the metadata. Finally, use KotlinClassMetadata.header
to obtain the raw data and write it to the kotlin.Metadata
annotation on a class file.
When using metadata writers from Kotlin source code, it's very convenient to use Kotlin scoping functions such as run
to reduce boilerplate:
// Writing metadata of a class val header = KotlinClassMetadata.Class.Writer().run { // Visiting the name and the modifiers on the class. // Flags are constructed by invoking "flagsOf(...)" visit(flagsOf(Flag.IS_PUBLIC), "MyClass") // Adding one public primary constructor visitConstructor(flagsOf(Flag.IS_PUBLIC, Flag.Constructor.IS_PRIMARY))!!.run { // Visiting JVM signature (for example, to be used by kotlin-reflect) (visitExtensions(JvmConstructorExtensionVisitor.TYPE) as JvmConstructorExtensionVisitor).run { visit(JvmMethodSignature("<init>", "()V")) } // Not forgetting to call visitEnd at the end of visit of the declaration visitEnd() } ... ... // Finally writing everything to arrays of bytes write().header } // Use header.kind, header.data1, header.data2, etc. to write values to kotlin.Metadata ...
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.
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.Writer
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 metadata = KotlinModuleMetadata.read(bytes) metadata.accept(object : KmModuleVisitor() { ... } // Write the module metadata val bytes = KotlinModuleMetadata.Writer().run { visitPackageParts(...) write().bytes } File("META-INF/main.kotlin_module").writeBytes(bytes)
Note that until you invoke accept
on a KotlinClassMetadata
or KotlinModuleMetadata
instance, the data is not completely parsed and verified. If you need to check if the data is not horribly corrupted before proceeding, make sure to call accept
, even with an empty visitor:
val metadata: KotlinClassMetadata.Class = ... try { // Guarantees eager parsing of the underlying data metadata.accept(object : KmClassVisitor() {}) } catch (e: Exception) { System.err.println("Metadata is corrupted!") }