Implementing an Echo Server in Coq with coqffi.1.0.0
In this article, we will demonstrate how coqffi
can be used to
implement an echo server, i.e., a TCP server which sends back any
input it receives from its clients. In addition to coqffi
, you will need to
install coq-simple-io
. The latter is available in the released
repository
of the Opam Coq Archive .
opam install coq-coqffi coq-simple-io
Besides, you can download the source tree presented in this article if you want to try to read the source directly, or modify it to your taste.
Project Layout
Before diving too much into the implementation of our echo server, we first give an overview of the resulting project’s layout. Since we aim at implementing a program, we draw our inspiration from the idiomatic way of organizing a OCaml project.
We have three directories at the root of the project.
ffi/
contains the low-level OCaml code: It provides an OCaml library (ffi
), and a Coq theory (FFI
) which gathers the FFI modules generated bycoqffi
.src/
contains the Coq implementation of our echo server: It provides a Coq theory (Echo
) which depends on theFFI
theory theSimpleIO
theory ofcoq-simple~io
. This theory provides the implementation of our echo server in Coq.bin/
contains the pieces of code to get an executable program: It contains a Coq module (echo.v
) which configures and uses the extraction mechanism to generate an OCaml module (echo.ml
). This OCaml module can be compiled to get an executable program.
Note that we could have decided to only have one Coq theory. We could
also have added a fourth directory (theories/
) for formal
verification specific code, but this is out of the scope of this
tutorial.
Overall, we use dune
to compile and compose the different parts of
the echo server. dune
has a native —yet unstable at the time of
writing— support for building Coq projects, with very convenient
stanzas like coq.theory
and coq.extraction
.
The following graph summarizes the dependencies between each component (plain arrows symbolize software dependencies).
We enable Coq-related stanza with (using coq 0.2)
in the
dune-project
. file.
(lang dune 2.7)
(using coq 0.2)
The rest of this tutorial proceeds by diving into each directory.
FFI Bindings
Our objective is to implement an echo server, i.e., a server which (1) accepts incoming connections, and (2) sends back any incoming messages. We will consider two classes of effects. One is related to creating and manipulating TCP sockets. The other is dedicated to process management, more precisely to be able to fork when receiving incoming connections.
Therefore, the ffi
library will provide two modules. Likewise, the
FFI
theory will provide two analogous modules generated by coqffi
.
In the ffi/
directory, we add the following stanza to the dune
file.
(library
(name ffi)
(libraries unix))
dune
will look for any .ml
and .mli
files within the directory and will
consider they belong to the ffi
library. We use the
unix
library
to implement the features we are looking for.
Then, we add the following stanza to the dune
file of the ffi/
directory.
(coq.theory
(name FFI))
This tells dune
to look for .v
file within the ffi/
directory,
in order to build them with Coq. A nice feature of dune
is that if we
automatically generate Coq files, they will be automatically “attached” to this
theory.
Sockets
Sockets are boring. The following OCaml module interface provides the necessary type and functions to manipulate them.
type socket_descr
val open_socket : string -> int -> socket_descr
val listen : socket_descr -> unit
val recv : socket_descr -> string
val send : socket_descr -> string -> int
val accept_connection : socket_descr -> socket_descr
val close_socket : socket_descr -> unit
Our focus is how to write the interface modules for coqffi
. Since the object
of this tutorial is not the implementation of an echo server in itself, the
implementation details of the ffi
library will not be discussed, but is
provided at the end of this article.
dune
generates .cmi
files for the .mli
files of our library, and
provides the necessary bits to easily locate them. Besides, the
action
stanza can be used here to tell to dune
how to generate the
module Socket.v
from file.cmi
. We add the following entry to
ffi/dune
.
(rule
(target Socket.v)
(action (run coqffi %{cmi:socket} -o %{target})))
We call coqffi
without any feature-related command-line argument,
which means only the simple-io
feature is enabled. As a consequence,
the socket_descr
type is axiomatized in Coq, and in addition to a
MonadSocket
monad, coqffi
will generate an instance for this monad
for the IO
monad of coq-simple-io
.
The stanza generates the following Coq module.
(* This file has been generated by coqffi. *)
Set Implicit Arguments.
Unset Strict Implicit.
Set Contextual Implicit.
Generalizable All Variables.
Close Scope nat_scope.
From CoqFFI Require Export Extraction.
From SimpleIO Require Import IO_Monad.
Axiom socket_descr : Type.
Extract Constant socket_descr => "Ffi.Socket.socket_descr".
(** * Impure Primitives *)
(** ** Monad Definition *)
Class MonadSocket (m : Type -> Type) : Type :=
{ open_socket : string -> i63 -> m socket_descr
; listen : socket_descr -> m unit
; recv : socket_descr -> m string
; send : socket_descr -> string -> m i63
; accept_connection : socket_descr -> m socket_descr
; close_socket : socket_descr -> m unit
}.
(** ** [IO] Instance *)
Axiom io_open_socket : string -> i63 -> IO socket_descr.
Axiom io_listen : socket_descr -> IO unit.
Axiom io_recv : socket_descr -> IO string.
Axiom io_send : socket_descr -> string -> IO i63.
Axiom io_accept_connection : socket_descr -> IO socket_descr.
Axiom io_close_socket : socket_descr -> IO unit.
Extract Constant io_open_socket
=> "(fun x1 x2 k__ -> k__ ((Ffi.Socket.open_socket x1 x2)))".
Extract Constant io_listen => "(fun x1 k__ -> k__ ((Ffi.Socket.listen x1)))".
Extract Constant io_recv => "(fun x1 k__ -> k__ ((Ffi.Socket.recv x1)))".
Extract Constant io_send
=> "(fun x1 x2 k__ -> k__ ((Ffi.Socket.send x1 x2)))".
Extract Constant io_accept_connection
=> "(fun x1 k__ -> k__ ((Ffi.Socket.accept_connection x1)))".
Extract Constant io_close_socket
=> "(fun x1 k__ -> k__ ((Ffi.Socket.close_socket x1)))".
Instance IO_MonadSocket : MonadSocket IO :=
{ open_socket := io_open_socket
; listen := io_listen
; recv := io_recv
; send := io_send
; accept_connection := io_accept_connection
; close_socket := io_close_socket
}.
(* The generated file ends here. *)
Process Management
In order to avoid a client to block the server by connecting to it without sending anything, we can fork a new process for each client.
type identity = Parent of int | Child
val fork : unit -> identity
This time, the proc.mli
module interface introduces a transparent
type, /i.e./, it also provides its definition. This is a good use case
for the transparent-types
feature of coqffi
. In the stanza for
generating Proc.v
, we enable it with the -ftransparent-types
command-line argument, like this.
(rule
(target Proc.v)
(action (run coqffi -ftransparent-types %{cmi:proc} -o %{target})))
which generates the following Coq module.
(* This file has been generated by coqffi. *)
Set Implicit Arguments.
Unset Strict Implicit.
Set Contextual Implicit.
Generalizable All Variables.
Close Scope nat_scope.
From CoqFFI Require Export Extraction.
From SimpleIO Require Import IO_Monad.
Inductive identity : Type :=
| Parent (x0 : i63) : identity
| Child : identity.
Extract Inductive identity => "Ffi.Proc.identity"
[ "Ffi.Proc.Parent" "Ffi.Proc.Child" ].
(** * Impure Primitives *)
(** ** Monad Definition *)
Class MonadProc (m : Type -> Type) : Type := { fork : unit -> m identity
}.
(** ** [IO] Instance *)
Axiom io_fork : unit -> IO identity.
Extract Constant io_fork => "(fun x1 k__ -> k__ ((Ffi.Proc.fork x1)))".
Instance IO_MonadProc : MonadProc IO := { fork := io_fork
}.
(* The generated file ends here. *)
We now have everything we need to implement an echo server in Coq.
Implementing an Echo Server
Our implementation will be part of a dedicated Coq theory, called Echo
.
This is done easily a dune
file in the src/
directory, with the
following content.
(coq.theory
(name Echo)
(theories FFI))
In the rest of this section, we will discuss the content of the unique module of this theory. Hopefully, readers familiar with programming impurity by means of monads will not find anything particularly surprising here.
Let us start with the inevitable sequence of import commands. We use
the Monad
and MonadFix
typeclasses of ExtLib
, and our
FFI modules from the FFI
theory we have previously defined.
From ExtLib Require Import Monad MonadFix.
From FFI Require Import Proc Socket.
Letting Coq guess the type of unintroduced variables using the `
annotation (e.g., in presence of `{Monad m}
, Coq understands m
is of type Type -> Type
) is always nice, so we enable it.
Generalizable All Variables.
We enable the monad notation provided by ExtLib
. In this article, we
prefer the let*
notation (as recently introduced by OCaml) over the
<-
notation of Haskell, but both are available.
Import MonadLetNotation.
Open Scope monad_scope.
Then, we define a notation to be able to define local, monadic
recursive functions using the mfix
combinator of the MonadFix
typeclass.
Notation "'let_rec*' f x ':`' p 'in' q" :`
(let f :` mfix (fun f x `> p) in q)
(at level 61, x pattern, f name, q at next level, right associativity).
Note that mfix
does /not/ check whether or not the defined function
will terminate (contrary to the fix
keyword of Coq). This is
fortunate because in our case, we do not want our echo server to
converge, but rather to accept an infinite number of connections.
We can demonstrate how this notation can be leveraged by defining a generic TCP server, parameterized by a handler to deal with incoming connections.
Definition tcp_srv `{Monad m, MonadFix m, MonadProc m, MonadSocket m}
(handler : socket_descr -> m unit)
: m unit :=
let* srv := open_socket "127.0.0.1" 8888 in
listen srv;;
let_rec* tcp_aux _ :=
let* client := accept_connection srv in
let* res := fork tt in
match res with
| Parent _ => close_socket client >>= tcp_aux
| Child => handler client
end
in
tcp_aux tt.
The handler for the echo server is straightforward: it just reads incoming bytes from the socket, sends it back, and closes the socket.
Definition echo_handler `{Monad m, MonadSocket m} (sock : socket_descr)
: m unit :=
let* msg := recv sock in
send sock msg;;
close_socket sock.
Composing our generic TCP server with our echo handler gives us an echo server.
Definition echo_server `{Monad m, MonadFix m, MonadProc m, MonadSocket m}
: m unit :=
tcp_srv echo_handler.
Because coqffi
has generated typeclasses for the impure primitives
of proc.mli
and socket.mli
, echo_server
is polymorphic, and can
be instantiated for different monads. When it comes to extracting our
program, we will generally prefer the IO
monad of coq-simple-io
.
But we could also imagine verifying the client handler with FreeSpec,
or the generic TCP server with Interaction Trees (which support
diverging computations). Overall, we can have different verification
strategies for different parts of our program, by leveraging the most
relevant framework for each part, yet being able to extract it in an
efficient form.
The next section shows how this last part is achieved using, once again, a convenient stanza of dune.
Extracting and Building an Executable
The 0.2
version of the Coq-related stanzas of dune
provides the
coq.extraction
stanza, which can be used to build a Coq module
expected to generate ml
files.
In our case, we will write bin/echo.v
to extract the echo_server
in a echo.ml
module, and uses the executable
stanza of dune
to
get an executable from this file. To achieve this, the bin/dune
file simply requires these two stanzas.
(coq.extraction
(prelude echo)
(theories Echo)
(extracted_modules echo))
(executable
(name echo)
(libraries ffi))
We are almost done. We now need to write the echo.v
module, which
mostly consists of (1) providing a MonadFix
instance for the IO
monad, (2) using the IO.unsafe_run
function to escape the IO
monad, (3) calling the Extraction
command to wrap it up.
From Coq Require Extraction.
From ExtLib Require Import MonadFix.
From SimpleIO Require Import SimpleIO.
From Echo Require Import Server.
Instance MonadFix_IO : MonadFix IO :=
{ mfix := @IO.fix_io }.
Definition main : io_unit :=
IO.unsafe_run echo_server.
Extraction "echo.ml" main.
Since we are using the i63
type (signed 63bits integers) of the
CoqFFI
theory, and since i63
is implemented under the hood with Coq
primitive integers, we also need to provide a Uint63
module with a
of_int
function. Fortunately, this module is straightforward to
write.
let of_int x = x
And voilà. A call to dune
at the root of the repository will
build everything (Coq and OCaml alike). Starting the echo server
is as simple as
dune exec bin/echo.exe
And connecting to it can be achieved with a program like telnet
.
$ telnet 127.0.0.1 8888
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
hello, echo server!
hello, echo server!
Connection closed by foreign host.
Appendix
The Socket
OCaml Module
There is not much to say, except that (as already stated) we use the
Unix
module to manipulate sockets, and we attach to each socket a
buffer to store incoming bytes.
let buffer_size = 1024
type socket_descr = {
fd : Unix.file_descr;
recv_buffer : bytes;
}
let from_fd fd =
let rbuff = Bytes.create buffer_size in
{ fd ` fd; recv_buffer ` rbuff }
let open_socket hostname port =
let open Unix in
let addr = inet_addr_of_string hostname in
let fd = socket PF_INET SOCK_STREAM 0 in
setsockopt fd SO_REUSEADDR true;
bind fd (ADDR_INET (addr, port));
from_fd fd
let listen sock = Unix.listen sock.fd 1
let recv sock =
let s = Unix.read sock.fd sock.recv_buffer 0 buffer_size in
Bytes.sub_string sock.recv_buffer 0 s
let send sock msg =
Unix.write_substring sock.fd msg 0 (String.length msg)
let accept_connection sock =
Unix.accept sock.fd |> fst |> from_fd
let close_socket sock = Unix.close sock.fd
The Proc
OCaml Module
Thanks to the Unix
module, the implementation is pretty straightforward.
type identity = Parent of int | Child
let fork x =
match Unix.fork x with
| 0 -> Child
| x -> Parent x