Building with MLton

This section describes how to build a stand-alone executable using MLton.

Source files

We assume that the main program for the application is a function main : unit -> unit, as described in the section Main Program, and that this is defined in a source file app.sml.

For MLton, the main program must be evaluated in SML. It is recommended that a separate source file does this, as follows, to keep app.sml portable:

mlton-main.sml

val () = main ()

An MLB file is required that includes these SML source files and their dependencies. The rest of this section describes the form of such an MLB file.

To avoid hard-coding the location of the Giraffe Library installation in MLB files, the MLB path variable GIRAFFE_SML_LIB is used as a placeholder for the directory installdir/lib/sml where installdir is the Giraffe Library installation directory.

The dependencies always include

$(SML_LIB)/basis/basis.mlb
$(SML_LIB)/basis/mlton.mlb

and include

$(GIRAFFE_SML_LIB)/namespace-version/mlton.mlb

for each namespace Namespace-Version whose structure Namespace is referenced in the SML source.

For example, if the SML source references the structure Gio to use the Gio-2.0 namespace, then the MLB file should include

$(GIRAFFE_SML_LIB)/gio-2.0/mlton.mlb

The directory name namespace-version is Namespace-Version in lowercase but this is not necessarily the same as the pkg-config name, which is provided by the contents of the file namespace-version/package.

For example, for the namespace Gtk-3.0, the directory name is gtk-3.0 but the pkg-config name is gtk+-3.0.

There is no need to include the MLB file for a namespace that is not referenced by the SML source, even if it is a dependency of a referenced namespace, because the MLB file for a namespace includes its own dependencies.

The overall form of an MLB file for the application could be follows:

mlton.mlb

local
  $(SML_LIB)/basis/basis.mlb
  $(SML_LIB)/basis/mlton.mlb
  $(GIRAFFE_SML_LIB)/namespace1-version1/mlton.mlb
  $(GIRAFFE_SML_LIB)/namespace2-version2/mlton.mlb
  …
in
  …
  app.sml
  mlton-main.sml
end

Build commands

At a minimum, the mlton command must do the following to compile the MLB file mlton.mlb:

  • Set the MLB path variable GIRAFFE_SML_LIB.

  • Specify the libraries required for linking and their paths.

  • Specify the MLB file mlton.mlb.

This is achieved by the following command:

mlton \
 -mlb-path-var "GIRAFFE_SML_LIB installdir/lib/sml" \
 -link-opt "-Linstalldir/lib/mlton \
            -lgiraffe-namespace1-version1 \
            -lgiraffe-namespace2-version2 \
            $(pkg-config --libs \
                $(cat installdir/lib/sml/namespace1-version1/package) \
                $(cat installdir/lib/sml/namespace2-version2/package) \
                …) \
            …" \
 mlton.mlb

where

  • installdir is the Giraffe Library installation directory;

  • namespace1-version1, namespace2-version2, … are the namespace directories whose MLB files are included in mlton.mlb.

The mlton command could usefully do the following:

  • Specify the name of the output executable file.

  • Set the constant GiraffeDebug.isEnabled for diagnostic output.

  • Set the constant Exn.keepHistory to keep exception history for diagnostic output. (Giraffe Library will print the history of an unhandled exception that occurs in a callback to SML.)

  • Set C compiler options, e.g. debugging information, optimization level.

This is achieved by the following command:

mlton \
 -mlb-path-var "GIRAFFE_SML_LIB installdir/lib/sml" \
 -output name \
 -const "GiraffeDebug.isEnabled debugEnabled" \
 -const "Exn.keepHistory true" \
 -cc-opt "-ggdb -std=c99 -O2" \
 -link-opt "-Linstalldir/lib/mlton \
            -lgiraffe-namespace1-version1 \
            -lgiraffe-namespace2-version2 \
            $(pkg-config --libs \
                $(cat installdir/lib/sml/namespace1-version1/package) \
                $(cat installdir/lib/sml/namespace2-version2/package) \
                …) \
            …" \
 mlton.mlb

where

  • name is the application executable name;

  • debugEnabled is true to include debugging diagnostics and false otherwise.

For example, to build the Hello World example, the following MLB file could be used:

mlton.mlb

local
  $(SML_LIB)/basis/basis.mlb
  $(SML_LIB)/basis/mlton.mlb
  $(GIRAFFE_SML_LIB)/general/mlton.mlb
  $(GIRAFFE_SML_LIB)/gobject-2.0/mlton.mlb
  $(GIRAFFE_SML_LIB)/gio-2.0/mlton.mlb
  $(GIRAFFE_SML_LIB)/gtk-3.0/mlton.mlb
in
  example-1.sml
  mlton-main.sml
end

and, given an environment variable for the installation directory as follows:

GIRAFFEHOME=installdir

the following mlton command could be used:

mlton \
 -mlb-path-var "GIRAFFE_SML_LIB ${GIRAFFEHOME}/lib/sml" \
 -output example-1-mlton \
 -const "GiraffeDebug.isEnabled true" \
 -const "Exn.keepHistory true" \
 -cc-opt "-ggdb -std=c99 -O2" \
 -link-opt "-L${GIRAFFEHOME}/lib/mlton \
            -lgiraffe-gobject-2.0 \
            -lgiraffe-gio-2.0 \
            -lgiraffe-gtk-3.0 \
            $(pkg-config --libs \
                $(cat ${GIRAFFEHOME}/lib/sml/gobject-2.0/package) \
                $(cat ${GIRAFFEHOME}/lib/sml/gio-2.0/package) \
                $(cat ${GIRAFFEHOME}/lib/sml/gtk-3.0/package))" \
 mlton.mlb

Issues

Compilation time

As MLton is a whole-program optimizing compiler, it is not possible to perform incremental compilaton. Consequently, all dependencies must be compiled every time. The GTK 3 stack without any application code is not quick to compile: with an Intel Core i5-7200U CPU at 2.50GHz it takes just over 40 seconds. With such a long compilation iteration time, this slows down development.

For portable applications, it is recommended that Poly/ML is used to check changes initially, with MLton builds left running in the background.

Compilation memory usage

When compiling SML code provided by Giraffe Library, garbage collection does not appear to be triggered until MLton uses all memory available to it. (During compilation, memory usage appears to saturate at the limit of available memory.) With other applications running, this can often result in virtual memory usage that leads to performance and usability issues on Linux systems, in particular, the user interface may be completely unresponsive for several minutes.

MLton does not need all the memory that it uses so it is recommended that the mlton command is run with soft memory limits applied. A limit of 2G appears to suffice.

Under Bash, a command cmd is run in an environment with a memory limit of n bytes by the command

(ulimit -Sv n ; cmd)

For example, when running a make command that builds with MLton, memory usage can be limited to 2G by the command

(ulimit -Sv $((2*1024*1024)) ; make mlton)