C++ in Zircon

A subset of the C++17 language is used in the Zircon tree. This includes both the kernel and userspace code. C++ is mixed with C (and some assembly) in both places. Some C++ language features are avoided or prohibited. Use of the C++ standard library features is very circumspect.

Language features

  • Not allowed
    • Exceptions
    • RTTI and dynamic_cast
    • Operator overloading
    • Virtual inheritance
    • Statically constructed objects
    • Trailing return type syntax
      • Exception: when necessary for lambdas with otherwise unutterable return types
    • Initializer lists
    • thread_local in kernel code
  • Allowed
    • Pure interface inheritance
    • Lambdas
    • constexpr
    • nullptr
    • enum classes
    • templates
    • Default parameters
      • But use judgment. One optional out parameter at the end is probably fine. Four optional bool arguments, probably not.
    • Plain old classes
    • auto
    • Multiple implementation inheritance
      • But be judicious. This is used widely for e.g. intrusive container mixins.
  • Needs more ruling TODO(cpu)
    • Global constructors
      • Currently we have these for global data structures.

TODO: pointer to style guide(s)?

C++ Standard Edition

Zircon code is built with -std=c++17 and in general can use C++ 17 language and library features freely (subject to style/feature constraints described above and library use guidelines described below). There is no general concern with staying compatible with C++ 14 or earlier versions. When a standard C++ 17 feature is the cleanest way to do something, do it that way.

All pure C code (.c source files and headers used by them) is C 11. Some special exceptions are made for code meant to be reused by out-of-tree boot loaders, which stick to a conservative C 89 subset for embedded code.

Standard Library

The C++ standard library API has many interfaces of widely varying characteristics. We subdivide the standard library API into several categories below, based on the predictability and complexity of each particular interface's code generation and use of machine and OS facilities. These can be thought of as widening concentric circles of the API from the most minimal C-like subset out to the full C++ 17 API.

Context Matters

This section gives guidelines for how to think about the impact of using a particular standard C++ library API on the system as a whole. There are no hard and fast rules, except for the kernel (see the next section)--and except for implementation constraints, which one always hopes should be temporary.

The overwhelming rule is be circumspect.

  • Consider how well you understand the time and space complexity, the dynamic allocation behavior (if any), and the failure modes of each API you use.

  • Then consider the specific context where it's being used, and how sensitive that context is to those various kinds of concerns.

  • Be especially wary about input-dependent behavior that can quickly become far harder to predict when using nontrivial library facilities.

If you're writing the main I/O logic in a driver, or anything that's in a hot path for latency, throughput, or reliability, in any kind of system service, then you should be pretty conservative in what library facilities you rely on. They're all technically available to you in userspace (though far fewer in the kernel; see the next section). But there's not so many you actually should use. You probably don't want to lean on a lot of std containers that do fancy dynamic allocation behind the scenes. They will make it hard for you to understand, predict, and control the storage/memory footprint, allocation behavior, performance, and reliability of your service.

Nonetheless, even a driver is a userspace program that starts up and parses configuration files or arguments and so on. For all those nonessential or start-time functions that are not part of the hot path, using more complex library facilities is probably fine when that makes the work easier. Just remember to pay attention to overall metrics for your code, such as minimal/total/peak runtime memory use, code bloat (which uses both device storage and runtime memory), and resilience to unexpected failure modes. Maybe don't double the code size and memory footprint of your driver just to leverage that fancy configuration-parsing library.

No std in kernel

The C++ std namespace cannot be used in kernel code, which also includes bootloader. The few C++ standard library headers that don't involve std:: APIs can still be used directly. See the next section.

No other C++ standard headers should be used in kernel code. Instead, any library facilities worthwhile to have in the kernel (such as std::move) are provided via kernel-specific APIs (such as ktl::move). The kernel's implementations of these APIs may in fact rely on toolchain headers providing std:: implementations that are aliased to kernel API names. But only those API implementations and very special cases in certain library headers should ever use std:: in source code built into the kernel.

Universal headers

These header APIs are safe to use everywhere, even in the kernel.

They include the C++ wrappers on the subset of standard C interfaces that the kernel supports:

The std namespace aliases for C library APIs from these headers should not be used in kernel code.

One pure C++ header is also available even in the kernel:

  • <new>

    The vanilla non-placement operator new and operator new[] are not available in the kernel. Use fbl::AllocChecker new instead.

Conservative userspace

These header APIs are safe to use everywhere. They're not allowed in the kernel because they're all entirely in the std namespace. But subsets of these APIs are likely candidates to get an in-kernel API alias if there is a good case for using such an API in kernel code.

These are pure header-only types and templates. They don't do any dynamic allocation of their own. The time and space complexity of each function should be clear from its description.

