Skip to content

Instantly share code, notes, and snippets.

@dictav
Last active February 10, 2017 07:59
Show Gist options
  • Save dictav/e30d87c9c3fbb41142929dc67c2393b9 to your computer and use it in GitHub Desktop.
Save dictav/e30d87c9c3fbb41142929dc67c2393b9 to your computer and use it in GitHub Desktop.

GoGenerator generates code that can not be built if the definition file uses the type of another namespace. I compared four proposals to solve this problem.

  • Proposal 1: Add options to namespace
  • Proposal 2: Add base_namespace keyword
  • Proposal 3: Refer to GOPATH
  • Proposal 4: Use command line flag

TL;DR

All proposals, it has some issues, can solve the problem. And I recommend that use command line flag.

Protocol Buffers's way

First I checked how Protocol Buffers deals with it. The Go plug-in of Protocol Buffers does not support multiple pacakge (namespace) in the first place. Also, the definition of namespace like package foo.bar; will be treated as package foo_bar in Go.

I tried with the following two definition files.

// foo.proto
syntax = "proto3";

package foo;

message Request {
}
==========================
// bar.proto
syntax = "proto3";

import "foo.proto";

package bar;

message BarRequest {
  foo.Request request = 1;
  string type = 2;
}

C++ generator can interpret these, but Go generator cannot.

$ protoc --version
libprotoc 3.0.0
$ protoc *.proto --cpp_out=cpp
$ protoc *.proto --go_out=plugins:.
2017/02/08 12:41:34 protoc-gen-go: error:inconsistent package names: foo bar
--go_out: protoc-gen-go: Plugin failed with status code 1.

Is it better to match FlatBuffers to Protocol Buffers? I do not think like that.

Proposal1: Add options to namespace

google/flatbuffers#342 (comment)

prototype: https://github.com/dictav/flatbuffers/tree/fix-go-import-namespace

PROS

  • It is intuitively configurable
  • It can be specified in definition file

CONS

  • It must be described in every namespace and it is cumbersome
  • Mixing multiple base namespace either generate files that can not be imported or fail to generate (by implementation)
  • Confused because of there is the -o option of the command line flag as the factor determining the import path
  • It is hard to treat the same definition file in different projects

Details

First, I considered the two definition files as following:

// protocol.fbs
namespace protocol (go: "github.com/company.com/proj");

table Name {
  first: string;
  last: string;
}

namespace protocol.user (go: "github.com/company.com/proj");

table User {
  name: protocol.Name;
}

Based on these I generated a Go code. Please pay attention to the current directory.

$ pwd
$GOPATH/src/github.com/company.com/proj
$ flatc -g -o flatc_out protocol.fbs
$ tree protocol
flatc_out/protocol/
├── Name.go
└── user
    └── User.go

Let's notice the User table that references another namespace type. For example, the generated code will be as follows:

// $GOPATH/src/github.com/company.com/proj/flatc_out/protocol/user/User.go
package user

import (
	flatbuffers "github.com/google/flatbuffers/go"
	protocol "github.com/company.com/flatc_out/proj/flatc_out/protocol"
)

