What's the state of the art in Zig ecosystem for Wayland+Vulkan without libc? 2

I’ve pushed a few small commits that fix the validation layer issue people keep running into as well as improved logs for other likely failure points during initialization

Oh, and in case anyone was wondering, having five compositors installed simultaneously does NOT help system stability :laughing:

3 Likes

can you elaborate on this?

commit a362b38fdc322bcc3d1f9bcead2c93d6d1e641cf
Author: Daskie <daskie.@gmail.com>
Date:   Wed Jul 8 16:04:59 2026 -0700

    back to llvm; better stability and not the focus of this example

when zig chooses the x86 backend by default, we expect it to have better stability than the llvm backend. it is actually more compliant with the zig behavior tests than the llvm backend.

did you run into an issue?

https://codeberg.org/Daskie/presentation-example/issues/1

I don’t hit that issue myself, but without LLVM I don’t see any information while debugging. So “stable” in terms of avoiding these two issues that shouldn’t be relevant to the example.

I see, thank you. That issue has already been fixed in master branch.

I cherry-picked your commits into my fork. My fork:

  • Integrates @TibboddiT’s work
  • Updates to zig 0.17.0-dev.1282+c0f9b51d8
  • Does not override the default zig codegen backend selection
  • Applies zig fmt and removes underscores from function names
1 Like

Hell yes, this is exactly what I was hoping for!

I have some larger Vulkan+Wayland projects I can try this out on, though it will take some time.

I’m super curious where the line is for this, if such a line exists. I’ve read through what @TibboddiT has written, but I confess most of it goes right over my head.

Like at this point, at a fundamental level, what can’t be shipped as a pure zig static binary?

  • User Input and Windowing :white_check_mark: (direct wayland protocol, pure zig)
  • Graphics :white_check_mark: (vulkan loaded via dyn-loader)
  • Audio ?? (surely this can’t be more difficult than vulkan?)

And is there a real advantage to shipping a significant graphical application, like a game or desktop app, as a single static binary, or is this mostly just a Cool Thing™?

1 Like

:white_check_mark: I solved this one already in my music player project!

Basically you can just use libpulseaudio statically compiled. Here’s a promising pure zig package I haven’t tried yet: klaji/pulsar. See also this amusing issue.

There is a material advantage. Distributing software as a single static binary reduces the barrier to who can download and install software from hardcore tech savvy people, to merely technically literate people. Crucially, it does this without a centralized distribution platform such as Steam or Apple Store.

Those who control the distribution channel decide which software is allowed to reach users and under what conditions. In order to protect Software Freedom, centralized distribution channels always need to be under threat of higher quality, Free Software reaching their users whether they like it or not.

8 Likes

So are we going to have a std that opening a window, draw the triangle, and playing a sound soon?

I remember I watched a YouTube video last year. The YouTuber’s name is something like Tosien or something like that. He said, “In 2025, the first example of a programming language should be opening a window, not hello world.” :rofl:

1 Like

Newb questions alert.

Are there also plans to side-step libvulkan to really achieve this? Is this even possible? (for me GPU stuff remains pretty magical).

Finally, are game devs publishing on steam because of technical reasons? Isn’t it mainly because it’s easier to get monetized this way?

I can understand your reasoning about the things you said, but let me share my opinion.

This seems to me like coupling Nix to NixOS (and linux) and later comparing to other OSes. I think this is not fair because Nix is a tool that can be used on pretty much any of the major OS, as far as I know. I would bet even windows with WSL, and probably other posix OSes. In this context I feel like Nix is much better to be compared to something like Ninja. Zig does not depend on it either, but it still provides examples how to use it to build Zig. Not as a hard dependency or requirement, but as a tool to maybe make your life a little bit easier.

I used to do some hacking at Zig compiler, I was able to make myself a nix shell script to build zig, because I wanted to build with pre-built dependencies, but I did not want to install them to my system. I managed to do it for 0.16, but after upgrade of LLVM in 0.17-dev I was no longer able to make it work.