These involve some dynamic allocation, but only what's explicit:

  • <any>
  • <memory>

    The std::shared_ptr, std::weak_ptr, and std::auto_ptr APIs should never be used. Use std::unique_ptr and fbl::RefPtr instead.

Userspace-only

These are not things that would ever be available at all or by any similar API or name in the kernel. But they are generally harmless everywhere in userspace. They do not involve dynamic allocation.

Kitchen sink

These involve dynamic allocation that is hard to predict and is generally out of your control. The exact runtime behavior and memory requirements are often hard to reason about. Think very hard before using these interfaces in any critical path for reliability or performance or in any component that is meant to be lean and space-efficient.

FBL

FBL is the Fuchsia Base Library, which is shared between kernel and userspace. As a result, FBL has very strict dependencies. For example, FBL cannot depend on the syscall interface because the syscall interface is not available within the kernel. Similarly, FBL cannot depend on C library features that are not available in the kernel.

  1. system/ulib/fbl, which is usable from both kernel and userspace.
  2. kernel/lib/fbl, which is usable only from the kernel.

FBL provides:

FBL has strict controls on memory allocation. Memory allocation should be explicit, using an AllocChecker to let clients recover from allocation failures. In some cases, implicit memory allocation is permitted, but functions that implicitly allocate memory must be #ifdef'ed to be unavailable in the kernel.

FBL not available outside the Platform Source Tree.

ZX

ZX contains C++ wrappers for the Zircon objects and syscalls. These wrappers provide type safety and move semantics for handles but offer no opinion beyond what's in syscalls.abigen. At some point in the future, we might autogenerate ZX from syscalls.abigen, similar to how we autogenerate the syscall wrappers in other languages.

ZX is part of the Fuchsia SDK.

FZL

FZL is the Fuchsia Zircon Library. This library provides value-add for common operations involving kernel objects and is free to have opinions about how to interact with the Zircon syscalls. If a piece of code has no dependency on Zircon syscalls, the code should go in FBL instead.

FZL not available outside the Platform Source Tree.

Hermetic C++

We encourage using C++ rather than C as the implementation language throughout Fuchsia. However, in many instances we require a narrow ABI bottleneck to simplify the problem of preventing, tracking, or adapting to ABI drift. The first key way to keep the ABI simple is to base it on a pure C API (which can be used directly from C++, and via foreign-function interfaces from many other languages) rather than a C++ API. When we link together a body of code into a module with a pure C external API and ABI but using C++ internally for its implementation, we call that hermetic C++.

  • The kernel itself could be said to be implemented in hermetic C++.
  • The vDSO is a shared library implemented in hermetic C++.
  • Fuchsia's standard C library, while largely implemented in C, also uses hermetic C++ in its implementation.
  • Most Fuchsia device drivers are implemented in hermetic C++.

It's a hard and fast rule for binaries exported in the Fuchsia's public SDK that shared libraries must have a pure C API and ABI. Such libraries can and should use C++ rather than C in their implementations, and they can use other statically-linked libraries with C++ APIs as long as ABI aspects of those internal C++ APIs don't leak out into the shared library's public ABI.

A "loadable module" (sometimes called a "plug-in" module) is very similar to a shared library. The same rules about pure a C ABI bottleneck apply for loadable module ABIs. Fuchsia device drivers are just such loadable modules that must meet the driver (pure C) ABI. Hence, every driver implemented in C++ must use hermetic C++.

The Fuchsia C++ toolchain provides the full C++17 standard library using the libc++ implementation. In C++ executables (and shared libraries with a C++ ABI) this is usually dynamically linked, and that's the default behavior of the compiler. The toolchain also provides libc++ for hermetic static linking via the -static-libstdc++ switch to the compiler (clang++). In the Zircon GN build system, a linking target such as executable(), test(), or library() (with shared = true), uses this line to request the hermetic C++ standard library:

    configs += [ "//zircon/public/gn/config:static-libc++" ]

This is required in each library() that is exported to the public IDK in binary form via sdk = "shared".

Every driver() automatically uses hermetic C++ and so this line is not required for them. (Drivers cannot depend on their own shared libraries, only the dynamic linking environment provided by the driver ABI.)

For executables and non-exported shared libraries, it's a judgment call whether to use static linking or dynamic linking for the standard C++ library. In Fuchsia's package deployment model, there is no particular updatability improvement to using shared libraries as in many other systems. The primary trade-off is between the savings in memory and storage from many stored packages and running processes on the system using exactly the same shared library binary and compactness and (sometimes performance) of the individual package. Since many packages in the system build will all use the same shared libc++ library, that's usually the right thing to do unless there are special circumstances. It's the default in the compiler and build system.