2021年10月6日水曜日

go で grpc の proto ファイルから生成されたソースをローカル利用する 備忘録

ウィンドウズ環境にて grpc の protoc から go のソースを生成してビルドするまでに、えらいハマったので、その備忘録。
まず、grpc の go チュートリアルを読みましたが、唐突に source_relative という未定義の単語が出てきたり、キーワードが、どこの項目と連動しているのか理解できなくて、ちんぷんかんぷんでした。また、生成されたパッケージを利用する方法がわからず、エラー頻出で、体力消耗しました。普段からgoに親しんでいる人にとっては簡単な事なんでしょうけど、普段goを使わない自分には的確な情報にたどり着く手段が少なくて、正解にたどり着くまでに3日ほど浪費しました。
以下のディレクトリ構成で作業をしている前提で話を進めます。
C:/Projects/Hello/
         bin/                           ... インストールで追加される exe のディレクトリ
         pkg/                           ... 追加されたパッケージのディレクトリ
         proto/hello.proto              ... proto 定義
         my.hello/hello.proto           ... proto 定義(仕様変更により自動生成されるのと同じ場所に置く必要あり)
         my.hello/hello.pb.go           ... 自動生成される proto の go ソース
         my.hello/hello_grpc.pb.go   ... 自動生成される grpc の go ソース
         client.go                      ... クライアント・アプリケーションの go ソース
         go.mod                         ...  go mod init で生成される go.mod ファイル
         go.sum                         ...  go mod init で生成される go.sum ファイル
         goenv.bat           ... go を使うための環境設定バッチファイル

参考として goenv.bat の中身
@echo off
set GOROOT=D:\go
echo 'set GO install directory : %GOROOT%'

rem 
rem GOPATH は非推奨なので使用しません
rem 
rem set GOPATH=D:\somewhere
rem echo 'set Build directory : %GOPATH%'

rem
rem とりあえず、x86 をターゲットとします
rem 
echo 'set GOARCH win32 application'
set GOARCH=386

echo 'set GOARCH : %GOARCH%'

rem
rem プロキシーを利用している環境では、プロキシーを設定する必要があります
rem
rem set http_proxy=192.168.0.254:8080
rem set https_proxy=%http_proxy%

rem echo 'set proxy settings : %http_proxy%'

rem 
rem vcpkg により、protobuf をインストールしているため、そちらを流用しています
rem   d:\vcpkg\installed\x64-windows\tools\protobuf 
rem  が、それに該当します。この下に protoc.exe が配置されています。
rem 
set path=%PATH%;%GOROOT%\bin;d:\vcpkg\installed\x64-windows\tools\protobuf

さて、コマンド・プロンプトで、
C:/Projects/Hello ディレクトリ下に移動して作業を行います。
最初に go mod init コマンドを実行し、go.mod ファイルを生成します
C:\Projects\Hello> go mod init test-client
test-client.exe を作成する下地の準備ができました。
次に必要なgrpcのパッケージ類をインストールします
C:\Projects\Hello> go get -u -v google.golang.org/grpc
C:\Projects\Hello> go get -u -v google.golang.org/protobuf/proto
C:\Projects\Hello> go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
C:\Projects\Hello> go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

C:\Projects\Hello\bin 下に protoc-gen-go.exe, protoc-gen-go-grpc.exe が展開されているので
環境変数 PATH に上記 exe が存在するディレクトリを追加します。
C:\Projects\Hello\bin\windows_386 下に exe が展開されていた場合の PATH の追加例は下記
C:\Projects\Hello> set PATH=%PATH%;C:\Projects\Hello\bin\windows_386
パスの追加を怠ると、protoc コマンド実行時に原因不明のエラーに悩まされます。
*.proto の書き方が悪いのか、protoc-gen-go.exe が見つからない事によるエラーなのか
切り分けが容易ではなくなるので、必ず PATH を設定しましょう。

補足:現在は非推奨であるが、GOPATH=c:\gopub というように GOPATHを指定していた場合には
C:\gopub\bin 下に protoc-gen-go.exe, protoc-gen-go-grpc.exe が展開されているかもしれません。
グローバルにインストールした場合は、もしかしたら、%USERPROFILE%\go\bin ディレクトリ下に展開されているかもしれません。
GOPATHが書き込み禁止の Program Files ディレクトリだった場合も、%USERPROFILE%\go\bin に展開されると思われます。

C:\Projects\Hello\proto\hello.proto から
C:\Projects\Hello\ のディレクトリ下に hello_grpc.pb.go 等のソースを展開したいので
下記コマンドを実行します。

仕様が変更されました。
option go_package="[パッケージのパス];[パッケージ名]"
とした上で、proto ファイルは、パッケージのパスに置く事が必須となりました。
option go_package="my.hello;my_hello" と指定した上で
下記コマンドを実行します。
仕様が変更されていました。hello_grpc.pb.go は、hello.pb.go を含まなくなりました。
なので、2つ出力する必要があります。
C:\Projects\Hello> protoc --go-grpc_out=./ my.hello/hello.proto
C:\Projects\Hello> protoc --go_out=./ my.hello/hello.proto

