Dear caml riders, I’m happy to announce the release of ocaml-protoc 4.0, alongside its runtime libraries pbrt, pbrt_yojson, and pbrt_services.
Ocaml-protoc is a pure OCaml implementation of a protobuf compiler and runtime. Protobuf is a binary serialization format and IDL (interface description language) first introduced by Google in the early 00s, and still in pervasive use there and elsewhere. It is faster (to encode/decode) and more compact than JSON, and is designed for backward compatibility on the wire.
Anyway, this new major release is breaking because it finally follows the standard’s semantics about field presence. By default, many fields in protobuf are somewhat optional[1] and are not serialized at all if not present. Ocaml-protoc now tracks this for every non-required field, even scalars, moving towards better compliance with the standard. It does that either via option or via a presence bitfield. Because of this, the generated code has changed significantly and looks more like what the official protoc produces.
In a nutshell, each protobuf message now becomes a private record type with mutable fields and a presence bitfield. All modification and creation for a type foo is done via make_foo and foo_set_<field> functions; presence can be checked with foo_has_<field>. This means the runtime knows which fields have been explicitly modified.
I don’t think ocaml-protoc is 100% compliant with the fine print on default values in proto3, etc. but this is a lot closer than it used to be. Thanks to the work of @lupus there’s also a new option validation layer.
detailed example
Let’s look at this simple example:
syntax = "proto3";
message Person {
string name = 1;
sint64 age = 2;
}
message Store {
string address = 1;
repeated Person employees = 2;
repeated Person clients = 3;
}
message Company {
string name = 1;
repeated Store stores = 2;
repeated Company subsidiaries = 3;
}
(* generated from "orgchart.proto", do not edit *)
(** {2 Types} *)
type person = {
name : string;
age : int64;
}
type store = {
address : string;
employees : person list;
clients : person list;
}
type company = {
name : string;
stores : store list;
subsidiaries : company list;
}
(** {2 Basic values} *)
val default_person :
?name:string ->
?age:int64 ->
unit ->
person
(** [default_person ()] is the default value for type [person] *)
val default_store :
?address:string ->
?employees:person list ->
?clients:person list ->
unit ->
store
(** [default_store ()] is the default value for type [store] *)
val default_company :
?name:string ->
?stores:store list ->
?subsidiaries:company list ->
unit ->
company
(** [default_company ()] is the default value for type [company] *)
(** {2 Formatters} *)
val pp_person : Format.formatter -> person -> unit
(** [pp_person v] formats v *)
val pp_store : Format.formatter -> store -> unit
(** [pp_store v] formats v *)
val pp_company : Format.formatter -> company -> unit
(** [pp_company v] formats v *)
(** {2 Protobuf Encoding} *)
val encode_pb_person : person -> Pbrt.Encoder.t -> unit
(** [encode_pb_person v encoder] encodes [v] with the given [encoder] *)
val encode_pb_store : store -> Pbrt.Encoder.t -> unit
(** [encode_pb_store v encoder] encodes [v] with the given [encoder] *)
val encode_pb_company : company -> Pbrt.Encoder.t -> unit
(** [encode_pb_company v encoder] encodes [v] with the given [encoder] *)
(** {2 Protobuf Decoding} *)
val decode_pb_person : Pbrt.Decoder.t -> person
(** [decode_pb_person decoder] decodes a [person] binary value from [decoder] *)
val decode_pb_store : Pbrt.Decoder.t -> store
(** [decode_pb_store decoder] decodes a [store] binary value from [decoder] *)
val decode_pb_company : Pbrt.Decoder.t -> company
(** [decode_pb_company decoder] decodes a [company] binary value from [decoder] *)
new code:
(** Code for orgchart.proto *)
(* generated from "orgchart.proto", do not edit *)
(** {2 Types} *)
type person = private {
mutable _presence: Pbrt.Bitfield.t; (** presence for 2 fields *)
mutable name : string;
mutable age : int64;
}
type store = private {
mutable _presence: Pbrt.Bitfield.t; (** presence for 1 fields *)
mutable address : string;
mutable employees : person list;
mutable clients : person list;
}
type company = private {
mutable _presence: Pbrt.Bitfield.t; (** presence for 1 fields *)
mutable name : string;
mutable stores : store list;
mutable subsidiaries : company list;
}
(** {2 Basic values} *)
val default_person : unit -> person
(** [default_person ()] is a new empty value for type [person] *)
val default_store : unit -> store
(** [default_store ()] is a new empty value for type [store] *)
val default_company : unit -> company
(** [default_company ()] is a new empty value for type [company] *)
(** {2 Make functions} *)
val make_person :
?name:string ->
?age:int64 ->
unit ->
person
(** [make_person … ()] is a builder for type [person] *)
val copy_person : person -> person
val person_has_name : person -> bool
(** presence of field "name" in [person] *)
val person_set_name : person -> string -> unit
(** set field name in person *)
val person_has_age : person -> bool
(** presence of field "age" in [person] *)
val person_set_age : person -> int64 -> unit
(** set field age in person *)
val make_store :
?address:string ->
?employees:person list ->
?clients:person list ->
unit ->
store
(** [make_store … ()] is a builder for type [store] *)
val copy_store : store -> store
val store_has_address : store -> bool
(** presence of field "address" in [store] *)
val store_set_address : store -> string -> unit
(** set field address in store *)
val store_set_employees : store -> person list -> unit
(** set field employees in store *)
val store_set_clients : store -> person list -> unit
(** set field clients in store *)
val make_company :
?name:string ->
?stores:store list ->
?subsidiaries:company list ->
unit ->
company
(** [make_company … ()] is a builder for type [company] *)
val copy_company : company -> company
val company_has_name : company -> bool
(** presence of field "name" in [company] *)
val company_set_name : company -> string -> unit
(** set field name in company *)
val company_set_stores : company -> store list -> unit
(** set field stores in company *)
val company_set_subsidiaries : company -> company list -> unit
(** set field subsidiaries in company *)
(** {2 Formatters} *)
val pp_person : Format.formatter -> person -> unit
(** [pp_person v] formats v *)
val pp_store : Format.formatter -> store -> unit
(** [pp_store v] formats v *)
val pp_company : Format.formatter -> company -> unit
(** [pp_company v] formats v *)
(** {2 Protobuf Encoding} *)
val encode_pb_person : person -> Pbrt.Encoder.t -> unit
(** [encode_pb_person v encoder] encodes [v] with the given [encoder] *)
val encode_pb_store : store -> Pbrt.Encoder.t -> unit
(** [encode_pb_store v encoder] encodes [v] with the given [encoder] *)
val encode_pb_company : company -> Pbrt.Encoder.t -> unit
(** [encode_pb_company v encoder] encodes [v] with the given [encoder] *)
(** {2 Protobuf Decoding} *)
val decode_pb_person : Pbrt.Decoder.t -> person
(** [decode_pb_person decoder] decodes a [person] binary value from [decoder] *)
val decode_pb_store : Pbrt.Decoder.t -> store
(** [decode_pb_store decoder] decodes a [store] binary value from [decoder] *)
val decode_pb_company : Pbrt.Decoder.t -> company
(** [decode_pb_company decoder] decodes a [company] binary value from [decoder] *)
the precise semantics of presence are, imho, quite messy. ↩︎