The musl preprocessor debate

Today, I would like to discuss a project that I care very deeply about: the musl libc. One of the most controversial and long-standing debates in the musl community is that musl does not define a preprocessor macro.

What’s in a macro?

Simply put, preprocessor macros allow C code to build parts of itself conditionally. For example, the GNU libc defines the “__GLIBC__” macro. If your code needs to do something specific to function properly on systems using that library, it can conditionally build that code using “#ifdef __GLIBC__”.

The authors of musl have said that they will not add a preprocessor macro identifying the platform as musl because:

It’s a bug to assume a certain implementation has particular properties rather than testing.

Rich Felker, “Re: #define __MUSL__ in features.h”, 2013-03-29

I agree with this sentiment in theory, and in an idealised world this would hold up. However, I’d like to discuss why I think this may need to be reconsidered moving forward.

Sometimes you can’t test

One major reason this is an issue is that sometimes it is not possible to do what the authors consider the “correct” form of testing, which is compile-testing.

This practice requires you to build a small test program, determine whether it built properly, determine its runtime characteristics, and then use the results of that test to influence how your actual software is built. This is an alternative to using the conditional code with preprocessor macros.

However, there are many reasons you may not be able to successfully perform such testing. Cross compilation is a large gap here. In fact, many years ago when I was starting the Adélie project, this caused failures in the base image I was building.

The Bash shell could not perform any compile-time or run-time checks because it was being cross-compiled from a GNU libc system to a musl libc system. This caused it to use “fallback” code that worked improperly. If musl had defined a __MUSL__ macro, Bash would not have needed to assume it was running on a pre-POSIX system.

Similarly, the mailing list thread that made me feel strongly enough to write this article involves a header-only library. These types of libraries are meant to be “drop-in” and function without any changes to a developer’s build system. If header-only libraries start requiring you to use build-time tests, you lose the main reason to use them in the first place.

The author of this thread correctly points out that FreeBSD versions their API with a preprocessor macro. Any software that requires a certain API can simply ensure that __FreeBSD_version is defined as greater-or-equal than the versions that introduced that API.

The main reason that the musl project is fearful of this approach, at least to my observation, is that features or APIs (or indeed, bug fixes) can be backported to prior versions. I feel very strongly that this is not the responsibility of the libc.

If a distribution backports a feature, API, or patch to an older version of a library, it is that distribution’s responsibility to ensure that the software they build against it continues to function. When I backported an API from Qt 5.10 to 5.9 to ensure KDE continued building for Adélie, it was my responsibility as maintainer of those packages to keep them building properly. It certainly does not mean Qt should stop defining a preprocessor macro to determine the version being built against.

Additionally, some APIs are privileged. Determining whether these APIs work correctly using run-time testing can prevent CI/CD from working properly because the CI user does not have permission to use them.

A versioned macro like FreeBSD’s makes sense

I feel that the best way forward for musl is to define a macro like FreeBSD’s. It monotonically increases as APIs or features are added.

I agree that simple bug fixes, and even behavioural changes, probably should not be tracked with this macro. However, this would make it significantly easier to use new APIs as they are introduced.

It also makes builds more efficient. The cost of compile-time tests racks up quickly. On my POWER9 Talos workstation, typical ./configure runs take longer than the builds themselves. This is because fork+exec is still a slow path on POWER. It is similar on ARM, MIPS, and many other RISC architectures.

Macros like these don’t fully eliminate the need for ./configure, but they lessen the workload. Compile-time tests make sense for behaviour detection, but they do not make sense for API detection.

2 thoughts on “The musl preprocessor debate”

  1. As has been said every time this issue comes up, the reason this will not be added is that *every single request* for it comes with someone wanting to hard-code wrong behavior when building for musl. Often it’s hard-coding “musl doesn’t have feature X” when the very next release of musl would have “feature X” if they asked for it, whereas hard-coding this would leave things broken for a long time to come. Other times it’s hard-coding “use Y instead of X on musl” when there is no good reason to do this and it makes the resulting behavior worse for musl users. Etc. etc. etc.

    I agree fully that it’s very useful to be able to detect functionality with the preprocessor without a configure script probing to generate macros for the things you want to check. The right way to do this is with macros that describe the particular functionality, not a macro identifying the name of the implementation for you to hard-code wrong things you think you know about it. I have had this open proposal for a long time:

    https://www.openwall.com/lists/libc-coord/2020/04/22/1

    yet nobody who wants to do this engages with it to move it forward. Folks who really want macros should get on board with fleshing this out in a way that will allow *removing* implementation-specific #ifdefs as implementations adopt it, rather than adding yet another case to the wrong-#ifdef-hell.

    Like

    1. The better thing about that proposal is that it is very simple to duplicate in a “config.h” or similar for implementations that can’t / won’t / don’t implement it. I could write one for old versions of MSVC or CodeWarrior and make it simpler to port newer software to retro toolchains. I could write one for newer MSVC since Microsoft has historically taken a lot of time to adopt standards.

      So yeah, that is a *great* option – but as you note, nobody wants to actually support that, so we’re left with _something_ needing to be done. Maybe musl should start with being the first one to just do it, so that other libcs could see it “in action” and have something to go with…

      Like

Leave a reply to Rich Felker Cancel reply