// ...
func (rcv *User) Name(obj *protocol.Name) *protocol.Name {
	o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
	if o != 0 {
// ...

An import statement is required to use protocol.Name. For GoGenerator, it is only this part that namespace is important.

To import protocol.user package in main.go, write as following:

// $GOPATH/src/github.com/company.com/proj/main.go
package main

import (
  "github.com/company.com/proj/flatc_out/protocol/user"
)

func main () {
  // get User binary
  name := user.GetRootAsUser(buf, 0)
  // do something
}

It works well.

Next, I changed the files to as following:

// protocol.fbs
namespace protocol (go: "github.com/company.com/proj_base");

table Name {
  first: string;
  last: string;
}

namespace protocol.user (go: "github.com/company.com/proj");

table User {
  name: protocol.Name;
}

Based on these I generated a Go code. The flatc command and the directory structure are the same as above. Perhaps User.go will be as following:

// $GOPATH/src/github.com/company.com/flatc_out/proj/flatc_out/protocol/user/User.go
package user

import (
	flatbuffers "github.com/google/flatbuffers/go"
	protocol "github.com/company.com/proj_base/flatc_out/protocol"
)

// ...
func (rcv *User) Name(obj *protocol.Name) *protocol.Name {
	o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
	if o != 0 {
// ...

It could not be built. Please look the line 6. The import path is github.com/company.com/proj_base/flatc_out/protocol, but actually the protocol package is in $GOPATH/src/github.com/company.com/proj/flatc_out/protocol.

Certainly, we can also write files to $GOPATH/src/github.com/company.com/proj_base/flatc_out/protocol. However, it is not good to write the files to a different repository than the current project. To write the files to another project's directory also changes the meaning of flatc -o. I think that all files generated by flatc should be created in the directory specified by -o.

Proposal2: Add base_namespace keyword

prototype: not implemented

It use the base_namespace keyword. Please notice that Proposal2 has some of the same problems as Proposal1.

namespace protocol;
base_namespace (go:"github.com/company.com/proj");

PROS

  • It can be specified in definition file
  • It is not necessary to describe in all namespaces

CONS

  • If there are more than one base_namespace definition, it is necessary to decide the behavior. Error?, Ignore? Or etc ...
  • If there are many definition files, it is hard to tell which file has base_namespace.
  • It is a little troublesome to treat the same definition file in different projects

Proposal3: Refer to GOPATH

prototype: https://github.com/dictav/flatbuffers/tree/fix-go-import-gopath

It compares GOPATH with the output directory and automatically sets the appropriate import path. It worked roughly well.

PROS

  • No need to change IDL and command line flag
  • The range of influence of change is small
  • There is no dependency on project (path)

CONS

  • Users might not set GOPATH. (Users who run flatc - go are not always user of Go at all times.)
  • The destination directory is not always the real import path as it is (the user may want to move the code after generating it).

Proposal4: Use command line flag

google/flatbuffers#342 (comment)

prototype: https://github.com/dictav/flatbuffers/tree/fix-go-import-cmdflag

Pass base namespace to the command line. Although it can add a flag like --namespace_prefix, since the treatment of namespace is different for each language, I propose a way to add it as option to language flags as following:

flatc --go="github.com/company.com/proj" \
      --java="com.company.proj" \
      protocol.fbs

PROS

  • There is no need to change IDL
  • It can uniquely determine the base namespace
  • Not dependent on project (path)

CONS

  • Command line flags become cumbersome

Conclusion

It is possible to make GoGenerator interpret multiple namespace's types in every proposals. Although there are advantages / disadvantages, we can reduce user confusion by increasing restrictions, increasing error handling and maintaining documents. For example in Proposal1 and Proposal2, it prohibit multiple base namespace and display as error then it can avoid troubles that do not work during import. However, I think it is better to use command line flag. It can tell the users how to use the base namespace with flatc --help.

FlatBuffers の Go のジェネレートにおいて他の定義ファイルをインポートするにあたり、GOPATH を考慮したインポートを行うための方法について4つの提案を上げ、それらについて考察した。

  • Proposal1: namespace にオプションを追加する
  • Proposal2: base_namespace キーワードを追加する
  • Proposal3: GOPATH 参照する
  • Proposal4: コマンドラインフラグを利用する

TL;DR

  • どのようなユースケースを想定するかが大事
  • きちんとドキュメント化すればユーザの混乱は減らせる
  • コマンドラインフラグの利用をオススメ

Protocol Buffers's way

最初に Protocol Buffers ではどのように扱っているか確認しました。 Protocol Buffers の Go プラグインはそもそも複数の pacakge (namespace) をサポートしていません。 また、package foo.bar; のような namespace の定義は Go では package foo_bar として扱われます。

次のような2つの定義ファイルで試しました。

// foo.proto
syntax = "proto3";

package foo;

message Request {
}
==========================
// bar.proto
syntax = "proto3";

import "foo.proto";

package bar;

message BarRequest {
  foo.Request request = 1;
  string type = 2;
}

C++ のジェネレータはこれを処理できますが、Go のジェネレータはこれを処理できません。

$ protoc --version
libprotoc 3.0.0
$ protoc *.proto --cpp_out=cpp
$ protoc *.proto --go_out=plugins:.
2017/02/08 12:41:34 protoc-gen-go: error:inconsistent package names: foo bar
--go_out: protoc-gen-go: Plugin failed with status code 1.

FlatBuffersもProtocol Buffers に合わせるのが良いでしょうか? わたしはそのようには考えません。

Proposal1: namespace にオプションを追加する

google/flatbuffers#342 (comment)

prototype: https://github.com/dictav/flatbuffers/tree/fix-go-import-namespace

PROS

  • 直感的に設定できる

CONS

  • 全ての namespace に記述する必要があり煩雑である
  • 複数の base_namespace が混在するとインポートできないファイルをジェネレートする
  • -o オプションの有無によってインポートパスが変わるので混乱する

Details

最初に以下のような定義を考えます。

// protocol.fbs
namespace protocol (go: "github.com/company.com/proj");

table Name {
  first: string;
  last: string;
}

namespace protocol.user (go: "github.com/company.com/proj");

table User {
  name: protocol.Name;
}

これを元にGoのコードをジェネレートします。 カレントディレクトリに注意してください。

$ pwd
$GOPATH/src/github.com/company.com/proj
$ flatc -g -o flatc_out protocol.fbs
$ tree protocol
flatc_out/protocol/
├── Name.go
└── user
    └── User.go

ジェネレートされたコードは例えば以下のようになるでしょう。 他のネームスペースの型を参照する User table に注目します。 他のネームスペースの型を参照するために import 文が必要になっている部分です。 Goにとって namespace が重要なのはこの部分だけです。

// $GOPATH/src/github.com/company.com/proj/flatc_out/protocol/user/User.go
package user

import (
	flatbuffers "github.com/google/flatbuffers/go"
	protocol "github.com/company.com/flatc_out/proj/flatc_out/protocol"
)

// ...
func (rcv *User) Name(obj *protocol.Name) *protocol.Name {
	o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
	if o != 0 {
// ...

例えば main.go で protocol.user package を import するためには以下のようなコードを書きます。

// $GOPATH/src/github.com/company.com/proj/main.go
package main

import (
  "github.com/company.com/proj/flatc_out/protocol/user"
)

func main () {
  // get User binary
  name := user.GetRootAsUser(buf, 0)
  // do something
}

これらはうまく動作します。 もしプロジェクトの名前を proj から proj2 に変更するとビルドできなくなりますが、私はそれはそれほど問題だとは思いません。 もう一度 flatc を実行し、いくつかのソースファイルの import 文を修正するだけです。

次に以下のような定義を考えてみます。

// protocol.fbs
namespace protocol (go: "github.com/company.com/proj_base");

table Name {
  first: string;
  last: string;
}

namespace protocol.user (go: "github.com/company.com/proj");

table User {
  name: protocol.Name;
}

これを元にGoのコードをジェネレートします。flatc コマンドとディレクトリ構造は上と同じとします。 おそらく、User.go は以下のようになるでしょう。

// $GOPATH/src/github.com/company.com/flatc_out/proj/flatc_out/protocol/user/User.go
package user

import (
	flatbuffers "github.com/google/flatbuffers/go"
	protocol "github.com/company.com/proj_base/flatc_out/protocol"
)

// ...
func (rcv *User) Name(obj *protocol.Name) *protocol.Name {
	o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
	if o != 0 {
// ...

しかし、これはビルドできません。 6行目に注目してください。import path は github.com/company.com/proj_base/flatc_out/protocol になっていますが、実際には protocol package は $GOPATH/src/github.com/company.com/proj/flatc_out/protocol にあります。

確かに、私たちは $GOPATH/src/github.com/company.com/proj_base/flatc_out/protocol にファイルを書き出すこともできます。しかし、カレントリポジトリと異なるリポジトリにファイルを書き出すのは良くないでしょう。また、flatc-o の意味も変わってしまいます。flatc がジェネレートしたファイルは全て -o で指定したディレクトリに作成されるべきだと私は思います。

Proposal2: base_namespace キーワードを追加する

prototype: not implemented

新しく base_namespace キーワードを追加する。

namespace protocol;
base_namespace (go:"github.com/company.com/proj");

PROS

  • namespace の挙動を変えなくて良い

CONS

  • 複数の base_namespace を定義した時の挙動を決める必要があります。
  • たくさんの fbs がある時に、どのファイルに base_namespace があるか分からり辛い。

Proposal3: GOPATH 参照する

prototype: https://github.com/dictav/flatbuffers/tree/fix-go-import-gopath

私はこの問題に取り組む時に、最初に GOPATH を利用することを考えました。 ファイルを出力するディレクトリとGOPATHを比較して、適切な import path を自動的に設定するのです。 これはだいたいうまく動きました。

PROS

  • IDL 及びコマンドラインオプションを変える必要がない
  • 変更の影響範囲が小さい

CONS

  • ユーザがGOPATH設定しているとは限らない。(flatc --go を実行する人が常にGoのユーザであるとは限りません。)
  • 出力先のディレクトリが常にそのまま本当の import path であるとは限りません(ユーザはコードをジェネレートした後に移動させたいと考えているかもしれません)。

Proposal4: コマンドラインフラグを利用する

google/flatbuffers#342 (comment)

prototype: https://github.com/dictav/flatbuffers/tree/fix-go-import-cmdflag

コマンドラインフラグに base_namespace を渡します。 --namespace_prefix のようなフラグを追加しても良いですが、言語毎にnamespace の扱いは異なるので、言語のフラグにオプションで追加できるようにする方法を提案します。

flatc --go="github.com/company.com/proj" \
      --java="com.company.proj" \
      protocol.fbs

PROS

  • IDL を変える必要がない
  • base namespace を一意に定めることができる
  • 1つの定義ファイルを複数のプロジェクトで共有できる

CONS

  • コマンドラインフラグが煩雑になる

まとめ

どのやり方でも GoGenerator で異なる namespace の型を利用できるようにすることは可能です。 それぞれ利点/欠点はありますが、制約を設けてエラー処理を増やしたり、ドキュメントを整備することでユーザの混乱は減らすことができるでしょう。 例えば Proposal1とProposal2 では複数の base namespace を禁止してエラーにすることで、インポート時に動作しないようなトラブルを回避することができます。 しかしながら、私はUsage 1つでユーザに必要なことを伝えられるコマンドラインフラグの追加がベターであると思います。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment