Intuitive, type-safe {fmt} formatting for C11, built with custom types in mind.
cfmtx brings modern {fmt} syntax, positional arguments, compile-time type dispatch, and user-defined type formatting to C11. It is specifically designed to make formatting user-defined C structures effortless.
// No more manual field extraction
// Format custom structs and built-in types seamlessly with {fmt} syntax
cfmtx_print(stdout, "user={:>10}, point={}, hex={:#x}\n", "alice", point, 255u);- Modern
{fmt}Syntax in C11: Positional arguments, custom formatting specs, alignment, padding, and dynamic width/precision. - Type-Safe Dispatch: Built on C11
_Generic, eliminating format specifier mismatches. - First-Class Custom Types: Format C structures directly without manual string building.
- Explicit Output Ownership: Write to fixed buffers and streams without allocation, or create an owned string in one formatting pass.
- Clean C ABI: Application source files compile with standard C11 without needing a C++ compiler.
- cfmtx
No registration or setup is needed for standard C types. Pass values directly:
#include <cfmtx/cfmtx.h>
#include <stdio.h>
int main(void) {
// Format directly to stdout
cfmtx_print(stdout, "value={:>10.2f}, hex={:#x}\n", 3.14159, 255u);
// Or format safely into a null-terminated buffer
char buffer[64];
cfmtx_format_z(buffer, sizeof(buffer), "value={:>10.2f}, hex={:#x}", 3.14159, 255u);
printf("Buffer: %s\n", buffer);
return 0;
}Output:
value= 3.14, hex=0xff
Buffer: value= 3.14, hex=0xff
Tip
Need a C string?
Use cfmtx_format_z for a caller-owned fixed buffer. Use cfmtx_format_alloc when the required size is not known.
To format a custom C struct, define a formatter with CFMTX_FORMATTER and register it using the #define / #include <cfmtx/register.h> pair:
#include <cfmtx/cfmtx.h>
#include <stdio.h>
typedef struct Vec2 {
double x;
double y;
} Vec2;
// 1. Define the formatter
static CFMTX_FORMATTER(format_vec2, Vec2) {
// spec contains custom format specifiers (e.g. {:compact}); empty for "{}"
(void)spec;
return cfmtx_writer_format(writer, "({}, {})", value->x, value->y);
}
// 2. Register the (Type, Formatter) pair
#define CFMTX_REGISTER (Vec2, format_vec2)
#include <cfmtx/register.h> /* Consumes CFMTX_REGISTER and expands registration */
int main(void) {
Vec2 point = {1.5, 2.5};
return cfmtx_print(stdout, "point is {}\n", point) == CFMTX_OK ? 0 : 1;
}Output:
point is (1.5, 2.5)
Note
Why #include <cfmtx/register.h>?
Standard C cannot generate file-scope type dispatch declarations from a single macro call. cfmtx uses this define-then-include pattern to safely configure _Generic table entries for your type at compile time.
In a multi-file project, put this pair in the type definition header. Consumers then only include that header.
Complete source: examples/vec2.c.
| Task | Recommended Function | Input Format | Output Destination | Success Return Value |
|---|---|---|---|---|
| Create an owned C string | cfmtx_format_alloc |
const char* format, varargs |
Newly allocated null-terminated string | Total characters (excluding \0) |
| Format to C string | cfmtx_format_z |
const char* format, varargs |
Null-terminated char[] |
Total characters (excluding \0) |
| Print to stream | cfmtx_print |
const char* format, varargs |
FILE* stream |
CFMTX_OK (0) |
| Format raw bytes | cfmtx_format |
const char* format, varargs |
Raw byte buffer | Total byte count |
| Allocate with pre-packed args | cfmtx_vformat_alloc |
cfmtx_string_view, cfmtx_arg[] |
Newly allocated null-terminated string | Total characters (excluding \0) |
| Format with pre-packed args | cfmtx_vformat_z |
cfmtx_string_view, cfmtx_arg[] |
Null-terminated char[] |
Total characters (excluding \0) |
| Print with pre-packed args | cfmtx_vprint |
cfmtx_string_view, cfmtx_arg[] |
FILE* stream |
CFMTX_OK (0) |
| Append in custom formatter | cfmtx_writer_format |
const char* format, varargs |
cfmtx_writer* |
CFMTX_OK (0) |
Important
The v prefix represents pre-packed argument arrays (cfmtx_arg[]), NOT va_list.
Unlike vprintf, cfmtx_v* functions accept an array of cfmtx_arg structures. This enables building reusable argument packs once and formatting them across multiple targets without re-evaluating expressions.
Use cfmtx_format_alloc when the output size is not known. The function grows the output while it formats. It does not run the formatter once to measure and again to write.
char* text = NULL;
int length = cfmtx_format_alloc(&text, "name={}, score={}", name, score);
if (length < 0) {
// text is NULL. Handle the formatting or allocation error.
return length;
}
use_text(text, (size_t)length);
cfmtx_free(text);The result always has a final null character. The returned length does not include that character. Release the result with cfmtx_free, including when cfmtx is a shared library.
Use cfmtx_format_z to format into fixed-size character arrays:
char buffer[64];
int result = cfmtx_format_z(buffer, sizeof(buffer), "answer={}", 42);
if (result >= 0 && (size_t)result >= sizeof(buffer)) {
// Output was truncated, but buffer is still safely null-terminated!
}- Capacity & Null Termination:
sizeis the total buffer capacity including space for'\0'. Whensize > 0, output is capped atsize - 1bytes and always appends'\0'. - Truncation Handling: Truncation is not an error. The function returns the total formatted length required, like
snprintf. - Size Query: Pass
cfmtx_format_z(NULL, 0, ...)to compute the required length. Use this only when an external allocator or an existing buffer requires the size first. A later formatting call formats the value again. Usecfmtx_format_allocfor an ordinary heap string.
Use cfmtx_print to write directly to stdout, stderr, or any FILE*:
if (cfmtx_print(stderr, "Error: code={:#x}, message={}\n", 0x404, "Not Found") != CFMTX_OK) {
// Handle I/O error
}cfmtx supports the standard {fmt} format specification mini-language:
char buffer[64];
cfmtx_format_z(buffer, sizeof(buffer), "value={:>10.2f}, hex={:#x}", 3.14159, 255u);
// Result: "value= 3.14, hex=0xff"Supported features for built-in types:
- Positional arguments:
"{0} {1} {0}" - Alignment & fill:
"{:<10}"(left),"{:>10}"(right),"{:^10}"(center),"{:*^10}"(custom fill) - Sign & radix:
"{:+}","{:#x}","{:#b}","{:#o}" - Width & precision:
"{:10.4f}" - Dynamic width/precision:
"{:{}.{}f}"with runtime arguments
Each registration consists of an adjacent #define and #include <cfmtx/register.h> pair at file scope:
#define CFMTX_REGISTER (TypeName, formatter_function)
#include <cfmtx/register.h>The header consumes CFMTX_REGISTER and undefines it automatically. For a shared type, put the pair in the header that defines the type. Keep it inside that header's include guard.
Registration changes the _Generic dispatch table in the current translation unit (TU). A type header applies that registration once in every C TU that includes the type.
Define the type, declare its formatter, and register the pair:
#ifndef VEC2_H_
#define VEC2_H_
#include <cfmtx/cfmtx.h>
typedef struct Vec2 {
double x;
double y;
} Vec2;
#ifdef __cplusplus
extern "C" {
#endif
CFMTX_FORMATTER(format_vec2, Vec2);
#ifdef __cplusplus
}
#else
// Make Vec2 format-ready in each C translation unit that includes this header
#define CFMTX_REGISTER (Vec2, format_vec2)
#include <cfmtx/register.h>
#endif
#endifImplement the formatter:
#include "vec2.h"
CFMTX_FORMATTER(format_vec2, Vec2) {
(void)spec;
return cfmtx_writer_format(writer, "({}, {})", value->x, value->y);
}Include the type header and use the type. Do not add registration code.
#include "vec2.h"
#include <stdio.h>
int main(void) {
Vec2 v = {3.0, 4.0};
cfmtx_print(stdout, "vector: {}\n", v);
}The formatter has external linkage because dispatch helpers in multiple TUs call it. Registration creates only TU-local helpers.
The CFMTX_FORMATTER(name, Type) macro expands to:
int name(cfmtx_writer *writer, cfmtx_string_view spec, const Type *value);| Parameter | Description |
|---|---|
writer |
Output sink for the current formatting operation. Valid only during the call. |
spec |
Custom format specifier view (the text between : and }). Non-null-terminated. |
value |
Pointer to the object being formatted. |
Inside the formatter, use the writer functions:
cfmtx_writer_write(writer, string_view): Append an unformatted string view.cfmtx_writer_format(writer, "...", ...): Format expressions and append output.cfmtx_writer_vformat(writer, format_view, args, num_args): Append from pre-packed arguments.
Tip
Always return the result of cfmtx_writer_* directly (or CFMTX_OK on success). If you encounter a validation error in your custom spec, return a negative integer status.
A custom formatter can format other registered types seamlessly.
typedef struct User {
uint64_t id;
cfmtx_string_view name;
} User;
typedef struct LogEvent {
User actor;
cfmtx_string_view message;
} LogEvent;
// Register User first...
#define CFMTX_REGISTER (User, format_user)
#include <cfmtx/register.h>
// Then LogEvent's formatter can format User with "{}"
static CFMTX_FORMATTER(format_log_event, LogEvent) {
return cfmtx_writer_format(writer, "[User: {}] msg: {}", value->actor, value->message);
}
#define CFMTX_REGISTER (LogEvent, format_log_event)
#include <cfmtx/register.h>Complete source: examples/log-event.c.
When formatting the same set of arguments repeatedly (e.g., logging to both a file and a memory buffer), you can pre-pack arguments into a cfmtx_arg array.
Vec2 point = {1.5, 2.5};
// Pack arguments once
cfmtx_arg args[] = {
CFMTX_MAKE_ARG("point"),
CFMTX_MAKE_ARG(point), // Shallow copy
CFMTX_REF(point), // Reference by pointer
};
size_t num_args = sizeof(args) / sizeof(args[0]);
// Format to an owned string
char* text = NULL;
int length = cfmtx_vformat_alloc(
&text, CFMTX_STRING_VIEW_LITERAL("{}: copied={}, ref={}"), args, num_args);
if (length >= 0) {
consume(text, (size_t)length);
cfmtx_free(text);
}
// Or format to a fixed buffer
char buffer[128];
cfmtx_vformat_z(buffer, sizeof(buffer), CFMTX_STRING_VIEW_LITERAL("{}: copied={}, ref={}"), args, num_args);
// Format to stream
cfmtx_vprint(stdout, CFMTX_STRING_VIEW_LITERAL("{}: copied={}, ref={}\n"), args, num_args);Complete source: examples/vformat.c.
| Macro | Storage Strategy | Lifetime | Best For |
|---|---|---|---|
CFMTX_MAKE_ARG(value) |
Shallow copy in temporary storage | Valid until the enclosing scope exits | Small structs, scalar values, and temporary values |
CFMTX_REF(value) |
Stores pointer to live lvalue | Caller must keep original object alive | Large structs or objects whose live values should be reflected |
Warning
Do not use CFMTX_MAKE_ARG inside loops to populate an external array!
CFMTX_MAKE_ARG allocates temporary stack storage at its lexical call site. In a loop, every iteration reuses the same storage slot, causing all array entries to point to the last element:
cfmtx_arg args[2];
for (size_t i = 0; i < 2; ++i) {
args[i] = CFMTX_MAKE_ARG(points[i]); // ❌ WRONG: Storage is reused!
}Correct Solutions:
- Use
CFMTX_REF(points[i])ifpointsremains alive:for (size_t i = 0; i < 2; ++i) { args[i] = CFMTX_REF(points[i]); // ✅ OK: References the live array element }
- Format synchronously inside the loop:
for (size_t i = 0; i < 2; ++i) { cfmtx_print(stdout, "{}\n", points[i]); // ✅ OK }
Because cfmtx relies on C11 _Generic compile-time type matching, keep these standard C behaviors in mind:
| C Expression | C Type | Recommended Syntax | Why |
|---|---|---|---|
'x' |
int |
(char)'x' |
Character constants have type int in C; without a cast, it formats as an integer (e.g. 120). |
true / false |
int / bool |
(bool)true |
In standard C11 <stdbool.h>, true may expand to 1 (int). |
| Bit-fields | Bit-field | (int)flags.bits |
Bit-fields cannot have their address taken or be directly matched in _Generic. |
| Compound Literals | Struct | ((Vec2){1.0, 2.0}) |
Wrap in extra parentheses so the comma inside {1.0, 2.0} is not treated as a macro argument separator. |
| Object Pointers | MyType* |
(const void*)ptr |
Arbitrary typed pointers must be cast to void* or const void* to format addresses ({:p}). |
cfmtx uses clear, consistent return values:
- String functions (
cfmtx_format_alloc,cfmtx_vformat_alloc,cfmtx_format_z,cfmtx_vformat_z): Return non-negative formatted length on success; negative error code on failure. - Stream & Writer functions (
cfmtx_print,cfmtx_writer_*): ReturnCFMTX_OK(0) on success; negative error code on failure.
int result = cfmtx_format_z(buffer, sizeof(buffer), "value={}", value);
if (result < 0) {
fprintf(stderr, "Formatting failed with error code: %d\n", result);
}| Constant | Value | Description |
|---|---|---|
CFMTX_OK |
0 |
Success (for stream and writer operations). |
CFMTX_ERROR |
-1 |
General error (invalid format syntax, allocation failure, I/O error, or output exceeding INT_MAX). |
CFMTX_ERROR_INVALID_ARGUMENT |
-2 |
Null pointer, invalid argument tag, or argument count exceeded. |
CFMTX_ERROR_CUSTOM |
-3 |
A custom formatter returned an invalid positive code. |
Negative values < -3 |
< -3 |
Application-defined error codes returned by custom formatters. |
- Consumer code: C11 compliant compiler with
_Genericand compound literals (GCC 4.9+, Clang 3.8+, MSVC 2019+ with conforming preprocessor/Zc:preprocessor). - Library build: C++11 compiler and {fmt} 12+.
- Integers:
char,signed char,unsigned char,short,unsigned short,int,unsigned int,long,unsigned long,long long,unsigned long long - Floats:
float,double,long double - Booleans:
bool - Strings:
char*,const char*,cfmtx_string_view(supports embedded nulls) - Pointers:
void*,const void* - Custom Types: Types registered via
register.h
| Constraint | Limit | Behavior When Exceeded |
|---|---|---|
| Arguments per formatting call | 16 | Compile-time macro error or CFMTX_ERROR_INVALID_ARGUMENT |
| Registered custom types | 32 per translation unit | Preprocessor compile error on 33rd registration |
| Maximum formatted output size | INT_MAX bytes |
Returns CFMTX_ERROR |
| Nested replacement fields in custom spec | Not supported | Returns CFMTX_ERROR |
cfmtx contains zero mutable global state. All formatting operations are fully reentrant and thread-safe as long as distinct threads do not write to the same buffer/FILE* or mutate objects passed via CFMTX_REF.
# Configure and build
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
# Run tests
ctest --test-dir build --output-on-failure
# Install
cmake --install build --prefix /usr/local./build/cfmtx-vec2
./build/cfmtx-vformat
./build/cfmtx-log-eventIn your project's CMakeLists.txt:
cmake_minimum_required(VERSION 3.20)
project(my_project C)
find_package(cfmtx CONFIG REQUIRED)
add_executable(my_app main.c)
target_compile_features(my_app PRIVATE c_std_11)
target_link_libraries(my_app PRIVATE cfmtx::cfmtx)This project is licensed under the Mozilla Public License 2.0.