Tutorial¶
This page has two parts: writing and reading a plain message, then
running a typed RPC client and server generated from an interface
schema. Both build on the same runtime and the same generated-code
conventions, so the second part only adds capnp_rpc and a socket.
1 Write and read a message¶
Toolchain (gfortran, fpm, fypp, the capnp tool) is pinned in
pixi.toml:
$ pixi install
$ pixi run build
$ pixi run test
Compile the tutorial schema with the capnp tool, pointing it at the
capnpc-fortran plugin built by fpm:
$ capnp compile -o build/gfortran_*/app/capnpc-fortran schema/addressbook.capnp
This writes addressbook_capnp.f90 next to the schema, one Fortran
module for the file. schema/addressbook.capnp declares:
struct Person {
id @0 :UInt32;
name @1 :Text;
email @2 :Text;
phones @3 :List(PhoneNumber);
# ...
}
struct AddressBook {
people @0 :List(Person);
}
Build a message, serialize it, then deserialize the bytes and read a field back:
program tutorial
use capnp
use addressbook_capnp
implicit none
type(capnp_message_t), target :: msg, rmsg
type(address_book_t) :: book
type(person_t) :: alice
type(capnp_ptr_t) :: people
integer(int8), allocatable :: bytes(:)
character(len=:), allocatable :: name
integer :: err
call capnp_message_init_builder(msg, err)
book = address_book_new_root(msg, err)
people = address_book_people_init(book, 1_int64, err)
alice%p = capnp_list_get_struct(people, 0, err)
call person_id_set(alice, 123_int64, err)
call person_name_set(alice, 'Alice', err)
call capnp_serialize_bytes(msg, bytes, err)
call capnp_deserialize_bytes(bytes, rmsg, err)
book = address_book_read_root(rmsg, err)
people = address_book_people_get(book, err)
alice%p = capnp_list_get_struct(people, 0, err)
call person_name_get(alice, name, err)
print '(a)', name ! Alice
end program tutorial
Two conventions to carry into every program that touches this API:
Messages carry
target. Handles (capnp_ptr_t, and every generated<type>_t) hold a pointer to their owning message, somsgandrmsgabove must both be declaredtarget.Every fallible call returns
err.CAPNP_OK(0) means success; readers never crash on malformed input, they return an error code and defaults instead. Production code checkserrafter each call; this listing omits the checks for brevity.
2 A typed RPC client and server¶
The code generator also emits client stubs and an abstract server base
for interface declarations. This section builds both ends of a
call over a single process, connected by a socket pair, using the
Adder interface from schema/adder.capnp:
interface Adder @0xea01e10cbc414411 {
add @0 (a :Int64, b :Int64) -> (sum :Int64);
}
Compile it the same way as the struct schema above; the generator
adds an adder_capnp module with:
adder_client_t– a client handle wrapping anrpc_cap_t.adder_server_t– an abstract, extensible server base with one deferred procedure,add(self, params, results, err).adder_add_begin/adder_add_wait– fill parameters and send a call, then block until the result returns.
2.1 Implement the server¶
Extend adder_server_t and implement add:
module rpc_adder_impl
use capnp
use adder_capnp
implicit none
private
public :: my_adder_t
type, extends(adder_server_t) :: my_adder_t
contains
procedure :: add => my_add
end type my_adder_t
contains
subroutine my_add(self, params, results, err)
class(my_adder_t), intent(inout) :: self
type(adder_add_params_t), intent(in) :: params
type(adder_add_results_t), intent(in) :: results
integer, intent(out) :: err
err = CAPNP_OK
call adder_add_results_sum_set(results, &
adder_add_params_a_get(params) + &
adder_add_params_b_get(params), err)
end subroutine my_add
end module rpc_adder_impl
The generated dispatch procedure on adder_server_t decodes the
interface and method ordinals from an incoming call and routes to
add – application code never touches ordinals directly.
2.2 Wire up a connection and call it¶
program adder_demo
use capnp
use adder_capnp
use capnp_posix
use capnp_rpc
use rpc_adder_impl
implicit none
type(rpc_conn_t), target :: cli, srv
type(my_adder_t), target :: impl
class(rpc_server_t), pointer :: boot
type(adder_client_t) :: client
type(capnp_message_t), target :: m
type(adder_add_params_t) :: params
type(adder_add_results_t) :: results
integer(int64) :: qid
integer :: fda, fdb, err
! A socketpair stands in for a TCP connection; rpc_conn_init takes
! any connected stream file descriptor.
call px_socketpair(fda, fdb, err)
boot => impl
call rpc_conn_init(srv, fdb, boot)
boot => null()
call rpc_conn_init(cli, fda, boot)
! Bootstrap resolves the typed client; the pipeline cap works
! before the bootstrap call even returns.
call rpc_bootstrap_send(cli, client%cap, err)
call rpc_pump_once(srv, err)
call adder_add_begin(cli, client, m, params, qid, err)
call adder_add_params_a_set(params, 19_int64, err)
call adder_add_params_b_set(params, 23_int64, err)
call rpc_call_send(cli, m, err)
call rpc_pump_once(srv, err)
call adder_add_wait(cli, qid, results, err)
print '(a,i0)', 'sum = ', adder_add_results_sum_get(results) ! 42
call rpc_conn_close(cli)
call rpc_conn_close(srv)
end program adder_demo
Both ends run in this one process for the tutorial; capnp_posix
also exposes px_tcp_listen / px_tcp_accept / px_tcp_connect
for a real client and server on separate hosts, and
rpc_pump_once is meant to sit inside a server’s event loop
(do while (ok); call rpc_pump_once(conn, err); end do) rather than
being called once per exchange as above. The full working version,
including a second call issued against the settled import capability,
is test/test_rpc_typed.f90; :doc:`rpc` covers the call lifecycle
(bootstrap, pipelining, finish/release) in more detail.