1. 84
  1.  

    1. 29

      As the author of a non-trivial sized Zig project and a user of Zig for a couple years now, I’ll add that this is huge. I haven’t verified the claims yet, but if what Andrew is saying is true (and I believe it), this is a really big deal.

      I know Andrew already says this but let me explain: despite Zig being very non-libc-friendly, for release builds you’ve mostly been forced into linking libc for malloc (using other allocators like mimalloc doesn’t help because they ultimately require libc anyways).

      For example, here is the source in Ghostty where we do this detection: https://github.com/ghostty-org/ghostty/blob/f95f636f1fff1cf449efa79f3f42dcb8d9bbfce5/src/global.zig#L65-L80 (Note: Ghostty has to link to libc anyways due to our dependencies on things like Harfbuzz, Freetype, etc. but removing a major libc dependency and not hurting performance for doing it would be huge)

      As a further aside, since people generally ask, yes, we’ve tried and benchmarked other allocators like mimalloc. There wasn’t a measurable improvement in our benchmark workloads so we don’t use them. It’s been maybe a year since I tested this so it might be worth coming back to benchmarking those again. This is pretty unrelated to this post but people usually ask when I start talking about allocator performance.

      Beyond this, the other benefits like runtime page size to support Asahi are also very welcome. :)

      1. 7

        There’s many community maintained allocators that outperform c_allocator and do not need libc:

        Though for me in general, the general purpose allocator speed does not really matter, as zig lets you fine tune allocation strategies where it matters and thus outperform any general allocator. “No libc” -land is beautiful.

        1. 6

          As a further aside, since people generally ask, yes, we’ve tried and benchmarked other allocators like mimalloc. There wasn’t a measurable improvement in our benchmark workloads so we don’t use them.

          Tbh I don’t think this is that surprising: glibc’s memory allocator isn’t great, but it’s decent until you enter workloads with a very high allocation frequency, especially if those are across multiple threads (mimalloc’s design was explicitly targeting better perf in multithreaded scenarios). By contrast, Zig’s previous GPA was straight-up comparatively slow, even on simpler workloads.

        2. 38

          Incidentally, I’m not a fan of languages going through the libc for interfacing with the OS. It makes cross compiling hell, while languages which just perform syscall directly just work. I think operating systems should:

          1. Separate out a syscall wrapper library from their libc, so that non-C languages don’t have to link against a library whose main purpose is to contain a ton of utility functions meant for C programmers; and
          2. Define a stable ABI for that syscall wrapper library, so that languages can target that ABI instead of targeting “whatever libc the build machine happens to have installed”

          It’s frankly baffling to me that this hasn’t happened yet, especially in OpenBSD which is the main driver behind the “every interaction with the kernel must go through the libc” cult.

          1. 26

            It makes cross compiling hell, while languages which just perform syscall directly just work.

            I think Zig’s outright trolling here:

            • Zig’s stdlib by default doesn’t depend on glibc and does syscalls directly, so cross-compilation just works
            • But you also can use Zig with glibc, and the cross-compilation still just works! When you specify a Zig target, you can request a specific version of glibc, you don’t need to compile on old Debian just to dodge symbol multiversioning.
            1. 27

              Hey I didn’t know that, that sounds excellent. Clearly, a lot of work has gone into this from Zig.

              However I maintain that operating systems are making this way harder than it needs to be. It’s ridiculous that every language has to bend over backwards to link against a gigantic C utility functions library with an incredibly unstable ABI and with no sort of ABI standardization across POSIX systems, just to access a few basic syscall wrapper functions.

              1. 11

                Fully agree with this, yeah!

                1. 3

                  That’s just the original sin of libc: it can’t decide if it’s the OS interface or the C standard library. There is no good reason to conflate the two.

                2. 6

                  When you specify a Zig target, you can request a specific version of glibc, you don’t need to compile on old Debian just to dodge symbol multiversioning.

                  Which honestly is key. It’s always nice to say “my language does not need libc” but then in the real world you will need to interface with this anyways and you’re back to square one.

                  1. 4

                    you don’t need to compile on old Debian just to dodge symbol multiversioning.

                    The fact that you are basically forced to do that with C/C++ (and also Rust, I believe) is incredibly infuriating.

                    1. 2

                      You aren’t forced to do that with C/C++ if you use Zig as your C/C++ cross-platform build tool. That’s an advertised feature of Zig I haven’t tried yet, so I don’t know about the limitations.

                      1. 4

                        The main problem is that Zig’s cross-build cleverness stops at libc, so if you have dependencies on (say) curl and openssl, you (I mean me) are better off doing a native build on the target machine.

                          1. 5

                            But that’s not build cleverness, that’s a manually created build script. It’s nice that someone made that build script, but it’s dishonest to compare it to the automated smartness of Zig’s libc handling.

                            1. 5

                              It’s the same thing… In both cases I ported the build over to zig

                            2. 3

                              If I remember correctly I ran into problems trying to use Zig to cross-build some Rust bindings. This is supposed to work, but Zig was unable to find the cross-build dependencies.

                              I was trying out Zig as a cross-build compiler because I had read that it has a lot of cleverness so that the cross-build environment is built in. I hoped that would save me from lots of tedious setup. But I had forgotten about my C dependencies, and the error messages suggested to me that I would need to set up a cross-build environment for them. Which nullified the point of the exercise.

                              1. 3

                                Yes, I have run into this same issue as well (something to do with rodio) when trying to cross-compile rust with zig.

                                Granted I don’t know zig, but gave up and committed instead to a much more vanilla static build of rust with musl, and it worked fine.

                        1. 2

                          If you don’t mind setting it up, you could statically link to musl to avoid this. I think the rustc *-musl targets will do it automatically. (I agree it’s infuriating)

                        2. 2

                          Yes i found that you could even target a particular version of libc, earlier even a compiled programme on a system with newer libc woudn’t work on a system with relatively older libc, then trying to install a linux os with older libc would also fail as it wouldn’t run the newer Makefile/build-script, it just became a stupid whack-a-mole game. Now i just cross compile even from windows for any desired linux distribution a lot of not so trivial nim/c code and it just works..

                          1. 2

                            Is the Zig -libc Linux only?

                            Because as the parent notes, there is a lot of operating systems that does not have a stable ABI for syscalls.

                            1. 2

                              libcless std is linux only AFAIK. There’s been contributions to add libcless freebsd as well.

                              1. 1

                                I guess they need to paramatise that by major versions since I think the syscalls is only guranteed stable within those.

                                1. 1

                                  Old FreeBSD binaries are supported when the kernel is compiled with the COMPAT options.

                          2. 5

                            I’m not a fan of languages going through the libc for interfacing with the OS. It makes cross compiling hell, while languages which just perform syscall directly just work.

                            That sounds backwards to me. The C library is standardized; syscalls aren’t (especially if you want to target Windows or embedded platforms!)

                            But I come from a world (Darwin) where the C library is just a piece of a bigger libSystem that all system calls go through, so the idea of a separate / optional libc is kinda foreign. I must be missing something.

                            1. 6

                              I think the point is “we want to free ourselves from all artifacts that C has imposed on us.” When libc is the only way to make syscalls, you can’t do that.

                              A library libsyscall seems to make sense, if the interface is stable (per OS), but you’re in some interesting conditional hell, that libc papers over with the “standard” interface, if you adopt that approach. Still, not sure all software implementing syscalls themselves makes sense…

                              1. 3

                                Libsyscall still imposes C constraints - it will require both a stack and calling convention.

                                1. 3

                                  It means that you need to follow C conventions – for those functions in particular. A language wouldn’t have to know how to call C functions in general, it could have standard library functions implemented in assembly which calls those functions using C stack and calling conventions. Or compiler intrinsics which produces the instruction sequence necessary to call those C functions.

                              2. 4

                                We’re not talking about the C standard library as defined by the C standard, but rather as defined by POSIX and implemented by UNIX-like systems.

                                Both glibc in GNU and libSystem in Darwin contain loads and loads of C utility functions meant for C programs, and they bring in things like the C locale model and the C file model. And at least glibc doesn’t have a stable ABI to link against; and I suspect Apple has similar mechanisms to GNU to allow them to break ABI. In either case, almost nothing of it is relevant for a language which just wants to do its own thing, but we still need to link against all of it just to perform syscalls.

                                If there was a standardized (by POSIX, perhaps?) system interface C library with a standardized and stable ABI, a language could implement cross compiling from any platform to any compliant platform mostly straightforwardly, with great UX. Today, it’s hell.

                                1. 1

                                  And at least glibc doesn’t have a stable ABI to link against; and I suspect Apple has similar mechanisms to GNU to allow them to break ABI.

                                  Apple’s libSystem is just a dynamic library that exports all the system calls and C library functions. You link to it like any other dylib, and you call it using regular C calling conventions. Apple couldn’t change that without breaking literally every program in existence!

                                  1. 2

                                    glibc is also just a dynamic library that exports all the system calls and C library functions which you link to like any other .so and call using regular C calling conventions. Yet they change the ABI constantly in a way that’s horrible to deal with when cross compiling, they just have mechanisms in place to make that not break every program in existence.

                                    1. 1

                                      You could imagine Apple creating a libMach.dylib that exports only low-level API/ABI specific to macOS, leaving out libc/POSIX concepts like fopen that are implemented as wrappers on top of the native functionality, or cruft like errno that is inherently tied to the programming model of 1970s C.

                                      If libSystem.dylib depended on libMach.dylib then existing programs would continue working. Programs that want to avoid a dependency on libc (for example because they’re written in a non-C language with its own stdlib) could link libMach.dylib directly.

                                      1. 1

                                        They could, but what would be the point? It wouldn’t make anything easier, or more performant. You’d just be mapping fewer code pages into your address space.

                                  2. 1

                                    If you are building on system X and want to target system Y, to cross compile you often need access to libc from Y on X ( this might be hard/impossible). Unless you are using a language that performs syscalls directly - in that case you don’t need the libc and cross compilation becomes simpler/possible.

                                    1. 2

                                      It sounds like the thing confusing me is that I’m thinking of dynamic libraries and you’re talking about static libraries. (Right?) If libc we’re dynamic your cross-compiler could just write the names of the library functions into the imports section of the binary, without having to have the actual libc.

                                      1. 6

                                        GNU libc is a dynamic library that has a dependency/compilation model similar to static libraries – the standard way to link against a specific GNU libc version is to have a chroot with that version installed. It’s not like macOS where you can compile on a v15.1 machine but target a minimum version of v14.0 (or whatever) via compiler flags.

                                        The header files have #define macros that unpack to implementation-defined pragmas to override linker settings, there’s linker scripts that do further manipulation of the symbols so that a call to stat() turns into a reference to xstat64/2.0 or whatever, the .so itself might require the binary to be linked to an auxiliary .a stub library of a forward- (but not backward-)compatible version. It’s not straightforward.

                                        Consequently, trying to cross-compile a Linux executable that links against GNU libc is a huge pain because you need to get your hands on a GNU libc binary build without going through the system package managers (that are often an assumed part of the development environment for the type of person who wants to use GNU libc).

                                        Other Linux libc implementations (read: musl) don’t have the same limitation because they don’t have the GNU project’s … idiosyncratic … philosophy about the Linux <-> libc <-> executable relationship.

                                  3. 5

                                    It’s frankly baffling to me that this hasn’t happened yet, especially in OpenBSD which is the main driver behind the “every interaction with the kernel must go through the libc” cult.

                                    Why are you surprised that this hasn’t happened in an OS where everything is C and the kernel and libc development are basically one and the same? As far as I can tell all it would do is add a layer of mapping they currently have no need for, what would the concrete benefit be from the project’s point of view?

                                    1. 7

                                      OpenBSD is obsessed with security, so you would think they would want to make it easier to write applications in memory-safe languages. But nope—your choices are either C, or some language that eventually pretends to be C.

                                      (I’m being a little snarky here. I’ve gotten the impression that OpenBSD’s interest in security does not extend as far as having the humility to try to migrate away from C. That’s fine; there are plenty of solid pragmatic reasons to make that decision—but in 2025 it’s not a recipe for security.)

                                      1. 4

                                        That seems like a better tack, but fundamentally I’m not sure it argues for the change in question? Would a raw assembly ABI for non-C languages to bind to be in any way safer or easier than a C-ABI one? And it’s not like you’d remove the dynamic binding since openbsd is actively trying to remove raw syscalls with sycall origin verification.

                                      2. 3

                                        Because not everything is C. Even in the BSD world, people run lots of userspace code that’s not written in C.

                                      3. 3

                                        OpenBSD doesn’t promise any kind of API or ABI stability from release to release and I believe doing so is a non-goal. This got brought up the better part of a decade ago as an issue for Rust’s libc crate and they (apparently) haven’t decided what to do about it.

                                        For better and for worse, when OpenBSD breaks something, porters usually go through the ports tree and deal with the fallout. I realize that using ports@openbsd.org as your cross-compilation tool is extremely high latency but it’s pretty reliable.

                                        1. 2

                                          Has OpenBSD ever made libc changes which take a function which was POSIX-compliant and makes it non-compliant? I don’t believe it has, and it’s certainly not typical. That means that there is some level of implicitly promised API stability.

                                          1. 2

                                            Offhand I think they nuked gets() from orbit but that one function is so bad that it’s fair game.

                                            1. 2

                                              That was actually removed from the C standard in C11.

                                              1. 1

                                                Nice! I am glad to hear that. ❤️

                                                I am pretty sure OpenBSD nuked it from orbit before C11. ;)

                                                1. 2

                                                  Apparently not, I am slightly surprised to discover. Tho all the BSDs had gets() print a noisy warning since the early 1990s, which was probably sufficient discouragement.

                                      4. 14

                                        This is exactly what I want from a native language: freedom from vcruntime, from glibc, and C locales shenanigans.

                                        1. 18

                                          And not to forget: errno! Syscalls return errors in-band as a value, the libc wrapper stashes it away into errno, which needs to hit TLS, then the Zig (or Rust etc.) libc wrapper needs to pull it out of TLS again. It’s so silly!

                                          Now these days with all the mitigations, context switches are so expensive that this is probably a drop in the bucket, but it still feels wasteful.

                                        2. 9

                                          As a Rust user who won’t give up Rust’s strong safety, I’m jealous. Yes, Rust has no-std, but it would be cool if Rust had full std without libc. Note: On Windows, that would mean not using the VC++ runtime and UCRT, while still using kernel32 and other Win32 APIs.

                                            1. 2

                                              FYI this uses c-gull which is a Rust implementation of libc. It might be cool to also have a std backend that skips the libc API and the C ABI entirely (maybe using this library does skip the C ABI, I’m not sure).

                                            2. 0

                                              Yes, Rust has no-std, but it would be cool if Rust had full std without libc.

                                              Contribute that? At the end of the day, wishes don’t do anything.

                                              https://doc.rust-lang.org/nightly/rustc/platform-support/x86_64-unknown-linux-none.html already exists, but is lacking std support.

                                              1. 1

                                                I got started on an attempt at this some years ago, but lost motivation early on when I realized that Rust’s std is quite enmeshed with libc. It’s 100% possible, but it’ll take more work than I myself can put in right now.