This is the beginning of a series on the Lucena Utilities Library.
The purpose of the Lucena Utilities Library (LUL) is to abstract the build environment. It’s a foundational tool that should allow code built on top of it to remain agnostic regarding the underlying compiler, Standard Library, build system, and aspects of the runtime. Note that LUL is specifically not a build system, but it does try to shield a project from details of the build system at the code level.
When the functionality was first isolated into a standalone library, a few other pieces came along for the ride, but they were mostly lifted out during the C++14 update. Having a clear mandate simplifies in/out decisions as well as other design issues, though it’s not always an automatic process. For example, lifting Unicode string conversions out was an easy call; leaving std::string abstractions in still feels like a slippery slope to Navel-Gazing Hell—more on that in a later post.
A few hopeful predictions guided the architecture of the Library. It was it was expected that C++ feature detection would become standardized, it was expected that Modules would make their way into the C++ Standard, and it was hoped that a (per-platform) C++ ABI would be settled on. Various aspects of the Library were put together in an effort to “play nice” when these predictions came to pass, intending that disruption to dependent codebases would be minimized. Reality has been a bit unkind to the predictions, but designing the Library in an as-if way has been mostly beneficial, as we’ll see in this series.
Today, I’ll be evaluating a potential update to the Library. Next time, I’ll cover the Library’s changing requirements (so, yes, spoiler alert: the evaluation indicates updating is a sound plan).
The last major overhaul of the Library, as implied earlier, was coincident with the release of C++14. At the time, support was generally promising, but sketchy on some platforms. Now, C++17 is official. All the major compilers have full support for the required parts of C++14 and mostly behave, and there’s every reason to believe full C++17 support will be done everywhere we care about by the summer—though full support for the many Technical Specifications (TS’s) may lag, if it ever manifests at all. Meanwhile, C++20-Let’s-Be-Coy-and-Call-It-2a is being actively worked on and has seen a number of TS’s promoted into the language and the Standard Library already, and it’s shaping up to be a library author’s Dream.
For a new project being started today, there are few good reasons to not target C++17. However, given the state of implementations of the new Standard, it is still useful to have an abstraction that simplifies that objective regardless of build environment. Updating LUL is one obvious choice, but it is a valuable exercise to consider alternatives. Cross-platform projects of any size have solutions for this; they almost always end up rolling their own. We’ll revisit this, but first, let’s do a quick analysis of some of the major options.
Boost doesn’t offer a general solution to this problem. The boost.config portion is a thorough and well-maintained feature detection suite, but the workarounds it offers are mostly specific to compilers. In itself, it doesn’t offer reference versions for missing library functionality so much as serve as a gateway to the whole alternative Boost ecosystem. It might serve as the basis for a more constrained tool, but it’s massive platform portfolio might imply support for configurations that will never, ever be developed. Note that its intended use as an internal tool for Boost library implementers is not an impediment; the library and the license it’s under are flexible, and it could be forked and edited into a solution.
This is an excellent general-purpose solution which handles build environment abstraction very well. It also includes random bits of gratuitous utility code, support for a small horde of meta-tools, and a pile of helper scripts for certain domain-specific tasks. Ignoring the bloat outside the context of our particular problem, the availability of reference library functionality is limited, and QuickCppLib has the unfortunate habit of stomping through territory restricted to use by Standard Library implementers, setting up potential issues with header inclusion order and other surprises (note that this latter issue is arguably only a philosophical one).
…and everything else
Every other similar piece of functionality I’m aware of is part of a larger project, typically deeply tied. For example, Qt exposes this functionality, and it gets used by many projects built on Qt tools and libraries, but feature detection, etc., does not exist in an encapsulated form, and extricating it means adhering to either the terms of the Qt commercial license or one of the potentially troublesome GPL licenses. Chromium has this sort of thing scattered across a dozen locations, and doesn’t appear to be focused on reusability or genericity. These two projects represent endpoints on a spectrum, but the common denominator of the examples I’ve found is that the problem was solved independently, in a way unique to the project.
Where does LUL fit in?
The Utility Library was originally designed to provide a minimal set of feature detection and Standard Library wrapper functionality for in-house projects. The broader scope planned for this release is to make something that is generally useful for outside projects, as well. Though not explicitly stated earlier, the analyses have revealed a bias on my part: I consider the other offerings fail either on the side of overreach or of over-specialization. Attempting to rectify this, at first glance, would seem to doom LUL to the same criticism. The way out that I’ve decided upon is to design the Library to act only as the basis for its users’ own solutions. While it’s certainly possible to simply use it as-is, users are welcome and encouraged to customize it to fit their needs.
One result of this design philosophy is that the Library works best when built as a static library, as opposed to a dynamic library. Note that distributing a header-only solution is impossible due to the requirement to provide reference implementations for missing Standard Library functionality; for example, preventing the potential to violate C++’s One Definition Rule while also providing access to complete types for destructors makes object files unavoidable for ancillary classes in <optional> and <any>. Of course, if we only cared about feature detection, a header-only solution would be possible.
Relatedly, the Library uses macros as namespace aliases, both in headers and source. Partly, this is done to abstract out versioned namespaces—even if the Library itself is not dynamic, it may be statically linked with another library that is—but it’s also to simplify putting the Library into a different namespace to avoid potential conflicts if multiple, potentially incompatible versions are linked into one project.
Many things can still go wrong—basically any of the issues that would normally affect ABI compatibility—but these things are the sorts of policy issues C++ project architects have to deal with all the time, anyway. This situation won’t improve till we get real, stable ABIs, a la C, or possibly a fancy module standard and universal support for certain proposed attributes; this will be explored in a future blog post.
Having perfunctorily covered the obligatory “why are we doing this?” phase, the next posts in the series will explore what will—or will not—change in the Utility Library.