src/log.h
Logging, error, and hexdump macros.
Walkthrough, interview notes & deep dive
Core Utility and Logging Strategy The log.h header defines a lightweight, macro-based logging system used throughout the ixy driver. Logging here is conditionally compiled to avoid overhead in the performance-critical fast path, and it provides severity-based reporting plus robust error checking.
Macro Design and Severity Levels The file implements four primary logging macros: debug, info, warn, and error. The debug macro is specifically tied to the NDEBUG flag. When debugging is disabled, the macro evaluates to an empty do {} while(0) block, ensuring zero performance penalty. All macros utilize C99 variadic arguments (... and __VA_ARGS__) and compiler-intrinsic constants like __FILE__, __LINE__, and __func__ to automatically attach file names, line numbers, and function names to every log entry.
#ifndef NDEBUG
#define debug(fmt, ...) do {\
fprintf(stderr, "[DEBUG] %s:%d %s(): " fmt "\n", __FILE__, __LINE__, __func__, ##__VA_ARGS__);\
} while(0)
#else
#define debug(fmt, ...) do {} while(0)
#endif
The do-while(0) Idiom and Statement Expressions A critical pattern in this file is the do { ... } while(0) block used for multi-line macros. This is a standard C idiom that ensures a macro behaves as a single statement, preventing syntax errors when the macro is invoked inside an if statement that lacks braces. Another advanced feature is found in check_err, which uses a GCC/Clang "statement expression" (({ ... })). This allows the macro to evaluate an expression, check for a -1 error code, and return the result to the caller, all while handling errno and generating a human-readable error via strerror_r.
#define check_err(expr, op) ({\
int64_t result = (int64_t) (expr);\
if ((int64_t) result == -1LL) {\
int err = errno;\
char buf[512];\
strerror_r(err, buf, sizeof(buf));\
fprintf(stderr, "[ERROR] %s:%d %s(): Failed to %s: %s\n", __FILE__, __LINE__, __func__, op, buf);\
exit(err);\
}\
result;\
})
Memory Inspection via Hexdump For networking tasks, such as inspecting raw packet headers or DMA descriptor rings, the hexdump function is indispensable. It provides a side-by-side hex and ASCII view of memory. It iterates through the provided void_ptr, using isprint to determine if a byte should be rendered as a character or a placeholder dot. This is essential for verifying that hardware is correctly writing to memory or that packet offsets are properly aligned.
Interview angles Q: Why is do { ... } while(0) preferred over a simple block { ... } in macros? A: A simple block causes a syntax error when the macro is used in an if-else construct without braces. The semicolon after the macro call would terminate the if block, causing the subsequent else to be orphaned.
Q: Why does check_err use a statement expression instead of a standard function? A: It allows the macro to be generic across different return types and ensures that the __FILE__ and __LINE__ macros reflect the caller's location rather than the location of a logger function.
Q: Why use strerror_r rather than strerror in check_err? A: strerror_r is thread-safe, avoiding the race on the static buffer that plain strerror returns.
Going deeper
Persistent Side-Effects in Assertions
#undef assert
#define assert(expr) (void) (expr)
Standard C assert is typically compiled out when NDEBUG is defined. This implementation explicitly redefines it to evaluate expr and cast the result to void. This ensures that any logic placed inside an assert()—such as a function call with critical side effects—continues to execute in release builds. While this silences "unused variable" warnings, it violates the common expectation that assertions should have zero runtime impact in production.
Integer Sign Extension in check_err
int64_t result = (int64_t) (expr);
if ((int64_t) result == -1LL) {
The macro casts the expression result to a signed 64-bit integer. If expr returns a 32-bit -1 (e.g., from a standard int function), it correctly sign-extends to 0xFFFFFFFFFFFFFFFF. However, if expr is an unsigned 64-bit pointer or memory offset that happens to be 0xFFFFFFFFFFFFFFFF, the macro will treat it as a failure (-1LL), creating a false-positive risk for valid high-memory addresses.
Harder interview questions
- Q: How does termination behavior differ between
error()andcheck_err()? A:error()callsabort(), triggeringSIGABRTfor an immediate core dump without flushing stdio buffers or runningatexithandlers.check_err()callsexit(), performing a clean shutdown. In drivers,abort()is often preferred to preserve the exact machine state for post-mortem debugging. - Q: Is
hexdumpsafe for use in a high-performance datapath? A: No.printfis not async-signal-safe and involves heavy internal locking. On the hot path, formatting hex and printing tostdoutintroduces millisecond-scale latency, which will likely cause ring buffer overflows and packet drops in a userspace driver.
Gotchas
- The
hexdumpfunction usesuint32_t ito iterate oversize_t len. On 64-bit systems, iflenexceeds 4GB, the loop counter will overflow and wrap to zero, causing an infinite loop. check_errpasseserrnotoexit(). Linux process exit codes are truncated to 8 bits. Iferrnois a value like 256, the shell sees exit code 0, falsely signaling success despite a hardware failure.
From ixy to a production driver
Production logging has identity and control. In log.h, debug, info, warn, and error are thin fprintf wrappers that stamp __FILE__, __LINE__, and __func__. That is perfect for reading the code path in a teaching driver, but a kernel NIC driver usually logs through wrappers such as dev_err, dev_warn, netdev_warn, dev_dbg, and pr_debug. The important difference is that device-aware wrappers carry the struct device or struct net_device context, so the log naturally identifies the PCI function or network interface. With CONFIG_DYNAMIC_DEBUG, dynamic_debug can enable dev_dbg/pr_debug callsites at runtime through /sys/kernel/debug/dynamic_debug/control, selecting by file, function, line, module, or format string. ixy only has compile-time NDEBUG behavior; production kernels need debug knobs after deployment.
Production error paths avoid making logging the failure. A real driver must assume that one bad interrupt, queue error, or firmware event can repeat thousands of times. Kernel code therefore uses tools such as printk_ratelimited or lower-level __ratelimit state so logs cannot flood the console, disk, or journald and become a denial of service. For assertions, the equivalent mental model is not ixy's error() calling abort() or check_err() calling exit(errno): kernel code cannot exit the process. It either returns an error, resets a queue or device, emits WARN_ON_ONCE for a survivable invariant violation, or, in truly unrecoverable cases, uses mechanisms like BUG_ON that oops/panic the kernel.
Production diagnostics move out of printf. ixy's hexdump() is convenient, but synchronous printf in a datapath ruins latency and is not async-signal-safe. Real NIC drivers expose counters through ethtool -S, often including per-queue RX/TX, error, and interrupt counters. Newer drivers may add devlink health reporters that diagnose, dump state, and auto-recover from TX, RX, or firmware failures. For low-overhead tracing, kernel tracepoints and ftrace let operators turn on events such as networking or XDP traces without recompiling and without leaving hot-path printfs in the normal case.
DPDK is the closer userspace comparison. DPDK still runs in userspace like ixy, but it adds a real logging framework: rte_log, RTE_LOG, registered dynamic log types via rte_log_register, and per-type levels set with rte_log_set_level. Datapath logging uses RTE_LOG_DP, which can be compiled out according to RTE_LOG_DP_LEVEL. DPDK also has a telemetry library so tools can query JSON state over a socket instead of scraping terminal output.
ixy omits all of this deliberately. The project is a roughly thousand-line teaching driver optimized for showing how ixgbe and virtio queues work, not for surviving production fault storms, multi-tenant observability, or long-running operations. log.h is intentionally small because logging infrastructure is not the lesson.
Sources
Source
#ifndef IXY_LOG_H
#define IXY_LOG_H
#include <errno.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <assert.h>
#ifndef NDEBUG
#define debug(fmt, ...) do {\
fprintf(stderr, "[DEBUG] %s:%d %s(): " fmt "\n", __FILE__, __LINE__, __func__, ##__VA_ARGS__);\
} while(0)
#else
#define debug(fmt, ...) do {} while(0)
#undef assert
#define assert(expr) (void) (expr)
#endif
#define info(fmt, ...) do {\
fprintf(stdout, "[INFO ] %s:%d %s(): " fmt "\n", __FILE__, __LINE__, __func__, ##__VA_ARGS__);\
} while(0)
#define warn(fmt, ...) do {\
fprintf(stderr, "[WARN ] %s:%d %s(): " fmt "\n", __FILE__, __LINE__, __func__, ##__VA_ARGS__);\
} while(0)
#define error(fmt, ...) do {\
fprintf(stderr, "[ERROR] %s:%d %s(): " fmt "\n", __FILE__, __LINE__, __func__, ##__VA_ARGS__);\
abort();\
} while(0)
#define check_err(expr, op) ({\
int64_t result = (int64_t) (expr);\
if ((int64_t) result == -1LL) {\
int err = errno;\
char buf[512];\
strerror_r(err, buf, sizeof(buf));\
fprintf(stderr, "[ERROR] %s:%d %s(): Failed to %s: %s\n", __FILE__, __LINE__, __func__, op, buf);\
exit(err);\
}\
result;\
})
static void hexdump(void* void_ptr, size_t len) {
uint8_t* ptr = (uint8_t*) void_ptr;
char ascii[17];
for (uint32_t i = 0; i < len; i += 16) {
printf("%06x: ", i);
int j = 0;
for (; j < 16 && i + j < len; j++) {
printf("%02x", ptr[i + j]);
if (j % 2) {
printf(" ");
}
ascii[j] = isprint(ptr[i + j]) ? ptr[i + j] : '.';
}
ascii[j] = '\0';
if (j < 16) {
for (; j < 16; j++) {
printf(" ");
if (j % 2) {
printf(" ");
}
}
}
printf(" %s\n", ascii);
}
}
#endif //IXY_LOG_H