Conventions#

A brief overview of conventions used for the xNVMe C codebase.

C: format#

clang-format is utilized to format the code. There are two clang-format-option files, one for the headers (toolbox/clang-format-h) and one for the source toolbox/clang-format-c.

You can setup your editor to use the format-files, or run clang-format via the toolbox: make format-all. This will format all files in the repository.

C: style#

  • Declare variables

    • At the top of code-blocks; before any logic, e.g.

      • at the top of a function-code-block / loop-nest / anonymous-block

      • for-loop “control/iterator” variables should be declared and initialized in the for-loop header.

    • In the code-block nearest to its utilization

      • in a function, the top-most scope is commonly used for argument-unpacking, and the idiomatic int err variable, along with auxilary variables needed by the function body in the outer-most scope

      • variable-declaration in nested-block-scope is encouraged; still at the top of that block-scope. For example, when a variable is only ever used in a nested-block, then declare it within that scope

  • Initialize variables

    • Using zero/constant-initialization; struct foo = {0};

    • To unpack arguments; int simple = arg->is->alot->simple;

    • Do not initialize using functions that require error-handling

  • Global variables

    • Prefix them with g_

  • Compiler-specific features

    • For example, gnu/gcc-only is discouraged, as it hinders portability

    • Code should preferably translate using any of gcc/clang/icc/pgi

    • Exceptions to this would be code in backends utilizing features which are only available on the platform that the backend services

C: API#

  • The API and its backend implementations aims to be consistent with the following

    • Pass pointers to “objects” e.g. xnvme_cmd_pass(*ctx, ...);

    • Pass double-pointers for “object-initialization” e.g. xnvme_queue(..., **queue);

    • On success, return 0. On error, Return negative errno.

    • Exceptions: API-calls mimicking legacy interfaces, such as the xnvme_buf_alloc() which is operating similar to malloc()/free().

  • The API and its backend implementation cannot be assumed to be thread-safe

  • Be minimal with definitions in the public API

    • E.g. the ‘sys/queue.h’ was removed from the public API as it gave several head-aches when building xNVMe on multiple platforms. However, internally, specifically in backend implementations, assumptions can be made on the availability of certain headers and their general availability

  • Each function in the API must have a command-line tool exercising it

    • The source-code for the command-line tools serve as examples of utilizing the C API in general and the tools are used during testing

    • The command-line utilities must use the libxnvme_cli.h as this provides a common command-line interface, the common-cli is nice as for usability, such that all cli-tools use the same arguments, it is also essential for instrumentation of not just the logic of the tool but also the library backend

C: Backend Implementations#

The backend interface is declared in the internal header xnvme_be.h. It consists of function-pointers for different tasks grouped together in structs. Those different set of tasks are referred to as the function-interfaces, they are labeled / grouped by:

  • dev : device handles and device enumeration

  • mem : memory allocation for command-payloads aka buffers

  • admin : synchronous admin-command processing

  • sync : synchronous I/O command processing

  • async : asynchronous I/O command submission/completion via queues

Additionally, a backend-state is available for specialization to a given backend and its function-interface implementations.

  • Should have a header-file defining the backend and what it exposes

    • Located in include/xnvme_be_<backend_name>.h

    • Have a state-struct named xnvme_be_<backend_name>_state

    • Provide extern struct xnvme_be_<interface> * definitions for the interface implementations, these are used when “mixing-in” the implementations into the backend

  • Should be implemented in a file named lib/xnvme_be_<backend_name>_<interface>_<ident>.c

    • The <ident> is a name uniquely identifying the interface-implementation

    • Example: the Linux backend, named linux, has the async implementation of the async interface utilizing io_uring in a file named: * xnvme_be_linux_async_liburing.c

    • Example: The SPDK backend, named spdk, has the async implementation using the SPDK NVMe driver in a file named: * xnvme_be_spdk_async_nvme.c

  • Should use static function declarations to avoid symbol-leak

    • The cmd_io() backend interface function should be declared static int cmd_io(...)

  • In case an interface-function should be shared by other backends, then provide the full “name”, backend/interface/ident/function

    • Example: for the Common Backend Implementations (CBI), the sync implementation using psync for the cmd_iov() interface function, then the function is named: xnvme_be_cbi_sync_psync_cmd_io()