2025-05-30 02:55:21 +02:00

5.7 KiB

Description

Enables hot-reloading of dwl; meaning almost all logic can be changed at runtime. This obviously requires some black magic so for now there's a glibc 2.0 or later dependency to this. In particular this allows for every option in config.h to be changed at runtime.

Reloading

To reload rebuild dwl.so, perhaps reinstall it and then run trigger reload function (bound to Mod+Shift+R by default). This currently calls notify-send in order to inform you of a reloading taking place. So in short:

  1. make changes to config.h or dwl.c
  2. run make to rebuild dwl.so
  3. run sudo make install to reinstall dwl

Limitations

Reloading the compositor will replace all functionality except for main, setup, run and the reload logic. Note that you're responsible yourself for reloading ressources like fonts, which may only get acquired once. A lot of components of dwl will also only get run on a trigger (the tiling for example). So not every change will be immediate. Furthermore, any patch adding more global state to dwl cannot currently be reloaded properly since we keep state in the cold part. These patches will still work and their functionality will (hopefully) be reloadable but you will need to restart the compositor once.

Notes

reduce compile errors

This patch triggers -Wpedantic a bunch (I don't think there's a way around this, dlsym yields void* pointers to functions). This will show a lot of warnings but cause no errors. So you may want to disable this compile option in order to get readable compiler output.

runtime dependencies

This does depend on you having a notification daemon like dunst or mako running as well as having notify-send installed in order for the compositor to inform you of the reload.

How?

Most of all dwl functionality is moved into a shared object file dwl.so, which can be reloaded at runtime.

How do I make this work with other patches?

Most patches should already put everything in more or less the correct place but if they don't, then here is where you learn how to fix it.

The concept itself is quite simple. We compile dwl.c twice once normally and once with the HOT macro defined. The first run will yield the executable and the second will yield a shared object file to be reloaded at runtime. From the cold part there are some newly available macros:

symbol names are written as-is, never as string literals

  • TSYM(T, s) dynamically loads the symbol s with type T from the shared object file use this if you need to call functions in the cold part (i.e. the setup function).
  • CSYM(T, v) dynamically accesses the value of the symbol v of type T from the shared object. Use this to query values from config.h for example.
  • LISTEN_GLOBAL(E, L) is similar to the LISTEN macro. E is an event and L the name of a global listener. Current implementation is a bit messy and I may fix it if someone bothers me about it.
  • UNLISTEN(L) takes a listener and unregisteres it. This is important for reloading.

When adding new code there are some considerations to be made. Since dwl decorates all symbols with static by default, we cannot access them as-is. C's macro system is a bit too powerful though and we use this to our advantage. We will repeatedly define and undefine a macro called static in order to replace the static keyword inside some sections. This allows us to do less refactoring and preserve a lot of the original patch compatability since we're only strategically adding lines. We're tring to be as minimally invasive as we can. As a general guide:

  • global state should be global for the cold part and extern in the cold part meaning it should be inside a block like this:
    #ifdef HOT
        #define static extern
    #else
        #define static 
    #endif
    ... // your global variables go here
    #undef static
    
  • function declarations should be visible in the hot part but not included in the cold part meaning they should be enclosed like this:
    #ifdef HOT
    #define static
    ... // your function declarations go here
    #undef static
    #endif
    
  • static data like the event handler structs in the current main branch are a bit more difficult but we will let them reside inside the hot part. Thus, we enclose them the same way we do functions:
    #ifdef HOT
    #define static
    ... // your struct wl_listener event handlers go here
    #undef static
    #endif
    
  • function definitions should go in the hot part, so they need to be inside a big block like this:
    #ifdef HOT
    ... // function definitions here
    #endif
    
  • enfore use of the LISTEN_GLOBAL and UNLISTEN macros (I know this sucks but what can I do, I need to get access to the callbacks somehow). So you want
    • wl_list_remove(listener.link) to become UNLISTEN(listener) and
    • wl_signal_add(event, global_listener) to become LISTEN_GLOBAL(event, global_listener).
  • Make sure that any patch you're using also uses static everywhere.
  • If a patch adds any config variables that are accessed in the cold part (i.e. probably setup), then you'll have to manually remove the static keyword from them.

Note that usually you do not have to create the big #ifdef blocks yourself. There is for example already a huge #ifdef HOT-delimited codeblock at the bottom of dwl.c where all the function definitions go.

If you have any troubles, feel free to reach out.

Download

Authors