Table of Contents
I recently enabled Windows support for my Raylib bindings library and a game of mine that uses it, Aero Fighter. The process was surprisingly smooth.
This article describes how to:
.dll
files into a Lisp image.exe
executable of a Lisp programWe can easily produce Windows executables from Linux with no extra configuration just by using the correct compiler toolchain. On Arch Linux:
pacman -S mingw-w64-toolchain
Then, given the following C code:
we can compile and run it like so:
> x86_64-w64-mingw32-gcc hello.c > wine ./a.exe Hello, World!
Feel free to skip this if you not interested in linking to C code.
Assuming you have a local clone of Raylib, it can be cross-compiled by setting the following within its src/Makefile
:
RAYLIB_LIBTYPE ?= SHARED HOST_PLATFORM_OS ?= LINUX PLATFORM_OS ?= WINDOWS CC = x86_64-w64-mingw32-gcc AR = x86_64-w64-mingw32-ar
Then a call to make PLATFORM_OS=WINDOWS
will produce a .dll
file. Now, given this sample C:
#include "raylib.h"
int main(void) {
const int screenWidth = 800;
const int screenHeight = 450;
InitWindow(screenWidth, screenHeight, "raylib example 01 - Windows");
SetTargetFPS(60);
while (!WindowShouldClose()) {
BeginDrawing();
ClearBackground(RAYWHITE);
DrawText("Saluete!", 190, 200, 20, LIGHTGRAY);
EndDrawing();
}
CloseWindow();
return 0;
}
Copy (or symlink) raylib.h
, raylib.dll
, and libraylibdll.a
to the same directory, and compile it with:
> x86_64-w64-mingw32-gcc hello.c -L"." -lraylib
Then, wine ./a.exe
should open a Raylib window. Quit with ESC
.
Visit the SBCL Downloads page and download the .msi
installer file by clicking on the Windows cell of the Binaries matrix. Run wine
on this. By default the sbcl.exe
binary is installed to:
$HOME/.wine/drive_c/Program Files/Steel Bank Common Lisp/
You can confirm its function by running:
wine $HOME/.wine/drive_c/Program Files/Steel Bank Common Lisp/sbcl.exe
Try inspecting the content of the *features*
list:
(:ARENA-ALLOCATOR :X86-64 :GENCGC :64-BIT :ANSI-CL :COMMON-LISP :IEEE-FLOATING-POINT :LITTLE-ENDIAN :PACKAGE-LOCAL-NICKNAMES :SB-LDB :SB-PACKAGE-LOCKS :SB-SAFEPOINT :SB-THREAD :SB-UNICODE :SBCL :WIN32)
Notice the presence of :WIN32
and the lack of :LINUX
and :UNIX
. It's Windows!
Feel free to ignore this section, it is optional information.
Filepaths work differently between Linux and Windows, and we can observe how SBCL handles this. In our REPL:
> (inspect #p"Z:/home/colin/code/foo.json") The object is a PATHNAME. 0. NAMESTRING: NIL 1. HOST: #<SB-IMPL::WIN32-HOST {1100039A13}> 2. DEVICE: "Z" 3. DIR+HASH: ((:ABSOLUTE "home" "colin" "code") . 3675096247244793922) 4. NAME: "foo" 5. TYPE: "json" 6. VERSION: :NEWEST
Notice that the :device
field is actually populated. On Linux this would have been nil
. Notice also that, for our convenience, the path component separator is /
and not those accursed backslashes!
The filepaths library offers additional convenience regarding paths, and supports both Linux and Windows.
vend is a dependency manager for Common Lisp, and luckily it helps us simplify our Windows setup, since we don't need to bother with a Quicklisp installation within Wine. Since vend repl
allows us to run any compiler and load our systems with all of our project dependencies available, why not just ask it to run SBCL through Wine as above?
> pwd /home/colin/code/common-lisp/filepaths > vend repl wine /home/colin/.wine/drive_c/Program\ Files/Steel\ Bank\ Common\ Lisp/sbcl.exe > (asdf:test-system :filepaths) ;; ... compiling, etc. ... Passed: 79 Failed: 0 Skipped: 0
Likewise, vend test
"just works":
> vend test wine /home/colin/.wine/drive_c/Program\ Files/Steel\ Bank\ Common\ Lisp/sbcl.exe [vend] Running tests. ;; ... yeah yeah ... Passed: 79 Failed: 0 Skipped: 0
It's able to find all of our vendored dependencies by virtue of the fact that our Linux filesystem is also available through Wine under Z:
. When vend
internally asks uiop
where we currently are, we get what we'd expect:
> (uiop:getcwd) #P"Z:/home/colin/code/common-lisp/filepaths/"
Now that we've proven we can load systems through Wine, we can configure Sly to use our Wine-based SBCL as an in-editor REPL:
(setq sly-lisp-implementations '((sbcl ("vend" "repl" "sbcl" "--dynamic-space-size" "4GB")) ;; ... other compilers ... (wine ("vend" "repl" "wine" "/home/colin/.wine/drive_c/Program Files/Steel Bank Common Lisp/sbcl.exe"))))
Adjust as necessary for Slime. Then, SPC m ;
in Doom (or C-u sly
) and selecting wine
will start our Windows SBCL. We can now develop interactively as normal, but with Windows assumptions.
Remember those Raylib DLLs we built above? My bindings library assures they're loaded upon asdf:load-system
via:
(defun load-shared-objects (&key (target nil)) "Dynamically load the necessary `.so' files. This is wrapped as a function so that downstream callers can call it again as necessary when the Lisp Image is being restarted. Note the use of `:dont-save' below. This is to allow the package to be compiled with `.so' files found in one location, but run with ones from another." (let ((dir (case target (:linux "/usr/lib/") (t "lib/")))) #+linux (progn (load-shared-object (merge-pathnames "liblisp-raylib.so" dir) :dont-save t) (load-shared-object (merge-pathnames "liblisp-raylib-shim.so" dir) :dont-save t)) #+win32 (progn (load-shared-object (merge-pathnames "lisp-raylib.dll" dir) :dont-save t) (load-shared-object (merge-pathnames "lisp-raylib-shim.dll" dir) :dont-save t)))) (load-shared-objects)
This works as-is. Essentially, you're able to call load-shared-object
on any .dll
file and it will be loaded into the Lisp image. Note also the presence of :dont-save t
, which is important when building executables.
Assuming all your .dll
files are in place (if necessary), which you can ensure via a Makefile, creating a .exe
file of your Lisp program is as simple as writing a short build.lisp
:
(require :asdf) ;; Force ASDF to only look here for systems. (asdf:initialize-source-registry `(:source-registry (:tree ,(uiop:getcwd)) :ignore-inherited-configuration)) (let ((bin (or #+win32 #p"aero-fighter.exe" #p"aero-fighter"))) (sb-ext:save-lisp-and-die bin :toplevel #'aero-fighter:launch :executable t :compression (if (member :sb-core-compression *features*) t)))
Note that Core Compression doesn't seem to be available for Windows, so the resulting binary will be quite a bit larger than its Linux one (~4x in the case of Aero Fighter, 11mb -> 40mb. The Linux binary under ECL is only 1mb).
See here for the full build script. Running sbcl --load build.lisp
will build your Windows executable.
That's it!
Blog Archive