C Library Readability Rubric

This document describes heuristics and rules for writing C libraries that are published in the Fuchsia SDK.

A different document will be written for C++ libraries. While C++ is almost an extension of C, and has some influence in this document, the patterns for writing C++ libraries will be quite different than for C.

Most of this document is concerned with the description of an interface in a C header. This is not a full C style guide, and has little to say about the contents of C source files. Nor is this a documentation rubric (though public interfaces should be well documented).

Some C libraries have external constraints that contradict these rules. For instance, the C standard library itself does not follow these rules. This document should still be followed where applicable.

Goals

ABI Stability

Some Fuchsia interfaces with a stable ABI will be published as C libraries. One goal of this document is to make it easy for Fuchsia developers to write and to maintain a stable ABI. Accordingly, we suggest not using certain features of the C language that have potentially surprising or complicated implications on the ABI of an interface. We also disallow nonstandard compiler extensions, since we cannot assume that third parties are using any particular compiler, with a handful of exceptions for the DDK described below.

Resource Management

Parts of this document describe best practices for resource management in C. This includes resources, Zircon handles, and any other type of resource.

Standardization

We would also like to adopt reasonably uniform standards for Fuchsia C libraries. This is especially true of naming schemes. Out parameter ordering is another example of standardization.

FFI Friendliness

Some amount of attention is paid to Foreign Function Interface (FFI) friendliness. Many non-C languages support C interfaces. The sophistication of these FFI systems varies wildly, from essentially sed to sophisticated libclang-based tooling. Some amount of consideration of FFI friendliness went into these decisions.

Language versions

C

Fuchsia C libraries are written against the C11 standard (with a small set of exceptions, such as unix signal support, that are not particularly relevant to our C library ABI). C99 compliance is not a goal.

In particular, Fuchsia C code can use the <threads.h> and <stdatomic.h> headers from the C11 standard library, as well as the _Thread_local and alignment language features.

The thread locals should use the thread_local spelling from <threads.h>, rather than the built in _Thread_local. Similarly, prefer alignas and alignof from <stdalign.h>, rather than _Alignas and _Alignof.

Note that compilers support flags that may alter the ABI of the code. For instance, GCC has a -m96bit-long-double flag that alters the size of a long double. We assume that such flags are not used.

