Native framing declarations
Framing belongs to ordered byte I/O. It is not specifically a socket feature. The same framing declaration can be used by subclasses of:
Linux::Event::IO::PipeLinux::Event::IO::TTYLinux::Event::IO::Sock::Stream
Public model
A framed type combines one concrete ordered-byte leaf, one declarative framer, and either a named message method or a constructor callback:
package MessageConnection;
use parent 'Linux::Event::IO::Sock::Stream';
use Linux::Event::Framer 'LengthPrefix',
bytes => 4,
endian => 'big',
max_frame => 16 * 1024 * 1024;
sub on_message ($self, $message) {
$self->data->{messages}++;
$self->send($message);
}
The declaration must appear after use parent. The import records immutable
class metadata; it does not construct a per-connection framer object.
The first construction of a concrete subclass resolves its declaration, callbacks, tuning policy, and native parser configuration into one cached private descriptor. Each object keeps only its changing parser, buffer, queue, and lifecycle state.
An instance may replace the class callback without changing framing policy:
my $stream = MessageConnection->new(
fh => $fh,
on_message => sub ($stream, $message) {
handle_message($application_state, $stream, $message);
},
);
The supplied closure becomes that object's effective native message CV. It is not looked up or selected again for each frame.
Framing pipes and terminals
Nothing about a delimiter or length prefix requires a socket. For example, an interactive terminal can be line framed:
package Console;
use parent 'Linux::Event::IO::TTY';
use Linux::Event::Framer 'Delimiter', "\n";
sub on_message ($self, $line) {
$self->write("You typed: $line\n");
}
A child-process or application pipe can use the same delimiter or a binary length prefix. The underlying public leaf identifies the Linux resource; framing describes how an ordered byte sequence is divided into messages.
Name resolution
The declaration name is validated as one Perl package component and expanded as:
Linux::Event::Framer::<Name>
Delimiter therefore loads Linux::Event::Framer::Delimiter. There is no
central alias table. Misspelled or incorrectly cased names fail during class
compilation.
Built-in wire contracts
| Name | Native input | Outbound send($payload) |
|---|---|---|
| Delimiter | payload through configured delimiter | appends delimiter |
| Fixed | exactly size bytes | requires exactly size bytes |
| LengthPrefix | unsigned 1/2/4-byte payload length | prepends configured binary length |
| U32BE | unsigned 4-byte network-order payload length | prepends network-order length |
| Netstring | canonical length:payload, | emits canonical netstring |
| Varint | canonical unsigned LEB128 payload length | prepends canonical LEB128 length |
| DecimalLength | ASCII length plus one separator byte | prepends decimal length and separator |
All lengths and limits are byte counts. Inbound boundary detection and parser
state machines run in XS. Outbound encoding is the named built-in framing
function used by send(), after which bytes enter the ordinary native write
queue.
Limits
Framers that accept max_frame reject larger inbound and outbound payloads.
Length-prefix families may accept include_prefix; Delimiter accepts
include_delimiter. Those options affect the message passed to the callback,
not the bytes consumed from the underlying resource.
The class-level ordered-byte option max_buffer is an independent hard bound
on framed input storage. Its default is 8 MiB:
sub stream_tuning ($class) {
return max_buffer => 32 * 1024 * 1024;
}
Use explicit limits for untrusted protocols rather than relying on process memory availability.
Raw mode
A readable ordered-byte object without a framer requires on_data, supplied
either as a subclass method or a constructor callback:
package RawConnection;
use parent 'Linux::Event::IO::Sock::Stream';
sub on_data ($self, $bytes) {
my $state = $self->data;
$state->{input} .= $bytes;
while ($state->{input} =~ s/\A([^\n]*\n)//) {
push @{ $state->{records} }, $1;
}
}
Read callback chunks are not protocol boundaries. A raw parser must retain partial application state itself.
Arbitrary next_frame parser objects and the former native-backed Buffer view
are not public APIs. Native built-ins avoid per-connection parser objects,
repeated dynamic dispatch, and XS-to-Perl calls whose only result would be a
frame-boundary tuple.
Inheritance
A derived ordered-byte type inherits the nearest ancestor framer declaration through normal Perl MRO and may replace its named callbacks:
package AuditedConnection;
use parent 'MessageConnection';
sub on_message ($self, $message) {
push @{ $self->data->{audit_log} }, length($message);
$self->send($message);
}
Each concrete subclass gets its own cached descriptor while immutable parser
configuration is still shared rather than copied per object. A class cannot
declare two framers or combine framed delivery with on_data.
Explicit message batching
Ordinary framed delivery calls on_message once per complete message. A
pipelined protocol can explicitly select bounded array delivery:
sub stream_tuning ($class) {
return message_batch_size => 32;
}
sub on_messages ($self, $messages) {
process_message($self, $_) for @$messages;
}
on_message and on_messages are mutually exclusive. A positive
message_batch_size requires an on_messages method or constructor callback,
and defining or supplying on_messages without the option is rejected.
XS flushes a partial message batch when the current read drain reaches EAGAIN, before EOF or framing error, and when the aggregate input guard requires a flush. It never waits for a future readiness event merely to fill the configured count.
Pause, close, and protocol transition take effect at the selected callback
boundary. A negotiation protocol that must change type immediately after one
specific message should use ordinary on_message delivery.
Raw callback batching
Raw mode can set read_batch_bytes in stream_tuning() to combine successful
native reads before calling on_data. A partial raw batch also flushes when the
current drain ends; it does not wait for a future readiness event.
Raw read batching and framed message batching are different policies and are validated accordingly.
Changing framing on a live resource
A protocol may transition between loaded ordered-byte subclasses with
transition_to():
$self->transition_to('BinaryProtocol', input => $remaining);
Unread native bytes are preserved and interpreted using the target descriptor.
The explicit input value represents an unconsumed suffix from a raw callback.
Native buffered bytes, when present, precede that suffix.
The old parser stops after the callback that changes the descriptor. Complete
target frames already present in buffered input can then be delivered without
waiting for another kernel readiness event. Read pause survives the transition
and postpones delivery until resume_read().
The target max_buffer and output limits are checked before the descriptor swap
is committed. Existing queued output is not reframed; later send() calls use
the target framing rule.
A transition changes application protocol policy, not the Linux resource. A connected stream socket remains a connected stream socket; a pipe remains a pipe.
When both source and target declare native consumers, transition_to() may
replace the native consumer provider while preserving the same native input
buffer. The target provider context is prepared before the source is disturbed;
the source flush/context lifetime is settled at a safe provider boundary; then
unread bytes are re-driven through the target provider.
A native consumer may also retire into an ordinary Perl target such as a raw
on_data Stream. The same source flush and lifetime rules apply, but there is
no target provider context to create. The source-reported consumed prefix is
removed first and the remaining native tail is then interpreted by the ordinary
target descriptor. This lets a protocol parser consume only its own prefix and
hand a same-read tail to the next protocol without first materializing that tail
in Perl before the transition.
The reverse ordinary-to-native direction remains rejected. See
ORDERED-BYTE-CONSUMER-ABI.md for the provider lifecycle and raw-input
contract.
Framing and serialization
Framing and serialization are intentionally separate layers:
bytes
-> buffering
-> framing
-> complete message bytes
-> codec / serialization
-> Perl value
send() performs framing, not arbitrary object serialization. A codec layer can
be built above the completed message boundary without changing the native I/O
engine.
Adding a native built-in
A complete framing family requires:
- a
Linux::Event::Framer::<Name>module validating declaration arguments and providing immutable native configuration plus outbound encoding; - corresponding XS parser support for partial input, multiple messages, limits, errors, and callback reentrancy;
- contract tests for encoding, split input, malformed input, limits,
per-object state independence, and
send(); - benchmark coverage in the native framer harness.
The module builder interface is distribution-internal. Copying a Perl module
without implementing its matching native parser is not a new framing family.
Applications with unusual protocols should use raw on_data; generally useful
families can be added to Linux::Event with native implementation and tests.
Error behavior
Malformed or oversized input produces a Linux::Event::Error with type
framing. The object stores it as last_error, invokes its cached on_error
callback when present, and closes through the normal ordered-byte lifecycle.
Application callback exceptions propagate rather than being silently converted.
Performance model
Immutable parser configuration and method callback CVs are resolved once per concrete class. An optional constructor callback replaces the corresponding CV once in native per-object state. XS appends bytes directly to native storage, finds all complete built-in frames available, and crosses into Perl only for semantic messages or lifecycle errors. Optional message batching can amortize that semantic crossing across several messages.
Measure framing changes with the framing microbenchmarks, native-framer
benchmarks, callback-batching throughput/fairness tests, and the lifecycle
performance baseline under bench/.