proto文件就是一个数据协议的描绘文件,根据其间的类型信息会被转化成对应的语言(比方java go OC等等)。

proto的长处就是协议字段非常稳定,并且可被追溯。举个栗子,咱们当时是四端同享一个proto库房,然后只需后端更新了字段内容,其他三端也会同样的更新出新的字段内容。这点是相对于json更好的。

可是咱们最近开端尝试kmp了,由于请求有一部分都是proto协议的,可是由于kmp的common层所有的类都必须是kotlin库而不能是jvm的。所以官方proto供给的java类就没办法直接被kmp所引用到。

由于上述原因,所以咱们现在急需的是一个proto插件,可以协助咱们把一个proto文件直接转化成kotlin的。当然咱们第一方针是最好能在kotlin官方找到这样一个才能,直接支撑。其次就是github找一个库房能转化proto到kt的工程。最最不行只能自己着手了啊。

kotlin serialization

serialization是支撑proto转化的,可是这个库并不支撑将proto文件转化成data class。不过serialization对于proto的反序列化支撑仍是非常ok的。并且转化方法也非常的简略。代码如下所示。

    val sample1 = Sample("66666666666")
    val encode = ProtoBuf.Default.encodeToByteArray(sample1)
    val newSample = ProtoBuf.Default.decodeFromByteArray<Sample>(encode)

只需引入kotlinx-serialization插件之后,在增加org.jetbrains.kotlinx:kotlinx-serialization-protobuf:version依赖就可以直接使用了。

可是由于官方库缺少将proto转化成kotlin class的才能,所以咱们一开端并没有直接选用它。只能去从github搜索下有没有其他更好支撑的库。

pbandk

pbandk 库房地址

这个库经过protobuf-java编写了一个proto插件。经过对proto的解析,生成了PluginProtos.CodeGeneratorRequest的数据结构,然后读取其间的字段,转义成一个新的data class。

// Below is mostly verbatim from [protobufsrc]/examples/addressbook.proto except
// some surrounding comments are removed and java/csharp options removed
syntax = "proto3";
package tutorial;
import "google/protobuf/timestamp.proto";
message Person {
    string name = 1;
    int32 id = 2;  // Unique ID number for this person.
    string email = 3;
    enum PhoneType {
        MOBILE = 0;
        HOME = 1;
        WORK = 2;
    }
    message PhoneNumber {
        string number = 1;
        PhoneType type = 2;
    }
    repeated PhoneNumber phones = 4;
    google.protobuf.Timestamp last_updated = 5;
}
// Our address book file is just one of these.
message AddressBook {
    repeated Person people = 1;
}

以上述proto文件为例,pbandk会把这个文件翻译成如下的kt文件。

