XS Roadmap

The generic reactor and the first native Stream engine are now implemented in the same Linux::Event distribution. The guiding rule remains:

Perl should receive semantic events; XS should absorb repetitive mechanical events whenever doing so preserves a clean, general API.

Completed foundation

These are permanent regression targets. New work must not trade them away without benchmark evidence.

Completed - unified object lifecycle

Linux::Event::Loop owns high-level Stream, Listener, Datagram, Timer, Signal, Wakeup, and Process objects through add(), while watch() creates an immediately attached opaque native registration. There is no public Watcher or IO base class. One logical object may own several internal epoll registrations.

Timer uses that same attachment contract without adding Loop-specific factory methods. The abstract public Timer class caches on_timer once per subclass; active instances live entirely in the Loop's native scheduler.

Signal uses the same attachment contract. Its abstract public class caches on_signal once per subclass, while each Loop's private native service owns the signalfd mask and supports multiple numbers per object plus multiple objects per number.

MyStream->connect() keeps one Stream identity through outbound acquisition and optional TLS readiness. Linux::Event::Listener with a configured stream_class owns inbound acquisition and constructs accepted Streams. The public hierarchy adds no generic Perl call to steady-state native readiness dispatch.

Completed - initial TLS provider implementation

HTTPS, secure WebSocket, and Discord require TLS. TLS is a byte transport transform, not a message framer. The internal native operation boundary and plain provider now exist. The bundled Linux::Event::TLS provider attaches without adding OpenSSL policy or calls to the reactor core or the plain Stream path.

The design must cover handshake readiness, encrypted and plaintext buffering, certificate/hostname errors, shutdown, deadlines, ALPN, and transitions such as STARTTLS without putting TLS policy into the reactor core. Initial client/server TLS, verification, ALPN, cross-direction readiness, and close notification landed with the 0.100_015 transport ABI. Version 0.100_016 added provider-owned deadline-watcher lifecycle and the original external Linux::Event::TLS 0.002 added default handshake and shutdown deadlines, clean/unclean EOF classification, native counters, and a same-Stream plain-versus-TLS benchmark. Version 0.100_017 merged that provider into the main distribution without merging its OpenSSL implementation into the native Stream engine.

The released attachment exact-versions the common ABI, retains provider lifetime, and implements cross-direction readiness (SSL_read wanting write and SSL_write wanting read). Established Stream deadlines now cover TLS after provider readiness. Richer shutdown diagnostics, provider bounded-buffer observability and live transport replacement remain follow-up work.

Completed - Stream connection layer

MyStream->connect() owns the public outbound lifecycle. Its private Linux::Event::Stream::_Connection engine implements strict IPv4/IPv6/Unix/packed address modes, typed errors, silent cancellation, and a default connection deadline. Socket creation uses SOCK_NONBLOCK | SOCK_CLOEXEC atomically. Immediate results are deferred so network callbacks never run inside the constructor.

The policy/state machine remains in cold Perl code. A small native timerfd helper supplies monotonic deadlines and deferred dispatch. Same-fd native replacement uses one EPOLL_CTL_MOD where an fd registration is replaced.

Completed - native Listener layer

Linux::Event::Listener creates or adopts listening stream sockets and constructs a configured Stream subclass for each accepted connection. A small private native extension drains accept4() with atomic nonblocking and close-on-exec flags and retains packed peer addresses for lazy conversion.

The default level-triggered fairness cap is safe because epoll reports a remaining backlog again. Edge-triggered listeners require an unlimited drain. No temporary accepted-socket registration is created before Stream attachment. Resource exhaustion pauses readiness before typed error delivery.

Completed - asynchronous Resolver and Happy Eyeballs

Hostname resolution is mechanically separate from Stream and Datagram socket policy in the private Linux::Event::_Resolver XS extension. Each Loop lazily owns two native resolver workers and one eventfd completion queue. Workers never enter Perl. The normal raw Loop watch path drains complete candidate collections, cancelled requests discard late results, IPv6/IPv4 attempts are staggered by 250 ms, and first-success ownership closes all losers. Literal, Unix, and packed addresses bypass the resolver. Dedicated integration, cancellation, ordering, and microbenchmark coverage protects the boundary.

Completed - signalfd signal handling

Linux::Event::Signal provides synchronous Loop-thread delivery without Perl signal handlers. One lazy signalfd and native subscription registry per Loop drain and aggregate records before fan-out. Cancellation is safe during callbacks, last-subscriber removal restores only mask entries Linux::Event changed, and one signal number has one owning Loop per process. Resolver workers block signals independently so native DNS cannot intercept them.

Completed - established Stream deadlines

