Zig programs are CRT-free unless you opt-in with -lc. On Windows, almost the entire standard library only depends on NtDll.dll (not even Kernel32.dll). On Linux, the standard library directly makes syscalls with inline assembly since the kernel guarantees a stable syscall ABI, which rules.
Unfortunately almost all other operating systems, including macOS and the BSDs, require linking libc as the only stable syscall ABI. You’re opting in to some nasty bitrot if you disregard and make syscalls directly. Go found that out the hard way. Last I checked, Hare didn’t learn from Go’s mistake and is busy finding out the hard way too.
I wish Linux required all syscalls go through a VDSO syscall stub, rather than allowing the syscall instruction directly. There are so many situations where I want the ability to trace or modify syscalls of an existing binary, but the only solution that works on all binaries is some ptrace-based thing which causes significant slowdowns. (Or a dynamic binary rewriting framework like Intel’s Pin, but that feels like it’s way overkill for this purpose.)
That sounds like a reasonable compromise to me, since it’s still possible to use the VDSO from static binaries. Plus, I imagine there would be a small perf boost from many syscalls handling certain hot paths in userland before resorting to switching to system mode.
Unfortunately almost all other operating systems, including macOS and the BSDs, require linking libc as the only stable syscall ABI.
This is not true of FreeBSD. On FreeBSD, the system call ABI is stable, though system call versions from prior releases that have been superseded are included only if you build the kernel with the right COMPAT flags (the generic build include these going back to FreeBSD 4, so 23 years). Since, I thing, FreeBSD 5, libc also uses symbol versioning, so you can also dynamically link libc and expect it to keep working for a long time.
Look in the GENERIC kernel config for the COMPAT knobs and syscalls.master for the definitions of old versions of system calls. I think the ABI guarantees are documented somewhere, but they’ve been the same since I started using FreeBSD I’m the 4.x era, so I haven’t looked for them in a canonical form. It was treated as axiomatic when I was on the core team and we were changing the support cycle (we now support a major release series for a long time, we don’t support individual point releases within a series and part of the reason that this was acceptable to downstream is that we have strong binary compat guarantees). You’ll see comments about ABIs on code reviews quite often: anything that would break an ABI needs a COMPAT thing before it can be merged.
Control interfaces (e.g. the ioctls used by ifconfig) are not guaranteed stable beyond a major release series, though they are increasingly supported so that people can run jails with usernames from a prior release. Some of the project infrastructure depends on being able to run jails from the last two major releases.
The KBI guarantees are weaker. KBIs break between major releases, so a kernel module compiled for 13.0 is guaranteed to work on 13.4 but not on 14.0.
Why use ntdll rather than kernel32? Doesn’t that increase the risk of Windows breaking compatibility with Zig programs at some point, since ntdll is mostly not officially documented?
A lot of ntdll functions are now documented and there are even import libraries provided by the normal Windows SDK.
Still, the higher level APIs in kernel32 do allow you to use more modern features of ntdll for free. E.g. At some point DeleteFile was changed to immediately delete a file instead of waiting until all file handles are closed. Applications using kernel32 will automatically get this new delete behaviour but those using ntdll directly will need to update their code and recompile.
At some point DeleteFile was changed to immediately delete a file instead of waiting until all file handles are closed.
Huh, where can I read more on this? In my experience we still regularly end up in situations where we cannot delete a file we have created because it is being scanned by Windows Defender or some such.
See the remarks in FILE_DISPOSITION_INFORMATION_EX. This will ultimately still depend on the kind of locks a scanner takes on the file, but Windows Defender is usually well behaved.
I couldn’t find any official documentation on the DeleteFile behavioural change, but this SO comment gives some details.
A win32 program will always load kernel32.dll. Even if your application doesn’t use it explicitly, there’s no way to free yourself from the tyranny of kernel32.dll (well actually, it’s kernelbase.dll nowadays but same difference).
My understanding is that they are building windows software specifically, so portability isn’t a concern. And on windows msvcrt is an ugly duckling — it isn’t an officially supported way to consume OS APIs, rather, its there to support the programs written against libc APIs specifically, but you are encouraged to use OS APIs directly. This is in contrast to, e.g., MacOS, where libc is the interface to the kernel.
In more general case, the problem of “just use libc” is “which libc” — it’s easier, eg, to support a particular version of jemalloc than a range of dynamically linked glibc allocators.
Right, Windows doesn’t actually privilege libc at all here, you can use as many (because Windows has proper symbol namespacing) or none as you want. Windows is doing this right here in putting any system interfaces in a language-neutral way. However, this is going through a lot of effort to avoid any libc whatsoever. Yes, picking one can be hard, but if you’re using MSVC or MinGW, there’s a path to less resistance on which to pick.
If you are picking libc yourself, instead of using the one provided by an OS, then there’s a temptation to just skip intermediary. Libc is not always great: I like how on Linux it diligently takes syscall return value from a register and sticks it into a threadlocal, because errno is the libc API.
Like, yes, of course, if you maximize code re-use and building on top of someone else’s sand, using libc is a smart choice and a path of least resistance. But if you are in the context where you want to own the interface between your software and the platform, using libc would make it harder. That’s a rare context theses days, but I think that’s the context the author prefers to work with: https://nullprogram.com/blog/2023/02/11/.
One benefit of this approach is that, if you have a platform layer for, eg, windows and Linux, you could easily write a virtual platform layer, and plug the whole thing into simulation testing.
There’s a couple of things here. The ucrt.dll is a library of C functions that’s distributed with the OS and provides all the normal fopen type libc functions. You don’t need this unless you want to write portable C code.
The C startup/shutdown code however is more useful to all applications. It does things like run pre-main functions and set the stack cookie. It also provides basic functions like memcpy etc as well as the ability to handle C++ exceptions.
Note that this is distinct from msvcrt.lib which, despite it’s name, has nothing to do with msvcrt.dll nowadays, It links in both the startup/shutdown code and the ucrt. See also C runtime .lib files.
There’s a difference between MSVCRT the old VC++6 runtime (that they tried bundling with the OS, to find out the limitations with that approach as you linked) and people using MSVCRT as shorthand for “whatever Visual C++ runtime” (which includes libc in the form of the UCRT).
Note in Windows static libc is totally possible while dynamically linking everything else, including system libraries.
Zig programs are CRT-free unless you opt-in with
-lc. On Windows, almost the entire standard library only depends on NtDll.dll (not even Kernel32.dll). On Linux, the standard library directly makes syscalls with inline assembly since the kernel guarantees a stable syscall ABI, which rules.Unfortunately almost all other operating systems, including macOS and the BSDs, require linking libc as the only stable syscall ABI. You’re opting in to some nasty bitrot if you disregard and make syscalls directly. Go found that out the hard way. Last I checked, Hare didn’t learn from Go’s mistake and is busy finding out the hard way too.
I wish Linux required all syscalls go through a VDSO syscall stub, rather than allowing the
syscallinstruction directly. There are so many situations where I want the ability to trace or modify syscalls of an existing binary, but the only solution that works on all binaries is someptrace-based thing which causes significant slowdowns. (Or a dynamic binary rewriting framework like Intel’s Pin, but that feels like it’s way overkill for this purpose.)That sounds like a reasonable compromise to me, since it’s still possible to use the VDSO from static binaries. Plus, I imagine there would be a small perf boost from many syscalls handling certain hot paths in userland before resorting to switching to system mode.
This is not true of FreeBSD. On FreeBSD, the system call ABI is stable, though system call versions from prior releases that have been superseded are included only if you build the kernel with the right COMPAT flags (the generic build include these going back to FreeBSD 4, so 23 years). Since, I thing, FreeBSD 5, libc also uses symbol versioning, so you can also dynamically link libc and expect it to keep working for a long time.
Hmmm this is news to me. Can you back this up with some documentation?
Look in the GENERIC kernel config for the COMPAT knobs and syscalls.master for the definitions of old versions of system calls. I think the ABI guarantees are documented somewhere, but they’ve been the same since I started using FreeBSD I’m the 4.x era, so I haven’t looked for them in a canonical form. It was treated as axiomatic when I was on the core team and we were changing the support cycle (we now support a major release series for a long time, we don’t support individual point releases within a series and part of the reason that this was acceptable to downstream is that we have strong binary compat guarantees). You’ll see comments about ABIs on code reviews quite often: anything that would break an ABI needs a COMPAT thing before it can be merged.
Control interfaces (e.g. the ioctls used by ifconfig) are not guaranteed stable beyond a major release series, though they are increasingly supported so that people can run jails with usernames from a prior release. Some of the project infrastructure depends on being able to run jails from the last two major releases.
The KBI guarantees are weaker. KBIs break between major releases, so a kernel module compiled for 13.0 is guaranteed to work on 13.4 but not on 14.0.
Thanks! I filed this on the zig issue tracker.
Why use ntdll rather than kernel32? Doesn’t that increase the risk of Windows breaking compatibility with Zig programs at some point, since ntdll is mostly not officially documented?
A lot of ntdll functions are now documented and there are even import libraries provided by the normal Windows SDK.
Still, the higher level APIs in kernel32 do allow you to use more modern features of ntdll for free. E.g. At some point
DeleteFilewas changed to immediately delete a file instead of waiting until all file handles are closed. Applications using kernel32 will automatically get this new delete behaviour but those using ntdll directly will need to update their code and recompile.Huh, where can I read more on this? In my experience we still regularly end up in situations where we cannot delete a file we have created because it is being scanned by Windows Defender or some such.
See the remarks in FILE_DISPOSITION_INFORMATION_EX. This will ultimately still depend on the kind of locks a scanner takes on the file, but Windows Defender is usually well behaved.
I couldn’t find any official documentation on the
DeleteFilebehavioural change, but this SO comment gives some details.A win32 program will always load kernel32.dll. Even if your application doesn’t use it explicitly, there’s no way to free yourself from the tyranny of kernel32.dll (well actually, it’s kernelbase.dll nowadays but same difference).
What’s the point of avoiding libc? This seems incredibly fragile to achieve worse portability with dubious performance benefits.
My understanding is that they are building windows software specifically, so portability isn’t a concern. And on windows msvcrt is an ugly duckling — it isn’t an officially supported way to consume OS APIs, rather, its there to support the programs written against libc APIs specifically, but you are encouraged to use OS APIs directly. This is in contrast to, e.g., MacOS, where libc is the interface to the kernel.
In more general case, the problem of “just use libc” is “which libc” — it’s easier, eg, to support a particular version of jemalloc than a range of dynamically linked glibc allocators.
Right, Windows doesn’t actually privilege libc at all here, you can use as many (because Windows has proper symbol namespacing) or none as you want. Windows is doing this right here in putting any system interfaces in a language-neutral way. However, this is going through a lot of effort to avoid any libc whatsoever. Yes, picking one can be hard, but if you’re using MSVC or MinGW, there’s a path to less resistance on which to pick.
If you are picking libc yourself, instead of using the one provided by an OS, then there’s a temptation to just skip intermediary. Libc is not always great: I like how on Linux it diligently takes syscall return value from a register and sticks it into a threadlocal, because errno is the libc API.
Like, yes, of course, if you maximize code re-use and building on top of someone else’s sand, using libc is a smart choice and a path of least resistance. But if you are in the context where you want to own the interface between your software and the platform, using libc would make it harder. That’s a rare context theses days, but I think that’s the context the author prefers to work with: https://nullprogram.com/blog/2023/02/11/.
One benefit of this approach is that, if you have a platform layer for, eg, windows and Linux, you could easily write a virtual platform layer, and plug the whole thing into simulation testing.
I assume this is a typo and you meant “dislike”?
There’s a couple of things here. The ucrt.dll is a library of C functions that’s distributed with the OS and provides all the normal
fopentype libc functions. You don’t need this unless you want to write portable C code.The C startup/shutdown code however is more useful to all applications. It does things like run pre-main functions and set the stack cookie. It also provides basic functions like memcpy etc as well as the ability to handle C++ exceptions.
Note that this is distinct from msvcrt.lib which, despite it’s name, has nothing to do with msvcrt.dll nowadays, It links in both the startup/shutdown code and the ucrt. See also C runtime .lib files.
Oh wow, it’s even worse than that. MSVCRT is explicitly something that applications should not use: https://devblogs.microsoft.com/oldnewthing/20140411-00/?p=1273
There’s a difference between MSVCRT the old VC++6 runtime (that they tried bundling with the OS, to find out the limitations with that approach as you linked) and people using MSVCRT as shorthand for “whatever Visual C++ runtime” (which includes libc in the form of the UCRT).
Note in Windows static libc is totally possible while dynamically linking everything else, including system libraries.