Skip to content

markushocke/modern_io

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

43 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Modern IO

A C++23 framework for type-safe I/O — files, networking, and async coroutines.
Built on modules, concepts, std::expected, and zero external runtime dependencies.


Architecture

The framework is split into five link targets. Consumers pick only what they need:

modern_io              sync file / data / buffered / iostream streams
modern_io_async        async task types, async buffered & data layers
net_io                 sync TCP / UDP / IPC primitives
net_io_async           EventLoop, awaiters, async sockets & server
net_io_adapters        bridges net_io transports into modern_io streams

Dependency rule: modern_io_asyncmodern_io, net_io_asyncnet_io.
Sync and async are never mixed implicitly.

Concept layers

Everything composes through a small set of concepts:

Layer Sync Async
Streams InputStream / OutputStream AsyncInputStream / AsyncOutputStream
Transports Transportable (open/close/read/write) AsyncTcpSocket, AsyncUdpSocket
Adapters TransportSource/TransportSink, make_stream() AsyncTcpStreamAdapter, AsyncUdpStreamAdapter
Servers Acceptable + ThreadExecutor AsyncTcpServer::accept() coroutine

A new transport only needs to satisfy the matching concept.
Adapters, data streams, and buffering compose on top automatically.


Build & Install

git clone https://github.com/markushocke/modern_io.git
cd modern_io
cmake -B build -G Ninja -DCMAKE_CXX_COMPILER=clang++
cmake --build build
# Install
cmake --install build --prefix /tmp/modern_io-install
find_package(modern_io CONFIG REQUIRED)
target_link_libraries(app PRIVATE
    modern_io::modern_io
    modern_io::net_io
    # modern_io::modern_io_async  modern_io::net_io_async   # opt-in
)

Requires C++23 (Clang recommended) and CMake 3.28+.


Examples

File I/O

import modern_io;
using namespace modern_io;

FileOutputStream out("data.bin");
DataOutputStream dout(std::move(out), std::endian::big);
dout.write_int32(42);
dout.write_string("Hello");
dout.flush();

FileInputStream in("data.bin");
DataInputStream din(std::move(in), std::endian::big);
auto value = din.read_int32();   // 42
auto msg   = din.read_string();  // "Hello"

Sync TCP — one-liner client via make_stream

import modern_io;
import net_io;
import net_io_adapters;
using namespace net_io;
using namespace net_io_adapters;

// Endpoint → socket → adapter: fully deduced
auto stream = make_stream(TcpEndpoint("127.0.0.1", 9000));
DataOutputStream out(stream, std::endian::big);
out.write_string("Hello TCP");
out.flush();

The same pattern works for UDP — just pass a UdpEndpoint instead.

Sync TCP Echo Server

import modern_io;
import net_io;
import net_io_adapters;
using namespace net_io;
using namespace net_io_adapters;

std::atomic<bool> running{true};
ThreadExecutor exec;

run_tcp_server(exec, [](auto&& stream) {
    DataInputStream  in(stream, std::endian::big);
    DataOutputStream out(stream, std::endian::big);
    out.write_string("Echo: " + in.read_string());
    out.flush();
}, running, TcpEndpoint{"127.0.0.1", 9050});

run_tcp_server is generic — replace the endpoint with any Acceptable server type and the handler keeps working.

Async TCP — coroutine server + client

import modern_io_async;
import net_io;
import net_io_async;
using namespace net_io;

// Accept one client, read a message, reply
ExpectedTask<void> serve_once(AsyncTcpServer& server) {
    auto cli = co_await server.accept();
    if (!cli) co_return std::unexpected(cli.error());

    AsyncTcpStreamAdapter stream(cli.value());
    char buf[64]{};
    auto r = co_await stream.read_async(std::span<char>(buf, sizeof(buf)));
    if (r && r.value() > 0) {
        std::string pong = "PONG";
        co_await stream.write_async({pong.data(), pong.size()});
    }
    co_return {};
}

// Connect, send a ping, read the reply
ExpectedTask<void> ping(std::string addr, uint16_t port) {
    auto sock = std::make_shared<AsyncTcpSocket>();
    auto sa   = TcpEndpoint(addr, port).to_sockaddr(false);
    auto conn = co_await sock->async_connect(sa, sizeof(sa));
    if (!conn) co_return std::unexpected(conn.error());

    AsyncTcpStreamAdapter stream(sock);
    std::string msg = "PING";
    co_await stream.write_async({msg.data(), msg.size()});

    char buf[64]{};
    auto r = co_await stream.read_async(std::span<char>(buf, sizeof(buf)));
    if (r) std::cout << std::string(buf, r.value()) << '\n';
    co_return {};
}

// Drive it
EventLoop::instance().start();
run_and_wait(serve_once(server));
run_and_wait(ping("127.0.0.1", 9000));
EventLoop::instance().stop();

Prefer named coroutine functions over lambda coroutines — it avoids lifetime pitfalls.

Async UDP follows the same pattern via AsyncUdpSocket + AsyncUdpStreamAdapter.

net_io_async is currently Linux-only (epoll). The sync surface is the portable baseline for Windows.


Error Handling

Sync APIs throw domain exceptions. Async APIs return std::expected<T, std::error_code>.

Sync — exception hierarchy

modern_io (all derive from IOException):

Type Context
FileIOException filepath, errno
ReadWriteException expected vs. actual bytes
UnexpectedEOFException
DataFormatException deserialization errors
BufferException buffer overflow / underflow
StreamPositionException seek errors

net_io (all derive from NetworkExceptionstd::runtime_error):

Type Context
SocketException system error code
ConnectionException endpoint info
BindException address, port
TimeoutException
ResolutionException hostname
try {
    FileInputStream in("missing.txt");
} catch (const FileIOException& e) {
    std::cerr << e.filepath() << ": errno " << e.error_code() << '\n';
}

Async — std::expected + error codes

auto r = co_await stream.read_async(buf);
if (!r) {
    std::cerr << "read failed: " << r.error().message() << '\n';
    co_return std::unexpected(r.error());
}

No exceptions cross coroutine suspension points.


Extending the Framework

The concept-based design means new transports plug in without modifying existing code.

Example — adding a TLS transport:

// 1. Satisfy the Transportable concept
class TlsClient {
public:
    void open();
    void close();
    std::size_t read(char* buf, std::size_t n);
    void write(const char* buf, std::size_t n);
};
// TlsClient now models Transportable — done.

// 2. All existing adapters and data streams work immediately
auto tls = std::make_shared<TlsClient>(/* ... */);
auto stream = make_stream(tls);   // TransportSource + TransportSink, deduced
DataOutputStream out(stream, std::endian::big);
out.write_string("Hello TLS");
out.flush();

For async transports: implement low_level_read/low_level_write + register with the EventLoop, then wrap in an AsyncStreamBase-derived adapter.


Tests

cd build && ctest --output-on-failure -j4

102 tests covering sync I/O, data streams, buffering, TCP/UDP endpoints, async sockets, event loop, and multi-client accept scenarios.
CI runs a dedicated ASAN async gate on every push.

See TESTING.md and UNITTEST_SUMMARY.md for details.


License

MIT — see LICENSE.


Authors

Markus Hocke — contributions welcome.

About

A modern, modular C++23 framework for high-level, type-safe, and efficient I/O: files, TCP/UDP networking, and data serialization. Designed for clarity, extensibility, and leveraging C++23 modules and concepts.

Topics

Resources

License

Stars

Watchers

Forks

Contributors