@file:OptIn(pbandk.PublicForGeneratedCode::class)
package pbandk.examples.addressbook.pb
@pbandk.Export
public data class Person(
    val name: String = "",
    val id: Int = 0,
    val email: String = "",
    val phones: List<pbandk.examples.addressbook.pb.Person.PhoneNumber> = emptyList(),
    val lastUpdated: pbandk.wkt.Timestamp? = null,
    override val unknownFields: Map<Int, pbandk.UnknownField> = emptyMap()
) : pbandk.Message {
    override operator fun plus(other: pbandk.Message?): pbandk.examples.addressbook.pb.Person = protoMergeImpl(other)
    override val descriptor: pbandk.MessageDescriptor<pbandk.examples.addressbook.pb.Person> get() = Companion.descriptor
    override val protoSize: Int by lazy { super.protoSize }
    public companion object : pbandk.Message.Companion<pbandk.examples.addressbook.pb.Person> {
        public val defaultInstance: pbandk.examples.addressbook.pb.Person by lazy { pbandk.examples.addressbook.pb.Person() }
        override fun decodeWith(u: pbandk.MessageDecoder): pbandk.examples.addressbook.pb.Person = pbandk.examples.addressbook.pb.Person.decodeWithImpl(u)
        override val descriptor: pbandk.MessageDescriptor<pbandk.examples.addressbook.pb.Person> by lazy {
            val fieldsList = ArrayList<pbandk.FieldDescriptor<pbandk.examples.addressbook.pb.Person, *>>(5)
            fieldsList.apply {
                add(
                    pbandk.FieldDescriptor(
                        messageDescriptor = this@Companion::descriptor,
                        name = "name",
                        number = 1,
                        type = pbandk.FieldDescriptor.Type.Primitive.String(),
                        jsonName = "name",
                        value = pbandk.examples.addressbook.pb.Person::name
                    )
                )
                add(
                    pbandk.FieldDescriptor(
                        messageDescriptor = this@Companion::descriptor,
                        name = "id",
                        number = 2,
                        type = pbandk.FieldDescriptor.Type.Primitive.Int32(),
                        jsonName = "id",
                        value = pbandk.examples.addressbook.pb.Person::id
                    )
                )
                add(
                    pbandk.FieldDescriptor(
                        messageDescriptor = this@Companion::descriptor,
                        name = "email",
                        number = 3,
                        type = pbandk.FieldDescriptor.Type.Primitive.String(),
                        jsonName = "email",
                        value = pbandk.examples.addressbook.pb.Person::email
                    )
                )
                add(
                    pbandk.FieldDescriptor(
                        messageDescriptor = this@Companion::descriptor,
                        name = "phones",
                        number = 4,
                        type = pbandk.FieldDescriptor.Type.Repeated<pbandk.examples.addressbook.pb.Person.PhoneNumber>(valueType = pbandk.FieldDescriptor.Type.Message(messageCompanion = pbandk.examples.addressbook.pb.Person.PhoneNumber.Companion)),
                        jsonName = "phones",
                        value = pbandk.examples.addressbook.pb.Person::phones
                    )
                )
                add(
                    pbandk.FieldDescriptor(
                        messageDescriptor = this@Companion::descriptor,
                        name = "last_updated",
                        number = 5,
                        type = pbandk.FieldDescriptor.Type.Message(messageCompanion = pbandk.wkt.Timestamp.Companion),
                        jsonName = "lastUpdated",
                        value = pbandk.examples.addressbook.pb.Person::lastUpdated
                    )
                )
            }
            pbandk.MessageDescriptor(
                fullName = "tutorial.Person",
                messageClass = pbandk.examples.addressbook.pb.Person::class,
                messageCompanion = this,
                fields = fieldsList
            )
        }
    }
    public sealed class PhoneType(override val value: Int, override val name: String? = null) : pbandk.Message.Enum {
        override fun equals(other: kotlin.Any?): Boolean = other is Person.PhoneType && other.value == value
        override fun hashCode(): Int = value.hashCode()
        override fun toString(): String = "Person.PhoneType.${name ?: "UNRECOGNIZED"}(value=$value)"
        public object MOBILE : PhoneType(0, "MOBILE")
        public object HOME : PhoneType(1, "HOME")
        public object WORK : PhoneType(2, "WORK")
        public class UNRECOGNIZED(value: Int) : PhoneType(value)
        public companion object : pbandk.Message.Enum.Companion<Person.PhoneType> {
            public val values: List<Person.PhoneType> by lazy { listOf(MOBILE, HOME, WORK) }
            override fun fromValue(value: Int): Person.PhoneType = values.firstOrNull { it.value == value } ?: UNRECOGNIZED(value)
            override fun fromName(name: String): Person.PhoneType = values.firstOrNull { it.name == name } ?: throw IllegalArgumentException("No PhoneType with name: $name")
        }
    }
    public data class PhoneNumber(
        val number: String = "",
        val type: pbandk.examples.addressbook.pb.Person.PhoneType = pbandk.examples.addressbook.pb.Person.PhoneType.fromValue(0),
        override val unknownFields: Map<Int, pbandk.UnknownField> = emptyMap()
    ) : pbandk.Message {
        override operator fun plus(other: pbandk.Message?): pbandk.examples.addressbook.pb.Person.PhoneNumber = protoMergeImpl(other)
        override val descriptor: pbandk.MessageDescriptor<pbandk.examples.addressbook.pb.Person.PhoneNumber> get() = Companion.descriptor
        override val protoSize: Int by lazy { super.protoSize }
        public companion object : pbandk.Message.Companion<pbandk.examples.addressbook.pb.Person.PhoneNumber> {
            public val defaultInstance: pbandk.examples.addressbook.pb.Person.PhoneNumber by lazy { pbandk.examples.addressbook.pb.Person.PhoneNumber() }
            override fun decodeWith(u: pbandk.MessageDecoder): pbandk.examples.addressbook.pb.Person.PhoneNumber = pbandk.examples.addressbook.pb.Person.PhoneNumber.decodeWithImpl(u)
            override val descriptor: pbandk.MessageDescriptor<pbandk.examples.addressbook.pb.Person.PhoneNumber> by lazy {
                val fieldsList = ArrayList<pbandk.FieldDescriptor<pbandk.examples.addressbook.pb.Person.PhoneNumber, *>>(2)
                fieldsList.apply {
                    add(
                        pbandk.FieldDescriptor(
                            messageDescriptor = this@Companion::descriptor,
                            name = "number",
                            number = 1,
                            type = pbandk.FieldDescriptor.Type.Primitive.String(),
                            jsonName = "number",
                            value = pbandk.examples.addressbook.pb.Person.PhoneNumber::number
                        )
                    )
                    add(
                        pbandk.FieldDescriptor(
                            messageDescriptor = this@Companion::descriptor,
                            name = "type",
                            number = 2,
                            type = pbandk.FieldDescriptor.Type.Enum(enumCompanion = pbandk.examples.addressbook.pb.Person.PhoneType.Companion),
                            jsonName = "type",
                            value = pbandk.examples.addressbook.pb.Person.PhoneNumber::type
                        )
                    )
                }
                pbandk.MessageDescriptor(
                    fullName = "tutorial.Person.PhoneNumber",
                    messageClass = pbandk.examples.addressbook.pb.Person.PhoneNumber::class,
                    messageCompanion = this,
                    fields = fieldsList
                )
            }
        }
    }
}
@pbandk.Export
public data class AddressBook(
    val people: List<pbandk.examples.addressbook.pb.Person> = emptyList(),
    override val unknownFields: Map<Int, pbandk.UnknownField> = emptyMap()
) : pbandk.Message {
    override operator fun plus(other: pbandk.Message?): pbandk.examples.addressbook.pb.AddressBook = protoMergeImpl(other)
    override val descriptor: pbandk.MessageDescriptor<pbandk.examples.addressbook.pb.AddressBook> get() = Companion.descriptor
    override val protoSize: Int by lazy { super.protoSize }
    public companion object : pbandk.Message.Companion<pbandk.examples.addressbook.pb.AddressBook> {
        public val defaultInstance: pbandk.examples.addressbook.pb.AddressBook by lazy { pbandk.examples.addressbook.pb.AddressBook() }
        override fun decodeWith(u: pbandk.MessageDecoder): pbandk.examples.addressbook.pb.AddressBook = pbandk.examples.addressbook.pb.AddressBook.decodeWithImpl(u)
        override val descriptor: pbandk.MessageDescriptor<pbandk.examples.addressbook.pb.AddressBook> by lazy {
            val fieldsList = ArrayList<pbandk.FieldDescriptor<pbandk.examples.addressbook.pb.AddressBook, *>>(1)
            fieldsList.apply {
                add(
                    pbandk.FieldDescriptor(
                        messageDescriptor = this@Companion::descriptor,
                        name = "people",
                        number = 1,
                        type = pbandk.FieldDescriptor.Type.Repeated<pbandk.examples.addressbook.pb.Person>(valueType = pbandk.FieldDescriptor.Type.Message(messageCompanion = pbandk.examples.addressbook.pb.Person.Companion)),
                        jsonName = "people",
                        value = pbandk.examples.addressbook.pb.AddressBook::people
                    )
                )
            }
            pbandk.MessageDescriptor(
                fullName = "tutorial.AddressBook",
                messageClass = pbandk.examples.addressbook.pb.AddressBook::class,
                messageCompanion = this,
                fields = fieldsList
            )
        }
    }
}
@pbandk.Export
@pbandk.JsName("orDefaultForPerson")
public fun Person?.orDefault(): pbandk.examples.addressbook.pb.Person = this ?: Person.defaultInstance
private fun Person.protoMergeImpl(plus: pbandk.Message?): Person = (plus as? Person)?.let {
    it.copy(
        phones = phones + plus.phones,
        lastUpdated = lastUpdated?.plus(plus.lastUpdated) ?: plus.lastUpdated,
        unknownFields = unknownFields + plus.unknownFields
    )
} ?: this
@Suppress("UNCHECKED_CAST")
private fun Person.Companion.decodeWithImpl(u: pbandk.MessageDecoder): Person {
    var name = ""
    var id = 0
    var email = ""
    var phones: pbandk.ListWithSize.Builder<pbandk.examples.addressbook.pb.Person.PhoneNumber>? = null
    var lastUpdated: pbandk.wkt.Timestamp? = null
    val unknownFields = u.readMessage(this) { _fieldNumber, _fieldValue ->
        when (_fieldNumber) {
            1 -> name = _fieldValue as String
            2 -> id = _fieldValue as Int
            3 -> email = _fieldValue as String
            4 -> phones = (phones ?: pbandk.ListWithSize.Builder()).apply { this += _fieldValue as Sequence<pbandk.examples.addressbook.pb.Person.PhoneNumber> }
            5 -> lastUpdated = _fieldValue as pbandk.wkt.Timestamp
        }
    }
    return Person(name, id, email, pbandk.ListWithSize.Builder.fixed(phones),
        lastUpdated, unknownFields)
}
@pbandk.Export
@pbandk.JsName("orDefaultForPersonPhoneNumber")
public fun Person.PhoneNumber?.orDefault(): pbandk.examples.addressbook.pb.Person.PhoneNumber = this ?: Person.PhoneNumber.defaultInstance
private fun Person.PhoneNumber.protoMergeImpl(plus: pbandk.Message?): Person.PhoneNumber = (plus as? Person.PhoneNumber)?.let {
    it.copy(
        unknownFields = unknownFields + plus.unknownFields
    )
} ?: this
@Suppress("UNCHECKED_CAST")
private fun Person.PhoneNumber.Companion.decodeWithImpl(u: pbandk.MessageDecoder): Person.PhoneNumber {
    var number = ""
    var type: pbandk.examples.addressbook.pb.Person.PhoneType = pbandk.examples.addressbook.pb.Person.PhoneType.fromValue(0)
    val unknownFields = u.readMessage(this) { _fieldNumber, _fieldValue ->
        when (_fieldNumber) {
            1 -> number = _fieldValue as String
            2 -> type = _fieldValue as pbandk.examples.addressbook.pb.Person.PhoneType
        }
    }
    return Person.PhoneNumber(number, type, unknownFields)
}
@pbandk.Export
@pbandk.JsName("orDefaultForAddressBook")
public fun AddressBook?.orDefault(): pbandk.examples.addressbook.pb.AddressBook = this ?: AddressBook.defaultInstance
private fun AddressBook.protoMergeImpl(plus: pbandk.Message?): AddressBook = (plus as? AddressBook)?.let {
    it.copy(
        people = people + plus.people,
        unknownFields = unknownFields + plus.unknownFields
    )
} ?: this
@Suppress("UNCHECKED_CAST")
private fun AddressBook.Companion.decodeWithImpl(u: pbandk.MessageDecoder): AddressBook {
    var people: pbandk.ListWithSize.Builder<pbandk.examples.addressbook.pb.Person>? = null
    val unknownFields = u.readMessage(this) { _fieldNumber, _fieldValue ->
        when (_fieldNumber) {
            1 -> people = (people ?: pbandk.ListWithSize.Builder()).apply { this += _fieldValue as Sequence<pbandk.examples.addressbook.pb.Person> }
        }
    }
    return AddressBook(pbandk.ListWithSize.Builder.fixed(people), unknownFields)
}