Finally, some libraries (such as Fuchsia's C standard library) in our IDK are a mix of externally defined interfaces and Fuchsia specific extensions. In these cases, we allow some pragmatism. For instance, libc defines functions like thrd_get_zx_handle and dlopen_vmo. These names are not strictly in accordance with the rules below: the name of the library is not a prefix. Doing so would make the names fit less well next to other functions like thrd_current and dlopen, and so we allow the exceptions.

C++

While C++ is not an exact superset of C, we still design C libraries to be usable from C++. Fuchsia C headers should be compatible with the C++11, C++14, and C++17 standards. In particular, function declarations must be extern "C", as described below.

C and C++ interfaces should not be mixed in one header. Instead, create a separate cpp subdirectory and place C++ interfaces in their own headers there.

Library Layout and Naming

A Fuchsia C library has a name. This name determines its include path (as described in the library naming document) as well as identifiers within the library.

In this document, the library is always named tag, and is variously referred to as tag or TAG or Tag or kTag to reflect a particular lexical convention. The tag should be a single identifier without underscores. The all-lowercase form of a tag is given by the regular expression [a-z][a-z0-9]*. A tag can be replaced by a shorter version of the library name, for example zx instead of zircon.

The include path for a header foo.h, as described by the library naming document, should be lib/tag/foo.h.

Header Layout

A single header in a C library contains a few kinds of things.

  • A copyright banner
  • A header guard
  • A list of file inclusions
  • Extern C guards
  • Constant declarations
  • Extern symbol declarations
    • Including extern function declarations
  • Static inline functions
  • Macro definitions

Header Guards

Use #ifndef guards in headers. These look like:

#ifndef SOMETHING_MUMBLE_H_
#define SOMETHING_MUMBLE_H_

// code
// code
// code

#endif // SOMETHING_MUMBLE_H_

The exact form of the define is as follows:

  • Take the canonical include path to the header
  • Replace all ., /, and - with _
  • Convert all letters to UPPERCASE
  • Add a trailing _

For example, the header located in the SDK at lib/tag/object_bits.h should have a header guard LIB_TAG_OBJECT_BITS_H_.

Inclusions

Headers should include what they use. In particular, any public header in a library should be safe to include first in a source file.

Libraries can depend on the C standard library headers.

Some libraries may also depend on a subset of POSIX headers. Exactly which are appropriate is pending a forthcoming libc API review.

Constant Definitions

Most constants in a library will be compile-time constants, created via a #define. There are also read-only variables, declared via extern const TYPE NAME;, as it sometimes is useful to have storage for a constant (particularly for some forms of FFI). This section describes how to provide compile time constants in a header.

There are several types of compile time constants.

  • Single integer constants
  • Enumerated integer constants
  • Floating point constants

Single integer constants

A single integer constants has some NAME in a library TAG, and its definition looks like the following.

#define TAG_NAME EXPR

where EXPR has one of the following forms (for a uint32_t)

  • ((uint32_t)23)
  • ((uint32_t)0x23)
  • ((uint32_t)(EXPR | EXPR | ...))

Enumerated integer constants

Given an enumerated set of integer constants named NAME in a library TAG, a related set of compile-time constants has the following parts.

First, a typedef to give the type a name, a size, and a signedness. The typedef should be of an explicitly sized integer type. For example, if uint32_t is used:

typedef uint32_t tag_name_t;

Each constant then has the form

#define TAG_NAME_... EXPR

where EXPR is one of a handful of types of compile-time integer constants (always wrapped in parentheses):

  • ((tag_name_t)23)
  • ((tag_name_t)0x23)
  • ((tag_name_t)(TAG_NAME_FOO | TAG_NAME_BAR | ...))

Do not include a count of values, which is difficult to maintain as the set of constants grows.

Floating point constants

Floating point constants are similar to single integer constants, except that a different mechanism is used to describe the type. Float constants must end in f or F; double constants have no suffix; long double constants must end in l or L. Hexadecimal versions of floating point constants are allowed.

// A float constant
#define TAG_FREQUENCY_LOW 1.0f

// A double constant
#define TAG_FREQUENCY_MEDIUM 2.0

// A long double constant
#define TAG_FREQUENCY_HIGH 4.0L

Function Declarations

Function declarations should all have names beginning with tag_....

Function declarations should be placed in extern "C" guards. These are canonically provided by using the __BEGIN_CDECLS and __END_CDECLS macros from compiler.h.

Function parameters

Function parameters must be named. For example,

// Disallowed: missing parameter name
zx_status_t tag_frob_vmo(zx_handle_t, size_t num_bytes);

// Allowed: all parameters named
zx_status_t tag_frob_vmo(zx_handle_t vmo, size_t num_bytes);

It should be clear which parameters are consumed and which are borrowed. Avoid interfaces in which clients may or may not own a resource after a function call. If this is infeasible, consider noting the ownership hazard in the name of the function, or one of its parameters. For example:

zx_status_t tag_frobinate_subtle(zx_handle_t foo);
zx_status_t tag_frobinate_if_frobable(zx_handle_t foo);
zx_status_t tag_try_frobinate(zx_handle_t foo);
zx_status_t tag_frobinate(zx_handle_t maybe_consumed_foo);

By convention, out parameters go last in a function's signature, and should be named out_*.

Variadic functions

Variadic functions should be avoided for everything except printf-like functions. Those functions should document their format string contract with the __PRINTFLIKE attribute from compiler.h.

Static inline functions

Static inline functions are allowed, and are preferable to function-like macros. Inline-only (that is, not also static) C functions have complicated linkage rules and few use cases.

Types

Prefer explicitly sized integer types (e.g. int32_t) to non-explicitly sized types (e.g. int or unsigned long int). An exemption is made for int when referring to POSIX file descriptors, and for typedefs like size_t from the C or POSIX headers.

When possible, pointer types mentioned in interfaces should refer to specific types. This includes pointers to opaque structs. void* is acceptable for referring to raw memory, and to interfaces that pass around opaque user cookies or contexts.

Opaque/Explicit types

Defining an opaque struct is preferable to using void*. Opaque structs should be declared like:

typedef struct tag_thing tag_thing_t;

Exposed structs should be declared like:

typedef struct tag_thing {
} tag_thing_t;

Reserved fields

Any reserved fields in a struct should be documented as to the purpose of the reservation.

A future version of this document will give guidance as to how to describe string parameters in C interfaces.

Anonymous types

Top-level anonymous types are not allowed. Anonymous structures and unions are allowed inside other structures, and inside function bodies, as they are then not part of the top level namespace. For instance, the following contains an allowed anonymous union.

typedef struct tag_message {
    tag_message_type_t type;
    union {
        message_foo_t foo;
        message_bar_t bar;
    };
} tag_message_t;

Function typedefs

Typedefs for function types are permitted.

Functions should not overload return values with a zx_status_t on failure and a positive success value. Functions should not overload return values with a zx_status_t that contains additional values not described in zircon/errors.h.

Status return

Prefer zx_status_t as a return value to describe errors relating to Zircon primitives and to I/O.

Resource Management

Libraries can traffic in several kinds of resources. Memory and Zircon handles are examples of resources common across many libraries. Libraries may also define their own resources with lifetimes to manage.

Ownership of all resources should be unambiguous. Transfer of resources should be explicit in the name of a function. For example, create and take connote a function transferring ownership.

Libraries should be memory tight. Memory allocated by a function like tag_thing_create should released via tag_thing_destroy or some such, not via free.

Libraries should not expose global variables. Instead, provide functions to manipulate that state. Libraries with process-global state must be dynamically linked, not statically. A common pattern is to split a library into a stateless static part, containing almost all of the code, and a small dynamic library holding global state.

In particular, the errno interface (which is a global thread-local global) should be avoided in new code.

Linkage

The default symbol visibility in a library should be hidden. Use either an allowlist of exported symbols, or explicit visibility annotations on symbols to exported.

C libraries must not export C++ symbols.

Evolution

Deprecation

Deprecated functions should be marked with the __DEPRECATED attribute from compiler.h. They should also be commented with a description about what to do instead, and a bug tracking the deprecation.

Disallowed or Discouraged Language Features

This section describes language features that cannot or should not be used in the interfaces to Fuchsia's C libraries, and the rationales behind the decisions to disallow them.

Enums

C enums are banned. They are brittle from an ABI standpoint.

  • The size of integer used to represent a constant of enum type is compiler (and compiler flag) dependent.
  • The signedness of an enum is brittle, as adding a negative value to an enumeration can change the underlying type.

Bitfields

C's bitfields are banned. They are brittle from an ABI standpoint, and have a lot of nonintuitive sharp edges.

Note that this applies to the C language feature, not to an API that exposes bit flags. The C bitfield feature looks like:

typedef struct tag_some_flags {
    // Four bits for the frob state.
    uint8_t frob : 4;
    // Two bits for the grob state.
    uint8_t grob : 2;
} tag_some_flags_t;

We instead prefer exposing bit flags as compile-time integer constants.

Empty Parameter Lists

C allows for function with_empty_parameter_lists(), which are distinct from functions_that_take(void). The first means "take any number and type of parameters", while the second means "take zero parameters". We ban the empty parameter list for being too dangerous.

Flexible Array Members

This is the C99 feature that allows declaring an incomplete array as the last member of a struct with more than one parameter. For example:

typedef struct foo_buffer {
    size_t length;
    void* elements[];
} foo_buffer_t;

As an exception, DDK structures are allowed to use this pattern when referring to an external layout that fits this header-plus-payload pattern.

The similar GCC extension of declaring a 0-sized array member is similarly disallowed.

Module Maps

These are part of a Clang extension to C-like languages that attempt to solve many of the issues with header-driven compilation. While the Fuchsia toolchain team is very likely to invest in these in the future, we currently do not support them.

Compiler Extensions

These are, by definition, not portable across toolchains.

This in particular includes packed attributes or pragmas, with one exception for the DDK.

DDK structures often reflect an external layout that does not match the system ABI. For instance, it may refer to an integer field that is less aligned than required by the language. This can be expressed via compiler extensions such as pragma pack.