From 0b3ed7bb5f632a4b29df10da0b14abb33aa40530 Mon Sep 17 00:00:00 2001 From: Khalil Estell Date: Sun, 23 Aug 2026 05:30:34 -0700 Subject: [PATCH] Share print body across buffer sizes via a span-based impl write() no longer wraps its own coroutine; it just returns the future from the call it forwards to, since it owns no local state that needs to survive the call. print() now owns its buffer and delegates to a non-exported print(span, ...) so that different buffer_size instantiations of the same parameter pack share one function body instead of each generating a full copy. Also drops a stray `override` on four test mock destructors (can/i2c/serial/spi) whose base class destructors are protected and non-virtual by design; masked previously by incremental build caching, surfaced by a clean conan create build. --- v6/modules/serial.cppm | 61 +++++++++++++++++++++++++++++++--------- v6/tests/can.test.cpp | 2 +- v6/tests/i2c.test.cpp | 2 +- v6/tests/serial.test.cpp | 2 +- v6/tests/spi.test.cpp | 2 +- 5 files changed, 52 insertions(+), 17 deletions(-) diff --git a/v6/modules/serial.cppm b/v6/modules/serial.cppm index 1b015f0..0353592 100644 --- a/v6/modules/serial.cppm +++ b/v6/modules/serial.cppm @@ -18,6 +18,7 @@ module; #include #include +#include #include export module hal.util:serial; @@ -45,7 +46,7 @@ export async::future write(async::context& p_ctx, hal::serial& p_serial, mem::scatter_span p_data_out) { - co_await p_serial.write(p_ctx, p_data_out); + return p_serial.write(p_ctx, p_data_out); } /** @@ -60,7 +61,46 @@ export async::future write(async::context& p_ctx, hal::serial& p_serial, std::string_view p_data_out) { - co_await write(p_ctx, p_serial, { as_bytes(p_data_out) }); + return write( + p_ctx, p_serial, mem::scatter_span{ as_bytes(p_data_out) }); +} + +/** + * @brief Shared implementation behind every print() + * instantiation. + * + * Not exported: this exists purely so that print() and + * print() (same Parameters, different buffer_size) share + * one function body instead of each generating its own copy of the + * snprintf-and-write logic. Only the caller-owned buffer and the thin + * templated wrapper below differ per buffer_size. + * + * @param p_ctx - async context for coroutine suspension and resumption. + * @param p_serial - serial port to write data to + * @param p_buffer - working buffer the formatted message is written into; + * its lifetime is owned by the caller and must outlive this call. + * @param p_format - printf style null terminated format string + * @param p_parameters - printf arguments + */ +template +async::future print(async::context& p_ctx, + hal::serial& p_serial, + std::span p_buffer, + char const* p_format, + Parameters... p_parameters) +{ + int const unterminated_max_string_size = + static_cast(p_buffer.size()) - 1; + + int length = std::snprintf( + p_buffer.data(), p_buffer.size(), p_format, p_parameters...); + + if (length > unterminated_max_string_size) { + // Print out what was able to be written to the buffer + length = unterminated_max_string_size; + } + + return write(p_ctx, p_serial, std::string_view(p_buffer.data(), length)); } /** @@ -89,18 +129,13 @@ async::future print(async::context& p_ctx, Parameters... p_parameters) { static_assert(buffer_size > 2); - constexpr int unterminated_max_string_size = - static_cast(buffer_size) - 1; + // buffer must be owned by this coroutine's own frame (not a plain + // function's stack frame) since the span handed to the non-exported + // print() above is retained across suspension points all the way down to + // the underlying serial driver's write(). std::array buffer{}; - int length = - std::snprintf(buffer.data(), buffer.size(), p_format, p_parameters...); - - if (length > unterminated_max_string_size) { - // Print out what was able to be written to the buffer - length = unterminated_max_string_size; - } - - co_await write(p_ctx, p_serial, std::string_view(buffer.data(), length)); + co_await print( + p_ctx, p_serial, std::span(buffer), p_format, p_parameters...); } } // namespace hal::inline v6 diff --git a/v6/tests/can.test.cpp b/v6/tests/can.test.cpp index 5cef5af..ce66ef4 100644 --- a/v6/tests/can.test.cpp +++ b/v6/tests/can.test.cpp @@ -62,7 +62,7 @@ void check_validity(hal::hertz p_operating_frequency, class test_can_transceiver : public hal::can_transceiver { public: - ~test_can_transceiver() override = default; + ~test_can_transceiver() = default; void add_to_received_messages(hal::can_message const& p_message) { diff --git a/v6/tests/i2c.test.cpp b/v6/tests/i2c.test.cpp index b6d53f1..c6fae1a 100644 --- a/v6/tests/i2c.test.cpp +++ b/v6/tests/i2c.test.cpp @@ -31,7 +31,7 @@ constexpr hal::byte filler_byte{ 0xA5 }; class test_i2c : public hal::i2c { public: - ~test_i2c() override = default; + ~test_i2c() = default; hal::byte m_address = hal::byte{ 0 }; mem::scatter_span m_out{}; diff --git a/v6/tests/serial.test.cpp b/v6/tests/serial.test.cpp index addd50a..9616366 100644 --- a/v6/tests/serial.test.cpp +++ b/v6/tests/serial.test.cpp @@ -29,7 +29,7 @@ constexpr hal::byte write_failure_byte{ 'C' }; class test_serial : public hal::serial { public: - ~test_serial() override = default; + ~test_serial() = default; mem::scatter_span m_out{}; diff --git a/v6/tests/spi.test.cpp b/v6/tests/spi.test.cpp index 66a9344..3affd81 100644 --- a/v6/tests/spi.test.cpp +++ b/v6/tests/spi.test.cpp @@ -30,7 +30,7 @@ constexpr hal::byte filler_byte{ 'C' }; class test_spi : public hal::spi_channel { public: - ~test_spi() override = default; + ~test_spi() = default; mem::scatter_span m_out{}; mem::scatter_span m_in{};