Introduce new string table optimized for JVM class files

This format of the string table allows to reduce the size of the Kotlin
metadata in JVM class files by reusing constants already present in the
constant pool. Currently the string table produced by JvmStringTable is not
fully optimized in serialization (in particular, the 'substring' operation
which will be used to extract type names out of generic signature, is not used
at all), but the format and its complete support in the deserialization
(JvmNameResolver) allows future improvement without changing the binary version
This commit is contained in:
Alexander Udalov
2015-09-21 20:35:27 +03:00
parent 542bfab96f
commit 1036506b25
19 changed files with 4719 additions and 67 deletions
@@ -22,6 +22,45 @@ import "core/deserialization/src/descriptors.proto";
option java_outer_classname = "JvmProtoBuf";
option optimize_for = LITE_RUNTIME;
message StringTableTypes {
message Record {
// The number of times this record should be repeated; this is used to collapse identical subsequent records in the list
optional int32 range = 1 [default = 1];
// Index of the predefined constant. If this field is present, the associated string is ignored
optional int32 predefined_index = 2;
enum Operation {
NONE = 0;
// replaceAll('$', '.')
// java/util/Map$Entry -> java/util/Map.Entry;
INTERNAL_TO_CLASS_ID = 1;
// substring(1, length - 1) and then replaceAll('$', '.')
// Ljava/util/Map$Entry; -> java/util/Map.Entry
DESC_TO_CLASS_ID = 2;
}
// Perform a described operation on the string
optional Operation operation = 3 [default = NONE];
// If this field is present, the "substring" operation must be performed with the first element of this list as the start index,
// and the second element as the end index.
// If an operation is not NONE, it's applied _after_ this substring operation
repeated int32 substring_index = 4 [packed = true];
// If this field is present, the "replaceAll" operation must be performed with the first element of this list as the code point
// of the character to replace, and the second element as the code point of the replacement character
repeated int32 replace_char = 5 [packed = true];
}
repeated Record record = 1;
// Indices of strings which are names of local classes or anonymous objects
repeated int32 local_name = 5 [packed = true];
}
message JvmMethodSignature {
required int32 name = 1 [(string_id_in_table) = true];
@@ -0,0 +1,136 @@
/*
* Copyright 2010-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.load.kotlin
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.serialization.deserialization.NameResolver
import org.jetbrains.kotlin.serialization.jvm.JvmProtoBuf
import org.jetbrains.kotlin.serialization.jvm.JvmProtoBuf.StringTableTypes.Record
import org.jetbrains.kotlin.serialization.jvm.JvmProtoBuf.StringTableTypes.Record.Operation.DESC_TO_CLASS_ID
import org.jetbrains.kotlin.serialization.jvm.JvmProtoBuf.StringTableTypes.Record.Operation.INTERNAL_TO_CLASS_ID
import org.jetbrains.kotlin.serialization.jvm.JvmProtoBuf.StringTableTypes.Record.Operation.NONE
import java.util.*
class JvmNameResolver(
private val types: JvmProtoBuf.StringTableTypes,
private val strings: Array<String>
) : NameResolver {
private val localNameIndices = types.localNameList.run { if (isEmpty()) emptySet() else toSet() }
// Here we expand the 'range' field of the Record message for simplicity to a list of records
private val records: List<Record> = ArrayList<Record>().apply {
val records = types.recordList
this.ensureCapacity(records.size())
for (record in records) {
repeat(record.range) {
this.add(record)
}
}
this.trimToSize()
}
override fun getString(index: Int): String {
val record = records[index]
var string =
if (record.hasPredefinedIndex() && record.predefinedIndex in PREDEFINED_STRINGS.indices)
PREDEFINED_STRINGS[record.predefinedIndex]
else strings[index]
if (record.substringIndexCount >= 2) {
val (begin, end) = record.substringIndexList
if (0 <= begin && begin <= end && end <= string.length()) {
string = string.substring(begin, end)
}
}
if (record.replaceCharCount >= 2) {
val (from, to) = record.replaceCharList
string = string.replace(from.toChar(), to.toChar())
}
when (record.operation ?: NONE) {
NONE -> {
// Do nothing
}
INTERNAL_TO_CLASS_ID -> {
string = string.replace('$', '.')
}
DESC_TO_CLASS_ID -> {
if (string.length() >= 2) {
string = string.substring(1, string.length() - 1)
}
string = string.replace('$', '.')
}
}
return string
}
override fun getName(index: Int) = Name.guess(getString(index))
override fun getClassId(index: Int): ClassId {
val string = getString(index)
val lastSlash = string.lastIndexOf('/')
val packageName =
if (lastSlash < 0) FqName.ROOT
else FqName(string.substring(0, lastSlash).replace('/', '.'))
val className = FqName(string.substring(lastSlash + 1))
return ClassId(packageName, className, index in localNameIndices)
}
companion object {
val PREDEFINED_STRINGS = listOf(
"kotlin/Any",
"kotlin/Nothing",
"kotlin/Unit",
"kotlin/Throwable",
"kotlin/Number",
"kotlin/Byte", "kotlin/Double", "kotlin/Float", "kotlin/Int",
"kotlin/Long", "kotlin/Short", "kotlin/Boolean", "kotlin/Char",
"kotlin/CharSequence",
"kotlin/String",
"kotlin/Comparable",
"kotlin/Enum",
"kotlin/Array",
"kotlin/ByteArray", "kotlin/DoubleArray", "kotlin/FloatArray", "kotlin/IntArray",
"kotlin/LongArray", "kotlin/ShortArray", "kotlin/BooleanArray", "kotlin/CharArray",
"kotlin/Cloneable",
"kotlin/Annotation",
"kotlin/Iterable", "kotlin/MutableIterable",
"kotlin/Collection", "kotlin/MutableCollection",
"kotlin/List", "kotlin/MutableList",
"kotlin/Set", "kotlin/MutableSet",
"kotlin/Map", "kotlin/MutableMap",
"kotlin/Map.Entry", "kotlin/MutableMap.MutableEntry",
"kotlin/Iterator", "kotlin/MutableIterator",
"kotlin/ListIterator", "kotlin/MutableListIterator"
)
private val PREDEFINED_STRINGS_MAP = PREDEFINED_STRINGS.withIndex().toMap({ it.value }, { it.index })
fun getPredefinedStringIndex(string: String): Int? = PREDEFINED_STRINGS_MAP[string]
}
}
@@ -17,10 +17,10 @@
package org.jetbrains.kotlin.serialization.jvm
import com.google.protobuf.ExtensionRegistryLite
import org.jetbrains.kotlin.load.kotlin.JvmNameResolver
import org.jetbrains.kotlin.serialization.ClassData
import org.jetbrains.kotlin.serialization.PackageData
import org.jetbrains.kotlin.serialization.ProtoBuf
import org.jetbrains.kotlin.serialization.deserialization.NameResolverImpl
import java.io.ByteArrayInputStream
public object JvmProtoBufUtil {
@@ -37,7 +37,7 @@ public object JvmProtoBufUtil {
@JvmStatic
public fun readClassDataFrom(bytes: ByteArray, strings: Array<String>): ClassData {
val input = ByteArrayInputStream(bytes)
val nameResolver = NameResolverImpl.read(input)
val nameResolver = JvmNameResolver(JvmProtoBuf.StringTableTypes.parseDelimitedFrom(input, EXTENSION_REGISTRY), strings)
val classProto = ProtoBuf.Class.parseFrom(input, EXTENSION_REGISTRY)
return ClassData(nameResolver, classProto)
}
@@ -49,7 +49,7 @@ public object JvmProtoBufUtil {
@JvmStatic
public fun readPackageDataFrom(bytes: ByteArray, strings: Array<String>): PackageData {
val input = ByteArrayInputStream(bytes)
val nameResolver = NameResolverImpl.read(input)
val nameResolver = JvmNameResolver(JvmProtoBuf.StringTableTypes.parseDelimitedFrom(input, EXTENSION_REGISTRY), strings)
val packageProto = ProtoBuf.Package.parseFrom(input, EXTENSION_REGISTRY)
return PackageData(nameResolver, packageProto)
}