已经是完美翻译了proto文件到kotlin了,并且这个库也写了一个protobuf的序列化和反序列化的库。可是由于在类描绘文件中使用了java8的语法糖,所以这个库的类数量会有点膨胀。导致了其输出的jvm library的体积会有点大。

我全要?

由于上述的种种原因,咱们仍是打算自己写一套protoc插件。将serializationpbandk的长处结合在一起,然后生成一个非常简略的kotlin data class,从而满足kmp工程的需要。

方针也比较简略,就是把上面的proto文件,转化成一个更简略的含有kotlin serialization注解的类,然后把其间的描绘文件还有承继关系都删除,只保留最简略的data class。

package tutorial
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import kotlinx.serialization.protobuf.ProtoPacked
@Serializable
public data class KPerson(
    @ProtoNumber(1)  val name: String = "",
    @ProtoNumber(2)  val id: Int = 0,
    @ProtoNumber(3)  val email: String = "",
    @ProtoNumber(4) @ProtoPacked  val phones: List<tutorial.KPerson.KPhoneNumber> = emptyList(),
    @ProtoNumber(5)  val lastUpdated: com.google.protobuf.KTimestamp? = null,
) :  Function0<String> {
        @Serializable
        public enum class KPhoneType(val value: Int){
            MOBILE(0),
            HOME(1),
            WORK(2),
            UNRECOGNIZED(-1);
            public companion object {
                public val values: List<KPerson.KPhoneType> by lazy { listOf(MOBILE, HOME, WORK) }
                fun fromValue(value: Int): KPerson.KPhoneType = values.firstOrNull { it.value == value } ?: UNRECOGNIZED
                fun fromName(name: String): KPerson.KPhoneType = values.firstOrNull { it.name == name } ?: throw IllegalArgumentException("No KPhoneType with name: $name")
            }
        }
        @Serializable
        public data class KPhoneNumber(
            @ProtoNumber(1)  val number: String = "",
            @ProtoNumber(2)  val type: Int = 0,
) :  Function0<String> {
                fun  typeEnum() : tutorial.KPerson.KPhoneType  = tutorial.KPerson.KPhoneType.fromValue(type)
                override fun invoke(): String ="tutorial.Person.PhoneNumber" 
        }
        override fun invoke(): String ="tutorial.Person" 
}
@Serializable
public data class KAddressBook(
    @ProtoNumber(1) @ProtoPacked  val people: List<tutorial.KPerson> = emptyList(),
) :  Function0<String> {
        override fun invoke(): String ="tutorial.AddressBook" 
}

