From 9d462b98ebd2de028141d4e650de91ed3247feba Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Tue, 20 Jan 2026 18:35:16 -0800 Subject: [PATCH] io_buffer_param is in Boost.Corosio --- doc/modules/ROOT/pages/guide/buffers.adoc | 8 +- include/boost/corosio/io_buffer_param.hpp | 385 ++++++++++++++++++++++ include/boost/corosio/io_stream.hpp | 6 +- include/boost/corosio/read.hpp | 1 - include/boost/corosio/socket.hpp | 2 +- include/boost/corosio/write.hpp | 1 - src/corosio/src/detail/epoll/sockets.hpp | 8 +- src/corosio/src/detail/iocp/sockets.cpp | 4 +- src/corosio/src/detail/iocp/sockets.hpp | 8 +- src/corosio/src/test/mocket.cpp | 8 +- src/openssl/src/openssl_stream.cpp | 4 +- src/wolfssl/src/wolfssl_stream.cpp | 4 +- test/unit/io_buffer_param.cpp | 347 +++++++++++++++++++ 13 files changed, 758 insertions(+), 28 deletions(-) create mode 100644 include/boost/corosio/io_buffer_param.hpp create mode 100644 test/unit/io_buffer_param.cpp diff --git a/doc/modules/ROOT/pages/guide/buffers.adoc b/doc/modules/ROOT/pages/guide/buffers.adoc index a62bbe1..1dc3f0c 100644 --- a/doc/modules/ROOT/pages/guide/buffers.adoc +++ b/doc/modules/ROOT/pages/guide/buffers.adoc @@ -160,15 +160,15 @@ consuming.consume(n); // Advance by bytes read This is used internally by `read()` and `write()` but can be used directly. -== buffer_param +== io_buffer_param -The `buffer_param` class type-erases buffer sequences: +The `io_buffer_param` class type-erases buffer sequences: [source,cpp] ---- -#include +#include -void accept_any_buffer(capy::buffer_param buffers) +void accept_any_buffer(corosio::io_buffer_param buffers) { capy::mutable_buffer temp[8]; std::size_t n = buffers.copy_to(temp, 8); diff --git a/include/boost/corosio/io_buffer_param.hpp b/include/boost/corosio/io_buffer_param.hpp new file mode 100644 index 0000000..4a14e1e --- /dev/null +++ b/include/boost/corosio/io_buffer_param.hpp @@ -0,0 +1,385 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_IO_BUFFER_PARAM_HPP +#define BOOST_COROSIO_IO_BUFFER_PARAM_HPP + +#include +#include + +#include + +namespace boost { +namespace corosio { + +/** A type-erased buffer sequence for I/O system call boundaries. + + This class enables I/O objects to accept any buffer sequence type + across a virtual function boundary, while preserving the caller's + typed buffer sequence at the call site. The implementation can + then unroll the type-erased sequence into platform-native + structures (e.g., `iovec` on POSIX, `WSABUF` on Windows) for the + actual system call. + + @par Purpose + + When building coroutine-based I/O abstractions, a common pattern + emerges: a templated awaitable captures the caller's buffer + sequence, and at `await_suspend` time, must pass it across a + virtual interface to the I/O implementation. This class solves + the type-erasure problem at that boundary without heap allocation. + + @par Restricted Use Case + + This is NOT a general-purpose composable abstraction. It exists + solely for the final step in a coroutine I/O call chain where: + + @li A templated awaitable captures the caller's buffer sequence + @li The awaitable's `await_suspend` passes buffers across a + virtual interface to an I/O object implementation + @li The implementation immediately unrolls the buffers into + platform-native structures for the system call + + @par Lifetime Model + + The safety of this class depends entirely on coroutine parameter + lifetime extension. When a coroutine is suspended, parameters + passed to the awaitable remain valid until the coroutine resumes + or is destroyed. This class exploits that guarantee by holding + only a pointer to the caller's buffer sequence. + + The referenced buffer sequence is valid ONLY while the calling + coroutine remains suspended at the exact suspension point where + `io_buffer_param` was created. Once the coroutine resumes, + returns, or is destroyed, all referenced data becomes invalid. + + @par Const Buffer Handling + + This class accepts both `ConstBufferSequence` and + `MutableBufferSequence` types. However, `copy_to` always produces + `mutable_buffer` descriptors, casting away constness for const + buffer sequences. This design matches platform I/O structures + (`iovec`, `WSABUF`) which use non-const pointers regardless of + the operation direction. + + @warning The caller is responsible for ensuring the type system + is not violated. When the original buffer sequence was const + (e.g., for a write operation), the implementation MUST NOT write + to the buffers obtained from `copy_to`. The const-cast exists + solely to provide a uniform interface for platform I/O calls. + + @code + // For write operations (const buffers): + void submit_write(io_buffer_param p) + { + capy::mutable_buffer bufs[8]; + auto n = p.copy_to(bufs, 8); + // bufs[] may reference const data - DO NOT WRITE + writev(fd, reinterpret_cast(bufs), n); // OK: read-only + } + + // For read operations (mutable buffers): + void submit_read(io_buffer_param p) + { + capy::mutable_buffer bufs[8]; + auto n = p.copy_to(bufs, 8); + // bufs[] references mutable data - safe to write + readv(fd, reinterpret_cast(bufs), n); // OK: writing + } + @endcode + + @par Correct Usage + + The implementation receiving `io_buffer_param` MUST: + + @li Call `copy_to` immediately upon receiving the parameter + @li Use the unrolled buffer descriptors for the I/O operation + @li Never store the `io_buffer_param` object itself + @li Never store pointers obtained from `copy_to` beyond the + immediate I/O operation + + @par Example: Correct Usage + + @code + // Templated awaitable at the call site + template + struct write_awaitable + { + Buffers bufs; + io_stream* stream; + + bool await_ready() { return false; } + + void await_suspend(std::coroutine_handle<> h) + { + // CORRECT: Pass to virtual interface while suspended. + // The buffer sequence 'bufs' remains valid because + // coroutine parameters live until resumption. + stream->async_write_some_impl(bufs, h); + } + + io_result await_resume() { return stream->get_result(); } + }; + + // Virtual implementation - unrolls immediately + void stream_impl::async_write_some_impl( + io_buffer_param p, + std::coroutine_handle<> h) + { + // CORRECT: Unroll immediately into platform structure + iovec vecs[16]; + std::size_t n = p.copy_to( + reinterpret_cast(vecs), 16); + + // CORRECT: Use unrolled buffers for system call now + submit_to_io_uring(vecs, n, h); + + // After this function returns, 'p' must not be used again. + // The iovec array is safe because it contains copies of + // the pointer/size pairs, not references to 'p'. + } + @endcode + + @par UNSAFE USAGE: Storing io_buffer_param + + @warning Never store `io_buffer_param` for later use. + + @code + class broken_stream + { + io_buffer_param saved_param_; // UNSAFE: member storage + + void async_write_impl(io_buffer_param p, ...) + { + saved_param_ = p; // UNSAFE: storing for later + schedule_write_later(); + } + + void do_write_later() + { + // UNSAFE: The calling coroutine may have resumed + // or been destroyed. saved_param_ now references + // invalid memory! + capy::mutable_buffer bufs[8]; + saved_param_.copy_to(bufs, 8); // UNDEFINED BEHAVIOR + } + }; + @endcode + + @par UNSAFE USAGE: Storing Unrolled Pointers + + @warning The pointers obtained from `copy_to` point into the + caller's buffer sequence. They become invalid when the caller + resumes. + + @code + class broken_stream + { + capy::mutable_buffer saved_bufs_[8]; // UNSAFE + std::size_t saved_count_; + + void async_write_impl(io_buffer_param p, ...) + { + // This copies pointer/size pairs into saved_bufs_ + saved_count_ = p.copy_to(saved_bufs_, 8); + + // UNSAFE: scheduling for later while storing the + // buffer descriptors. The pointers in saved_bufs_ + // will dangle when the caller resumes! + schedule_for_later(); + } + + void later() + { + // UNSAFE: saved_bufs_ contains dangling pointers + for(std::size_t i = 0; i < saved_count_; ++i) + write(fd_, saved_bufs_[i].data(), ...); // UB + } + }; + @endcode + + @par UNSAFE USAGE: Using Outside a Coroutine + + @warning This class relies on coroutine lifetime semantics. + Using it with callbacks or non-coroutine async patterns is + undefined behavior. + + @code + // UNSAFE: No coroutine lifetime guarantee + void bad_callback_pattern(std::vector& data) + { + capy::mutable_buffer buf(data.data(), data.size()); + + // UNSAFE: In a callback model, 'buf' may go out of scope + // before the callback fires. There is no coroutine + // suspension to extend the lifetime. + stream.async_write(buf, [](error_code ec) { + // 'buf' is already destroyed! + }); + } + @endcode + + @par UNSAFE USAGE: Passing to Another Coroutine + + @warning Do not pass `io_buffer_param` to a different coroutine + or spawn a new coroutine that captures it. + + @code + void broken_impl(io_buffer_param p, std::coroutine_handle<> h) + { + // UNSAFE: Spawning a new coroutine that captures 'p'. + // The original coroutine may resume before this new + // coroutine uses 'p'. + co_spawn([p]() -> task { + capy::mutable_buffer bufs[8]; + p.copy_to(bufs, 8); // UNSAFE: original caller may + // have resumed already! + co_return; + }); + } + @endcode + + @par UNSAFE USAGE: Multiple Virtual Hops + + @warning Minimize indirection. Each virtual call that passes + `io_buffer_param` without immediately unrolling it increases + the risk of misuse. + + @code + // Risky: multiple hops before unrolling + void layer1(io_buffer_param p) { + layer2(p); // Still haven't unrolled... + } + void layer2(io_buffer_param p) { + layer3(p); // Still haven't unrolled... + } + void layer3(io_buffer_param p) { + // Finally unrolling, but the chain is fragile. + // Any intermediate layer storing 'p' breaks everything. + } + @endcode + + @par UNSAFE USAGE: Fire-and-Forget Operations + + @warning Do not use with detached or fire-and-forget async + operations where there is no guarantee the caller remains + suspended. + + @code + task caller() + { + char buf[1024]; + // UNSAFE: If async_write is fire-and-forget (doesn't + // actually suspend the caller), 'buf' may be destroyed + // before the I/O completes. + stream.async_write_detached(capy::mutable_buffer(buf, 1024)); + // Returns immediately - 'buf' goes out of scope! + } + @endcode + + @par Passing Convention + + Pass by value. The class contains only two pointers (16 bytes + on 64-bit systems), making copies trivial and clearly + communicating the lightweight, transient nature of this type. + + @code + // Preferred: pass by value + void process(io_buffer_param buffers); + + // Also acceptable: pass by const reference + void process(io_buffer_param const& buffers); + @endcode + + @see capy::ConstBufferSequence, capy::MutableBufferSequence +*/ +class io_buffer_param +{ +public: + /** Construct from a const buffer sequence. + + @param bs The buffer sequence to adapt. + */ + template + io_buffer_param(BS const& bs) noexcept + : bs_(&bs) + , fn_(©_impl) + { + } + + /** Fill an array with buffers from the sequence. + + Copies buffer descriptors from the sequence into the + destination array, skipping any zero-size buffers. + This ensures the output contains only buffers with + actual data, suitable for direct use with system calls. + + @param dest Pointer to array of mutable buffer descriptors. + @param n Maximum number of buffers to copy. + + @return The number of non-zero buffers copied. + */ + std::size_t + copy_to( + capy::mutable_buffer* dest, + std::size_t n) const noexcept + { + return fn_(bs_, dest, n); + } + +private: + template + static std::size_t + copy_impl( + void const* p, + capy::mutable_buffer* dest, + std::size_t n) + { + auto const& bs = *static_cast(p); + auto it = capy::begin(bs); + auto const end_it = capy::end(bs); + + std::size_t i = 0; + if constexpr (capy::MutableBufferSequence) + { + for(; it != end_it && i < n; ++it) + { + capy::mutable_buffer buf(*it); + if(buf.size() == 0) + continue; + dest[i++] = buf; + } + } + else + { + for(; it != end_it && i < n; ++it) + { + capy::const_buffer buf(*it); + if(buf.size() == 0) + continue; + dest[i++] = capy::mutable_buffer( + const_cast( + static_cast(buf.data())), + buf.size()); + } + } + return i; + } + + using fn_t = std::size_t(*)(void const*, + capy::mutable_buffer*, std::size_t); + + void const* bs_; + fn_t fn_; +}; + +} // namespace corosio +} // namespace boost + +#endif diff --git a/include/boost/corosio/io_stream.hpp b/include/boost/corosio/io_stream.hpp index d5807ab..256fffb 100644 --- a/include/boost/corosio/io_stream.hpp +++ b/include/boost/corosio/io_stream.hpp @@ -13,7 +13,7 @@ #include #include #include -#include +#include #include #include @@ -201,7 +201,7 @@ class BOOST_COROSIO_DECL io_stream : public io_object virtual void read_some( std::coroutine_handle<>, capy::any_executor_ref, - capy::buffer_param, + io_buffer_param, std::stop_token, system::error_code*, std::size_t*) = 0; @@ -209,7 +209,7 @@ class BOOST_COROSIO_DECL io_stream : public io_object virtual void write_some( std::coroutine_handle<>, capy::any_executor_ref, - capy::buffer_param, + io_buffer_param, std::stop_token, system::error_code*, std::size_t*) = 0; diff --git a/include/boost/corosio/read.hpp b/include/boost/corosio/read.hpp index 5e9759d..49fa35c 100644 --- a/include/boost/corosio/read.hpp +++ b/include/boost/corosio/read.hpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #include #include diff --git a/include/boost/corosio/socket.hpp b/include/boost/corosio/socket.hpp index fbf95e8..5573158 100644 --- a/include/boost/corosio/socket.hpp +++ b/include/boost/corosio/socket.hpp @@ -14,7 +14,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/include/boost/corosio/write.hpp b/include/boost/corosio/write.hpp index 1c4dcbf..031483f 100644 --- a/include/boost/corosio/write.hpp +++ b/include/boost/corosio/write.hpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #include #include diff --git a/src/corosio/src/detail/epoll/sockets.hpp b/src/corosio/src/detail/epoll/sockets.hpp index 4d6c7ad..656af39 100644 --- a/src/corosio/src/detail/epoll/sockets.hpp +++ b/src/corosio/src/detail/epoll/sockets.hpp @@ -124,7 +124,7 @@ class epoll_socket_impl void read_some( std::coroutine_handle<>, capy::any_executor_ref, - capy::buffer_param, + io_buffer_param, std::stop_token, system::error_code*, std::size_t*) override; @@ -132,7 +132,7 @@ class epoll_socket_impl void write_some( std::coroutine_handle<>, capy::any_executor_ref, - capy::buffer_param, + io_buffer_param, std::stop_token, system::error_code*, std::size_t*) override; @@ -308,7 +308,7 @@ epoll_socket_impl:: read_some( std::coroutine_handle<> h, capy::any_executor_ref d, - capy::buffer_param param, + io_buffer_param param, std::stop_token token, system::error_code* ec, std::size_t* bytes_out) @@ -372,7 +372,7 @@ epoll_socket_impl:: write_some( std::coroutine_handle<> h, capy::any_executor_ref d, - capy::buffer_param param, + io_buffer_param param, std::stop_token token, system::error_code* ec, std::size_t* bytes_out) diff --git a/src/corosio/src/detail/iocp/sockets.cpp b/src/corosio/src/detail/iocp/sockets.cpp index 9614292..214e819 100644 --- a/src/corosio/src/detail/iocp/sockets.cpp +++ b/src/corosio/src/detail/iocp/sockets.cpp @@ -418,7 +418,7 @@ win_socket_impl_internal:: read_some( capy::any_coro h, capy::any_executor_ref d, - capy::buffer_param param, + io_buffer_param param, std::stop_token token, system::error_code* ec, std::size_t* bytes_out) @@ -492,7 +492,7 @@ win_socket_impl_internal:: write_some( capy::any_coro h, capy::any_executor_ref d, - capy::buffer_param param, + io_buffer_param param, std::stop_token token, system::error_code* ec, std::size_t* bytes_out) diff --git a/src/corosio/src/detail/iocp/sockets.hpp b/src/corosio/src/detail/iocp/sockets.hpp index 336f1c9..714a696 100644 --- a/src/corosio/src/detail/iocp/sockets.hpp +++ b/src/corosio/src/detail/iocp/sockets.hpp @@ -150,7 +150,7 @@ class win_socket_impl_internal void read_some( capy::any_coro, capy::any_executor_ref, - capy::buffer_param, + io_buffer_param, std::stop_token, system::error_code*, std::size_t*); @@ -158,7 +158,7 @@ class win_socket_impl_internal void write_some( capy::any_coro, capy::any_executor_ref, - capy::buffer_param, + io_buffer_param, std::stop_token, system::error_code*, std::size_t*); @@ -206,7 +206,7 @@ class win_socket_impl void read_some( std::coroutine_handle<> h, capy::any_executor_ref d, - capy::buffer_param buf, + io_buffer_param buf, std::stop_token token, system::error_code* ec, std::size_t* bytes) override @@ -217,7 +217,7 @@ class win_socket_impl void write_some( std::coroutine_handle<> h, capy::any_executor_ref d, - capy::buffer_param buf, + io_buffer_param buf, std::stop_token token, system::error_code* ec, std::size_t* bytes) override diff --git a/src/corosio/src/test/mocket.cpp b/src/corosio/src/test/mocket.cpp index 2e3e4ef..979db59 100644 --- a/src/corosio/src/test/mocket.cpp +++ b/src/corosio/src/test/mocket.cpp @@ -90,7 +90,7 @@ class mocket_impl void read_some( std::coroutine_handle<> h, capy::any_executor_ref d, - capy::buffer_param buffers, + io_buffer_param buffers, std::stop_token token, system::error_code* ec, std::size_t* bytes_transferred) override; @@ -98,7 +98,7 @@ class mocket_impl void write_some( std::coroutine_handle<> h, capy::any_executor_ref d, - capy::buffer_param buffers, + io_buffer_param buffers, std::stop_token token, system::error_code* ec, std::size_t* bytes_transferred) override; @@ -257,7 +257,7 @@ mocket_impl:: read_some( std::coroutine_handle<> h, capy::any_executor_ref d, - capy::buffer_param buffers, + io_buffer_param buffers, std::stop_token token, system::error_code* ec, std::size_t* bytes_transferred) @@ -299,7 +299,7 @@ mocket_impl:: write_some( std::coroutine_handle<> h, capy::any_executor_ref d, - capy::buffer_param buffers, + io_buffer_param buffers, std::stop_token token, system::error_code* ec, std::size_t* bytes_transferred) diff --git a/src/openssl/src/openssl_stream.cpp b/src/openssl/src/openssl_stream.cpp index 167ffe3..362ea31 100644 --- a/src/openssl/src/openssl_stream.cpp +++ b/src/openssl/src/openssl_stream.cpp @@ -647,7 +647,7 @@ struct openssl_stream_impl_ void read_some( std::coroutine_handle<> h, capy::any_executor_ref d, - capy::buffer_param param, + io_buffer_param param, std::stop_token token, system::error_code* ec, std::size_t* bytes) override @@ -662,7 +662,7 @@ struct openssl_stream_impl_ void write_some( std::coroutine_handle<> h, capy::any_executor_ref d, - capy::buffer_param param, + io_buffer_param param, std::stop_token token, system::error_code* ec, std::size_t* bytes) override diff --git a/src/wolfssl/src/wolfssl_stream.cpp b/src/wolfssl/src/wolfssl_stream.cpp index 3e51ae6..7e0feca 100644 --- a/src/wolfssl/src/wolfssl_stream.cpp +++ b/src/wolfssl/src/wolfssl_stream.cpp @@ -862,7 +862,7 @@ struct wolfssl_stream_impl_ void read_some( std::coroutine_handle<> h, capy::any_executor_ref d, - capy::buffer_param param, + io_buffer_param param, std::stop_token token, system::error_code* ec, std::size_t* bytes) override @@ -880,7 +880,7 @@ struct wolfssl_stream_impl_ void write_some( std::coroutine_handle<> h, capy::any_executor_ref d, - capy::buffer_param param, + io_buffer_param param, std::stop_token token, system::error_code* ec, std::size_t* bytes) override diff --git a/test/unit/io_buffer_param.cpp b/test/unit/io_buffer_param.cpp new file mode 100644 index 0000000..31e22f5 --- /dev/null +++ b/test/unit/io_buffer_param.cpp @@ -0,0 +1,347 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +// Test that header file is self-contained. +#include + +#include +#include +#include + +#include "test_suite.hpp" + +namespace boost { +namespace corosio { + +struct io_buffer_param_test +{ + // Helper to reduce repeated copy_to assertion pattern + static void + check_copy( + io_buffer_param p, + std::initializer_list> expected) + { + capy::mutable_buffer dest[8]; + auto n = p.copy_to(dest, 8); + BOOST_TEST_EQ(n, expected.size()); + std::size_t i = 0; + for(auto const& e : expected) + { + BOOST_TEST_EQ(dest[i].data(), e.first); + BOOST_TEST_EQ(dest[i].size(), e.second); + ++i; + } + } + + // Helper for checking empty/zero-byte sequences + static void + check_empty(io_buffer_param p) + { + capy::mutable_buffer dest[8]; + BOOST_TEST_EQ(p.copy_to(dest, 8), 0); + } + + void + testConstBuffer() + { + char const data[] = "Hello"; + capy::const_buffer cb(data, 5); + check_copy(cb, {{data, 5}}); + } + + void + testMutableBuffer() + { + char data[] = "Hello"; + capy::mutable_buffer mb(data, 5); + check_copy(mb, {{data, 5}}); + } + + void + testConstBufferPair() + { + char const data1[] = "Hello"; + char const data2[] = "World"; + capy::const_buffer_pair cbp{{ + capy::const_buffer(data1, 5), + capy::const_buffer(data2, 5) }}; + check_copy(cbp, {{data1, 5}, {data2, 5}}); + } + + void + testMutableBufferPair() + { + char data1[] = "Hello"; + char data2[] = "World"; + capy::mutable_buffer_pair mbp{{ + capy::mutable_buffer(data1, 5), + capy::mutable_buffer(data2, 5) }}; + check_copy(mbp, {{data1, 5}, {data2, 5}}); + } + + void + testSpan() + { + char const data1[] = "One"; + char const data2[] = "Two"; + char const data3[] = "Three"; + capy::const_buffer arr[3] = { + capy::const_buffer(data1, 3), + capy::const_buffer(data2, 3), + capy::const_buffer(data3, 5) }; + span s(arr, 3); + check_copy(s, {{data1, 3}, {data2, 3}, {data3, 5}}); + } + + void + testArray() + { + char const data1[] = "One"; + char const data2[] = "Two"; + char const data3[] = "Three"; + std::array arr{{ + capy::const_buffer(data1, 3), + capy::const_buffer(data2, 3), + capy::const_buffer(data3, 5) }}; + check_copy(arr, {{data1, 3}, {data2, 3}, {data3, 5}}); + } + + void + testCArray() + { + char const data1[] = "One"; + char const data2[] = "Two"; + char const data3[] = "Three"; + capy::const_buffer arr[3] = { + capy::const_buffer(data1, 3), + capy::const_buffer(data2, 3), + capy::const_buffer(data3, 5) }; + check_copy(arr, {{data1, 3}, {data2, 3}, {data3, 5}}); + } + + void + testLimitedCopy() + { + char const data1[] = "One"; + char const data2[] = "Two"; + char const data3[] = "Three"; + capy::const_buffer arr[3] = { + capy::const_buffer(data1, 3), + capy::const_buffer(data2, 3), + capy::const_buffer(data3, 5) }; + + io_buffer_param ref(arr); + + // copy only 2 buffers + capy::mutable_buffer dest[2]; + auto n = ref.copy_to(dest, 2); + BOOST_TEST_EQ(n, 2); + BOOST_TEST_EQ(dest[0].data(), data1); + BOOST_TEST_EQ(dest[0].size(), 3); + BOOST_TEST_EQ(dest[1].data(), data2); + BOOST_TEST_EQ(dest[1].size(), 3); + } + + void + testEmptySequence() + { + // Zero total bytes returns 0, regardless of buffer count + capy::const_buffer cb; + check_empty(cb); + } + + void + testZeroByteConstBuffer() + { + // Explicit zero-byte const buffer + char const* data = "Hello"; + capy::const_buffer cb(data, 0); + check_empty(cb); + } + + void + testZeroByteMultiple() + { + // Multiple zero-byte buffers should still return 0 + char const data1[] = "Hello"; + char const data2[] = "World"; + capy::const_buffer arr[3] = { + capy::const_buffer(data1, 0), + capy::const_buffer(data2, 0), + capy::const_buffer(nullptr, 0) }; + check_empty(arr); + } + + void + testZeroByteBufferPair() + { + // Buffer pair with both zero-byte buffers + char const data1[] = "Hello"; + char const data2[] = "World"; + capy::const_buffer_pair cbp{{ + capy::const_buffer(data1, 0), + capy::const_buffer(data2, 0) }}; + check_empty(cbp); + } + + void + testMixedZeroAndNonZero() + { + // Mix of zero-byte and non-zero buffers + // Zero-size buffers are skipped, only non-zero returned + char const data1[] = "Hello"; + char const data2[] = "World"; + capy::const_buffer arr[3] = { + capy::const_buffer(data1, 0), + capy::const_buffer(data2, 5), + capy::const_buffer(nullptr, 0) }; + check_copy(arr, {{data2, 5}}); + } + + void + testOneZeroOneNonZero() + { + // Buffer pair with one zero-byte, one non-zero + // Zero-size buffer is skipped + char const data1[] = "Hello"; + char const data2[] = "World"; + capy::const_buffer_pair cbp{{ + capy::const_buffer(data1, 0), + capy::const_buffer(data2, 5) }}; + check_copy(cbp, {{data2, 5}}); + } + + void + testZeroByteMutableBuffer() + { + // Zero-byte mutable buffer + char data[] = "Hello"; + capy::mutable_buffer mb(data, 0); + check_empty(mb); + } + + void + testZeroByteMutableBufferPair() + { + // Mutable buffer pair with zero-byte buffers + char data1[] = "Hello"; + char data2[] = "World"; + capy::mutable_buffer_pair mbp{{ + capy::mutable_buffer(data1, 0), + capy::mutable_buffer(data2, 0) }}; + check_empty(mbp); + } + + void + testEmptySpan() + { + // Empty span (no buffers at all) + span s; + check_empty(s); + } + + void + testEmptyArray() + { + // Empty std::array (zero-size) + std::array arr{}; + check_empty(arr); + } + + // Helper function that accepts io_buffer_param by value + static std::size_t + acceptByValue(io_buffer_param p) + { + capy::mutable_buffer dest[8]; + return p.copy_to(dest, 8); + } + + // Helper function that accepts io_buffer_param by const reference + static std::size_t + acceptByConstRef(io_buffer_param const& p) + { + capy::mutable_buffer dest[8]; + return p.copy_to(dest, 8); + } + + void + testPassByValue() + { + // Test that io_buffer_param works when passed by value + char const data[] = "Hello"; + capy::const_buffer cb(data, 5); + + // Pass buffer directly (implicit conversion) + auto n = acceptByValue(cb); + BOOST_TEST_EQ(n, 1); + + // Pass io_buffer_param object + io_buffer_param p(cb); + n = acceptByValue(p); + BOOST_TEST_EQ(n, 1); + + // Pass buffer sequence directly + std::array arr{{ + capy::const_buffer(data, 2), + capy::const_buffer(data + 2, 3) }}; + n = acceptByValue(arr); + BOOST_TEST_EQ(n, 2); + } + + void + testPassByConstRef() + { + // Test that io_buffer_param works when passed by const reference + char const data[] = "Hello"; + capy::const_buffer cb(data, 5); + + // Pass io_buffer_param object by const ref + io_buffer_param p(cb); + auto n = acceptByConstRef(p); + BOOST_TEST_EQ(n, 1); + + // Pass buffer sequence directly (creates temporary io_buffer_param) + n = acceptByConstRef(std::array{{ + capy::const_buffer(data, 2), + capy::const_buffer(data + 2, 3) }}); + BOOST_TEST_EQ(n, 2); + } + + void + run() + { + testConstBuffer(); + testMutableBuffer(); + testConstBufferPair(); + testMutableBufferPair(); + testSpan(); + testArray(); + testCArray(); + testLimitedCopy(); + testEmptySequence(); + testZeroByteConstBuffer(); + testZeroByteMultiple(); + testZeroByteBufferPair(); + testMixedZeroAndNonZero(); + testOneZeroOneNonZero(); + testZeroByteMutableBuffer(); + testZeroByteMutableBufferPair(); + testEmptySpan(); + testEmptyArray(); + testPassByValue(); + testPassByConstRef(); + } +}; + +TEST_SUITE( + io_buffer_param_test, + "boost.corosio.io_buffer_param"); + +} // corosio +} // boost