This is the end of the tutorial about writing a collaborative Web drawing in OCaml. Have a look at the full tutorial if you haven’t read the first part or if you want a version with full colors and links.
In the last part, we’ve seen how to create a client-server Web application in OCaml. The server generates a Web page and sends it together with an OCaml program (compiled to JavaScript) to the browser.
We will now see how to draw on the canvas, program mouse events with Lwt, and do server to client communication on a bus.
We now want to draw something on the page using an HTML5 canvas. The
drawing primitive is defined in the client-side function called
draw
that just draws a line between two given points in a canvas.
To start our collaborative drawing application, we define another
client-side function init_client
, which just draws a single
line for now.
Here is the (full) new version of the program:
Here we use the function Js.string
from Js_of_ocaml’s library to convert an OCaml string
into a JS string.
What sounds a bit weird at first, is a very convenient practice for processing request in a client-server application: If a client value is created while processing a request, it will be evaluated on the client once it receives the response and the document is created; the corresponding side effects are then executed. For example, the line
creates a client value for the sole purpose of performing side
effects on the client. The client value can also be named (as
opposed to ignored via _
), thus enabling server-side
manipulation of client-side values (see below).
(Lwt, Mouse events with Lwt)
We now want to catch mouse events to draw lines with the mouse like
with the brush tools of any classical drawing application. One
solution would be to mimic typical JavaScript code in OCaml; for
example by using function Dom_events.listen
that is the Js_of_ocaml’s equivalent of
addEventListener
. However, this solution is at least as verbose
as the JavaScript equivalent, hence not satisfactory. Js_of_ocaml’s
library provides a much easier way to do that with the help of Lwt.
Replace the init_client
of the previous example by the
following piece of code, then compile and draw!
We use two references x
and y
to record the last mouse
position. The function set_coord
updates those references from
mouse event data. The function compute_line
computes the
coordinates of a line from the initial (old) coordinates to the new
coordinates–the event data sent as a parameter.
The last four lines of code implement the event-handling loop. They
can be read as follows: for each mousedown
event on the canvas,
do set_coord
, then line
(this will draw a dot), then
behave as the first
of the two following lines that terminates:
line
(never
terminates)line
.Functions in Eliom and Js_of_ocaml which do not implement just a
computation or direct side effect, but rather wait for user activity,
or file system access, or need a unforeseeable amount of time to return
are defined with Lwt; instead of returning a value of type a
they return an Lwt thread of type a Lwt.t
.
The only way to use the result of such functions (ones that return
values in the Lwt monad), is to use Lwt.bind
.
It is convenient to define an infix operator like this:
Then the code
is conceptually similar to
but only for functions returning a value in the Lwt monad.
For more clarity, there is a syntax extension for Lwt, defining
let%lwt
to be used instead of let
for Lwt functions:
Lwt.return
creates a terminated thread from a value: Lwt.return : 'a -> 'a Lwt.t
Use it when you must
return something in the Lwt monad (for example in a service handler,
or often after a Lwt.bind
).
An Eliom application is a cooperative program, as the server must be able to handle several requests at the same time. Ocsigen is using cooperative threading instead of the more widely used preemptive threading paradigm. It means that no scheduler will interrupt your functions whenever it wants. Switching from one thread to another is done only when there is a cooperation point.
We will use the term cooperative functions to identify functions
implemented in cooperative way, that is: if something takes
(potentially a long) time to complete (for example reading a value
from a database), they insert a cooperation point to let other threads
run. Cooperative functions return a value in the Lwt monad
(that is, a value of type 'a Lwt.t
for some type 'a
).
Lwt.bind
and Lwt.return
do not introduce cooperation points.
In our example, the function Lwt_js_events.mouseup
may introduce
a cooperation point, because it is unforeseeable when this event
happens. That’s why it returns a value in the Lwt monad.
Using cooperative threads has a huge advantage: given that you know precisely where the cooperation points are, you need very few mutexes and you have very low risk of deadlocks!
Using Lwt is very easy and does not cause trouble, provided you never use blocking functions (non-cooperative functions). Blocking functions can cause the entre server to hang! Remember:
Lwt_unix
instead of module
Unix
,Lwt_preemptive.detach
,Lwt_unix.yield
,Lwt.bind
does not introduce any cooperation point.The module Lwt_js_events
allows easily defining event listeners using Lwt. For example,
Lwt_js_events.click
takes a
DOM element and returns an Lwt thread that will wait until a click
occures on this element.
Functions with an ending “s” (Lwt_js_events.clicks
,
Lwt_js_events.mousedowns
, …) start again waiting after the
handler terminates.
Lwt.pick
behaves as the first thread
in the list to terminate, and cancels the others.
(Client server communication)
In order to see what other users are drawing, we now want to do the following:
We first declare a type, shared by the server and the client, describing the color (as RGB values) and coordinates of drawn lines.
We annotate the type declaration with [@@deriving json]
to allow
type-safe deserialization of this type. Eliom forces you to use this
in order to avoid server crashes if a client sends corrupted data.
This is defined using a JSON plugin for
ppx_deriving, which you
need to install. You need to do that for each type of data sent by the
client to the server. This annotation can only be added on types
containing exclusively basic types, or other types annotated with
[@@deriving json]
.
Then we create an Eliom bus to broadcast drawing events to all client
with the function Eliom_bus.create
.
This function take as parameter the type of
values carried by the bus.
To write draw commands into the bus, we just replace the function
line
in init_client
by:
Finally, to interpret the draw orders read on the bus, we add the
following line at the end of function init_client
:
Now you can try the program using two browser windows to see that the lines are drawn on both windows.
Eliom provides multiple ways for the server to send unsolicited data to the client:
Eliom_bus.t
are broadcasting channels where
client and server can participate (see also «a_api project=”eliom”
subproject=”client” | type Eliom_bus.t » in the client
API).Eliom_react
allows sending
React events from
the server to the client, and conversely.Eliom_comet.Channel.t
are one-way communication channels
allowing finer-grained control. It allows sending Lwt_stream
to the client.
Eliom_react
and Eliom_bus
are implemented over
Eliom_coment
.
It is possible to control the idle behaviour with module
Eliom_comet.Configuration
.
(Widgets with Ocsigen-widgets)
In this section, we add a color picker and slider to choose the size
of the brush. For the colorpicker we used a widget available in
Ocsigen-widgets
.
To install Ocsigen widgets, do:
opam pin add ocsigen-widgets https://github.com/ocsigen/ocsigen-widgets.git
opam install ocsigen-widgets
In Makefile.options
, created by Eliom’s distillery, add
ocsigen-widgets.client
to the
CLIENT_PACKAGES
:
CLIENT_PACKAGES := ... ocsigen-widgets.client
To create the widget, we add the following code in the
init_client
immediately after canvas configuration:
We subsequently add a simple HTML5 slider to change the size of the
brush. Near the canvas_elt
definition, simply add the following
code:
Form.int
is a typing information telling that this input takes
an integer value. This kind of input can only be associated to
services taking an integer as parameter.
We then add the slider to the page body, as follows:
To change the size and the color of the brush, we replace the last
line of the function compute_line
in init_client
by:
Finally, we need to add a stylesheet in the headers of our page. To
easily create the head
HTML element, we use the function
Eliom_tools.F.head
:
You need to install the corresponding stylesheets and images into your
project. The stylesheet files should go to the directory
static/css
.
File graffiti.css is a custom-made CSS file.
You can then test your application (make test.byte
).
Ocsigen-widgets is a Js_of_ocaml library providing useful widgets for your Eliom applications. You can use it for building complex user interfaces.
(Services sending other data types)
To finish the first part of the tutorial, we want to save the current drawing on server side and send the current image when a new user arrives. To do that, we will use the Cairo binding for OCaml.
For using Cairo, first, make sure that it is installed (it is
available as cairo2
via OPAM). Second, add it to the
SERVER_PACKAGES
in your Makefile.options
: SERVER_PACKAGES := ... cairo2
The draw_server
function below is the equivalent of the
draw
function on the server side and the image_string
function outputs the PNG image in a string.
We also define a service that sends the picture:
The module Eliom_registration
defines several modules with
registration functions for a variety of data types. We have already
seen Eliom_registration.Html5
and Eliom_registration.App
.
The module Eliom_registration.String
sends arbitrary byte output
(represented by an OCaml string). The handler function must return
a pair consisting of the content and the content-type.
There are also several other output modules, for example:
Eliom_registration.File
to send static filesEliom_registration.Redirection
to create a redirection towards another pageEliom_registration.Any
to create services that decide late what
they want to sendEliom_registration.Ocaml
to send any OCaml data to be used in a
client side programEliom_registration.Action
to create service with no output
(the handler function just performs a side effect on the server)
and reload the current page (or not). We will see an example of actions
in the next chapter.We now want to load the initial image once the canvas is created. Add the following lines just between the creation of the canvas context and the creation of the slider:
You are then ready to try your graffiti-application by
make test.byte
.
Note, that the Makefile
from the distillery automatically adds
the packages defined in SERVER_PACKAGES
as an extension in your
configuration file local/etc/graffiti/graffiti-test.conf
:
<extension findlib-package="cairo2" />