以前の記事で、ProtocolBuffersでOCaml/TypeScript間の定義を作成して色々やっていたのですが、いくつか問題が出てきたので書いてみます。

ocaml-protoc-pluginに乗り換えた

前回は ocaml-protocを使いましたが、ocaml-protoc-pluginに乗り換えました。理由としては以下となります。

ts-protoc-gen + protoc標準から、pbjs(protobufjs)に乗り換えた

前はprotocに標準添付されているJavaScript実装と、ts-protoc-genを組み合わせていたのですが、以下のような問題があったので乗り換えました。

発生した問題

今作っているapplicationでは、Electronとbackend server間をWebsocketでつなぎ、RPCとしてJSON-RPCを利用しています。この構成、面倒ではありますが、割と使い勝手がよいのです。しかし、protoファイルから生成した型を使っていると、困る割にいい解決策が無い問題にあたりました。

その問題とは、 OCamlでの型定義とProtocolBufferでの型定義との互換性 です。ナンノコッチャですが、要はocaml-protoc-pluginが生成してくれる型と、protocol_conv_jsonのでのJSON変換が一筋縄では行かない、ということです。

私は大きく2つの問題にあたりましたが、そのうちの一つを例に上げます。

enum Types {
  Unknown = 0;
  String = 1;
  Number = 2;
}
message Foo {
  Types value = 0
}

こんな感じのprotoファイルからOCamlの定義を作成すると、以下のような感じになります(moduleの定義は省略しています)。

open Ocaml_protoc_plugin.Runtime [@@warning "-33"]
module rec Types : sig
  type t = Unknown | String | Number
  val to_int: t -> int
  val from_int: int -> (t, [> Runtime'.Result.error]) result
end
and Foo : sig
  val name': unit -> string
  type t = Types.t
  val to_proto: t -> Runtime'.Writer.t
  val from_proto: Runtime'.Reader.t -> (t, [> Runtime'.Result.error]) result
end

ここでポイントとなるのが、protoファイルにおいてenumと定義した部分と、OCaml版におけるTypes moduleです。OCamlにおいては、Enumを代数的データ型として表すのはごく自然だと思うのですが、問題はProtocol Buffersにおいては、enumは 単なる数値 でしかありません。 to_int とかがそれを物語っています。

これに、protocol_conv_jsonのannotationをつけてみると、以下のようになります。

open Ocaml_protoc_plugin.Runtime [@@warning "-33"]
module rec Types : sig
  type t = Unknown | String | Number [@@deriving protocol ~driver:(module Protocol_conv_json.Json)]
  val to_int: t -> int
  val from_int: int -> (t, [> Runtime'.Result.error]) result
end and Foo : sig
  val name': unit -> string
  type t = { value: Types.t } [@@deriving protocol ~driver:(module Protocol_conv_json.Json)]
  val to_proto: t -> Runtime'.Writer.t
  val from_proto: Runtime'.Reader.t -> (t, [> Runtime'.Result.error]) result
end

annotationが付きました。さて、ここでprotocol_conv_jsonが代数的データ型をどう変換するかと言うと、シンプルに 型名 を使います。上の例だと、 "Unknown" とかになります。普通に変換すると、enumの名前ではなく値を利用する必要があるため、割と分かりづらいエラーになります。

なお、今回はデータ型の構造も超シンプルだったので、手で変換する部分を書きました。書いた後に、pbjsで生成された実装に、enumの名前を利用して変換できるようにする仕組みを見つけました・・・。ドキュメントはちゃんと読みましょう。

異なるシステム間の構造共有は難しい

OpenAPIとかでもそうですが、こういった構造の共有、加えて当初は想定されていない組み合わせを使うと、色々と問題が起こりがちです。今回は、大人しくProtocol Buffersだけを使って実行するような仕組みにしていたら良かった気もしていますが・・・。

protocolbuffersとOCamlでは、 oneof の扱いがどうしてもprotocol_conv_jsonでは対応できない、という問題もありました。この辺り、Protoファイルを型定義の共有としてだけ使い、ProtocolBuffers以外の仕組みで使う際に意外とハマるポイントなのでは?と思いました。 proto3では、JSONとの相互変換が仕様としてありますが、protocol buffers自体の要請ではありません。そのため、各言語からみたら、この変換そのものは必須ではありません。

システム間におけるデータ構造をどうするか?というのについては、これからも色々試していきたいと思います。