This document describes the C++ coding conventions and style guidelines for the XDP2 project. These guidelines are derived from the existing codebase patterns and should be followed for all C++ contributions.
- File Organization
- Naming Conventions
- Formatting
- Include Directives
- Comments and Documentation
- Classes and Structs
- Memory Management
- Error Handling
- Templates and Metaprogramming
- Const Correctness
- Modern C++ Features
- Macros
- Debugging
- Assertions
- Testing
src/tools/compiler/
├── src/ # Implementation files (.cpp)
├── include/
│ └── xdp2gen/ # Public headers
│ ├── ast-consumer/ # Clang AST consumer headers
│ ├── llvm/ # LLVM IR analysis headers
│ ├── program-options/# CLI argument handling
│ ├── json/ # JSON metadata specs
│ └── clang-ast/ # Clang AST metadata
| Extension | Usage |
|---|---|
.h |
Traditional C++ headers |
.hpp |
Alternative C++ headers |
.h2 |
Cppfront source files |
.cpp |
Implementation files |
All source files must begin with the BSD-2-Clause-FreeBSD SPDX license header:
// SPDX-License-Identifier: BSD-2-Clause-FreeBSD
/*
* Copyright (c) 2024 SiXDP2 Inc.
*
* Authors: [Author Name] <email@domain.com>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/Use snake_case consistently throughout the codebase. Avoid PascalCase or camelCase.
Use hierarchical lowercase namespaces with :: separators:
namespace xdp2gen {
namespace ast_consumer {
// ...
}
}Use snake_case for class and struct names:
// Good
class llvm_graph { };
struct tlv_node { };
class xdp2_proto_node_consumer { };
// Bad
class LlvmGraph { };
struct TlvNode { };Use snake_case for all functions:
// Good
void transfer_data_from_proto_node();
auto find_table_by_name(std::string const &name);
int extract_struct_constants();
// Bad
void TransferDataFromProtoNode();
auto findTableByName();Use snake_case for variables:
// Good
std::string proto_node_data;
size_t curr_size;
std::vector<node_type> index_node_map;
// Bad
std::string protoNodeData;
size_t currSize;- Use plain
snake_casefor public members - Prefix with single underscore
_for protected members - Prefix with double underscore
__for private helper methods
class example_class {
public:
std::string public_data;
protected:
std::string _protected_data;
private:
std::string private_data;
void __private_helper(); // Private helper method
};- Use
_tsuffix for type aliases - Use
_refsuffix for reference wrapper types
using python_object_t = std::unique_ptr<PyObject, python_object_deleter_t>;
using tlv_node_ref = std::reference_wrapper<tlv_node>;
using tlv_node_ref_const = std::reference_wrapper<tlv_node const>;Use _v suffix for variable templates:
template <typename... Ts>
constexpr auto args_size_v = args_size<Ts...>::value;
template <typename Test, typename... Args>
constexpr bool one_of_v = one_of<Test, Args...>::value;Use lowercase with hyphens for multi-word file names:
proto-tables.h
graph-consumer.cpp
program-options.h
Use 4 spaces for indentation. Do not use tabs.
if (condition) {
do_something();
if (another_condition) {
do_something_else();
}
}Opening braces go on the same line:
// Good
if (condition) {
// ...
}
class my_class {
// ...
};
void function() {
// ...
}
// Bad
if (condition)
{
// ...
}Aim for 80-100 characters per line. Maximum 120 characters. Break long lines logically:
// Good - break at logical points
if ((type == "const struct xdp2_proto_def" ||
type == "const struct xdp2_proto_tlvs_def" ||
type == "const struct xdp2_proto_flag_fields_def") &&
var_decl->hasInit()) {
// ...
}
// Good - break function parameters
void long_function_name(
std::string const &first_parameter,
std::vector<int> const &second_parameter,
std::optional<std::string> third_parameter);// Space after control flow keywords
if (condition)
while (condition)
for (auto &item : container)
// No space before function call parentheses
function_name()
object.method()
// Space around binary operators
a + b
x == y
ptr != nullptr
// Space after commas
function(arg1, arg2, arg3)
// Space after semicolons in for loops
for (int i = 0; i < n; ++i)Place the * and & with the type, not the variable name:
// Good
int *ptr;
std::string const &ref;
T const *const_ptr;
// Bad
int* ptr;
int*ptr;
std::string const& ref;Organize includes in the following order, separated by blank lines:
- Standard library headers
- System headers
- Third-party library headers (Boost, LLVM, Clang)
- Project headers
// Standard library
#include <filesystem>
#include <functional>
#include <iostream>
#include <optional>
#include <ranges>
#include <sstream>
#include <string>
// System headers
#include <arpa/inet.h>
// Third-party libraries
#include <boost/wave.hpp>
#include <boost/program_options.hpp>
#include <llvm/ADT/Twine.h>
#include <llvm/IR/Module.h>
#include <clang/Tooling/Tooling.h>
// Project headers
#include "xdp2gen/graph.h"
#include "xdp2gen/python_generators.h"
#include "xdp2gen/ast-consumer/graph_consumer.h"Use traditional #ifndef guards. #pragma once is acceptable but not preferred:
#ifndef XDP2GEN_AST_CONSUMER_GRAPH_H
#define XDP2GEN_AST_CONSUMER_GRAPH_H
// Header content
#endif // XDP2GEN_AST_CONSUMER_GRAPH_HHandle compiler version differences with conditional compilation:
#ifdef __GNUC__
#if __GNUC__ > 6
#include <optional>
namespace xdp2gen { using std::optional; }
#else
#include <experimental/optional>
namespace xdp2gen { using std::experimental::optional; }
#endif
#endifAfter the license header, include a brief description of the file's purpose:
// SPDX-License-Identifier: BSD-2-Clause-FreeBSD
/* ... license ... */
/*
* This file implements the LLVM IR pattern matching functionality
* for extracting TLV (Type-Length-Value) structures from compiled
* protocol definitions.
*/Use /* */ for multi-line documentation:
/*
* The following pattern matches a calculation of a tlv parameter value
* that is performed by just loading the value of a memory region at an
* offset of a struct pointer as the first argument of the function.
*
* This pattern would match the following LLVM block:
* `%2 = getelementptr inbounds %struct.tcp_opt, ptr %0, i64 0, i32 1`
*/Use // for single-line comments:
// Process all declarations in the group
for (auto *decl : D) {
process(decl);
}Use consistent TODO format:
// TODO: insert the asserts and exceptions later?
// TODO: maybe insert sorted to avoid repetition?When adding patches or debug code, use descriptive tags:
// [nix-patch] Process ALL declarations in the group, not just single decls.
// XDP2_MAKE_PROTO_TABLE creates TWO declarations which may be grouped.
// [nix-debug] Added for troubleshooting segfault issueUse struct for plain data holders with public members:
struct xdp2_proto_node_extract_data {
std::string decl_name;
std::optional<std::string> name;
std::optional<std::size_t> min_len;
std::optional<std::string> len;
friend inline std::ostream &
operator<<(std::ostream &os, xdp2_proto_node_extract_data const &data) {
// Output implementation
return os;
}
};Use class when encapsulation is needed:
class llvm_graph {
public:
using node_type = ::llvm::Value const *;
// Public interface
size_t add_node(node_type node);
bool has_edge(size_t from, size_t to) const;
private:
static constexpr size_t npos = -1;
::llvm::BasicBlock const *bb_ptr = nullptr;
size_t curr_size = 0;
std::vector<node_type> index_node_map;
size_t __increase_graph(node_type const &n, node_type ptr);
};Inherit from Clang's ASTConsumer for AST processing:
class xdp2_proto_node_consumer : public clang::ASTConsumer {
private:
std::vector<xdp2_proto_node_extract_data> &consumed_data;
public:
explicit xdp2_proto_node_consumer(
std::vector<xdp2_proto_node_extract_data> &consumed_data)
: consumed_data{ consumed_data } {}
bool HandleTopLevelDecl(clang::DeclGroupRef D) override {
// Process declarations
return true;
}
};Use factory classes for creating complex objects:
template <typename T>
struct frontend_factory_for_consumer : clang::tooling::FrontendActionFactory {
std::unique_ptr<T> consumer;
template <typename... Args>
explicit frontend_factory_for_consumer(Args &&...args)
: consumer{ std::make_unique<T>(std::forward<Args>(args)...) } {}
std::unique_ptr<clang::FrontendAction> create() override {
return std::make_unique<frontend>(std::move(consumer));
}
};Use std::unique_ptr for exclusive ownership. Avoid raw new/delete:
// Good
auto consumer = std::make_unique<xdp2_proto_node_consumer>(data);
std::unique_ptr<clang::FrontendAction> action = factory->create();
// Bad
auto *consumer = new xdp2_proto_node_consumer(data);
delete consumer;Use custom deleters for C API resources:
using python_object_deleter_t = std::function<decltype(decref)>;
using python_object_t = std::unique_ptr<PyObject, python_object_deleter_t>;
auto make_python_object(PyObject *obj) {
return python_object_t{ obj, decref };
}Use std::reference_wrapper for storing references in containers:
using tlv_node_ref = std::reference_wrapper<tlv_node>;
using unordered_tlv_node_ref_set = std::unordered_set<tlv_node_ref, tlv_node_hash>;Implement move constructors and use std::move appropriately:
pattern_match_factory(pattern_match_factory &&other)
: patterns{ std::move(other.patterns) } {}
std::unique_ptr<clang::ASTConsumer>
CreateASTConsumer(clang::CompilerInstance &ci, llvm::StringRef file) override {
return std::move(consumer);
}Raw pointers from external APIs (Clang/LLVM) are not owned by our code:
// These pointers are managed by Clang/LLVM - do NOT delete
clang::RecordDecl *record;
::llvm::Value const *value;Use std::runtime_error for error conditions:
template <typename T>
auto ensure_not_null(T *t, std::string const &msg) {
if (t == nullptr) {
throw std::runtime_error(msg);
}
return t;
}Catch exceptions at appropriate boundaries:
try {
auto res = xdp2gen::python::generate_root_parser_c(
filename, output, graph, roots, record);
if (res != 0) {
plog::log(std::cout) << "failed python gen?" << std::endl;
return res;
}
} catch (const std::exception &e) {
plog::log(std::cerr) << "Failed to generate " << output
<< ": " << e.what() << std::endl;
return 1;
}Use integer return codes for function success/failure (0 = success):
int extract_struct_constants(
std::string cfile,
std::string llvm_file,
std::vector<const char *> args,
xdp2gen::graph_t &graph) {
// ... implementation ...
return 0; // Success
}Use the project's logging utilities:
plog::log(std::cout) << "Processing file: " << filename << std::endl;
plog::warning(std::cerr) << "<Warning> - Invalid input detected" << std::endl;Define type traits following standard library conventions:
template <typename... Ts>
struct args_size {
static constexpr size_t value = sizeof...(Ts);
};
template <size_t I, typename... Ts>
struct select_type {
using type = typename std::tuple_element<I, std::tuple<Ts...>>::type;
};
template <size_t I, typename... Ts>
using select_type_t = typename select_type<I, Ts...>::type;template <typename Test, typename... Args>
struct one_of : std::disjunction<std::is_same<Test, Args>...> {};
template <typename Test, typename... Args>
constexpr bool one_of_v = one_of<Test, Args...>::value;Use concepts for cleaner template constraints:
template <typename N>
std::pair<node_type, size_t> __search_and_insert(N const *n)
requires std::is_base_of_v<::llvm::Value, N> ||
std::is_same_v<::llvm::BasicBlock, N> {
// Implementation
}template <typename match_type>
class pattern_match_factory {
std::vector<match_type> patterns;
public:
template <typename... Ts, typename G>
std::vector<std::variant<Ts...>>
match_all(G const &g, std::initializer_list<size_t> idxs) const {
return match_all_aux<Ts...>(
g, idxs, std::make_index_sequence<args_size_v<Ts...>>{});
}
};Use const & for input parameters that won't be modified:
void validate_json_metadata(const nlohmann::ordered_json &data);
void process_data(xdp2_proto_node_extract_data const &data);Mark methods that don't modify state as const:
class pattern_match_factory {
public:
template <typename... Ts, typename G>
std::vector<std::variant<Ts...>>
match_all(G const &g, std::initializer_list<size_t> idxs) const;
size_t size() const { return patterns.size(); }
};Use const for values that won't change:
if (auto const *fd = clang::dyn_cast<clang::FunctionDecl>(decl);
fd && fd->getNameAsString() == function_name) {
// ...
}
for (auto const &item : container) {
process(item);
}Place const after what it modifies:
int const *ptr_to_const_int; // Pointer to const int
int *const const_ptr_to_int; // Const pointer to int
int const *const const_ptr_const; // Const pointer to const intUse C++17/C++20 features when available:
// Structured bindings
auto [key, value] = *map.begin();
// If with initializer
if (auto it = map.find(key); it != map.end()) {
use(it->second);
}
// std::optional
std::optional<std::string> find_name(int id);
// Range-based for with references
for (auto const &item : container) {
process(item);
}
// std::filesystem
namespace fs = std::filesystem;
if (fs::exists(path)) {
// ...
}Use brace initialization:
std::vector<int> values{ 1, 2, 3, 4, 5 };
std::string name{ "example" };Use auto for complex types, but be explicit for simple ones:
// Good uses of auto
auto it = container.begin();
auto result = complex_function_returning_template_type();
auto ptr = std::make_unique<complex_type>();
// Prefer explicit types for clarity
int count = 0;
std::string name = "test";Prefer C++ features over macros:
// Prefer constexpr over #define
constexpr size_t MAX_SIZE = 1024;
// Prefer templates over macro functions
template <typename T>
constexpr T max(T a, T b) { return (a > b) ? a : b; }String stringification:
#define XDP2_STRINGIFY_A(X) #X
#define XDP2_STRINGIFY(X) XDP2_STRINGIFY_A(X)Conditional compilation:
#ifdef XDP2_CLANG_RESOURCE_PATH
Tool.appendArgumentsAdjuster(
clang::tooling::getInsertArgumentAdjuster(
"-resource-dir=" XDP2_STRINGIFY(XDP2_CLANG_RESOURCE_PATH)));
#endifHeader guards (see Include Directives).
The project uses a custom plog (program log) system for runtime logging. Logging can be enabled/disabled at runtime:
#include "xdp2gen/program-options/log_handler.h"
// Basic logging
plog::log(std::cout) << "Processing file: " << filename << std::endl;
// Warning messages
plog::warning(std::cerr) << "<Warning> - Invalid input detected" << std::endl;
// Check if logging is enabled before expensive operations
if (plog::is_display_log()) {
var_decl->dump(); // Only dump AST if logging enabled
}
// Control logging programmatically
plog::enable_log();
plog::disable_log();
plog::set_display_log(verbose_flag);For low-level protocol code, use bit-flag based debug masks:
// Define debug flags using XDP2_BIT macro
#define UET_DEBUG_F_PDC XDP2_BIT(0) // 0x1
#define UET_DEBUG_F_TRANS XDP2_BIT(1) // 0x2
#define UET_DEBUG_F_PACKET XDP2_BIT(2) // 0x4
#define UET_DEBUG_F_FEP XDP2_BIT(3) // 0x8
// Check debug flag before output
if (fep->debug_mask & UET_DEBUG_F_FEP) {
// Debug output
}Use colored terminal output for debug messages:
// Color definitions (from utility.h)
#define XDP2_TERM_COLOR_RED "\033[1;31m"
#define XDP2_TERM_COLOR_GREEN "\033[1;32m"
#define XDP2_TERM_COLOR_YELLOW "\033[1;33m"
#define XDP2_TERM_COLOR_BLUE "\033[1;34m"
#define XDP2_TERM_COLOR_MAGENTA "\033[1;35m"
#define XDP2_TERM_COLOR_CYAN "\033[1;36m"
// Debug macro pattern with color support
#define MODULE_DEBUG(CTX, ...) do { \
if (!(CTX->debug_mask & MODULE_DEBUG_FLAG)) \
break; \
XDP2_CLI_PRINT_COLOR(CTX->debug_cli, COLOR, __VA_ARGS__); \
} while (0)When adding temporary debug code, use descriptive tags:
// [nix-debug] Added for troubleshooting segfault issue
plog::log(std::cout) << "[DEBUG] ptr value: " << ptr << std::endl;
// [debug] Temporary - remove after fixing issue #123Use standard assert() for runtime invariant checks:
#include <cassert>
// Check preconditions
assert(ptr != nullptr);
assert(index < container.size());
// Check invariants
assert(source < curr_size && target < curr_size);
// Document unexpected conditions
assert(!"ImplicitCastExpr should not have more than one child");Use static_assert for compile-time checks:
// Type constraints
static_assert(std::is_enum<ENUM_TYPE>::value, "ENUM_TYPE must be an enum!");
static_assert(std::is_trivially_copyable_v<T>, "T must be trivially copyable");
// Size/alignment checks
static_assert(sizeof(header) == 16, "Header size mismatch");For C code requiring kernel-style compile-time checks:
#include "flowdis/build_bug.h"
// Fail build if condition is true
BUILD_BUG_ON(sizeof(struct my_struct) > 64);
// Power-of-two validation
BUILD_BUG_ON_NOT_POWER_OF_2(BUFFER_SIZE);
// With custom message
BUILD_BUG_ON_MSG(condition, "Descriptive error message");When using Cppfront (.h2 files), use cpp2::assert_not_null:
// Safe null dereference
auto range = CPP2_UFCS_0(children, (*cpp2::assert_not_null(expr)));
// Member access with null check
auto decl = CPP2_UFCS_0(getMemberDecl, (*cpp2::assert_not_null(member_expr)));Create explicit validation functions for complex checks:
void validate_json_metadata_ents_type(const nlohmann::ordered_json &ents) {
for (auto const &elm : ents) {
if (elm.contains("type") && elm.contains("length")) {
auto type = elm["type"].get<std::string>();
auto length = elm["length"].get<std::size_t>();
if (type == "hdr_length" && length != 2) {
plog::warning(std::cerr)
<< "<Warning> - hdr_length type should have a size of 2 bytes"
<< std::endl;
}
}
}
}Always check pointers from external APIs before dereferencing:
// Defensive null-checking pattern
auto *record_type = type.getAs<RecordType>();
if (record_type == nullptr) {
// Handle null case - skip or log warning
plog::log(std::cout) << "[WARNING] Skipping null RecordType" << std::endl;
return;
}
auto *decl = record_type->getDecl();src/test/
├── parser/ # Parser unit tests
│ ├── test-parser-core.h
│ └── test-parser-out.h
├── bitmaps/ # Bitmap operation tests
│ └── test_bitmap.h
├── tables/ # Table lookup tests
│ ├── test_table.h
│ └── test_tables.h
├── falcon/ # Protocol-specific tests
│ └── test.h
├── uet/
│ └── test.h
└── router/
└── test.h
nix/tests/ # Integration tests (Nix-based)
├── default.nix
├── simple-parser.nix
└── simple-parser-debug.nix
Use the plugin-style test framework for parser tests:
struct test_parser_core {
const char *name;
void (*help)(void);
void *(*init)(const char *args);
const char *(*process)(void *pv, void *data, size_t len,
struct test_parser_out *out, unsigned int flags,
long long *ptr);
void (*done)(void *pv);
};
// Test flags
#define CORE_F_NOCORE 0x1
#define CORE_F_HASH 0x2
#define CORE_F_VERBOSE 0x4
#define CORE_F_DEBUG 0x8
// Declare a test core
#define CORE_DECL(name) \
struct test_parser_core test_parser_core_##name = { \
.name = #name, \
.help = name##_help, \
.init = name##_init, \
.process = name##_process, \
.done = name##_done \
}Define structured output for test results:
struct test_parser_out_control {
unsigned short int thoff;
unsigned char addr_type;
};
#define ADDR_TYPE_OTHER 1
#define ADDR_TYPE_IPv4 2
#define ADDR_TYPE_IPv6 3
#define ADDR_TYPE_TIPC 4
struct test_parser_out_basic {
unsigned short int n_proto;
unsigned char ip_proto;
};Use a global verbose flag for test output:
extern int verbose;
// In test code
if (verbose >= 10) {
printf("Debug: processing packet %d\n", packet_num);
}
// Different verbosity levels
if (verbose >= 1) // Basic progress
if (verbose >= 5) // Detailed info
if (verbose >= 10) // Debug outputDefine clear status enums for test results:
enum test_status {
NO_STATUS,
HIT_FORWARD = 1000,
HIT_DROP,
HIT_NOACTION,
MISS = -1U,
};
struct test_context {
char *name;
int status;
};
static inline void test_forward(struct test_context *ctx, int code) {
if (verbose >= 10)
printf("%s: Forward code: %u\n", ctx->name, code);
ctx->status = code;
}Write integration tests as Nix shell scripts:
# nix/tests/simple-parser.nix
pkgs.writeShellApplication {
name = "xdp2-test-simple-parser";
text = ''
set -euo pipefail
echo "=== Test: simple_parser ==="
# Test 1: Basic functionality
echo "--- Test 1: Basic run ---"
OUTPUT=$(./parser_notmpl "$PCAP" 2>&1) || {
echo "FAIL: parser exited with error"
exit 1
}
if echo "$OUTPUT" | grep -q "IPv6:"; then
echo "PASS: Produced expected output"
else
echo "FAIL: Missing expected output"
exit 1
fi
# Test 2: With optimization flag
echo "--- Test 2: Optimized mode ---"
OUTPUT_OPT=$(./parser_notmpl -O "$PCAP" 2>&1) || {
echo "FAIL: Optimized mode failed"
exit 1
}
echo "All tests passed!"
'';
}Organize tests in a central default.nix:
# nix/tests/default.nix
{ pkgs, xdp2 }:
{
simple-parser = import ./simple-parser.nix { inherit pkgs xdp2; };
simple-parser-debug = import ./simple-parser-debug.nix { inherit pkgs xdp2; };
# Run all tests
all = pkgs.writeShellApplication {
name = "xdp2-test-all";
text = ''
echo "=== Running all XDP2 tests ==="
${import ./simple-parser.nix { inherit pkgs xdp2; }}/bin/xdp2-test-simple-parser
echo "=== All tests completed ==="
'';
};
}| Type | Location | Naming Pattern |
|---|---|---|
| Unit test headers | src/test/<module>/ |
test_<module>.h or test-<module>.h |
| Test implementations | src/test/<module>/ |
test_<module>.c |
| Integration tests | nix/tests/ |
<feature>.nix |
| Test binaries | Build output | test_<feature> or <feature>_test |
| Aspect | Convention |
|---|---|
| Namespaces | lowercase::with::colons |
| Classes/Structs | snake_case |
| Functions | snake_case |
| Variables | snake_case |
| Type aliases | snake_case_t |
| Constants | constexpr with _v suffix |
| Indentation | 4 spaces |
| Braces | Same line |
| Pointers | T *ptr |
| Comments | SPDX headers, /* */ blocks, // inline |
| Errors | Exceptions (std::runtime_error) |
| Memory | std::unique_ptr, std::make_unique |
| Const | Extensive const correctness |
| Logging | plog::log(), plog::warning() |
| Assertions | assert(), static_assert, BUILD_BUG_ON |
| Tests | Plugin-style cores, Nix integration tests |
This style guide is a living document. Update it as conventions evolve.