Skip to content

feat(xpp): introduce xpp::io::Error (bit-packed, replaces SocketError) - #206

Merged
mivinci merged 2 commits into
mainfrom
claude/io-error-v2
May 30, 2026
Merged

mivinci merged 2 commits into
mainfrom
claude/io-error-v2

Conversation

@mivinci

@mivinci mivinci commented May 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • New xpp::io::Error in libx++/xpp/io/error.{h,cpp} modeled after std::io::Error. Single uintptr_t of storage, four variants packed into the two low tag bits:
    • SimpleMessage — pointer to a static (kind, msg), zero allocation
    • Custom — heap-allocated (kind, len, char[]), one allocation
    • Os — i32 errno + kind() derived via errno_to_kind
    • Simple — bare ErrorKind
  • 26-variant ErrorKind mirrors std::io::ErrorKind plus ResolveFailed / NoAddress.
  • Option<io::Error> niche specialization keeps it at one pointer.
  • Drop the old SocketError enum. Net failure paths now carry the underlying errno (so callers can distinguish ECONNREFUSED from EHOSTUNREACH, which the old enum lumped together).

Why

SocketError's 8 categories collapsed every connect / bind / accept failure into one of CreateFailed/BindFailed/ConnectFailed. Tokio uses std::io::Error everywhere because that single type carries the OS errno, the cross-platform kind, and an optional message all in 8 bytes — debuggability and unification in one shot.

Static guarantees

static_assert(sizeof(io::Error) == sizeof(void *));            // 8 bytes
static_assert(sizeof(Option<io::Error>) == sizeof(void *));    // niche preserved
static_assert(!std::is_copy_constructible<io::Error>::value);  // move-only

Allocation table

Constructor Allocs
io::Error(ErrorKind) (implicit) 0
io::Error::from_errno(int) 0
io::Error::from_static(SimpleMessage*) 0
io::Error::with_message(Kind, str) 1

Test plan

  • build/xpp_test: 671 pass (was 649; +22 IoErrorTest).
  • build-cxx20/xpp_test: 702 pass.
  • build-cxx20/xpp_coro_test: 31 pass.
  • New IoErrorTest (22 cases): size invariants, every variant constructor, errno mapping (POSIX subset), Option niche, move semantics, to_string golden output, long messages, edge cases (from_errno(0), negative, null message).
  • Existing TCP/UDP/DNS tests adapted — EXPECT_EQ(err, SocketError::X) → EXPECT_EQ(err.kind(), io::ErrorKind::Y).

Notable behavior changes

  • TcpListener::bind, TcpStream::connect, UdpSocket::bind/connect, accept, set_nodelay/nodelay now return Result<…, io::Error> instead of Result<…, SocketError>.
  • Errno-bearing failures (bind, connect, accept) carry the original code: err.raw_os_error() returns Some(EADDRINUSE), err.kind() is ErrorKind::AddrInUse.
  • socket.h's SocketError enum and socket_error_message() helper are deleted.

🤖 Generated with Claude Code

mivinci and others added 2 commits May 30, 2026 09:53
Bring net error reporting in line with std::io::Error:

- New xpp::io::Error in libx++/xpp/io/error.{h,cpp}.  Single
  uintptr_t storage; tag bits in the low 2 bits encode one of four
  variants (SimpleMessage / Custom / Os / Simple).  sizeof(Error) ==
  sizeof(void*) and sizeof(Option<Error>) == sizeof(void*) via a
  niche specialization.
- 26-variant ErrorKind covering std's cross-platform set plus
  ResolveFailed / NoAddress (DNS-specific).  errno_to_kind() maps
  the POSIX subset; raw_os_error() preserves the original code so
  callers can distinguish ECONNREFUSED from EHOSTUNREACH (which
  the old SocketError collapsed into BindFailed/ConnectFailed).
- Custom variant uses a single allocation with a flexible-array
  payload, so with_message(Kind, str) costs 1 alloc, not 2.
- XPP_IO_DEFINE_MSG macro for ODR-safe static (kind, message)
  pairs — zero allocation at use sites.
- Migrated tcp / udp / dns / their tests.  TcpListener::bind_addr,
  TcpStream::connect_addr, accept(), and UdpSocket::bind_addr now
  capture errno via from_errno(); the hostname overloads bubble up
  the underlying io::Error.  socket.h's old SocketError enum and
  socket_error_message() helper are gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adopt the codebase's m_-prefix convention for the lone storage field
of io::Error, the matching field on Option<io::Error>, and pick up the
incidental clang-format pass on the io/ headers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mivinci
mivinci merged commit b901058 into main May 30, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant