Type-Erased Echo
Echo server demonstrating the compilation firewall pattern.
What You Will Learn
-
Using
any_streamfor transport-independent code -
Physical isolation through separate compilation
-
Build time benefits of type erasure
Prerequisites
-
Completed Mock Stream Testing
-
Understanding of type erasure from Physical Isolation
Source Code
echo.hpp
#ifndef ECHO_HPP
#define ECHO_HPP
#include <boost/capy/io/any_stream.hpp>
#include <boost/capy/task.hpp>
namespace myapp {
// Type-erased interface: no template dependencies
boost::capy::task<> echo_session(boost::capy::any_stream& stream);
} // namespace myapp
#endif
echo.cpp
#include "echo.hpp"
#include <boost/capy/read.hpp>
#include <boost/capy/write.hpp>
#include <boost/capy/cond.hpp>
#include <boost/capy/buffers/make_buffer.hpp>
namespace myapp {
using namespace boost::capy;
task<> echo_session(any_stream& stream)
{
char buffer[1024];
for (;;)
{
// Read some data
// ec: std::error_code, n: std::size_t
auto [ec, n] = co_await stream.read_some(make_buffer(buffer));
if (ec == cond::eof)
co_return; // Client closed connection
if (ec)
throw std::system_error(ec);
// Echo it back
// wec: std::error_code, wn: std::size_t
auto [wec, wn] = co_await write(stream, const_buffer(buffer, n));
if (wec)
throw std::system_error(wec);
}
}
} // namespace myapp
main.cpp
#include "echo.hpp"
#include <boost/capy.hpp>
#include <boost/capy/test/stream.hpp>
#include <boost/capy/test/fuse.hpp>
#include <boost/capy/test/run_blocking.hpp>
#include <iostream>
using namespace boost::capy;
void test_with_mock()
{
test::fuse f;
test::stream mock(f);
mock.provide("Hello, ");
mock.provide("World!\n");
// Stream returns eof when no more data is available
// Using pointer construction (&mock) for reference semantics - the
// wrapper does not take ownership, so mock must outlive stream.
any_stream stream{&mock}; // any_stream
test::run_blocking()(myapp::echo_session(stream));
std::cout << "Echo output: " << mock.data() << "\n";
}
// With real sockets (using Corosio), you would write:
//
// task<> handle_client(corosio::tcp::socket socket)
// {
// // Value construction moves socket into wrapper (transfers ownership)
// any_stream stream{std::move(socket)};
// co_await myapp::echo_session(stream);
// }
int main()
{
test_with_mock();
return 0;
}
Build
add_library(echo_lib echo.cpp)
target_link_libraries(echo_lib PUBLIC capy)
add_executable(echo_demo main.cpp)
target_link_libraries(echo_demo PRIVATE echo_lib)
Walkthrough
The Interface
// echo.hpp
task<> echo_session(any_stream& stream);
The header declares only the signature. It includes any_stream and task, but no concrete transport types.
Clients of this header:
-
Can call
echo_sessionwith any stream -
Do not depend on implementation details
-
Do not recompile when implementation changes
Exercises
-
Add logging to
echo_sessionand observe that clients don’t recompile -
Create a second implementation file with different behavior (e.g., uppercase echo)
-
Measure compile times with and without type erasure in a larger project
Next Steps
-
Timeout with Cancellation — Stop tokens for timeout