Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -532,3 +532,51 @@ if (FMT_MASTER_PROJECT AND EXISTS ${gitignore})
set(CPACK_RESOURCE_FILE_README ${PROJECT_SOURCE_DIR}/README.md)
include(CPack)
endif ()

# C API Wrapper
add_library(fmt_c STATIC src/fmt-c.cc)

target_compile_features(fmt_c PUBLIC cxx_std_11)
target_link_libraries(fmt_c PUBLIC fmt::fmt)

target_include_directories(fmt_c PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)

set_target_properties(fmt_c PROPERTIES
VERSION ${FMT_VERSION}
SOVERSION ${CPACK_PACKAGE_VERSION_MAJOR}
DEBUG_POSTFIX "${CMAKE_DEBUG_POSTFIX}"
C_VISIBILITY_PRESET default
CXX_VISIBILITY_PRESET hidden
)

add_library(fmt::fmt_c ALIAS fmt_c)
if(FMT_INSTALL)
install(TARGETS fmt_c
EXPORT fmt-targets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
install(FILES include/fmt/fmt-c.h
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/fmt
)
endif()

if(FMT_TEST)
enable_language(C)
Comment thread
vitaut marked this conversation as resolved.
message(STATUS "Adding C API test executable")

add_executable(test-c-api test/test_c.c)
target_link_libraries(test-c-api PRIVATE fmt::fmt_c)
set_target_properties(test-c-api PROPERTIES
C_STANDARD 11
C_STANDARD_REQUIRED ON
)
if(MSVC)
target_compile_options(test-c-api PRIVATE /Zc:preprocessor)
endif()

add_test(NAME c-api-test COMMAND test-c-api)
endif()

200 changes: 200 additions & 0 deletions include/fmt/fmt-c.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
#ifndef FMT_C_H
#define FMT_C_H
#include <stddef.h>
#ifdef __cplusplus
# define _Bool bool
#endif

void fmt_error_unsupported_type_detected(void);

enum { fmt_c_max_args = 16 };

typedef enum {
fmt_err_exception = -1,
fmt_err_memory = -2,
fmt_err_invalid_arg = -3
} fmt_error;

#ifdef __cplusplus
extern "C" {
#endif

typedef enum {
fmt_int,
fmt_uint,
fmt_float,
fmt_double,
fmt_long_double,
fmt_string,
fmt_ptr,
fmt_bool,
fmt_char
} fmt_type;

typedef struct {
fmt_type type;
union {
long long i64;
unsigned long long u64;
float f32;
double f64;
long double f128;
const char* str;
const void* ptr; // Used for FMT_PTR and custom data
_Bool bool_val;
char char_val;
} value;
} fmt_arg;

int fmt_vformat(char* buffer, size_t capacity, const char* format_str,
const fmt_arg* args, size_t arg_count);

static inline fmt_arg fmt_from_int(long long x) {
fmt_arg arg;
arg.type = fmt_int;
arg.value.i64 = x;
return arg;
}

static inline fmt_arg fmt_from_uint(unsigned long long x) {
fmt_arg arg;
arg.type = fmt_uint;
arg.value.u64 = x;
return arg;
}

static inline fmt_arg fmt_from_float(float x) {
fmt_arg arg;
arg.type = fmt_float;
arg.value.f32 = x;
return arg;
}

static inline fmt_arg fmt_from_double(double x) {
fmt_arg arg;
arg.type = fmt_double;
arg.value.f64 = x;
return arg;
}

static inline fmt_arg fmt_from_long_double(long double x) {
fmt_arg arg;
arg.type = fmt_long_double;
arg.value.f128 = x;
return arg;
}

static inline fmt_arg fmt_from_str(const char* x) {
fmt_arg arg;
arg.type = fmt_string;
arg.value.str = x;
return arg;
}

static inline fmt_arg fmt_from_ptr(const void* x) {
fmt_arg arg;
arg.type = fmt_ptr;
arg.value.ptr = x;
return arg;
}

static inline fmt_arg fmt_from_bool(_Bool x) {
fmt_arg arg;
arg.type = fmt_bool;
arg.value.bool_val = x;
return arg;
}

static inline fmt_arg fmt_from_char(int x) {
fmt_arg arg;
arg.type = fmt_char;
arg.value.char_val = x;
return arg;
}

#ifdef __cplusplus
}
#endif

#ifndef __cplusplus

// Require modern MSVC with conformant preprocessor
# if defined(_MSC_VER) && (!defined(_MSVC_TRADITIONAL) || _MSVC_TRADITIONAL)
# error "C API requires MSVC 2019+ with /Zc:preprocessor flag."
# endif
Comment thread
soumik15630m marked this conversation as resolved.

# define FMT_MAKE_ARG(x) \
_Generic((x), \
_Bool: fmt_from_bool, \
char: fmt_from_char, \
unsigned char: fmt_from_uint, \
short: fmt_from_int, \
unsigned short: fmt_from_uint, \
int: fmt_from_int, \
unsigned int: fmt_from_uint, \
long: fmt_from_int, \
unsigned long: fmt_from_uint, \
long long: fmt_from_int, \
unsigned long long: fmt_from_uint, \
float: fmt_from_float, \
double: fmt_from_double, \
long double: fmt_from_long_double, \
char*: fmt_from_str, \
const char*: fmt_from_str, \
void*: fmt_from_ptr, \
const void*: fmt_from_ptr, \
default: fmt_error_unsupported_type_detected)(x)

# define FMT_CAT(a, b) FMT_CAT_(a, b)
# define FMT_CAT_(a, b) a##b

# define FMT_NARG_(_id, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, \
_13, _14, _15, _16, N, ...) \
N
# define FMT_NARG(...) \
FMT_NARG_(dummy, ##__VA_ARGS__, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, \
4, 3, 2, 1, 0)

