👌

手書きでProtocol Buffersの生成コードを書いてみる

2023/01/21に公開

ここではProtocol Buffersの文字列だけのメッセージをエンコード/デコードできるコードを手書きで再現してみます。コード生成はbufを使っています。もしbufを使ったことがなければ、ここのチュートリアルを見ると使い方が分かると思います。

Goプロジェクトを作成

$ mkdir proto-go-example
$ cd proto-go-example
$ go mod init example

protoファイルを定義

$ mkdir -p str/v1
$ touch str/v1/str.proto

str/v1/str.protoを編集して、次のようにします。

syntax = "proto3";

package str.v1;

option go_package = "example/gen/str/v1;strv1";

message Str {
  string str_value = 1;
}

コードの生成

プロジェクトのトップでbuf mod initを実行すると、buf.gen.yamlが生成されます。buf lintでチェックして、何も出ないのを確認してbuf generategen/str/v1/str.pb.goを生成します。

簡単なコードの実行

Protocol Buffersのエンコード/デコードを試してみます。次のようなコードになります。

main.go
package main

import (
	strv1 "example/gen/str/v1"
	"fmt"
	"log"

	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/descriptorpb"
)

func main() {
	f := strv1.File_str_v1_str_proto
	fmt.Println(f.Syntax())
	fmt.Println(f.Path())
	opts := proto.Clone(f.Options()).(*descriptorpb.FileOptions)
	fmt.Println(opts.GetGoPackage())
	fmt.Println(f.Package())

	messages := f.Messages()
	fmt.Println(messages.Len())
	message := messages.Get(0)
	fmt.Println(message.FullName())

	fields := message.Fields()
	fmt.Println(fields.Len())
	field := fields.Get(0)
	fmt.Println(field.FullName())
	fmt.Println(field.Kind())
	fmt.Println(field.Cardinality())
	fmt.Println(field.JSONName())
	fmt.Println(field.TextName())

	s := &strv1.Str{StrValue: "hello, protocol buffers!"}
	out, err := proto.Marshal(s)
	if err != nil {
		log.Fatalln("Failed to encode str", err)
	}
	fmt.Println(out)
	var ss strv1.Str
	if err := proto.Unmarshal(out, &ss); err != nil {
		log.Fatalln(err)
	}
	fmt.Printf("%s\n", ss.StrValue)
}

go run .した実行結果は次のようになります。

proto3
str/v1/str.proto
example/gen/str/v1;strv1
str.v1
1
str.v1.Str
1
str.v1.Str.str_value
string
optional
strValue
str_value
[10 24 104 101 108 108 111 44 32 112 114 111 116 111 99 111 108 32 98 117 102 102 101 114 115 33]
hello, protocol buffers!

手書きで再現してみる

str.pb.gofile_str_v1_str_proto_rawDescが一番謎だと思いますが、protocolbuffers/protobuf-goの実装を見ながら、手で作ってみます。結果は次の通りです。やたらと定数の定義があるのは、internalな定数でインポートできないためです。

main.go
package main

import (
	"fmt"
	"log"
	"reflect"

	"google.golang.org/protobuf/encoding/protowire"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/reflect/protoreflect"
	"google.golang.org/protobuf/runtime/protoimpl"
	"google.golang.org/protobuf/types/descriptorpb"
)

const (
	FileDescriptorProto_Name_field_number        protoreflect.FieldNumber = 1
	FileDescriptorProto_Package_field_number     protoreflect.FieldNumber = 2
	FileDescriptorProto_MessageType_field_number protoreflect.FieldNumber = 4
	FileDescriptorProto_Options_field_number     protoreflect.FieldNumber = 8
	FileDescriptorProto_Syntax_field_number      protoreflect.FieldNumber = 12
)

const DescriptorProto_Field_field_number protoreflect.FieldNumber = 2

const FileOptions_GoPackage_field_number protoreflect.FieldNumber = 11

const (
	FieldDescriptorProto_Name_field_number     protoreflect.FieldNumber = 1
	FieldDescriptorProto_Number_field_number   protoreflect.FieldNumber = 3
	FieldDescriptorProto_Label_field_number    protoreflect.FieldNumber = 4
	FieldDescriptorProto_Type_field_number     protoreflect.FieldNumber = 5
	FieldDescriptorProto_JsonName_field_number protoreflect.FieldNumber = 10
	FieldDescriptorProto_Options_field_number  protoreflect.FieldNumber = 8
)

type Str struct {
	//lint:ignore U1000 state is used in ProtoReflect
	state    protoimpl.MessageState
	StrValue string `protobuf:"bytes,1,opt,name=str_value,json=strValue,proto3" json:"str_value,omitempty"`
}

func (x *Str) ProtoReflect() protoreflect.Message {
	mi := &file_str_v1_str_proto_msgTypes[0]
	if protoimpl.UnsafeEnabled && x != nil {
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
		if ms.LoadMessageInfo() == nil {
			ms.StoreMessageInfo(mi)
		}
		return ms
	}
	return mi.MessageOf(x)
}

