Live Reloading GUI From Scratch

Here’s a tutorial on using ghcid to live reload a dear-imgui application. This post also introduces how to use the upcoming cabal 3.12 multi-repl feature to reload the executable while working on the library:

14 Likes

Very cool!

I believe the killer feature is the REPL interpreted mode which lets you run the code without linking, which is often the slowest part of a live reload workflow. For example, even with all the tweaks prescribed in the Bevy Rust engine compile optimizations doc, including using the new mold linker, I couldn’t make my toy moonracer game reload in less than a few seconds.

I 100% agree. ghci is a top reason why I continue to use Haskell for gamedev and consider languages such as Rust to be insufficient.

Speaking from gamedev experience, you can probably take the hot reloading even further. For instance:

  1. You can re-use the existing window handle by storing it with foreign-store.
  2. Dunno if imgui could do this, but you can spin up your app in an OS thread and have it listen on an MVar for an entire app loop. Stuff that MVar in a foreign-store and then on reload, pass in the newly-loaded app loop. Depending on how your state & loop is managed, you can have new code operate on pre-existing state. No need to bounce main! I’ve done this with games and apecs and it’s pretty cool.
  3. You can also use the MVar approach to make ghci your development console - have your app loop listen to an MVar that has effects in it and execute them. You can even have it pass results back with some Dynamic magic - [example code]. I banged my head against the wall trying to add a “dev console” using a hand-rolled interpreter for a while. And then I tried hint. But then it dawned on me that ghci was all you need.

Here’s an example Main.hs using these techniques.

9 Likes

Thank you @Ambrose for such great feedback! I initially meant to write a response to the loglog post about leaving rust gamedev by explaining how their arguments do not match the user experience I get with Haskell. Though this quickly got unwieldy and I decided to focus on showing the feedback loop instead. I am glad to hear I’m not the only one using Haskell for that particular capability :slight_smile:

Now am I thinking about writing a few more parts such as using an IDE with HLS (import completion, go to definition, …), hot reload as you suggested with foreign-store or essence-of-live-coding and perhaps finish with binary distribution.

Though I’m also trying to keep this guide as simple as possible so that new comers can give it a try without much complications.

Thank you for your work on macaroni and the mayhem-engine, I’m looking forward your next release!

6 Likes

I wonder if any of these live reload strategies eg foreignstore could work in a Servant web backend?

My dev workflow would greatly benefit from being able to make lucid html changes and instantly refreshing the browser to see the changes.

2 Likes

For WebUI, you can get live reload by opening a websocket and calling window.location.reload() in the javascript reconnection handler, that way, when your backend restart, the web client can automatically refresh the page. Here is an example with htmx: demo-websockets, and L140 for the setup.

4 Likes

A note from my experiments here:

Some context window creation library like GLFW require the program to be run from the main thread, but GHCi can launch your program in a fresh unbounded thread. In any case, the workaround is

:set -fno-ghci-sandbox

before running Main, to force computations to be run in the main thread rather than in a forked thread.

4 Likes

I should note that for 10 years foreign-store has remained the same with no commits, with a terrible segfaulty bug that nobody noticed was there, until a few months ago when Johannes Gerer fixed it: GHC segfaults (aka "The perils of using a non memory safe language") · Issue #4 · esoeylemez/foreign-store · GitHub

So if you tried foreign-store in the past and experienced segfaults, it might be fixed now.

3 Likes

I just gave foreign-store a try and it worked really well for this use case, thanks you @chrisdone! I asked a question to improve the ergonomics with ghcid: How to check if a global store is initialized · Issue #6 · esoeylemez/foreign-store · GitHub .

It looks like a great workflow for persisting the window across reload (and avoid loosing the focus on the IDE), and I’m now tempted to propose simple wrappers for sdl, glfw and gloss to handle the hot reload.

3 Likes

I should note that I’m also using pulse-simple to submit audio buffers near real-time (30 times per second), the library was last uploaded 13 years ago, and it still works fine with ghc-9.6. Makes me wonder what is the oldest package still working today :slight_smile: As far as I can tell, Haskell is remarkably stable!

4 Likes

IIUC, when performing a reload in ghci the REPL bindings are lost, but any already started thread will keep chugging along, still running the old code?

I wonder if we could harness that to create an alternative to foreign-store which, instead of FFI, used helper threads to store values that could later be queried using asynchronous exceptions. I tried that in this repo; it’s very hacky but it kinda seems to work.

Perhaps some form of binding persistence functionality should be integrated in the :reload command itself. :thinking: Seems like a useful feature, despite the dangers.

Does this mean all the threads of your program need to be aware of receiving this exception? Oh, nope, you’re using labelThread to identify the participating threads, even if it is O(n). Both listThreads and labelThread are opening up interesting possibilities!

Another use case that springs to mind would be a chaos monkey style thread killer, which would highlight how robust your thread supervisors are.