For that reason I think having a maintained Nix script to build Zig would be a huge service to community as pretty much anyone would be able to use it to build the compiler while having to deal with only one tool instead of multiple tools and dependencies. There exists some scripts/resources on github [1], [2], [3], but all of them seem to be outdated. Having one officially maintained directly in Zig repo, or semi-officially under zigtools/ would be just awesome, even if it was more of just an example instead of one-for-all solution. Maybe even just snippet in main README.md would be good enough.

Hi, back from vacation and catching up here.

Really cool :slight_smile:

For my linux desktop app library hobby project, when using wayland I load libwayland-client.so.0 (and others like libwayland-cursor.so.0) thanks to the dynamic loader and use zig-wayland as bindings. I assume these helper libraries are present anyway when a wayland display is active. I really should add a wayland-vulkan-triangle example in the dynamic loader repo to illustrate this.

Again, I personally will load libpulse.so.0 using the dynamic loader when it is present on the user’s system.

The philosophy of my app framework diverges a bit from what Andrew is doing / proposing here: he skips using a client dynamic library if possible for a given functionality (by implementing it in zig), whereas I prefer using the system provided one through handcrafted bindings. The static dlopen approach with optional runtime probing is my go to now, and it serves me very well. Having dlopen implemented in zig is so nice, and the fact that it is at all possible to load dynamic libraries from a static executable is such a breath of fresh air to me. I tried this many times before, and now that it works (with thread local storage, with good stack traces thanks to the customizable SelfInfo, etc.) it makes me think it will finally be possible to easily solve any problems related to the diversity of linux distributions. To me, the fact that by default a zig program on linux is not linked to libc without giving up any feature is a blessing, and it is this very fact that makes zig special for this use case. If I understand correctly, rust standard library just needs libc, and with go, calling foreign functions will also require it one way or another (I might be wrong here). And I suspect TLS will be a problem anyway in other languages. There is this line in the dynamic loader:

std.os.linux.tls.area_desc = new_tls_area_desc;

This is what makes it possible to correctly “colocate” TLS between C and zig. Maybe the fact that TLS handling on linux is in std lib and that std.os.linux.tls.area_desc is a public declaration is accidental, but I think that in other languages chances are that it will be much more closed than that.

Absolutely. This is my core motivation. I can accept that a system library is distributed as a dynamic library by an OS, and that this dynamic library is also linked to an OS tailored libc with the right version, etc., but I disagree that an end user should have to compile a program on their OS (or be basically forced to use a system distributed version of this program) to make it work. The logical conclusion is that you, as a developer, should avoid linking your program to a system provided resource, even if it is libc. Instead, you should be able to load it into your process address space and call its functions transparently.

I think having dlopen from non-libc static executables makes it a non issue: libvulkan (or opengl, egl, etc.) is provided by the end user’s hardware drivers, the program will just load it at runtime (and thus will theoretically not depend on any specifics of it at compile time) and make use of it :).

As a side note, if this vision gains some traction, I’d be willing to implement the same functionality for aarch64. As I don’t personally own a machine with such a CPU, I suspect the first step will be learning how to properly run an aarch64 system on an x86_64 one using qemu or something.

Also, if I may, I would like to bring attention to this topic and this issue, as I very often depend on “cross module” stack traces quality :slight_smile:

5 Likes

You lost me here. I don’t see how having a static binary use dlopen(), or having a dynamically linked binary is different. They both have dependencies on system shared libraries.

Ok so I was puzzled too and had a chat with “someone”. So my understanding is that libvulkan.so is not your regular dynlib. The ABI is actually specified and versioned. It is then implemented by the driver providers. Functions will be looked up at runtime and used by your program. If the function is present, you know it will follow the specified ABI on which there can’t be no breaking change (backward compatibility) and if the function is not present, your program handle it gracefully (forward compatibility).

That is why it is a “non issue”. As long as you properly handle the runtime lookup, breaking changes are impossible and you will be able to run on any platform for all time.

