First, cd
into a new directory and initialize the repository:
git init
The Mirage toolchain generates a lot of files automatically as part of the build process. Since those are site-specific, you do not need or want to share them with others. We can make git ignore them by adding them to the .gitignore
file:
cat >.gitignore <<EOF
*.swp
Makefile
_build/
key_gen.ml
log
main.ml
*.xe
*.xl
*.xl.in
*.xml
EOF
The mirage-skeleton
repository contained an intimidating amount of repositories.
This repository is an attempt to simplify the process of getting started with Mirage.
The parts we will need to get our first unikernel running are:
mirage
: the command-line tool used to generate the require build code and configuration files (think of it likemake
)mirage configure
: takes switches like--unix
or--xen
to specify the target of the build.mirage clean
: deletes generated files (likemake clean
)
config.ml
: themirage
tool will look for this specially named file in the current directory.
To find the definitions of Mirage types, look into the ~/.opam/*/lib/mirage/
directory.
The Mirage ecosystem makes use of the functoria domain-specific language for piecing things together, so if you encounter alien syntax, the documentation for that might serve as a helpful utility. For example, the documentation for the @-> syntax.
The compiled unikernel can run a set of "jobs" concurrently using the Lwt lightweight thread module (a library for cooperative concurrency, like Python's "green threads").
Mirage uses OCaml's type system to be able to accomodate parametric compilation (that is, support different backends). Specifically, you need to be familiar with OCaml's "modules" and the concept of "functors".
Each of the unikernel jobs may depend on various Mirage components like the console to provide console input/output (Mirage_console
), or the read-only key-value store (elegantly named V1_LWT.KV_RO
TODO this has been changed in v3).
To make use of these components in your job, your module needs to be "parameterized over them" - that is, you need to implement the job module as a functor to use these components.
Additionally, each job needs to implement a start
function which returns an Lwt handle (for more information, find a tutorial on concurrency in OCaml using Lwt TODO).
Example of the type definition of a job module for a Mirage:
module type Job_t =
sig
val start : unit Lwt.t
end
A very basic implementation of a no-op job (noop.ml
):
module Job =
struct
val start = Lwt.return_unit
end
A config.ml
for the no-op job:
open Mirage
let my_noop_job =
foreign "Noop.Job" (Mirage.job)
(* https://mirage.github.io/functoria/Functoria.html#VALjob *)
let () =
(* Mirage.register takes a string (the name of your unikernel) and a list of jobs to run concurrently *)
Mirage.register "mything"
[ my_noop_job ]
Compiling the no-op unikernel:
mirage configure && make
Running the no-op unikernel:
./mir-mything
Now that we can compile a unikernel, we would like to extend it to print "Hello, World!" to the console. In order to do that we need to make a job that uses the Mirage "console" component.
We make another module to contain this job (hello_world.ml
):
module type Job_t =
(* note that providing a signature / module type like this is entirely optional *)
functor (Console : Mirage_console.S) ->
sig
val start : Console.t -> unit Console.io Lwt.t
end
module Job : Job_t =
functor (Console : Mirage_console.S) ->
struct
let start (my_console : Console.t) =
Lwt.return (Console.log my_console "Hello, World!")
end
And we need to change config.ml
to include our job:
open Mirage
let my_noop_job =
foreign "Noop.Job" (Mirage.job)
(* https://mirage.github.io/functoria/Functoria.html#VALjob *)
let hello_world =
foreign "Hello_world.Job" (Mirage.console @-> Mirage.job)
(* "@->" is Functoria syntax: https://mirage.github.io/functoria/Functoria.html#VAL%28@-%3E%29 *)
(* Mirage.console: https://mirage.github.io/mirage/Mirage.html#VALconsole *)
let () =
(* Mirage.register takes a string (the name of your unikernel) and a list of jobs to run concurrently *)
Mirage.register "mything"
[ my_noop_job
; hello_world $ Mirage.default_console
(* dollar sign: https://mirage.github.io/functoria/Functoria.html#VAL%28$%29 *)
]
Example run:
root@localhost:~/ocaml/mirage-examples# mirage configure && make
ocamlbuild -use-ocamlfind -pkgs functoria.runtime,mirage-console.unix,mirage-types.lwt,mirage-unix,mirage.runtime -tags "warn(A-4-41-44),debug,bin_annot,strict_sequence,principal,safe_string" -tag-line "<static*.*>: warn(-32-34)" -cflag -g -lflags -g,-linkpkg main.native
Finished, 13 targets (0 cached) in 00:00:00.
ln -nfs _build/main.native mir-mything
root@localhost:~/ocaml/mirage-examples# ./mir-mything
Hello, World!
root@localhost:~/ocaml/mirage-examples#
Mirage can take command-line options using "keys" (see the Mirage_key module).
The keys can be specified both at build-time and (when compiling for the --unix
target) at run-time, using mirage configure --my-key
at build-time, or invoking it with ./mir-mything --my-key
at run-time.
The keys are registered with Mirage using Key.create
in config.ml
and are accessible as a function Key_gen.<name> : unit -> string
from the job modules (a file called key_gen.ml
containing the code to enable this is generated by mirage configure
).
The hello_xyz job looks like this:
module type Job_t =
functor (Console : Mirage_console.S) ->
sig
val start : Console.t -> unit Console.io Lwt.t
end
module Job : Job_t =
functor (Console : Mirage_console.S) ->
struct
let start (my_console : Console.t) =
Lwt.return @@
Console.log my_console
("Hello, " ^ Key_gen.(my_name () ) ^ "!" )
end
We need to make some modifications to the config.ml
, and mirage configure
used to allows us to have multiple config files in the same directory by using the -f
switch, but now someone decided it would be a great idea to remove that, so to avoid cluttering the old examples, we create a new directory hello_xyz
and a hello_xyz/config.ml
:
open Mirage
let my_hello_xyz =
let key =
let doc = Mirage.Key.Arg.info
~doc:"Specify a name for the hello_world_xyz job"
["name"]
in
Mirage.Key.(create "my_name" Arg.(opt string "John Doe" doc) )
in
Mirage.foreign
~keys:[Mirage.Key.abstract key]
(* https://mirage.github.io/functoria/Functoria_key.html#VALabstract *)
"Hello_xyz.Job"
(Mirage.console @-> Mirage.job)
let () =
Mirage.register
"hello_xyz"
[ my_hello_xyz $ Mirage.default_console
]
Finally we can compile:
cd hello_xyz/
mirage configure
make
Running it looks like:
root@localhost:~/ocaml/mirage-examples/hello_xyz# ./mir-hello_xyz
Hello, John Doe!
root@localhost:~/ocaml/mirage-example/hello_xyzs# ./mir-hello_xyz --name Jane
Hello, Jane!
(* TODO nice example code at https://github.com/Engil/Canopy/blob/master/config.ml#L62 *)
(* TODO section on implement an argument converter: https://mirage.github.io/mirage/Mirage_key.Arg.html https://mirage.github.io/mirage/Mirage_runtime.Arg.html *)
(* TODO Unfortunately I have to venture AFK now, but I hope to continue this log of my adventures with Mirage. *)