はじめに
今回は.proto
に記載されているコメントをランタイムで取得する方法を紹介したいと思います。
// example.proto syntax = "proto3"; import "google/protobuf/descriptor.proto"; option csharp_namespace = "Protobuf.Sample"; // メッセージ定義にカスタムオプションを追加 extend google.protobuf.MessageOptions { string my_option = 50000; } // メッセージ定義 <= ここらへんのコメントをランタイムで取得したい message MyMessage { // 上で定義したカスタムオプションを使用 option (my_option) = "Hello world!"; // メッセージフィールド <= ここらへんのコメントを取得したい string message = 1; }
概要
例えば以下のような.proto
があったとします。
// example.proto syntax = "proto3"; import "google/protobuf/descriptor.proto"; option csharp_namespace = "Protobuf.Sample"; // メッセージ定義にカスタムオプションを追加 extend google.protobuf.MessageOptions { string my_option = 50000; } // メッセージ定義 message MyMessage { // 上で定義したカスタムオプションを使用 option (my_option) = "Hello world!"; // メッセージフィールド string message = 1; }
まず単純にprotoc
で--csharp_out
を指定して出力します。Google.Protobuf
が入っていないと生成コードのコンパイルが通らないので注意です。
$ cd <.protoが入っているフォルダ> $ protoc --proto_path="." --csharp_out="." example.proto
一応XMLドキュメントでコメントがついていたりはしますが、直接コメントをプログラムから取得することはできません。
/// <summary> /// メッセージ定義 /// </summary> [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] public sealed partial class MyMessage : pb::IMessage<MyMessage>
ただし上手いこと工夫をすると、これを取得することができるのでそのやり方を紹介したいと思います。
やり方
protocを利用してdescriptor_set
を生成する
下記コマンドを打ち込むことで、descriptor_set
を生成することができます。
$ protoc --proto_path="." --descriptor_set_out="./descriptor_set.desc" --include_source_info --include_imports example.proto
FileDescriptorProtoとは
FileDescriptorProto
は、Protocol Buffersのスキーマ定義を表現するためのメッセージです。具体的には、.proto
ファイルの内容を表現します。メッセージ、列挙型、サービス、フィールドなどの定義が含まれており、コメントもしっかりと含まれています。
github.com
FileDescriptorSet(descriptor_set)とは
FileDescriptorSet(descriptor_set)
は、複数のFileDescriptorProtoをまとめたものです。これは、複数の.protoファイルのスキーマ定義を一つにまとめて扱うために使用されます。protoc
を実行する際に--descriptor_set_out
を指定すると生成することができます。
github.com
protocのインストール先
globalにprotocをインストールして利用することもできます。
Installing protoc | proto-lens
$ brew install protobuf
またGoogle.Protobuf.Tools
をNuGetで取得して、そこに入っているprotoc
を利用もできます。
www.hanachiru-blog.com
protocのオプションについて
ヘルプコマンドを打つと良いでしょう。
$ protoc --help -IPATH, --proto_path=PATH Specify the directory in which to search for imports. May be specified multiple times; directories will be searched in order. If not given, the current working directory is used. If not found in any of the these directories, the --descriptor_set_in descriptors will be checked for required proto file. -oFILE, Writes a FileDescriptorSet (a protocol buffer, --descriptor_set_out=FILE defined in descriptor.proto) containing all of the input files to FILE. --include_imports When using --descriptor_set_out, also include all dependencies of the input files in the set, so that the set is self-contained. --include_source_info When using --descriptor_set_out, do not strip SourceCodeInfo from the FileDescriptorProto. This results in vastly larger descriptors that include information about the original location of each decl in the source file as well as surrounding comments. --csharp_out=OUT_DIR Generate C# source file. @<filename> Read options and filenames from file. If a relative file path is specified, the file will be searched in the working directory. The --proto_path option will not affect how this argument file is searched. Content of the file will be expanded in the position of @<filename> as in the argument list. Note that shell expansion is not applied to the content of the file (i.e., you cannot use quotes, wildcards, escapes, commands, etc.). Each line corresponds to a single argument, even if it contains spaces.
今回利用したものは上記になります。
FileDescriptorSetを利用してコメントを取り出す
先ほど生成したFileDescriptorSetをランタイムで読み込み、コメントを取得します。
using var descriptorSetFile = File.OpenRead("---/descriptor_set.desc"); var descriptorSet = FileDescriptorSet.Parser.ParseFrom(descriptorSetFile); foreach (var file in descriptorSet.File) { Console.WriteLine($"File Name : {file.Name}"); // SourceCodeInfoは元のソースコードに関するオプションの情報が含まれます。--include_source_infoを有効にして生成された場合にのみ含まれます。 foreach (var location in file.SourceCodeInfo.Location) { if(location.Path == null || location.Path.Count == 0) { continue; } // 要素の前にあるコメント if (location.HasLeadingComments) { Console.WriteLine($"- LeadingComments : {location.LeadingComments}"); } // 要素の後にあるコメント if (location.HasTrailingComments) { Console.WriteLine($"- TrailingComments : {location.TrailingComments}"); } } }
File Name : example.proto - LeadingComments : メッセージ定義にカスタムオプションを追加 - LeadingComments : メッセージ定義 - LeadingComments : 上で定義したカスタムオプションを使用 - LeadingComments : メッセージフィールド
ただネタバレをするとFileDescriptorSet
をそのまま使うと非常に使いづらいです。上記でコメントを取得したとしても、どのmessage
やservice
と紐づいているかを確認するのも一苦労です。
FileDescriptorを利用する
そこでFileDescriptor
というラッパークラスを用います。これはGoogle.Protobuf.Reflection
に入っているclassになります。
// FileDescriptorSetを読み込む using var descriptorSetFile = File.OpenRead("---/descriptor_set.desc"); FileDescriptorSet descriptorSet = FileDescriptorSet.Parser .ParseFrom(descriptorSetFile); // FileDescriptorProtoをFileDescriptorに変換するために一度ByteStringに変換する List<ByteString> byteStrings = descriptorSet.File .Select(x => x.ToByteString()) .ToList(); // FileDescriptorに変換 IReadOnlyList<FileDescriptor> fileDescriptors = FileDescriptor.BuildFromByteStrings(byteStrings); foreach (var fileDescriptor in fileDescriptors) { Console.WriteLine(fileDescriptor.Name); foreach (var fileDescriptorMessageType in fileDescriptor.MessageTypes) { Console.WriteLine(fileDescriptorMessageType.Declaration.LeadingComments); Console.WriteLine(fileDescriptorMessageType.Declaration.TrailingComments); } foreach (var fileDescriptorService in fileDescriptor.Services) { Console.WriteLine(fileDescriptorService.Declaration.LeadingComments); Console.WriteLine(fileDescriptorService.Declaration.TrailingComments); } }
LeadingComments
が前についているコメントで、TrailingComments
が後ろについているコメントです。これでどのServiceやMessageなどについているコメントかどうかを一目瞭然で調べることができました。
カスタムオプションについて
カスタムオプションを読み取るには、少し工夫がいるのでそれも紹介したいと思います。ExtensionRegistry
というものを用意しておいて、FileDescriptor.BuildFromByteStrings
メソッドの引数で渡してあげる必要があります。
// CustomOptionを利用している場合はExtensionRegistryを用意しなければならないので注意 ExtensionRegistry extensionRegistry = [ ExampleExtensions.MyOption ]; // FileDescriptorSetを読み込む using var descriptorSetFile = File.OpenRead("---/descriptor_set.desc"); FileDescriptorSet descriptorSet = FileDescriptorSet.Parser .WithExtensionRegistry(extensionRegistry) .ParseFrom(descriptorSetFile); // FileDescriptorProtoをFileDescriptorに変換するために一度ByteStringに変換する List<ByteString> byteStrings = descriptorSet.File .Select(x => x.ToByteString()) .ToList(); // FileDescriptorに変換 IReadOnlyList<FileDescriptor> fileDescriptors = FileDescriptor.BuildFromByteStrings(byteStrings, extensionRegistry); foreach (var fileDescriptor in fileDescriptors) { Console.WriteLine(fileDescriptor.Name); foreach (var fileDescriptorMessageType in fileDescriptor.MessageTypes) { var options = fileDescriptorMessageType.GetOptions(); if (options != null && options.HasExtension(ExampleExtensions.MyOption)) { var myOption = options.GetExtension(ExampleExtensions.MyOption); Console.WriteLine($"Custom Option (my_option): {myOption}"); } } }
$ dotnet run example.proto Custom Option (my_option): Hello world!