Portable software is more complex than you think

I’m someone who cares about making software portable. In fact, I actually have a job basically doing so. For most Unix-shaped things (better known as things, since Unix destroyed all competition), the POSIX standard exists to codify common attributes and provide a common ground. Unfortunately, this is made far more complicated by systems both doing many things outside of POSIX’s lowest common denominator, and systems just not implementing POSIX correctly. People tend to think “portability” is whatever operating systems they use, and assuming the lowest common denominator is that. While many guides recommend writing software in a disciplined (or tortured, if you disagree) manner with separate compilation units for platform differences when possible, the reality is your codebase will have #ifdefs and a configure script if it does anything useful. Not to mention the increasing irrelevance of the standard itself.

Things you need outside of POSIX (or C)

If you’re writing real-world software of a certain type (for example, I do a lot of work on language runtimes, which cover many domains because of things like JITs), you will hit some domain that POSIX doesn’t cover, but your system diverges in its own direction anyways. POSIX rarely covers such things because they tend to be both implementation details specific to a machine, but also because a commercial implementation did something differently, and the standard needs to acommodate. For example:

  • Linkage is a major factor, both at build time and run time. Even if you aren’t writing an ELF parser, you need to worry about things like executable formats because they inform linkage issues. I’ve dealt with many specifics about the XCOFF format and limitations of AIX’s dynamic loader; for example, its inability to deal with multiple libraries having similar symbols very well. This is how you quickly learn libtool actually has a purpose.
  • Service management is just papered over, if covered at all. Of course, this is because most systems then were either System V or BSD based, and they had two different init systems. Of course, now things like launchd, SMF, and systemd arose to address how anemic these were in providing services; now there’s even less common ground even if it’s all fleshed out.
  • Compilers and architecture. Yeah, we all use GCC now. Except when we use clang. Or that vendor compiler that really optimizes math code (Ever used MSVC as a compiler for a POSIX target? I have.). Not to mention how they can differ across architectures can differ. Drawing from experience: with GCC, -export-dynamic is usually used as a quick way to specify you want dynamic library exports in a program in CCLD mode. Except on AIX, it passes -e to the linker, which turns xport-dynamic into the entry point. Hope you weren’t using main for anything!

Sometimes people simply assume POSIX implements something, because many things they’re used to include a facility. A simple example would be the which command; while even conservative Unices like AIX implement it, it’s not part of the POSIX standard. While POSIX alternatives to it exist (like types), it is very easy to get into cases when you think you’re following the letter of the law, but you’re not. (Then said implementations different in flags as well…) Its existence inspires pedantry.

Such simplicity by ignoring things leads people to think “well, why not plain makefiles, and get rid of autotools and CMake?” Your makefile would be simple, until it has to support multiple operating systems, at which point you’re basically reinventing autotools from first principles as you deal with i.e. linking differences. At which point, why not use a proper build system generator like CMake that actually is portable beyond POSIX too?

It’s not just POSIX too. Because for years the only practical way of writing portable systems is C, you’ll hit trivial things that should be covered, like population count or dealing with endianness. While a lot of it is compiler-specific extensions that can be papered over, things like C’s ignorance of endianness issues causes people to do things like write subtly broken byte swapping code or forget to swap when they should. I hope you aren’t blitting out raw structs to disk! It’s truly hard to write something “C89” and actually be able to mean it, for reasons previously covered.

Things that don’t matter

POSIX, like many things, has a lot of legacy cruft. An example misfeature imported from System V would be its hashtables, which are a singleton (!) that calls malloc when it wants; something less common with modern C APIs. Realistically, everyone will import some kind of library for it, ranging from header-only libraries to behemoths like APR or glib.

Something that may matter more is the focus on conforming to limited versions of tools and libraries that may be irrelevant for most situations (i.e outside of embedded or purists). GNU extensions to the libraries and commands are common, even on legacy proprietary Unices, where they’re easily packaged. Likewise, it may be the case you could use actual standards like C99 or even C++, if the platforms you’re targeting have it.

New ways of thinking

POSIX itself is an old standard, but even old things change, implementations included. Linux is adding new APIs like event or timer file descriptors. FreeBSD is had jails at the beginning of the millennium. OpenBSD created simple sandboxing APIs. New applications want to use containers, but POSIX itself exists in a world where the only container was a chroot. While some of this is just niceties one can #ifdef away, some of this can be foundational to how an application is structured; timers as file descriptors instead of signals or other things means you can poll on it like other resources. Trying to paper over this would significantly complicate an application.

But none of those new ideas will head to POSIX (except the most simple ones, like dprintf and getline), because POSIX reflects what commercial Unix vendors want – which is to be frozen in time. You’re making my job of getting things to work on AIX easier, and selfishly, it makes it easier for me if I recommend others do so. However, it means what POSIX specifies outstrips what real applications need, outside of purposely minimalist Unix enthusiast types’ own hobbies. Standardizing things like timerfds would be great, but I don’t really see it happening any time soon. (Even then, systems like illumos will likely implement the Linux versions even if they have their own, out of desire to be widely compatible with software using it at a source of syscall ABI level.)

Newer applications simply don’t care, and who can blame them? Governments and enterprise, long the target of standards bodies, simply want you to target whatever RHEL/SLES exists (It’s not like they cared back when they claimed to; Microsoft sold NT like gangbusters in the 90’s, when POSIX was a tickbox compliance item for them no one would use.). Programmers will assume Linux/BSD/Mac, because it’s not like the other POSIX implementations will do what they want to do nowadays, if they can even get a hold of them. It’s to the point Microsoft accepted defeat, but they looked beyond POSIX – they just implement Linux now, as WSL.

And for what it’s worth: this article is partially a reply to Joe Nelson’s article on portability, which I don’t think paints an accurate view of the topic. The bits about trying to isolate platform-specific things to translation units rapidly fails contact with the messiness of reality, in my opinion. (Extreme case in point: have fun when the system defines BAD and clobbers your enum’s value. Have fun not being tempted to use #ifdef!) There’s more in that article like Motif I’d like to talk about, but I’m keeping it focused on POSIX.

Leave a Reply

Your email address will not be published.