OCaml-RPC -- remote procedure calls (RPC) library

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. The odoc generated documentation is available at mirage.github.io/ocaml-rpc/rpclib.

Build Status

RPC types

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
  | Base64 of string
  | Null

Generating code

The 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:

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:

It also supports the @key annotations for having different field names:

type t = { foo: int [@key "type"]; bar: int [@key "let"]; } [@@deriving rpc]

Conversion functions

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.t

So 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)

IDL generator

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) }
end

Then we can generate various "bindings" from it by passing a module implementing the RPC signature to this functor:

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.

Base64 Decoding

The treatment of line feeds (and other characters) in XML-RPC base64-encoded data is underspecified.

By default, this library decodes values using the Base64.decode_exn function of ocaml-base64. This function implements RFC4648 which requires the rejection of non-alphabet characters for security reasons (see section 3.3 and also section 3.1).

This is problematic when communicating with servers that are less strict. For instance, the encode function of the Python xmlrpc.client refers to section 6.8 of RFC2045 to justify inserting a newline character every 76 characters. For this reasons, the functions in Xmlrpc allow the caller to override the base64_decoder. The following declaration gives a rough-and-ready “dangerous” implementation based on the Base64.rfc2045 package. A better implementation would only accept a \n every 76 characters.

let base64_2045_decoder s =
  let open Base64_rfc2045 in
  let buf = Buffer.create 1024 in
  let d = decoder (`String s) in
  let rec go () =
    match decode d with
    | `Flush s -> (Buffer.add_string buf s; go ())
    | `End -> Buffer.contents buf
    (* best-effort *)
    | `Malformed _   (* ignore missing '\r' before '\n', etc. *)
    | `Wrong_padding (* ignore *)
    | `Await -> go ()
  in
  go ()

Building

To build, first install the dependencies:

opam install dune base64 ppxlib async js_of_ocaml-ppx lwt cow cmdliner rresult yojson xmlm

For tests:

opam install alcotest alcotest-lwt