A GUI "Hello World" with cl-gtk2 and SBCL

We will attempt to make a simple GUI app that we'll be able to redistribute and launch as a standalone app. It won't do anything useful – it'll just show a window with a “Hello, world” label. This is a step-by-step guide.

To build our GUI we'll use the cl-gtk2 library.

First of all, we should load the SLIME and load cl-gtk2:

(asdf:oos 'asdf:load-op :cl-gtk2-gtk)

Then we'll create the hello-world.lisp source file with the following content:

(defpackage :hello-world
  (:use :cl :gobject :gtk)
  (:export :main :run))

(in-package :hello-world)

(defun main ()
  (within-main-loop
    (let ((w (make-instance 'gtk-window :title "Hello, world"))
          (l (make-instance 'label :label "Hello, world!")))
      (container-add w l)
      (connect-signal w "destroy" (lambda (w)
                                    (declare (ignore w))
                                    (gtk-main-quit)))
      (widget-show w))))

(defun run ()
  (main)
  (join-main-thread))

This code defines the hello-world package which has two functions defined: main and run. The main function is used during development – it will run the app in the background thread when run from SLIME REPL (the macro within-main-loop is responsible for this), and the run function is used when the app is run by itself, without SLIME.

The join-main-thread function awaits the main Gtk+ event loop to finish. When the window is closed and receives the destroy signal, this loop will be finished by the (gtk-main-quit) call.

We can test this app straight from the SLIME:

(hello-world:main)

After evaluating this thread in REPL, a new background thread with the Gtk+ event loop will start to run, and the control will return to the SLIME REPL.

Now we will create a program that can be executed as a normal standalone application.

We will start by defining an ASDF system for our app.

For this, we will create the hello-world.asd file in the same directory that contains hello-world.lisp with the following content:

(defsystem :hello-world
  :name "hello-world"
  :components ((:file "hello-world"))
  :depends-on (:cl-gtk2-gtk))

This definition specifies how to build the program:

  • the program depends on cl-gtk2-gtk system
  • the program contains a single source file hello-world.lisp (the .lisp extension is appended automatically).

Usually, ASDF is used to define systems that contain libraries and that are placed in the system-wide directory /usr/share/common-lisp/systems. In this case, however, a system is defined for an application.

Using the cl-launch, we can turn this definition into a runnable binary.

First, we need to install the cl-launch. In Gentoo Linux, we would add the lisp-overlay and install cl-launch from it:

layman -a lisp
emerge dev-lisp/cl-launch

In other Linux distros we can use the ASDF-INSTALL to install it into a system-wide directory or a home directory:

(require :asdf-install)
(asdf-install:install :cl-launch)

cl-launch contains the cl-launch.sh shell script which is used to prepare Lisp apps to run as normal apps.

First, we will create the Lisp image with our app with all of its dependencies. Unless we make an image, we will have to either compile the cl-gtk2 library from source or load it from previously compiled fasl-files. Both of those are quite cumbersome and slow (even just loading fasl-files takes on the order of 30 seconds). But during development creating an image is not necessary, since everything is loaded just one time and the startup.

So, in order to create a Lisp image, cd into the source directory and enter the following command:

cl-launch.sh -s hello-world -d hello-world-image

cl-launch will load the specified system (hello-world in this case) with all its dependencies and saves the image into the hello-world-image file.

If several Lisp implementations are available, then we can choose what to use:

cl-launch.sh --lisp sbcl -s hello-world -d hello-world-image

Next, we will use cl-launch to create a shell script to launch the app from a freshly made image:

cl-launch.sh -m hello-world-image -i '(hello-world:run)' -o hello-world

(if we would have multiple Lisps installed, we can also add the --lisp argument)

As a result, a new hello-world script is created that can be used to launch our app.

Let's try to run it:

./hello-world

Almost immediately we will see a new window popping up:

screenshot

The launch delay is quite acceptable: the window appeared almost instantly.

But this method of creating an executable has a drawback – the size of the resulting image is quite noticeable. In my case (SBCL on 64-bit Linux) the size is 64 megabytes. The biggest chunk of the image is the SBCL itself (42 megabytes), the second largest chunk is the cl-gtk2 and just a tiny part is the app itself. I.e., should the app get bigger and more complex, the size of an image won't grow as much.

We can go even further and alleviate this problem using gzexe:

gzexe hello-world-image

On my computer, the image size from 64Mb down to just 12Mb. This has the effect of increasing the load time (up to 1.4 seconds). 12Mb is acceptable even for downloading the app from the internet. Different Lisp implementations create images differently, and sizes vary from one implementation to another.

Note: saving images with having the cl-gtk2 loaded works only for SBCL for now, but it's just a matter of time until it will get implemented for other Lisp implementations.