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
25 changes: 14 additions & 11 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,7 @@ jobs:
- uses: actions/checkout@v7

- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install --yes \
clang-format \
libgtest-dev \
ninja-build \
pipx
pipx install cmakelang
pipx install cmakelint
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
run: ./scripts/build-dependencies.sh

- name: Check C++ formatting
run: ./scripts/lint.sh cpp-format
Expand All @@ -38,4 +29,16 @@ jobs:

- name: Configure, build, and test
working-directory: ctprintf
run: cmake --workflow --preset debug-workflow
run: cmake --workflow --preset release-workflow

- name: Build Debian package
run: ./scripts/build-package.sh

- name: Upload packages
uses: actions/upload-artifact@v7
with:
name: ctprintf-packages
path: |
ctprintf/build/release/*.tar.zst
build/debian/
if-no-files-found: error
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,20 @@ vcpkg_installed/
# test output & cache
Testing/
.cache/

# Debian package build artifacts
*.build
*.buildinfo
*.changes
*.deb
*.debian.tar.*
*.dsc
*.orig.tar.*
/debian/.debhelper/
/debian/debhelper-build-stamp
/debian/files
/debian/libctprintf-dev/
/debian/tmp/
/debian/*.log
/debian/*.substvars
/obj-*/
178 changes: 178 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# ctprintf

`ctprintf` is a small C++20, header-only formatter for embedded and
freestanding-oriented applications. It provides familiar `printf`-style
formatting, validates literal format strings and argument types at compile
time, and writes one character at a time to an application-provided output.

The library is intended for diagnostic output such as UART, ITM/SWO, log
buffers, or host-side test buffers. It does not use `printf`, iostreams,
`std::string`, or dynamic allocation itself.

## Features

- C++20 header-only library
- Compile-time validation of literal format strings and argument types
- No dynamic allocation in the formatter
- No dependency on libc formatting functions
- Pluggable output through a single `put(char)` operation
- Integer, character, string, pointer, width, flag, and escaped-percent
formatting

## Quick start

```cpp
#include <ctprintf/format.hpp>

struct Uart {
void put(char character)
{
// Transmit character.
}
};

int main()
{
Uart uart;

ctprintf::format(
uart,
"PC=%08x LR=%08x\n",
0x08001234U,
0x08005678U);
}
```

The output is:

```text
PC=08001234 LR=08005678
```

## Output interface

The first argument to `ctprintf::format` can be any type that provides:

```cpp
void put(char character);
```

For example, a fixed-size buffer can be used in a test or a logging adapter:

```cpp
struct BufferOutput {
char *buffer;
std::size_t position = 0;

void put(char character)
{
buffer[position++] = character;
}
};
```

`ctprintf` does not own the output or perform bounds checking; the output type
is responsible for transport, storage, synchronization, and capacity handling.

## Format strings

Format strings must be string literals. They are validated during compilation:
the number of conversions must match the number of arguments, and each
argument must have a supported type for its conversion.

```cpp
ctprintf::format(output, "value=%08x\n", 42U); // Valid.
ctprintf::format(output, "value=%08x\n", "42"); // Compile-time error.
```

The following conversions are supported:

| Conversion | Accepted argument | Description |
| --- | --- | --- |
| `%d`, `%i` | Signed integral type | Signed decimal |
| `%u` | Unsigned integral type, excluding `bool` | Unsigned decimal |
| `%o` | Unsigned integral type, excluding `bool` | Octal |
| `%x` | Unsigned integral type, excluding `bool` | Lowercase hexadecimal |
| `%X` | Unsigned integral type, excluding `bool` | Uppercase hexadecimal |
| `%c` | Integral type | Character |
| `%s` | Type convertible to `const char *` | Null-terminated string |
| `%p` | Object pointer, `void` pointer, or `nullptr` | Pointer in hexadecimal |
| `%%` | No argument | Literal percent sign |

`%s` formats a null pointer as `(null)`. `%p` always includes a `0x` prefix;
for example, `nullptr` is formatted as `0x0`.

### Flags and width

The formatter supports the following flags and a decimal minimum field width:

| Option | Meaning |
| --- | --- |
| `-` | Left-align within the field width |
| `+` | Prefix non-negative signed decimal values with `+` |
| space | Prefix non-negative signed decimal values with a space |
| `#` | Add an octal or hexadecimal prefix where applicable |
| `0` | Pad numeric values with zeroes when not left-aligned |
| width | Minimum field width, for example `%08x` or `%-6s` |

Precision, length modifiers, floating-point conversions, positional arguments,
and runtime-provided format strings are not supported.

## Build and test

Requirements:

- A C++20-capable compiler
- CMake 3.25 or newer
- GoogleTest, when building the test suite

Configure and build the library:

```bash
cmake -S ctprintf -B build
cmake --build build
```

To build and run the tests, enable them explicitly:

```bash
cmake -S ctprintf -B build -DENABLE_TESTING=ON
cmake --build build
ctest --test-dir build --output-on-failure
```

## Creating a package

CPack creates a Zstandard-compressed tarball containing the installable headers,
CMake package files, README, and license:

```bash
cpack --config build/CPackConfig.cmake
```

The archive is written to the build directory and is named
`ctprintf-<version>-<system>.tar.zst`, for example
`ctprintf-0.1.0-Linux.tar.zst`.

