Playing with Cap’n Proto
Mark Elvers
2 min read

Categories

  • capnpproto

Tags

  • tunbury.org

Cap’n Proto has become a hot topic recently and while this is used for many OCaml-CI services, I spent some time creating a minimal application.

Firstly create a schema with a single interface whch accepts a file name and returns the content.

interface Foo {
  get      @0 (name :Text) -> (reply :Text);
}

This schema can then be compiled into the bindings for your required language. e.g. capnp compile -o ocaml:. schema.capnp

In practice this need not be done by hand as we can use a dune rule to do this.

(rule
 (targets foo_api.ml foo_api.mli)
 (deps    foo_api.capnp)
 (action (run capnp compile -o %{bin:capnpc-ocaml} %{deps})))

On the server side we now need to extend the automatically generate code to actually implement the interface. This code is largely boilerplate.

module Api = Foo_api.MakeRPC(Capnp_rpc)

open Capnp_rpc.Std

let read_from_file filename = In_channel.with_open_text filename @@ fun ic -> In_channel.input_all ic

let local =
  let module Foo = Api.Service.Foo in
  Foo.local @@ object
    inherit Foo.service

    method get_impl params release_param_caps =
      let open Foo.Get in
      let name = Params.name_get params in
      release_param_caps ();
      let response, results = Service.Response.create Results.init_pointer in
      Results.reply_set results (read_from_file name);
      Service.return response
  end

The server needs to generate the capability file needed to access the service and wait for incoming connections.

let cap_file = "echo.cap"

let serve config =
  Switch.run @@ fun sw ->
  let service_id = Capnp_rpc_unix.Vat_config.derived_id config "main" in
  let restore = Restorer.single service_id (Foo.local) in
  let vat = Capnp_rpc_unix.serve ~sw ~restore config in
  match Capnp_rpc_unix.Cap_file.save_service vat service_id cap_file with
  | Error `Msg m -> failwith m
  | Ok () ->
    traceln "Server running. Connect using %S." cap_file;
    Fiber.await_cancel ()

The client application imports the capability file and calls the service Foo.get.

let run_client service =
  let x = Foo.get service "client.ml" in
  traceln "%S" x

let connect net uri =
  Switch.run @@ fun sw ->
  let client_vat = Capnp_rpc_unix.client_only_vat ~sw net in
  let sr = Capnp_rpc_unix.Vat.import_exn client_vat uri in
  Capnp_rpc_unix.with_cap_exn sr run_client

Where Foo.get is defined like this

module Foo = Api.Client.Foo

let get t name =
  let open Foo.Get in
  let request, params = Capability.Request.create Params.init_pointer in
  Params.name_set params name;
  Capability.call_for_value_exn t method_id request |> Results.reply_get

Run the server application passing it parameters of where to save the private key and which interface/port to listen on.

$ dune exec -- ./server.exe --capnp-secret-key-file ./server.pem --capnp-listen-address tcp:127.0.0.1:7000
+Server running. Connect using "echo.cap".

The .cap looks like this

capnp://sha-256:f5BAo2n_2gVxUdkyzYsIuitpA1YT_7xFg31FIdNKVls@127.0.0.1:7000/6v45oIvGQ6noMaLOh5GHAJnGJPWEO5A3Qkt0Egke4Ic

In another window, invoke the client.

$ dune exec -- ./client.exe ./echo.cap

The full code is available on Github.