Add ReadMe for kotlinx-metadata and kotlinx-metadata-jvm

#KT-23198
This commit is contained in:
Alexander Udalov
2018-04-19 00:52:29 +02:00
parent 9dbeb79340
commit 002310ff6e
2 changed files with 162 additions and 0 deletions
+3
View File
@@ -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).
+159
View File
@@ -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("<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`](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!")
}
```