Skip to content

Latest commit

 

History

History
96 lines (78 loc) · 7.74 KB

building-c++-reliably.org

File metadata and controls

96 lines (78 loc) · 7.74 KB

Building C++ Reliably

1 Introduction

C++ is notable among modern languages in not providing a package management system by which developers can easily install and reuse code developed by others into their own projects. It shares this with C, and for similar reasons. Until recently, source distribution was the exception, rather than the rule, and pre-compiled libraries are fragile, as they must, in general, use the exact same set of dependent libraries, compiled the same way. This largely limits reuse of packages to those provided by an OS or distro vendor. A modern Linux or BSD system may provide 1000s of such packages, however changing any of them may cause the entire system to fail. There is huge tension between Long Term Support versions and making current packages available.

2 Risks

2.1 No standardised package manager

Unlike tools such as node.js, python, or rust, there is no standard package manager. There isn’t even a widely used one that isn’t what comes with an OS. This is partly because C++ is not a single vendor system, so no vendor is in a position to standardise a solution, and also because of actual technical difficulties with shippint C++ binaries that will work.

2.2 Library ABI is a problem – C++ leaks across boundaries

C++ libraries usually pass by value, which means that layout of an object must match exactly. Inline functions also cross between translation units, which means that even if an object is passed by reference or pointer, manipulating it requires agreement on layout. This is also true transitively, so that any type used by a library must be the same everywhere in a program. There’s an official rule, the One Definition Rule, and if it is broken, there is no defined behavior, which usually means crashing.

2.3 Dependency changes require rebuilds

If anything your C++ code depends on changes, to be safe you must recompile your code, as well as all other code that depends on that. If you are library code, you now have to rebuild everything that depends on you. Package build managers like debian’a sbuild do some of this work, keeping a distro in sync.

2.4 Replacing OS supplied packages is risky

You’re using system supplied packages because they are easily available and most Linux systems provide a wide range of them. But you want to upgrade one of them because you need a new feature. So you upgrade it, put it in /usr/lib and now your OS won’t boot properly because some other critical component used it. The standard solution for this is install in /usr/local, but that has similar scaling issues. The more software in /usr/local, the harder it is to just swap out one library.

2.5 Shared libraries are risky - DLLHell

Shared libraries have versioning issues. It’s possible to specify that a shared library is compatible with older versions, but it’s very easy to get wrong. See the library ABI issues from before.

3 Approaches that can work

3.1 Source Integration

A common approach for smaller applications is to integrate libraries into the build of the application itself. The upside is that the library should be built consistently with your application. The downside is adapting the libraries builds into your own. Some build systems make this more straightforward than others. For cmake, for example, it may be as simple as:

add_subdirectory(extern/googletest EXCLUDE_FROM_ALL)

Then googletest will be built as part of your cmake build, and the gtest and gmock targets will be available to be depended upon. This approach has scalability issues. As the number of applications grows, integrating and building each library consumes more time and effort.

3.2 Use Stock OS Libraries

Modern Linux and BSD based systems provide a huge number of libraries that can be used from the system. Adding new libraries is as simple as apt install libxyzzy-dev. The downside is that you are then stuck with those versions, and upgrading the OS becomes painful because it may also mean rewriting parts of your code to match the new libraries.

3.3 Consistent Isolated Builds

Producing a consistent, if small, distro of the libraries that are used, built against each other, using the same toolchain and options, gives the most flexibility and reliability. Upgrading a package is under your control. If the promotion into the distro fails, no harm is done. Binary artifacts can be shared and reused, so individual developers don’t have to waste time rebuilding everything from scratch. It is also feasible using modern tools without needing extra machines to keep things isolated. The downside is that it does require some discipline. Upgrading a package at the bottom of the dependency DAG can take time, even if you know it is “safe”.

4 Building C++

4.1 Containers and Build Isolation

Dpkg has been using isolation in the form of `chroot` jails basically forever. This forces software being built to not look outside a particular directory tree in the filesystem, changing the root of the filesystem. Containers such as docker take this to a greater level, providing even more isolation. Isolating the build of your system from everything else can give you fine grained control of your environment and during the build. It can be useful to build software to be deployed into an organization standard location that is separate from any other system software. For example building software systems to run in `/opt/bb/` with a GNU style FHS within there – `/opt/bb/bin/`, `/opt/bb/etc/`, `/opt/bb/share`, and so forth. This helps prevent collision, and since the only things in the container are what you choose to put there, the chances of accident are low.

4.2 Deployment Isolation

Containers also provide deployment isolation. You don’t have to worry about incompatible shared object libraries from some other application or system because there are no other applications or systems in the container. However, because of the dependency problem, shared objects do not provide huge benefits. Many C++ experts prefer and recommend static linking, rather than deferring the link to runtime.

5 Quick automated demo

Hey, Rocky! Watch me pull the rabbit out of my hat!

Build and run a medium sized C++ project using docker, cmake, library export and import, then run the application in a container.