diff --git a/libraries/kotlinx-metadata/ReadMe.md b/libraries/kotlinx-metadata/ReadMe.md new file mode 100644 index 00000000000..c2f0236349f --- /dev/null +++ b/libraries/kotlinx-metadata/ReadMe.md @@ -0,0 +1,3 @@ +## kotlinx-metadata + +This is the platform-agnostic part of the Kotlin Metadata library, intended to provide the possibility to read and modify metadata of Kotlin declarations in the binary (`.class`, `.js`) files emitted by the Kotlin compiler. This part of the library is currently not released on its own. Please refer to [kotlinx-metadata-jvm](jvm/ReadMe.md) for instructions on how to load and modify Kotlin Metadata in JVM binaries (`.class` and `.kotlin_module` files). diff --git a/libraries/kotlinx-metadata/jvm/ReadMe.md b/libraries/kotlinx-metadata/jvm/ReadMe.md new file mode 100644 index 00000000000..5fcfb117b75 --- /dev/null +++ b/libraries/kotlinx-metadata/jvm/ReadMe.md @@ -0,0 +1,159 @@ +# 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. + +## Overview + +The entry point for reading the Kotlin metadata of a `.class` file is [`KotlinClassMetadata.read`](src/kotlinx/metadata/jvm/KotlinClassMetadata.kt). The data it takes is encapsulated in [`KotlinClassHeader`](src/kotlinx/metadata/jvm/KotlinClassHeader.kt) which is basically what is written in the [`kotlin.Metadata`](../../stdlib/jvm/runtime/kotlin/Metadata.kt) 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](https://youtrack.jetbrains.com/issue/KT-23602).) + +```kotlin +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: + +```kotlin +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`](../src/kotlinx/metadata/visitors.kt) to handle the incoming information (`Km` is a shorthand for “Kotlin metadata”): + +```kotlin +metadata.accept(object : KmClassVisitor() { + override fun visitFunction(flags: Int, 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`](test/kotlinx/metadata/test/MetadataSmokeTest.kt) for an example where all inline functions are read from the class metadata along with their JVM signatures. + +## Flags + +Numerous `visit*` methods are declared with 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 [`Flags`](../src/kotlinx/metadata/Flags.kt) 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 `Flags.Function` flags: + +```kotlin +override fun visitFunction(flags: Int, name: String): KmFunctionVisitor? { + if (Flags.IS_PUBLIC(flags)) { + println("function $name is public") + } + if (Flags.Function.IS_SUSPEND(flags)) { + println("function $name has the 'suspend' modifier") + } + ... +} +``` + +## Extensions + +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*](../src/kotlinx/metadata/extensions.kt) 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: + +```kotlin +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 +} +``` + +## Writing metadata + +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: + +```kotlin +// 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 "Flags(...)" + visit(Flags(Flags.IS_PUBLIC), "MyClass") + + // Adding one public primary constructor + visitConstructor(Flags(Flags.IS_PUBLIC, Flags.Constructor.IS_PRIMARY))!!.run { + // Visiting JVM signature (for example, to be used by kotlin-reflect) + (visitExtensions(JvmConstructorExtensionVisitor.TYPE) as JvmConstructorExtensionVisitor).run { + visit("()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`](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.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`: + +```kotlin +// 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) +``` + +## Laziness + +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: + +```kotlin +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!") +} +``` + + +