# define FMT_MAP_0(...)
# define FMT_MAP_1(f, a) f(a)
# define FMT_MAP_2(f, a, b) f(a), f(b)
# define FMT_MAP_3(f, a, b, c) f(a), f(b), f(c)
# define FMT_MAP_4(f, a, b, c, d) f(a), f(b), f(c), f(d)
# define FMT_MAP_5(f, a, b, c, d, e) f(a), f(b), f(c), f(d), f(e)
# define FMT_MAP_6(f, a, b, c, d, e, g) f(a), f(b), f(c), f(d), f(e), f(g)
# define FMT_MAP_7(f, a, b, c, d, e, g, h) \
f(a), f(b), f(c), f(d), f(e), f(g), f(h)
# define FMT_MAP_8(f, a, b, c, d, e, g, h, i) \
f(a), f(b), f(c), f(d), f(e), f(g), f(h), f(i)
# define FMT_MAP_9(f, a, b, c, d, e, g, h, i, j) \
f(a), f(b), f(c), f(d), f(e), f(g), f(h), f(i), f(j)
# define FMT_MAP_10(f, a, b, c, d, e, g, h, i, j, k) \
f(a), f(b), f(c), f(d), f(e), f(g), f(h), f(i), f(j), f(k)
# define FMT_MAP_11(f, a, b, c, d, e, g, h, i, j, k, l) \
f(a), f(b), f(c), f(d), f(e), f(g), f(h), f(i), f(j), f(k), f(l)
# define FMT_MAP_12(f, a, b, c, d, e, g, h, i, j, k, l, m) \
f(a), f(b), f(c), f(d), f(e), f(g), f(h), f(i), f(j), f(k), f(l), f(m)
# define FMT_MAP_13(f, a, b, c, d, e, g, h, i, j, k, l, m, n) \
f(a), f(b), f(c), f(d), f(e), f(g), f(h), f(i), f(j), f(k), f(l), f(m), f(n)
# define FMT_MAP_14(f, a, b, c, d, e, g, h, i, j, k, l, m, n, o) \
f(a), f(b), f(c), f(d), f(e), f(g), f(h), f(i), f(j), f(k), f(l), f(m), \
f(n), f(o)
# define FMT_MAP_15(f, a, b, c, d, e, g, h, i, j, k, l, m, n, o, p) \
f(a), f(b), f(c), f(d), f(e), f(g), f(h), f(i), f(j), f(k), f(l), f(m), \
f(n), f(o), f(p)
# define FMT_MAP_16(f, a, b, c, d, e, g, h, i, j, k, l, m, n, o, p, q) \
f(a), f(b), f(c), f(d), f(e), f(g), f(h), f(i), f(j), f(k), f(l), f(m), \
f(n), f(o), f(p), f(q)

