LIBFEBUG++(3) Library Functions Manual LIBFEBUG++(3)

febug::controlled_socket, febug::wrapper, febug::formatters, febug::debug_handler()
User-space debugfs ABI wrapper library for C++

#include <libfebug.hpp>
c++ -lfebug++


#define FEBUG_DONT 0
#define FEBUG_SOCKET "/var/run/febug.sock"
#define FEBUG_SIGNUM SIGUSR2

getenv("FEBUG_DONT");
getenv("FEBUG_SOCKET");


struct febug::controlled_socket;
const febug::controlled_socket febug::global_controlled_socket;


struct febug::wrapper;

febug::wrapper::wrapper(const T & data, const char * name, ...);

febug::wrapper::wrapper(const T & data, uint8_t signal, const char * name, ...);

febug::wrapper::wrapper(const T & data, uint8_t signal, const char * name, va_list ap);

std::map<size_t, void (*)(int, size_t)> febug::formatters;

void febug::debug_handler(int);

Simplifies writing C++ programs debuggable with febug(8) by presenting a high-level interface to febug-abi(5).

There are three compile-time macros that allow customising libfebug++ behaviour:

If non-zero, all symbols become static, functions turn into no-ops, and therefore no symbols from libfebug++.a|.so are imported at link-time; this is intended as a way to easily disable febug(8) integration completely on release builds.
The signal to request from febug(8) when using febug_wrap(). Defaults to SIGUSR2.
The path to connect to febug(8) on. Defaults to /var/run/febug.sock.

There are two environment variables that allow a user to customise its behaviour:

If set, don't try to connect to febug(8), so all library functions become no-ops.
If set, use its value instead of the built-in FEBUG_SOCKET to connect to febug(8).

The febug::controlled_socket structure is defined as follows:

struct febug::controlled_socket {
    int fd = -1;
    inline operator int() const noexcept { return this->fd; }
    controlled_socket(const char * path = FEBUG_SOCKET) noexcept;
    ~controlled_socket() noexcept;
};

There is a global instance at febug::global_controlled_socket which, if path (FEBUG_SOCKET) isn't the null pointer, attempts to connect to febug(8) and will set fd, if successful. Similarly, destroying it will hang up and reset fd.

The program needs to install febug::debug_handler() (or a wrapper around it) as the signal handler for FEBUG_SIGNUM (and any other signals, if different ones are explicitly requested); if notifications are disabled (by requesting SIGKILL), some event loop that answers on febug::global_controlled_socket must be in place. It's a no-op if febug::global_controlled_socket is -1.

The program should register handlers for types of variables it wishes to handle by adding entries to febug::formatters — the key is typeid(std::decay_t<T>).hash_code(), so if this yields different results for two types that should have the same handler, multiple entries need to be registered. If no handler was registered for a type, febug::debug_handler() will instead return a generic "not found" message. The handler takes the write end of the pipe as the first argument, and the variable ID as the second; it shouldn't close the pipe, as that is done by febug::debug_handler() regardless, and the program would then run the risk of closing another file with the same descriptor simultaneously opened by another thread.

The febug::wrapper structure is defined as follows:

template <class T>
struct febug::wrapper {
    const T * data;
    wrapper(const T & data, const char * name, ...) noexcept;
    wrapper(const T & data, uint8_t signal, const char * name, ...) noexcept;
    wrapper(const T & data, uint8_t signal, const char * name, va_list ap) noexcept;
    ~wrapper() noexcept;
};

If the program wishes to debug a variable, it should construct a febug::wrapper referencing it; the constructor will send a febug_message with the type corresponding to typeid(std::decay_t<T>).hash_code(), ID corresponding to the pointer to data, signal being either specified or defaulting to FEBUG_SIGNUM, and name formatted according to printf(3). The destructor will send a stop_febug_message. Both become no-ops if febug::global_controlled_socket is -1.

The following program sorts a std::vector<int> with std::sort() but waits a second between each comparison; the vector and the amount of comparisons can be inspected via a febug(8) mount:
// SPDX-License-Identifier: MIT


#include <libfebug.hpp>

#include <algorithm>
#include <cstring>
#include <errno.h>
#include <unistd.h>
#include <vector>


int main() {
	if(febug::global_controlled_socket != -1) {
		febug::formatters.emplace(
		    typeid(std::vector<int>).hash_code(), [](int retpipe, std::size_t vid) {
			    const std::vector<int> & data = *(const std::vector<int> *)vid;
			    for(auto num : data)
				    dprintf(retpipe, "%d ", num);
			    write(retpipe, "\n", 1);
		    });
		febug::formatters.emplace(
		    typeid(std::size_t).hash_code(), [](int retpipe, std::size_t vid) {
			    const std::size_t & data = *(const std::size_t *)vid;
			    dprintf(retpipe, "%zu\n", data);
		    });
	}


	struct sigaction handler {};
	handler.sa_handler = febug::debug_handler;
	if(sigaction(FEBUG_SIGNUM, &handler, nullptr) == -1)
		std::fprintf(stderr, "sigaction: %s\n", std::strerror(errno));


	{
		std::vector<int> data{-1, -2, -3, 0, 1, 2, 3, -1, -2, -3, 0, 1, 2, 3,
		                      -1, -2, -3, 0, 1, 2, 3, -1, -2, -3, 0, 1, 2, 3,
		                      -1, -2, -3, 0, 1, 2, 3, -1, -2, -3, 0, 1, 2, 3};
		std::size_t comparisons_done{};
		febug::wrapper data_w{data, "cool_data"};
		febug::wrapper comparisons_done_w{comparisons_done, "comparisons"};

		std::sort(data.begin(), data.end(), [&](auto lhs, auto rhs) {
			sleep(1);
			++comparisons_done;
			return lhs < rhs;
		});
	}

	sleep(2);
}

febug-abi(5) — the ABI wrapped by this library.
libfebug(3) and libfebug,rs(3) — equivalent C and Rust libraries.

To all who support further development, in particular:

febug tracker

febug mailing list: <~nabijaczleweli/febug@lists.sr.ht>, archived at https://lists.sr.ht/~nabijaczleweli/febug

June 12, 2023 febug 0.2.1