At least that is my understanding as of this moment.

I’m new to Wayland and seem to have a trouble with finding linux-drm-syncobj-v1 extension. When compiling, I get this error:

warning(vk_general): Path to given binary /usr/lib/libvulkan_virtio.so was found to differ from OS loaded path libvulkan_virtio.so
warning(vk_general): Path to given binary /usr/lib/libvulkan_radeon.so was found to differ from OS loaded path libvulkan_radeon.so
info(vulkan): Selected physical device name: "AMD Radeon RX 7900 XTX (RADV NAVI31)", type: discrete_gpu, API version: 1.4.348, driver version: 109056003
info(vulkan): Selected device local memory type index: 0, host visible: false, host coherent: false, host cached: false, heap size: 23.98GiB
error(window): Missing required Wayland global interface `wp_linux_drm_syncobj_manager_v1`
thread 12490 panic: 
/home/georg/Repos/presentation-example/window/window.zig:152:21: 0x12d34a4 in init (window.zig)
                    @panic("");
                    ^
/home/georg/Repos/presentation-example/example/main.zig:29:16: 0x129a6cc in main (main.zig)
    window.init(.{
               ^
/home/georg/Downloads/zig-x86_64-linux-0.17.0-dev.956+2dca73595/lib/std/start.zig:790:30: 0x129b68d in callMain (std.zig)
    return wrapMain(root.main(.{
                             ^
/home/georg/Downloads/zig-x86_64-linux-0.17.0-dev.956+2dca73595/lib/std/start.zig:217:5: 0x1250ac1 in _start (std.zig)
    asm volatile (switch (native_arch) {

However, I do have this protocol listed in /usr/share/wayland-protocols/staging/linux-drm-syncobj. I’m running this on river wm. What could be the issue?

1 Like

With libwayland, which we are specifically trying to avoid, using WAYLAND_DEBUG=1 zig build run would dump the messages parsed over the wire to stderr, and you would see a bunch of events like:

3953717.007]  -> wl_display@1.get_registry(new id wl_registry@2)
[3953717.043]  -> wl_display@1.sync(new id wl_callback@3)
[3953717.321] wl_display@1.delete_id(3)
[3953717.354] wl_registry@2.global(1, "wl_compositor", 5)
[3953717.370] wl_registry@2.global(2, "wl_drm", 2)
[3953717.383] wl_registry@2.global(3, "wl_shm", 1)
[3953717.394] wl_registry@2.global(4, "wl_output", 4)
[3953717.453] wl_registry@2.global(9, "xdg_wm_base", 4)
[3953717.521] wl_registry@2.global(15, "wl_seat", 8)
[3953717.600] wl_registry@2.global(22, "zwp_linux_dmabuf_v1", 4)
[3953717.656] wl_registry@2.global(27, "xdg_activation_v1", 1)
[3953717.667] wl_callback@3.done(185275)

The compositor will declare what parts of the protocol it supports by providing globals associated with those protocols, it’s a little bit like Vulkan layers or extensions if you are familiar with those.

The error would usually mean that the compositor doesn’t support that protocol, since it doesn’t provide that global.

However, this table says that River does support it, so it could be some other issue.

The important difference is that a normally dynamically linked executable has mandatory shared library dependencies. Before main() is entered, the system dynamic loader must recursively resolve every DT_NEEDED dependency, including libc and every other linked library. This means the executable cannot even start unless the target system provides compatible versions of all required libraries. For example if the executable is linked against libwayland-client, then a system without wayland libraries cannot run it at all, even if the application could fall back to x11.

With dlopen-like functionality available from a non-libc static executable, the executable itself has no libc dependency and no mandatory shared library dependencies, and so it can start on its own. And at runtime, it can decide which libraries are appropriate for the current system and load them on demand. Since the executable itself does not depend on libc, it does not care which implementation is present. In other words, the executable only depends on the linux kernel. Every userspace library dependency is discovered and resolved at runtime on the target system. The benefit is not just using dlopen: the important point is that the executable itself, being statically linked, is no longer subject to the startup dependency resolution done by the system dynamic loader. The only compatibility requirement is between the system libraries we load and the libc they were built against, which is generally guaranteed by the target distribution. On a glibc-based system, the loaded libraries will use the system’s glibc. On a musl-based system, they will use musl.

From an end-user perspective, this makes distribution much simpler. Instead of needing a build that matches their distribution and libc implementation, users can just download and run the executable. Optional features become available when the corresponding system libraries are present, instead of preventing the application from starting. And there is no need to worry about whether the application was built against the “right” glibc version or even the same libc implementation as the target system.

10 Likes

I pushed a small commit that will log all of the globals advertised if you want to see precisely what your compositor supports.

Also, which WM are you using with river, or are you using river-classic? I can try to reproduce the issue on my end

Ok, I can see where you’re coming from with this, but…

That suggests that the dynamic linker also has the ability to start the program without loading libraries that DON’T use DT_NEEDED. Are we just reinventing the wheel here?

I’m sorry, for some reason it feels hard to articulate my thoughts on this subject in English.

If your executable is dynamically linked to libwayland-client.so, then it will have libwayland-client.so in its DT_NEEDED dependencies. And so it will not run on a system that doesn’t have this library installed. This becomes a problem if your intention is to fall back to x11 in this case.

So we should use dlopen. This is what GLFW does, for instance. That way we can probe for the presence of the wayland library at runtime, and if it is absent then we dynamically load x11.

But dlopen is normally provided by the system’s C runtime and dynamic loader, and as such you will generally need to dynamically link your executable to libc at compile time. This requires you (the developer) to link against the specific libc available on the system where you compile the executable. But you can’t be sure that end users will have a compatible libc on their system. For example they may be using an older glibc, or musl.

Providing an implementation of dlopen that doesn’t use libc, and therefore doesn’t require your executable to be dynamically linked against it (giving you a non-libc static executable) can really help with this situation.

Of course, implementing dlopen is in fact equivalent to implementing a compatible ELF loader. It requires recursively loading dependencies, processing relocations, handling thread local storage, etc. It also requires forwarding calls to allocation functions (malloc, free, etc.), dl functions (dlopen, dlsym, etc.), thread functions (pthread_create, pthread_join, pthread_mutex_init, etc.) and others to implementations provided by the executable. This is what the custom dynamic loader I made tries to do.

15 Likes

Yup, river-classic. Not sure if it matters, but I use void linux and musl libc. River version is 0.3.7_1. Here’s the output when I run it now:

 ./zig-out/bin/example 
info(vulkan): Selected physical device name: "AMD Radeon RX 7900 XTX (RADV NAVI31)", type: discrete_gpu, API version: 1.4.348, driver version: 109056003
info(vulkan): Selected device local memory type index: 0, host visible: false, host coherent: false, host cached: false, heap size: 23.98GiB
info(window): Wayland reports global "wl_compositor" version 6
info(window): Wayland reports global "wp_security_context_manager_v1" version 1
info(window): Wayland reports global "wl_shm" version 1
info(window): Wayland reports global "wp_single_pixel_buffer_manager_v1" version 1
info(window): Wayland reports global "wp_viewporter" version 1
info(window): Wayland reports global "wp_fractional_scale_manager_v1" version 1
info(window): Wayland reports global "wl_subcompositor" version 1
info(window): Wayland reports global "wp_cursor_shape_manager_v1" version 1
info(window): Wayland reports global "xdg_wm_base" version 5
info(window): Wayland reports global "zxdg_decoration_manager_v1" version 1
info(window): Wayland reports global "zwlr_layer_shell_v1" version 4
info(window): Wayland reports global "xdg_activation_v1" version 1
info(window): Wayland reports global "wl_data_device_manager" version 3
info(window): Wayland reports global "zwp_primary_selection_device_manager_v1" version 1
info(window): Wayland reports global "zwlr_data_control_manager_v1" version 2
info(window): Wayland reports global "zwlr_export_dmabuf_manager_v1" version 1
info(window): Wayland reports global "zwlr_screencopy_manager_v1" version 3
info(window): Wayland reports global "zwlr_foreign_toplevel_manager_v1" version 3
info(window): Wayland reports global "wp_tearing_control_manager_v1" version 1
info(window): Wayland reports global "wp_alpha_modifier_v1" version 1
info(window): Wayland reports global "wl_drm" version 2
info(window): Wayland reports global "zwp_linux_dmabuf_v1" version 4
info(window): Wayland reports global "wp_presentation" version 1
info(window): Wayland reports global "zxdg_output_manager_v1" version 3
info(window): Wayland reports global "zwlr_output_manager_v1" version 4
info(window): Wayland reports global "zwlr_output_power_manager_v1" version 1
info(window): Wayland reports global "zwlr_gamma_control_manager_v1" version 1
info(window): Wayland reports global "ext_idle_notifier_v1" version 1
info(window): Wayland reports global "zwp_relative_pointer_manager_v1" version 1
info(window): Wayland reports global "zwp_pointer_gestures_v1" version 3
info(window): Wayland reports global "zwlr_virtual_pointer_manager_v1" version 2
info(window): Wayland reports global "zwp_virtual_keyboard_manager_v1" version 1
info(window): Wayland reports global "zwp_pointer_constraints_v1" version 1
info(window): Wayland reports global "zwp_input_method_manager_v2" version 1
info(window): Wayland reports global "zwp_text_input_manager_v3" version 1
info(window): Wayland reports global "zwp_tablet_manager_v2" version 1
info(window): Wayland reports global "wl_seat" version 9
info(window): Wayland reports global "zriver_control_v1" version 1
info(window): Wayland reports global "zriver_status_manager_v1" version 4
info(window): Wayland reports global "river_layout_manager_v3" version 2
info(window): Wayland reports global "zwp_idle_inhibit_manager_v1" version 1
info(window): Wayland reports global "ext_session_lock_manager_v1" version 1
info(window): Wayland reports global "wl_output" version 4
error(window): Missing required Wayland global interface `wp_linux_drm_syncobj_manager_v1`
thread 15784 panic: 
/home/georg/Repos/exemple2/presentation-example/window/window.zig:165:21: 0x11a8ae9 in init (example)
                    @panic("");
                    ^
/home/georg/Repos/exemple2/presentation-example/example/example.zig:15:16: 0x1186615 in main (example)
    window.init(.{
               ^
/home/georg/Downloads/zig-x86_64-linux-0.16.0/lib/std/start.zig:699:88: 0x1186c9e in callMain (example)
    if (fn_info.params[0].type.? == std.process.Init.Minimal) return wrapMain(root.main(.{
                                                                                       ^
../sysdeps/nptl/libc_start_call_main.h:58:16: 0x7fe71296cbfb in __libc_start_call_main (../sysdeps/x86/libc-start.c)
../csu/libc-start.c:360:3: 0x7fe71296ccb4 in __libc_start_main_impl (../sysdeps/x86/libc-start.c)
/home/georg/Downloads/zig-x86_64-linux-0.16.0/lib/libc/glibc/sysdeps/x86_64/start-2.33.S:120: 0x1051d09 in ??? (/home/georg/Downloads/zig-x86_64-linux-0.16.0/lib/libc/glibc/sysdeps/x86_64/start-2.33.S)
	call *__libc_start_main@GOTPCREL(%rip)

zsh: abort      ./zig-out/bin/example
1 Like

Yep, it’s not in the list so your compositor doesn’t support explicit sync. Taking a closer look at the protocol support, it does say river 0.3.13, so it seems that you’re a few minor versions off

Also, I believe /usr/share/wayland-protocols just provides a copy of the (versioned-immutable) protocol definitions, installed as a separate package, and doesn’t directly correlate with any currently installed compositor’s capabilities