大概生成的类信息如上图所示。其他对于proto3还有proto中的特殊语法比方oneof等等都进行了一系列的支撑。

syntax = "proto3";
package foobar;
message Value {
    oneof value {
        int32 int_val = 1;
        string str_val = 2;
    }
}

翻译出来的kotlin内容如下。

package foobar
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import kotlinx.serialization.protobuf.ProtoPacked
@Serializable
public data class KValue(
    @ProtoNumber(1) private val intVal: Int?  = null,
    @ProtoNumber(2) private val strVal: String?  = null,
) :  Function0<String> {
        @delegate:Transient
        private val valueNumber by lazy { 
            if( intVal != null) {
                0 
            } else if( strVal != null){
                1 
            } else {
                -1
            }
        }
        public sealed class KValue(val value:Int)
        public object KIntVal : KValue (0)
        public object KStrVal : KValue (1)
        fun <T> valueValue() : T? {
            if(intVal != null){
                return intVal  as T
            } else if(strVal != null){
                return strVal  as T
            } else { return null }
        }
        fun valueType(): KValue ? = valueValues.firstOrNull { it.value == valueNumber }
        companion object {
            val valueValues : List<KValue> by lazy {
                listOf(KIntVal,KStrVal)
            }
        }
        override fun invoke(): String ="foobar.Value" 
}

总结

库房后续会考虑将这个库直接开源出来。真的特别感谢pbandk,写的非常的牛逼。