ocaml-rpc is a library that provides remote procedure calls (RPC)
using XML or JSON as transport encodings. The transport mechanism itself
is outside the scope of this library as all conversions are from and to
strings.
An RPC value is defined as follow:
type t =
Int of int64
| Int32 of int32
| Bool of bool
| Float of float
| String of string
| DateTime of string
| Enum of t list
| Dict of (string * t) list
| NullThe idea behind ocaml-rpc is to generate type definitions that can be used to
convert values of a given type to and from their RPC representations.
In order to do so, it is sufficient to add [@@deriving rpcty] to the
corresponding type definition. Hence :
type t = ... [@@deriving rpcty]This will give a value typ_of_t of type Rpc.Types.typ, which can be used in
conjunction with the Rpcmarshal module to:
-
Convert values of type
tto values of typeRpc.t:let rpc_of_t t = Rpcmarshal.marshal typ_of_t t
-
Convert values of type
Rpc.tto values of typet:let t_of_rpc rpc = Rpcmarshal.unmarshal typ_of_t rpc
Optionally, it is possible to have different field name in the OCaml
type (if it is a record) and in the dictionary argument (the first
elements of Dict):
type t = { foo: int [@key "type"]; bar: int [@key "let"]; } [@@deriving rpcty]This will replace "foo" by "type" and "bar" by "let" in the RPC representation. This is particularly useful when you want to integrate with an existing API and the field names are not valid OCaml identifiers.
The library also provides the [@@deriving rpc] ppx, which is similar to
rpcty, but directly generates the conversion functions.
type t = ... [@@deriving rpc]will give two functions:
-
A function to convert values of type
tto values of typeRpc.t:val rpc_of_t : t -> Rpc.t -
A function to convert values of type
Rpc.tto values of typet:val t_of_rpc : Rpc.t -> (t,string) Result.result
It also supports the @key annotations for having different field names:
type t = { foo: int [@key "type"]; bar: int [@key "let"]; } [@@deriving rpc]ocaml-rpc currently support two protocols: XMLRPC and JSON(RPC). Function
signatures are:
val Xmlrpc.to_string : Rpc.t -> string
val Xmlrpc.of_string : string -> Rpc.t
val Jsonrpc.to_string : Rpc.t -> string
val Jsonrpc.of_string : string -> Rpc.tSo if you want to marshal a value x of type t to JSON, you can use the following function:
Jsonrpc.to_string (rpc_of_t x)The Idl module makes it possible to define an abstract interface in OCaml
using the following pattern:
module CalcInterface(R : Idl.RPC) = struct
open R
let int_p = Idl.Param.mk Rpc.Types.int
let add = R.declare "add"
["Add two numbers"]
(int_p @-> int_p @-> returning int_p Idl.DefaultError.err)
let mul = R.declare "mul"
["Multiply two numbers"]
(int_p @-> int_p @-> returning int_p Idl.DefaultError.err)
let implementation = implement
{ Idl.Interface.name = "Calc"; namespace = Some "Calc"; description = ["Calculator supporting addition and multiplication"]; version = (1,0,0) }
endThen we can generate various "bindings" from it by passing a module
implementing the RPC signature to this functor:
-
OCaml bindings for clients or servers can be generated using one of the
GenClient*orGenServer*functors, respectively.For example one can generate an RPC client this way:
module CalcClient : sig val add : (Rpc.call -> Rpc.response) -> int -> int -> (int, Idl.DefaultError.t) result val mul : (Rpc.call -> Rpc.response) -> int -> int -> (int, Idl.DefaultError.t) result end = CalcInterface(Idl.GenClient ())
The functions in the resulting
CalcClientmodule can be used to call their corresponding RPC methods.CalcClientdoes not implement the transport mechanism itself, that should be provided by passing an a rpc function of typeRpc.call -> Rpc.response.CalcClient.add rpc 4 5will marshal the parameters4and5into their RPC representations, construct anRpc.call, pass that call to the givenrpcfunction, and return either anOkcontaining the unmarshalled result or anErrorwith the error description depending on the response returned byrpc.There are variations of the
GenClientmodule:GenClientExnraises an exception in case the response indicates a failure, instead of returning aresult:module CalcClient : sig val add : (Rpc.call -> Rpc.response) -> int -> int -> int val mul : (Rpc.call -> Rpc.response) -> int -> int -> int end = CalcInterface(Idl.GenClientExn ())
and
GenClientExnRpcallows one to specify the rpc function once when constructing the client module:module CalcClient : sig val add : int -> int -> int val mul : int -> int -> int end = CalcInterface(Idl.GenClientExnRpc (struct let rpc = rpc end))
Bindings for a server can be generated in a similar way:
module CalcServer : sig val add : (int -> int -> (int, Idl.DefaultError.t) result) -> unit val mul : (int -> int -> (int, Idl.DefaultError.t) result) -> unit val implementation : Idl.server_implementation end = CalcInterface(Idl.GenServer ())
The implementations of each RPC method should be specified by passing it to the corresponding function in
CalcServer:CalcServer.add (fun a b -> Ok (a + b)); CalcServer.mul (fun a b -> Ok (a * b));
Then we can generate our server from the
implementation(in case ofGenClient,implementationis unused):let rpc : (Rpc.call -> Rpc.response) = Idl.server CalcServer.implementation
Again, the transport mechanism is not implemented by
CalcServer. We just get an rpc function that, given anRpc.call, calls the implementation of that RPC method and performs the marshalling and unmarshalling. It is up to the user of this library to connect thisrpcfunction to a real server that responds to client requests.Here we also have a
GenServerExnfunctor, for server implementations that raise exceptions instead of returning aresult.The
rpclib-lwtandrpclib-asyncpackages provide similar client and server generators that useLwtandAsync, respectively.The
XmlrpcandJsonrpcmodules can be helpful when implementing therpcfunction for an XML-RPC or JSON-RPC client/server: they provide functions for converting rpc requests and responses to/from their respective wire formats. -
Commandline interfaces can be generated using
Cmdlinergen:module CalcCli : sig val implementation : unit -> ((Rpc.call -> Rpc.response) -> (unit -> unit) Cmdliner.Term.t * Cmdliner.Term.info) list end = CalcInterface(Cmdlinergen.Gen ())
We can use the
implementationto construct the CLI. Again, we need to pass anrpcfunction that knows how to make RPC calls.let () = let cmds = (List.map (fun t -> t rpc) (CalcCli.implementation ())) in let open Cmdliner in Term.(exit @@ eval_choice default_cmd cmds)
-
Some generators use the output of
Codegen. This functor generates a structure that contains information about the methods, their parameters, return types, etc. Currently these generators that use the output ofCodegenrequire the method parameters to be named.module CalcInterface(R : Idl.RPC) = struct open R let int_p_1 = Idl.Param.mk ~name:"int1" Rpc.Types.int let int_p_2 = Idl.Param.mk ~name:"int2" Rpc.Types.int let int_p = Idl.Param.mk Rpc.Types.int let add = R.declare "add" ["Add two numbers"] (int_p_1 @-> int_p_2 @-> returning int_p Idl.DefaultError.err) let implementation = implement { Idl.Interface.name = "Calc"; namespace = Some "Calc"; description = ["Calculator supporting addition and multiplication"]; version = (1,0,0) } end module CalcCode : sig val implementation : unit -> Codegen.Interface.t end = CalcInterface(Codegen.Gen ()) let interfaces = Codegen.Interfaces.create ~name:"calc" ~title:"Calculator" ~description:["Interface for a Calculator"] ~interfaces:[CalcCode.implementation ()]
Markdowngencan generate a markdown file documenting these interfaces:let md = Markdowngen.to_string interfaces
Pythongencan generate Python code that contains various classes wrapping a Python implementation, providing typechecking & method dispatch, and a CLI.let code = Pythongen.of_interfaces interfaces |> Pythongen.string_of_ts
The possibilities are not limited to the above generators provided by
ocaml-rpc. Any third-party module implementing the RPC signature can be
used to generate something from an interface defined following the above
pattern. For example, it is possible to write an RPC implementation that
generates a GUI for a given interface.