var file_str_v1_str_proto_rawDesc []byte

var file_str_v1_str_proto_msgTypes = make([]protoimpl.MessageInfo, 1)

func init() {
	var rawDesc []byte

	rawDesc = protowire.AppendTag(rawDesc, FileDescriptorProto_Name_field_number, protowire.BytesType)
	rawDesc = protowire.AppendBytes(rawDesc, []byte("str/v1/str.proto"))

	rawDesc = protowire.AppendTag(rawDesc, FileDescriptorProto_Package_field_number, protowire.BytesType)
	rawDesc = protowire.AppendBytes(rawDesc, []byte("str.v1"))

	var messageDesc []byte
	messageDesc = protowire.AppendTag(messageDesc, FileDescriptorProto_Name_field_number, protowire.BytesType)
	messageDesc = protowire.AppendBytes(messageDesc, []byte("Str"))

	var fieldDesc []byte
	fieldDesc = protowire.AppendTag(fieldDesc, FieldDescriptorProto_Name_field_number, protowire.BytesType)
	fieldDesc = protowire.AppendBytes(fieldDesc, []byte("str_value"))

	fieldDesc = protowire.AppendTag(fieldDesc, FieldDescriptorProto_Number_field_number, protowire.VarintType)
	fieldDesc = protowire.AppendVarint(fieldDesc, 1)
	fieldDesc = protowire.AppendTag(fieldDesc, FieldDescriptorProto_Label_field_number, protowire.VarintType)
	fieldDesc = protowire.AppendVarint(fieldDesc, uint64(protoreflect.Optional))
	fieldDesc = protowire.AppendTag(fieldDesc, FieldDescriptorProto_Type_field_number, protowire.VarintType)
	fieldDesc = protowire.AppendVarint(fieldDesc, uint64(protoreflect.StringKind))
	fieldDesc = protowire.AppendTag(fieldDesc, FieldDescriptorProto_JsonName_field_number, protowire.BytesType)
	fieldDesc = protowire.AppendBytes(fieldDesc, []byte("strValue"))

	messageDesc = protowire.AppendTag(messageDesc, DescriptorProto_Field_field_number, protowire.BytesType)
	messageDesc = protowire.AppendBytes(messageDesc, fieldDesc)

	rawDesc = protowire.AppendTag(rawDesc, FileDescriptorProto_MessageType_field_number, protowire.BytesType)
	rawDesc = protowire.AppendBytes(rawDesc, messageDesc)

	var optDesc []byte
	optDesc = protowire.AppendTag(optDesc, FileOptions_GoPackage_field_number, protowire.BytesType)
	optDesc = protowire.AppendBytes(optDesc, []byte("example/gen/str/v1;strv1"))

	rawDesc = protowire.AppendTag(rawDesc, FileDescriptorProto_Options_field_number, protowire.BytesType)
	rawDesc = protowire.AppendBytes(rawDesc, optDesc)

	rawDesc = protowire.AppendTag(rawDesc, FileDescriptorProto_Syntax_field_number, protowire.BytesType)
	rawDesc = protowire.AppendBytes(rawDesc, []byte("proto3"))
	file_str_v1_str_proto_rawDesc = rawDesc
}

func main() {
	a := (*Str)(nil)
	t := reflect.TypeOf(a)
	s := &Str{StrValue: "hello, protocol buffers"}
	o := protoimpl.DescBuilder{
		GoPackagePath: "main",
		RawDescriptor: file_str_v1_str_proto_rawDesc,
		NumEnums:      0,
		NumMessages:   1,
		NumExtensions: 0,
		NumServices:   0,
	}.Build()
	f := o.File
	fmt.Println(f.Syntax())
	fmt.Println(f.Path())
	opts := f.Options().(*descriptorpb.FileOptions)
	fmt.Println(opts.GetGoPackage())
	fmt.Println(f.Package())
	messages := f.Messages()
	fmt.Println(messages.Len())
	message := messages.Get(0)
	fmt.Println(message.FullName())

	fields := message.Fields()
	fmt.Println(fields.Len())
	field := fields.Get(0)
	fmt.Println(field.FullName())
	fmt.Println(field.Kind())
	fmt.Println(field.Cardinality())
	fmt.Println(field.JSONName())
	fmt.Println(field.TextName())

	mi := &file_str_v1_str_proto_msgTypes[0]
	mi.GoReflectType = t
	mi.Desc = &o.Messages[0]

	out, err := proto.Marshal(s)
	if err != nil {
		log.Fatalln("Failed to encode str", err)
	}
	fmt.Println(out)
	var ss Str
	if err := proto.Unmarshal(out, &ss); err != nil {
		log.Fatalln(err)
	}

	fmt.Printf("%s\n", ss.StrValue)
}

Discussion