Development Guidelines#
Note
This page is for amongoc
developers and the documented components and
behavior are not part of any public API guarantees.
See also
Macros & Preprocessor#
When defining C preprocessor macros, adhere to the following:
Prefer snake_case
#
If the macro is intended to be used as an expression, statement, attribute, declaration, or declaration specifier, use
snake_case
.If a function-like macro performs nontrivial token manipulation, use
SHOUTING_CASE
.
Beware the global-scope specifier#
Avoid writing expression macros that generate a syntax error in C++ if they are expanded following a global-scope resolution operator:
// Bad:
#define function_macro() \
(_calls_a_function(x, y, z))
// Causes a syntax error:
auto n = ::function_macro();
// Okay:
#define better_constant_value \
_calls_a_function(x, y, z)
// Okay:
auto n = ::function_macro()
If the parenthesis are absolutely necessary, use
mlib_parenthesized_expression
:
#define function_macro(X, Y) mlib_parenthesized_expression((X) + (Y))
// Okay:
auto z = ::function_macro(4, 5);
Reasoning
The fact that a function is implemented using a macro is an implementation
detail. If a C++ user uses a global-scope qualifier on a non-macro amongoc
function, and we later refactor that function to be a macro, it would be a
breaking change for any C++ users that expect the ::
qualifier to continue
to work.
Avoid Conditional Compilation#
Use of conditional compilation should be avoided if possible. Conditional
attributes and specifiers should be hidden behind a macro that performs the
conditional expansion internally (e.g. mlib_extern_c
). Conditional
compilation should be localized to the smallest lexical scope required.
Use terse conditional compilation#
Prever to use MLIB_IF_CXX
and MLIB_IF_NOT_CXX
for more
concise conditional compilation/macro expansions.
Avoid using #ifdef
or #if
with object macros#
Instead, use #if
with function-like macros
Bad:
#ifdef __cpllusplus // Oops: Spelling error
// this block never compiles
#endif
Good:
#if mlib_is_cxx()
// Okay
#endif // C++
#if milb_is_cxx() // Compile-error: milb_is_cxx is not defined (typo)
Linkage Blocks#
Prefer to use mlib_extern_c_begin
and
mlib_extern_c_end
to conditional extern "C"
.
See also
Beware R-values that look like L-values#
Avoid writing object-like macros that expand to rvalues. Users may expect to be able to take the address of such an expression since it looks like an lvalue:
#define my_special_constant 42
const int* p = &my_special_constant; // Error!
Instead, use the same idiom as used for C errno
:
inline const int* _mySpecialConstantPtr() {
const int value = 42;
return &value;
}
#define my_special_constant mlib_parenthesized_expression(*_mySpecialConstantPtr())
Alternatively, write it as a function-like macro that accepts no arguments:
#define my_special_constant() mlib_parenthesized_expression(42)
While this is less than ideal, it does not look like an lvalue when it appears in source code.
Declaration/Statement macros should require a semicolon#
If a macro is intended to be used like a statement or a declaration, it should expand such that it requires a following semicolon. Bad examples:
#define declare_an_int(Name) int Name = 0;
#define declares_a_func(Name) int Name() { return 42; }
#define early_return(Cond) \
if (!(Cond)) { puts("Condition failed"); return EINVAL; }
Better:
#define declare_an_int(Name) \
int Name = 0
#define declares_a_func(Name) \
int Name() { return 42; } \
mlib_static_assert(true, "")
#define early_return(Cond) \
if (!(Cond)) { puts("Condition failed"); return EINVAL; } \
else ((void)0)
Requiring a semicolon prevents ambiguity and compiler warnings about extra semicolons when a user adds a semicolon to a macro expansion that doesn’t need one.
Add comments to distant endif
and #else
directives#
If an #endif
or #else
directive is far away from its associated
conditional, add a comment explaining what it’s for:
#if mlib_is_cxx()
// many many many many lines
#else // ↑ C++ / C ↓
// many many many more lines
#endif // C
Utility Macros#
-
MLIB_LANG_PICK#
A special function-like macro that takes two argument lists in two sets of parenthesis. If compiled as C, the first argument list will be expanded. In C++, the second argument list will be expanded. The unused argument list will be discarded. Neither argument will undergo immediate macro expansion:
puts(MLIB_LANG_PICK("I am compiled as C!")("I am compiled as C++!"));
-
MLIB_IF_CXX(...)#
-
MLIB_IF_NOT_CXX(...)#
Expands to the given arguments when compiled in C++ or C, resepectively. Prefer
MLIB_LANG_PICK
if you have something to say in both languages.
-
MLIB_IF_CLANG(...)#
-
MLIB_IF_GCC(...)#
-
MLIB_IF_MSVC(...)#
-
MLIB_IF_GNU_LIKE(...)#
Function-like macros that expand to their arguments only when compiled using the associated compiler.
-
mlib_parenthesized_expression(...)#
In C, expands to
(__VA_ARGS__)
. In C++, expands tomlib::identity{}(__VA_ARGS__)
. This allows the expression to be preceded by a global-scope name qualifier when the macro is expanded in C++. See: Beware the global-scope specifier
-
mlib_extern_c#
Expands to
extern "C"
when compiling as C++, otherwise an empty attribute. Enforces C linkage on an entity.
-
mlib_extern_c_begin()#
-
mlib_extern_c_end()#
Declaration-like function macros that expand to the
extern "C"
block for wrapping APIs with C linkage. Note that these expand to declarations and require a following semicolon:mlib_extern_c_begin(); extern int meow; mlib_extern_c_end();
For declaring a single item, it may be more ergonomic to use
mlib_extern_c
.
-
mlib_is_cxx()#
-
mlib_is_not_cxx()#
-
mlib_is_gcc()#
-
mlib_is_clang()#
-
mlib_is_msvc()#
-
mlib_is_gnu_like()#
Expression-macros that evaluate to
0
or1
depending on the compiler and the compile language.
-
mlib_init(T)#
Usage of this macro is mandatory in C headers when writing a compound initializer expression. This is required because C++ does not support compound initializers, but does support the same syntax with brace initializers:
my_struct get_thing(int a) { return mlib_init(my_struct){a, 42}; }
Note
The type
T
cannot use an elaborated name, as that does not work with C++ brace initializers
-
mlib_static_assert(Cond, Msg)#
Expands to a static assertion appropriate for the current language.
Public Headers#
Header Sorting#
Headers should be generally sorted and grouped as follows:
Primary
#include
directives (this will be the primary associated header(s) for a source file).Implementation details
<amongoc/...>
headers<mlib/...>
headersNon-standard third-party headers
System headers
Standard library headers
This sort order is intended to minimize accidental dependencies by catching use of required symbols as soon as possible.
The .clang-format
file for amongoc
will generally perform the appropriate
header sorting automatically. This automatic header sorting can be overriding by
placing a comment between #include
directives.
Inclusion Syntax#
Relative #include
directives should use double-quote style, and should
begin with a dot or dot-dot path element to emphasize the relative-style header
resolution.
Absolute inclusions should use angle-bracket style.
Run the make format
Makefile target to automatically rewrite and sort all
#include
directives into the appropriate style.
Use Linkage Specifiers#
All non-static C functions and global variable declarations should be annotated
with the appropriate linkage specifiers for C++ compatibility. Use
mlib_extern_c_begin
and mlib_extern_c_end
before and after
most C declarations.
In general, you should not use linkage specifiers around type definitions, since
they do not require it, and adding C++ members to types will break if it is
wrapped in extern "C"
.
See also
Be aware of C’s inline
rules#
If a C function is declared inline
and not static
, then there must exist
an extern inline
declaration of that function in exactly one C
translation unit. This differs from C++, where an inline
function is emitted
in each TU in which it is used, and the linker merges them at the final step. In
C, this consolidation must be done explicitly using an extern inline
declaration.
Note
There is no way to automatically verify this, because it will only generate an error if the compiler decides not to do the inlining and expects an external definition.
Do not use a static
function in a non-static
inline
function in C#
While probably benign, this will generate an unignorable compiler warning.
C++ Compatibility#
All public headers that have a .h
extension must be able to be compiled
as C11 and as C++17. Usage of C++20 features should either be moved to a
.hpp
header or guarded with mlib_have_cxx20()
.
To write a C++-only header, use the .hpp
file extension. The .hpp
headers may use C++20.
Inclusion of C++ APIs in C Headers#
Public C++ APIs may be included in C headers if they are not a substantial
portion of the file. These should be simple wrappers around the C types (e.g.
amongoc::unique_emitter
)
Avoid adding C++ constructors to C structs#
This can create a semantic ambiguity when a C struct is constructed in a C
header. If you really need it, make sure that all calls to that constructor
within C headers are syntactically valid and semantically equivalent when
compiled in C and C++ modes (See: amongoc_status
).
Instead, prefer to use the named-constructor idiom: Use static
member
functions that construct instances of the object (e.g. amongoc_status::from()
).
Do not add copy/move constructors to C structs, nor add a destructor to C structs#
This will change the definition of inline functions defined in C headers that use such types, leading to ODR violations.
For easier automatic destruction of C types, follow the rules in C Object Deletion.