Stream subclasses cache idle, read, and write inactivity defaults, while each instance may override them and own one explicit overall-operation deadline. Established policy begins only after plain or TLS readiness and reports typed timeout errors through the ordinary Stream close lifecycle.

At most one private Timer per Stream represents the earliest condition in the Loop's existing timerfd/native heap. XS records successful transport activity only when inactivity policy is enabled. Ordinary Streams perform no timestamp syscalls, and enabled I/O progress does not enter Perl merely to move a heap entry. Plain, TLS, transition, pause/resume, EOF, queued-write, and regression benchmark coverage protect the contract.

Completed - eventfd Wakeup boundary

Linux::Event::Wakeup exposes one subclass-defined eventfd notification object. Foreign native threads, fork children, and cloned ithread handles may increment its counter without entering the Loop interpreter. The Loop drains one counter value per turn and delivers one semantic on_wakeup callback.

Wakeup deliberately carries no Perl payload and is not a coderef posting queue. Applications publish data through their own thread-safe queue or IPC channel before signalling. Thread clones own duplicate descriptors, while callback state, Loop ownership, and application data remain confined to the creating interpreter.

Completed - production socket configuration

Stream class policy and constructor overrides cover local address binding, TCP_NODELAY, keepalive tuning, TCP_USER_TIMEOUT, buffer sizing, and Linux interface binding. Built-in policy is applied to every outbound candidate and to accepted or adopted sockets before transport attachment. A cached configure_socket hook follows built-in policy for advanced cold-path setup.

Public timeout values use seconds. Options with meaningful Linux live behavior also have getters/setters that return the effective kernel value. Failures are structured socket_configuration Errors; incompatible address families never fall through to an unconfigured candidate.

Completed - packet-preserving Datagram layer

Linux::Event::Datagram provides connected and unconnected UDP plus filesystem Unix datagrams. Native recvmsg(MSG_TRUNC) batching retains packet boundaries, original packet sizes, and packed peers. Native send/sendto preserves whole packets through bounded output queues, soft backpressure, and hard byte or packet limits. Connected hostname mode uses the private asynchronous resolver with datagram hints.

Ownership, detach, Unix-path cleanup, source-specific option validation, truncation reporting, and Loop-thread callback ordering are part of the public contract.

Completed - pidfd Process layer

Linux::Event::Process uses posix_spawnp, pidfd readiness, waitid(P_PIDFD), and pidfd_send_signal. One object owns lifecycle, decoded exit status, and optional asynchronous stdin/stdout/stderr pipes. Output is drained before on_exit; stdin writes suppress SIGPIPE without changing process-wide signal policy.

Detached spawn specifications are side-effect free until Loop attachment. No Perl code runs in a post-fork child, caller filehandles stay caller-owned, and partial setup failure kills and reaps a newly spawned child before releasing resources.

The essential completion surface is complete in version 0.101. The remaining items below are optional expansion and evidence-driven optimization, not release blockers.

Priority 1 - General native framing families

Expand the built-in framing catalog while keeping one rule: built-in boundary detection is native, while application-specific protocols parse raw on_data bytes. Do not reintroduce a second arbitrary framer-object contract.

Near-term framing families include:

Keep protocol-specific state machines separate when the work is more than message-boundary detection. A new general family needs a declarative module, corresponding XS parser mode, wire-contract tests, and a native-framer benchmark row. Its exact package name becomes the declaration name automatically.

Priority 2 - Native Stream watcher-state transitions

Reduce remaining Perl transitions for writable interest, read suspension, close, and half-close when profiling shows the boundary is material.

Priority 3 - Callback coalescing/batching

Investigate fewer Perl entries without changing the ordinary one-message API:

Priority 4 - Native connect attempt completion

Profile before moving the remaining attempt state machine below Perl. If high-churn connection workloads justify it, native code may:

The public Stream connection contract must not change merely to eliminate a few cold Perl calls.

Priority 5 - Additional Linux fd drain helpers

Profile native draining or aggregation for additional descriptor families only when Perl currently receives repetitive mechanical events.

Priority 6 - Buffer representation experiments

Only if profiling justifies them:

Do not optimize allocation speculatively.

Priority 7 - Protocol acceleration above Stream

After the generic framing catalog is broad, consider reusable protocol engines such as HTTP or WebSocket parsing. Application semantics remain Perl even when mechanical parsing moves native.

Benchmark policy

Keep the reactor comparison as the low-level regression standard. Keep Stream transport/output-limit, framing, protocol-transition, and lifecycle/retained-memory benchmarks separate. Cross-runtime Stream benchmarks compare high-level facilities and must continue to report exact fairness contracts, server CPU per message, throughput, latency, and memory.