参考として hello.proto の一部抜粋を記載します
syntax = "proto3";

option java_multiple_files = true;
option java_package = "my.hello";
option java_outer_classname = "Hello";

// 仕様が変更されたので設定に注意!
option go_package="my.hello;my_hello";

package hello;

service Hello {

  rpc SayHello (SendRequest) returns (SendResponse) {}

}

message SendRequest {
  string msg = 1;
}
  
message SendResponse {
  string msg = 1;
}
そうすると、
C:\Projects\Hello\my.hello というディレクトリ[パッケージのパス]下に
hello.pb.go ([protoの拡張子を抜いたファイル名].pb.go) というソースが生成されます。

更に仕様が変更されて、下記は解消されました。
仕様変更により下記は、どうなったか知りません
参考として、下記コマンドを実行すると hello.grpc.pb.go も生成されます

C:\Projects\Hello> protoc --go_out=plugins=grpc:./ --go-grpc_out=./ proto/hello.proto

しかしながら、hello.grpc.pb.go で定義されている内容は hello.pb.go に定義されているため
hello.grpc.pb.go を出力してしまうと、再定義のエラーが発生します。
従って、こちらは出力しません。
C++のソースを生成する場合は、対応する両方のファイルが必要だったので、言語間でインターフェイスの統一が取れていないと感じました。


出力されたパッケージをローカルパッケージとして利用するために下記コマンドを実行する必要があります。
C:\Projects\Hello> go mod edit --require my.hello@v0.0.0-local --replace my.hello@v0.0.0-local=./my.hello
コマンド中の v0.0.0 はバージョン情報です。公開して管理するわけではないので、version 0.0.0 の指定をしています。
コマンド中の -local は、ローカルパッケージだよというオマジナイです。
コマンド中で指定するパッケージ名は、あくまで protoファイル中で指定した option go_package="my.hello" を指定します。
go のソースに展開された時にパッケージ名が my_hello へ変更されていたとしても、元の名前を指定します。

このコマンドを実行すると、go.mod ファイルへ
   require () 節中に my.hello v0.0.0-local の行が追加され、
   replace () 節中に my.hello v0.0.0-local => ./my.hello の行が追加されます。
これにより、my.hello が外部モジュールから、ローカルの ./my.hello ディレクトリへとマッピング検索されるようになります。

仕様が変更されました
go ソース内での go_package は、proto ファイルで "my.hello" を指定していますが
"my_hello" という . を _ に置換した名前に変換される仕様となっています。
尚、proto ファイルで指定した package の名前は、go では利用されません。

これでようやく、client.go ファイルに
 import (
   myhello "my.hello"
 )
と書いて、インポートが可能になります。
名前が紛らわしいので、import 時にエイリアス指定("myhello")しておきましょう。パッケージ名に振り回されなくて済みます。
風が吹いたから、雨が降ったから、というような理由で、仕様がコロコロと変更されます。エイリアス指定しておくのが吉だと思います。
大事なことだから、もう一度書きます。
風が吹いたから、雨が降ったから、というような理由で、仕様がコロコロと変更されます。エイリアス指定しておくのが吉だと思います。

go build するのには、もう一手間が必要です。
C:\Projects\Hello> cd my.hello
C:\Projects\Hello\my.hello> go mod init my_hello
C:\Projects\Hello\my.hello> go mod tidy
C:\Projects\Hello\my.hello> cd ..
これで、protoc から生成された go パッケージが利用できるようになり、go build が可能になりました。

整理すると import myhello "my.hello" は、
   go.mod の require の my.hello v0.0.0-local で参照され、
   go.mod の replace 指定によりローカルの ./my.hello に置き換えられ
   ローカルディレクトリ ./my.hello のソース my_hello パッケージが読み込まれ、
   my_hello を myhello というエイリアスにより参照する事ができる、
という流れになります。

疲れた。ほんとに疲れた。

2021/12/07 追記:書いたとたんに仕様がどんどん変わっていく。疲れる。ほんとに疲れる。コンパイルする毎に仕様が変わっていてコンパイルできる状態にもっていくのに、時間を取られる。素晴らしいツールには違いないが、毎回コンパイルできる状態にもっていくのに時間的リソースの消費を余儀なくされるため、生産性の高いツールという位置づけから生産性の低いツールというレッテルを貼りたくなる。ツールを書いているやつは、「API design for C++」を読めや!!!!と、ぼやきたくなる。
Read "API design for C++"
please!
コマンド実行したら、android studio の gradle コマンド同様に、オプション変更の予告があります。そのうち、この内容も変更される事でしょう。
2021/12/08 追記: なんか、修正に次ぐ修正に次ぐ修正で、書いてる事の整合性が崩れてる…。もう疲れたので、お家帰りたい