# define FMT_MAP(f, ...) \
FMT_CAT(FMT_MAP_, FMT_NARG(__VA_ARGS__))(f, ##__VA_ARGS__)

# define fmt_format(buf, cap, fmt, ...) \
fmt_vformat( \
buf, cap, fmt, \
(fmt_arg[]){{fmt_int}, FMT_MAP(FMT_MAKE_ARG, ##__VA_ARGS__)} + 1, \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the extra argument (the {fmt_int} before the FMT_MAP()) a workaround for MSVC?

The extra argument doesn't appear to be needed on GCC/Clang and (at least according to a quick check with godbolt) appears to cause worse codegen since the extra argument has to be written to the stack as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly! It's a workaround for the zero-argument case. When __VA_ARGS__ is empty, FMT_MAP(...) expands to nothing, which would leave an empty compound literal (fmt_arg[]){} — valid as a GCC/Clang extension (alongside ##__VA_ARGS__), but a hard error in MSVC and technically non-conforming in standard C. The dummy {fmt_int} guarantees at least one element, and + 1 skips it. You're right that it costs a stack write on GCC/Clang;

@Ferdi265 Ferdi265 Mar 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least according to cppreference, empty compound literals (and __VA_OPT__) are part of C23, so at least for compilers that support that the variant without the initial argument could be conditionally used instead as an optimization.

We'd still need the workaround for MSVC of course.

(EDIT: nevermind, zero-size arrays are still an extension, but I think there might be a trick with __VA_OPT__ to remove / replace the compund initializer with something else when there are no arguments; I'll experiment with that a bit)

@Ferdi265 Ferdi265 Mar 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found an ugly trick that could remove the need for the extra argument, even for MSVC. It uses the same trick used in FMT_NARGS(...) to implement something akin to a fake __VA_OPT__ : FMT_VA_SELECT selects between two cases for empty (e) and non-empty (n) argument lists. This allows us to replace the problematic empty (fmt_arg[]){} with NULL.

#  define FMT_VA_SELECT(e, n, ...)                                            \
    FMT_NARG_(dummy, ##__VA_ARGS__, n, n, n, n, n, n, n, n, n, n, n, n, n, n, \
              n, n, e)

#  define fmt_format(buffer, size, fmt, ...)                                   \
    fmt_vformat((buffer), (size), (fmt),                                       \
                FMT_VA_SELECT(                                                 \
                    NULL, ((fmt_arg[]){FMT_MAP(FMT_MAKE_ARG, ##__VA_ARGS__)}), \
                    ##__VA_ARGS__),                                            \
                FMT_NARG(__VA_ARGS__))

This expands to

fmt_vformat((buffer), (size), (fmt), NULL, 0)

if __VA_ARGS__ is empty and to

fmt_vformat((buffer), (size), (fmt), ((fmt_arg[]){FMT_MAP(FMT_MAKE_ARG, ##__VA_ARGS__)}), FMT_NARG(__VA_ARGS__))

otherwise

EDIT: removed the need for FMT_UNPACK()/FMT_EXPAND()
EDIT2: godbolt link

FMT_NARG(__VA_ARGS__))

#endif // __cplusplus

#endif // FMT_C_H
55 changes: 55 additions & 0 deletions src/fmt-c.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#include "fmt/fmt-c.h"

#include <fmt/core.h>

#include <cassert>

extern "C" {

using format_arg = fmt::basic_format_arg<fmt::format_context>;

static bool populate_store(format_arg* out, const fmt_arg* c_args,
size_t arg_count) {
if (arg_count > fmt_c_max_args) {
return false;
}

for (size_t i = 0; i < arg_count; ++i) {
switch (c_args[i].type) {
case fmt_int: out[i] = c_args[i].value.i64; break;
case fmt_uint: out[i] = c_args[i].value.u64; break;
case fmt_float: out[i] = c_args[i].value.f32; break;
case fmt_double: out[i] = c_args[i].value.f64; break;
case fmt_long_double: out[i] = c_args[i].value.f128; break;
case fmt_ptr: out[i] = c_args[i].value.ptr; break;
case fmt_char: out[i] = c_args[i].value.char_val; break;
case fmt_bool: out[i] = c_args[i].value.bool_val; break;
case fmt_string: out[i] = c_args[i].value.str; break;
default: return false;
}
}
return true;
}

int fmt_vformat(char* buffer, size_t capacity, const char* format_str,
const fmt_arg* args, size_t arg_count) {
assert(format_str);

format_arg format_args[fmt_c_max_args];

if (arg_count > 0) {
assert(args);
if (!populate_store(format_args, args, arg_count)) {
return fmt_err_invalid_arg;
}
}

auto format_args_view = fmt::basic_format_args<fmt::format_context>(
format_args, static_cast<int>(arg_count));

auto result =
fmt::vformat_to_n(buffer, capacity, format_str, format_args_view);
return static_cast<int>(result.size);
}

} // extern "C"
Loading