## Installation and CMake integration

Install the header and CMake package files with:

```bash
cmake --install build --prefix /desired/prefix
```

An application can then consume the installed package:

```cmake
find_package(ctprintf CONFIG REQUIRED)

target_link_libraries(my_application PRIVATE ctprintf::ctprintf)
```

The exported target supplies the include directory and requires C++20.


## License

MIT License. See [LICENSE](LICENSE).
43 changes: 36 additions & 7 deletions ctprintf/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
cmake_minimum_required(VERSION 3.25)
project(ctprintf LANGUAGES CXX)
project(ctprintf VERSION 0.1.0 LANGUAGES CXX)

include(GNUInstallDirs)

option(ENABLE_TESTING "Build and enable tests" OFF)

Expand All @@ -9,18 +11,45 @@ add_library(ctprintf::ctprintf ALIAS ctprintf)

target_sources(
ctprintf
INTERFACE include/ctprintf/format.hpp
include/ctprintf/output.hpp
include/ctprintf/types.hpp
include/ctprintf/detail/formatter.hpp
include/ctprintf/detail/parser.hpp)
INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/ctprintf/format.hpp>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/ctprintf/output.hpp>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/ctprintf/types.hpp>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/ctprintf/detail/formatter.hpp>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/ctprintf/detail/parser.hpp>)

target_include_directories(ctprintf INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_include_directories(ctprintf INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)

target_compile_features(ctprintf INTERFACE cxx_std_20)

install(
TARGETS ctprintf
EXPORT ctprintf-targets
INCLUDES
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

install(
EXPORT ctprintf-targets
NAMESPACE ctprintf::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ctprintf)

install(FILES cmake/ctprintf-config.cmake DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/ctprintf)

install(
FILES ${CMAKE_CURRENT_SOURCE_DIR}/../README.md ${CMAKE_CURRENT_SOURCE_DIR}/../LICENSE
DESTINATION ${CMAKE_INSTALL_DOCDIR})

if(ENABLE_TESTING)
include(CTest)
enable_testing()
add_subdirectory(tests)
endif() # ENABLE_TESTING

set(CPACK_GENERATOR "TZST")
set(CPACK_PACKAGE_CONTACT "Thomas Willetal <t.willetal@googlemail.com>")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Compile-time checked printf-style formatting library")
set(CPACK_PACKAGE_DIRECTORY "${CMAKE_BINARY_DIR}")
set(CPACK_PACKAGE_VENDOR "embtom")
include(CPack)
22 changes: 20 additions & 2 deletions ctprintf/CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,20 @@
}
}
],
"packagePresets": [
{
"name": "debug",
"configurePreset": "debug"
},
{
"name": "release",
"configurePreset": "release"
}
],
"workflowPresets": [
{
"name": "debug-workflow",
"displayName": "Debug: configure, build, and test",
"displayName": "Debug: configure, build, test, and package",
"steps": [
{
"type": "configure",
Expand All @@ -71,12 +81,16 @@
{
"type": "test",
"name": "debug"
},
{
"type": "package",
"name": "debug"
}
]
},
{
"name": "release-workflow",
"displayName": "Release: configure, build, and test",
"displayName": "Release: configure, build, test, and package",
"steps": [
{
"type": "configure",
Expand All @@ -89,6 +103,10 @@
{
"type": "test",
"name": "release"
},
{
"type": "package",
"name": "release"
}
]
}
Expand Down
3 changes: 3 additions & 0 deletions ctprintf/cmake/ctprintf-config.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
if(NOT TARGET ctprintf::ctprintf)
include("${CMAKE_CURRENT_LIST_DIR}/ctprintf-targets.cmake")
endif()
3 changes: 2 additions & 1 deletion ctprintf/include/ctprintf/detail/formatter.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <cstddef>
#include <cstdint>
#include <type_traits>
#include <utility>

namespace ctprintf::detail {

Expand Down Expand Up @@ -213,7 +214,7 @@ void write_formatted_arguments(O &output, const char *&cursor, const First &firs

const parsed_spec parsed = parse_spec(cursor);
write_value(output, parsed.spec, first);
write_formatted_arguments(output, cursor, static_cast<Rest &&>(rest)...);
write_formatted_arguments(output, cursor, std::forward<Rest>(rest)...);
}

} // namespace ctprintf::detail
3 changes: 2 additions & 1 deletion ctprintf/include/ctprintf/format.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "ctprintf/detail/parser.hpp"

#include <type_traits>
#include <utility>

namespace ctprintf {

Expand All @@ -17,7 +18,7 @@ void format(O &output, format_text<Args...> format_text, Args &&...args)
if constexpr (sizeof...(Args) == 0)
detail::write_formatted_arguments(output, cursor);
else
detail::write_formatted_arguments(output, cursor, static_cast<Args &&>(args)...);
detail::write_formatted_arguments(output, cursor, std::forward<Args>(args)...);
}

} // namespace ctprintf
5 changes: 5 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ctprintf (0.1.0-1) unstable; urgency=medium

* Initial release.

-- Thomas Willetal <t.willetal@googlemail.com> Sun, 04 Oct 2026 00:00:00 +0000
Loading
Loading