Hypersonic
view release on metacpan or search on metacpan
lib/Hypersonic.pm view on Meta::CPAN
sub async_pool {
my ($self, %opts) = @_;
require Hypersonic::Future;
require Hypersonic::Future::Pool;
# Create a Pool instance (OO - allows multiple pools)
my $pool = Hypersonic::Future::Pool->new(
workers => $opts{workers} // 8,
queue_size => $opts{queue_size} // 4096,
);
# Store in array for event loop registration
push @{$self->{_async_pools} //= []}, $pool;
# Mark that async pool is enabled - JIT code gen will include thread pool
$self->{_async_enabled} = 1;
# Return the Pool object (not $self anymore)
return $pool;
}
# Compression configuration - JIT-compiled gzip compression in C
# Only loads compression module when called (JIT philosophy)
sub compress {
my ($self, %opts) = @_;
require Hypersonic::Compress;
# Check for zlib
unless (Hypersonic::Compress::check_zlib()) {
warn "Warning: zlib not found, compression disabled\n";
return $self;
}
# Configure compression
my $config = Hypersonic::Compress->configure(%opts);
# Mark that compression is enabled - JIT code gen will include zlib code
$self->{_compression_enabled} = 1;
$self->{_compression_config} = $config;
return $self;
}
sub _add_route {
my ($self, $method, $path, $handler, $opts) = @_;
die "Path must start with /" unless $path =~ m{^/};
die "Handler must be a code ref" unless ref($handler) eq 'CODE';
# Check for dynamic option and feature flags
my $dynamic = 0;
my %features = (
parse_query => 0, # Parse ?key=value query strings
parse_headers => 0, # Parse HTTP headers
parse_cookies => 0, # Parse Cookie header
parse_json => 0, # Parse JSON body (requires Cpanel::JSON::XS)
parse_form => 0, # Parse form-urlencoded body
response_helpers => 0, # JIT compile response helper methods
streaming => 0, # Streaming response handler
need_xs_builder => 0, # Handler receives XS::JIT::Builder
);
if (ref($opts) eq 'HASH') {
$dynamic = $opts->{dynamic} ? 1 : 0;
# Copy feature flags from options
for my $feat (keys %features) {
$features{$feat} = $opts->{$feat} ? 1 : 0 if exists $opts->{$feat};
}
}
# Parse path parameters (supports multiple: /users/:user_id/posts/:post_id)
my @params;
my @segments = split '/', $path;
shift @segments; # Remove leading empty string
for my $i (0 .. $#segments) {
if ($segments[$i] =~ /^:(\w+)$/) {
push @params, { name => $1, position => $i };
$dynamic = 1; # Path params imply dynamic
}
}
# Streaming handlers are always dynamic
if ($features{streaming}) {
$dynamic = 1;
}
# need_xs_builder handlers are always dynamic (but handled specially at compile time)
if ($features{need_xs_builder}) {
$dynamic = 1;
}
push @{$self->{routes}}, {
method => $method,
path => $path,
handler => $handler,
dynamic => $dynamic,
streaming => $features{streaming},
need_xs_builder => $features{need_xs_builder},
params => \@params,
segments => \@segments,
features => \%features,
# Per-route middleware (optional)
before => $opts->{before} // [],
after => $opts->{after} // [],
};
return $self;
}
sub compile {
my ($self) = @_;
die "No routes defined" unless @{$self->{routes}} || @{$self->{static_dirs} // []};
die "Already compiled" if $self->{compiled};
# ============================================================
# STATIC FILE PROCESSING - bake files into C at compile time
# ============================================================
if (my $static_dirs = $self->{static_dirs}) {
$self->_compile_static_files($static_dirs);
}
# ============================================================
# ROUTE ANALYSIS - determine what code to generate (JIT philosophy)
# ============================================================
my %analysis = (
methods_used => {}, # GET => 1, POST => 1, etc.
has_dynamic => 0, # Any dynamic routes?
has_static => 0, # Any static routes?
has_path_params => 0, # Any routes with :param?
has_body_access => 0, # Any routes that need body?
route_count => scalar(@{$self->{routes}}),
all_same_prefix => undef, # Common prefix like /api/*
single_method => undef, # Only one HTTP method used?
# JIT feature flags - only generate code for features actually used
needs_query => 0, # Any route needs query string parsing?
needs_headers => 0, # Any route needs header access?
needs_cookies => 0, # Any route needs cookie parsing?
needs_json => 0, # Any route needs JSON body parsing?
needs_form => 0, # Any route needs form data parsing?
needs_response_helpers => 0, # Any route needs response helper methods?
needs_streaming => 0, # Any route uses streaming responses?
needs_xs_builder => 0, # Any route uses need_xs_builder?
# Middleware flags - JIT: only generate middleware code if actually used
has_global_before => scalar(@{$self->{before_middleware}}) > 0,
has_global_after => scalar(@{$self->{after_middleware}}) > 0,
has_route_middleware => 0, # Any route has before/after hooks?
has_any_middleware => 0, # Global OR per-route middleware?
);
# First pass: collect method usage and route characteristics
for my $route (@{$self->{routes}}) {
$analysis{methods_used}{$route->{method}} = 1;
if ($route->{dynamic}) {
$analysis{has_dynamic} = 1;
# Dynamic routes might need body access (POST/PUT/PATCH typically do)
if ($route->{method} =~ /^(POST|PUT|PATCH)$/) {
$analysis{has_body_access} = 1;
}
# JIT FEATURE DETECTION: Analyze handler code to detect what request
# features it actually uses. This avoids generating unused parsing code.
my $f = $route->{features} // {};
# Explicit flags take precedence
$analysis{needs_query} = 1 if $f->{parse_query};
$analysis{needs_headers} = 1 if $f->{parse_headers};
$analysis{needs_cookies} = 1 if $f->{parse_cookies};
$analysis{needs_json} = 1 if $f->{parse_json};
$analysis{needs_form} = 1 if $f->{parse_form};
$analysis{needs_response_helpers} = 1 if $f->{response_helpers};
$analysis{needs_streaming} = 1 if $f->{streaming};
$analysis{needs_xs_builder} = 1 if $f->{need_xs_builder};
# Auto-detect by analyzing handler code
my $handler_code = _deparse_handler($route->{handler});
if ($handler_code) {
# Look for $req->{query} or ->{query} access patterns
$analysis{needs_query} = 1 if $handler_code =~ /\{['"]*query['"]*\}/;
$analysis{needs_headers} = 1 if $handler_code =~ /\{['"]*headers['"]*\}/;
$analysis{needs_cookies} = 1 if $handler_code =~ /\{['"]*cookies['"]*\}/;
$analysis{needs_json} = 1 if $handler_code =~ /\{['"]*json['"]*\}/;
$analysis{needs_form} = 1 if $handler_code =~ /\{['"]*form['"]*\}/;
}
} else {
$analysis{has_static} = 1;
}
if (@{$route->{params}}) {
$analysis{has_path_params} = 1;
}
# Check for per-route middleware
if (@{$route->{before}} || @{$route->{after}}) {
$analysis{has_route_middleware} = 1;
}
}
# Session support: force cookie parsing when sessions are enabled
if ($self->{_session_enabled}) {
$analysis{needs_cookies} = 1;
}
# Compression support: force header parsing to check Accept-Encoding
if ($self->{_compression_enabled}) {
$analysis{needs_headers} = 1;
}
# Async Pool support: enable thread pool integration.
# Disabled on Windows - Pool uses pthread + eventfd, neither of
# which exists there. (Future/Pool deserve a Win32 port; not in
# the 0.14 minimum-viable Windows scope.)
if ($self->{_async_enabled}) {
if ($^O eq 'MSWin32') {
warn "Hypersonic: async pool not supported on Windows; "
. "ignoring.\n";
} else {
$analysis{needs_async_pool} = 1;
}
}
# Classify middleware as builder (inline C) or Perl (call_sv)
# Builder middleware has build_before/build_after methods and generates C at compile time
my (@builder_before, @perl_before, @builder_after, @perl_after);
for my $mw (@{$self->{before_middleware}}) {
if (blessed($mw) && ($mw->can('build_before') || $mw->can('build_after'))) {
push @builder_before, $mw;
} else {
push @perl_before, $mw;
}
}
for my $mw (@{$self->{after_middleware}}) {
lib/Hypersonic.pm view on Meta::CPAN
}
# need_xs_builder routes are handled later in code generation
if ($route->{need_xs_builder}) {
$route->{dynamic} = 1; # Treat as dynamic for dispatch
push @dynamic_handlers, $route->{handler};
push @route_param_info, $route->{params};
$route->{handler_idx} = $#dynamic_handlers;
next;
}
# RUN THE HANDLER ONCE - this is the magic
my $result = $route->{handler}->();
# Support both string and [status, headers, body] format
my ($status, $headers, $body);
if (ref($result) eq 'ARRAY') {
($status, $headers, $body) = @$result;
$status //= 200;
$headers //= {};
} elsif (ref($result) eq 'HASH') {
$status = $result->{status} // 200;
$headers = $result->{headers} // {};
$body = $result->{body} // '';
} else {
$status = 200;
$headers = {};
$body = $result;
}
die "Handler for $route->{method} $route->{path} must return a string or response structure"
unless defined $body && !ref($body);
# Build COMPLETE HTTP response via Protocol module (JIT at compile time)
my $security_hdrs = $self->{enable_security_headers}
? $self->_get_security_headers_string()
: '';
my $full_response = $PROTOCOL->build_response(
status => $status,
headers => $headers,
body => $body,
keep_alive => 1,
security_headers => $security_hdrs,
);
push @full_responses, $full_response;
$route->{response_idx} = $#full_responses;
} else {
# Dynamic route - store handler index and param info
push @dynamic_handlers, $route->{handler};
push @route_param_info, $route->{params};
$route->{handler_idx} = $#dynamic_handlers;
}
}
# Store dynamic handlers and param info for runtime access
$self->{dynamic_handlers} = \@dynamic_handlers;
$self->{route_param_info} = \@route_param_info;
# JIT: Build streaming handlers lookup table (only if streaming is enabled)
if ($self->{route_analysis}{needs_streaming}) {
my @streaming_flags;
for my $route (@{$self->{routes}}) {
next unless $route->{dynamic};
push @streaming_flags, $route->{streaming} ? 1 : 0;
}
$self->{_streaming_flags} = \@streaming_flags;
}
# JIT: Build WebSocket handlers lookup table
if ($self->_has_websocket_routes()) {
my @ws_handlers;
my @ws_paths;
for my $route (@{$self->{websocket_routes}}) {
push @ws_handlers, $route->{handler};
push @ws_paths, $route->{path};
# Check for Room usage in route options
if ($route->{opts}{rooms}) {
$self->{route_analysis}{needs_websocket_rooms} = 1;
}
}
$self->{_websocket_handlers} = \@ws_handlers;
$self->{_websocket_paths} = \@ws_paths;
$self->{route_analysis}{needs_websocket} = 1;
# Handler is needed if we have websocket routes
$self->{route_analysis}{needs_websocket_handler} = 1;
# WebSocket uses Stream for connection handling
$self->{route_analysis}{needs_streaming} = 1;
}
# JIT: Explicit opt-in for Room support (can also be set in new())
if ($self->{websocket_rooms}) {
$self->{route_analysis}{needs_websocket_rooms} = 1;
}
# JIT: Build per-route middleware arrays (only if route middleware is present)
my $analysis = $self->{route_analysis};
if ($analysis->{has_route_middleware}) {
my @route_before_mw;
my @route_after_mw;
for my $route (@{$self->{routes}}) {
next unless $route->{dynamic};
# Store arrays of middleware handlers per route (by handler_idx)
push @route_before_mw, $route->{before};
push @route_after_mw, $route->{after};
}
$self->{_route_before_mw} = \@route_before_mw;
$self->{_route_after_mw} = \@route_after_mw;
}
# JIT: Store Perl-only middleware for runtime call_sv dispatch
# Builder middleware is handled at compile time (inline C), not runtime
if ($analysis->{has_global_before} || $analysis->{has_global_after}) {
$self->{_perl_before_mw} = $analysis->{perl_before};
$self->{_perl_after_mw} = $analysis->{perl_after};
}
# Generate C code with pure C event loop
my $c_code = $self->_generate_server_code(\@full_responses);
# Compile via XS::JIT
#
# Derive a STABLE module id from a hash of the generated C source
# (plus this dist's $VERSION and the target perl's archname/version
# so a cache built for one perl is never loaded into another).
#
# Pre-0.18 we used 'Hypersonic::_Server_' . int(rand(100000)) which
# gave a different module name on every fresh perl process; because
# XS::JIT's on-disk cache lives at
# <cache_dir>/lib/auto/<safe_name>/<safe_name>.<dlext>
# (see XS-JIT/lib/XS/JIT/xs_jit.c xs_jit_cache_path()), a different
# $name on every run forced a full gcc/cc re-invocation EVERY time
# the test suite or a user re-ran their server. On slow CPAN smoker
# boxes that gcc invocation takes 30-60+ seconds per server, which
# is what caused the SIGKILL cascade in CPAN tester reports for
# 0.17 (t/0035-e2e-streaming.t, t/2012..t/2017, t/2102).
#
# Using a content hash means: identical route+option configurations
# produce the same module name -> warm cache hit -> dlopen() of a
# 100ms .so instead of a 30s gcc rebuild. The random fallback id is
# retained for the degenerate case where Digest::MD5 isn't available.
my $module_id;
{
my $hash_input = join("\0",
$c_code,
$VERSION,
(defined $Config::Config{archname}
? $Config::Config{archname} : ''),
$] || '',
);
if (eval { require Digest::MD5; 1 }) {
# 16 hex chars (64 bits) is plenty of namespace for one
# process and short enough to keep the on-disk file names
# readable. md5_hex is core since perl 5.7.3.
$module_id = substr(Digest::MD5::md5_hex($hash_input), 0, 16);
} else {
# No Digest::MD5? Fall back to the legacy random id. Cache
# won't be reused across runs but at least nothing breaks.
$module_id = $self->{id};
}
}
my $module_name = 'Hypersonic::_Server_' . $module_id;
# Build compile options - add TLS flags if enabled
my %functions = (
"${module_name}::run_event_loop" => {
source => 'hypersonic_run_event_loop',
is_xs_native => 1,
},
"${module_name}::dispatch" => {
source => 'hypersonic_dispatch',
is_xs_native => 1,
},
);
# Add Stream and SSE XS functions if streaming is enabled
if ($self->{route_analysis}{needs_streaming}) {
%functions = (%functions, %{Hypersonic::Stream->get_xs_functions()});
%functions = (%functions, %{Hypersonic::SSE->get_xs_functions()});
}
# Add WebSocket XS functions if WebSocket routes are registered
if ($self->{route_analysis}{needs_websocket}) {
require Hypersonic::WebSocket;
%functions = (%functions, %{Hypersonic::WebSocket->get_xs_functions()});
}
# Add WebSocket Handler XS functions
if ($self->{route_analysis}{needs_websocket_handler}) {
require Hypersonic::WebSocket::Handler;
%functions = (%functions, %{Hypersonic::WebSocket::Handler->get_xs_functions()});
}
# Add WebSocket Room XS functions
if ($self->{route_analysis}{needs_websocket_rooms}) {
require Hypersonic::WebSocket::Room;
%functions = (%functions, %{Hypersonic::WebSocket::Room->get_xs_functions()});
}
# Add Future/Pool XS functions if async pool is enabled
if ($self->{route_analysis}{needs_async_pool}) {
require Hypersonic::Future;
require Hypersonic::Future::Pool;
%functions = (%functions, %{Hypersonic::Future->get_xs_functions()});
%functions = (%functions, %{Hypersonic::Future::Pool->get_xs_functions()});
}
# Add need_xs_builder route additional XS functions (if any)
# Note: The main handler functions are C functions called by call_xs_builder_handler,
# NOT XS functions callable from Perl, so we don't register them.
if (my $xsr = $self->{_xs_builder_routes}) {
for my $entry (@$xsr) {
my $result = $entry->{result};
# Add any additional XS functions the handler defined
if ($result->{xs_functions}) {
%functions = (%functions, %{$result->{xs_functions}});
}
}
}
my %compile_opts = (
code => $c_code,
name => $module_name,
cache_dir => $self->{cache_dir},
functions => \%functions,
);
# Add OpenSSL flags for TLS support
if ($self->{tls}) {
$compile_opts{extra_cflags} = Hypersonic::TLS::get_extra_cflags();
$compile_opts{extra_ldflags} = Hypersonic::TLS::get_extra_ldflags();
}
# Add nghttp2 flags for HTTP/2 support
if ($self->{http2}) {
require Hypersonic::Protocol::HTTP2;
my $h2_cflags = Hypersonic::Protocol::HTTP2::get_extra_cflags();
my $h2_ldflags = Hypersonic::Protocol::HTTP2::get_extra_ldflags();
$compile_opts{extra_cflags} = ($compile_opts{extra_cflags} // '') . " $h2_cflags";
$compile_opts{extra_ldflags} = ($compile_opts{extra_ldflags} // '') . " $h2_ldflags";
}
# Add zlib flags for compression support
if ($self->{_compression_enabled}) {
require Hypersonic::Compress;
my ($cflags, $ldflags) = Hypersonic::Compress::get_zlib_flags();
$compile_opts{extra_cflags} = ($compile_opts{extra_cflags} // '') . " $cflags";
$compile_opts{extra_ldflags} = ($compile_opts{extra_ldflags} // '') . " $ldflags";
}
# Add pthread flags for async pool (thread pool)
if ($self->{route_analysis}{needs_async_pool}) {
$compile_opts{extra_cflags} = ($compile_opts{extra_cflags} // '') . " -pthread";
$compile_opts{extra_ldflags} = ($compile_opts{extra_ldflags} // '') . " -lpthread";
}
# Add event backend flags (e.g., io_uring needs -luring)
if ($self->{_event_backend}) {
my $backend = $self->{_event_backend};
if ($backend->can('extra_cflags')) {
my $ev_cflags = $backend->extra_cflags // '';
$compile_opts{extra_cflags} = ($compile_opts{extra_cflags} // '') . " $ev_cflags"
if $ev_cflags;
}
if ($backend->can('extra_ldflags')) {
my $ev_ldflags = $backend->extra_ldflags // '';
$compile_opts{extra_ldflags} = ($compile_opts{extra_ldflags} // '') . " $ev_ldflags"
if $ev_ldflags;
}
}
# Windows: link Winsock so socket()/recv()/send()/etc. resolve in
# the JIT-compiled .so. The select backend already adds this, but
# belt-and-braces - the main compile uses these symbols too.
if ($^O eq 'MSWin32') {
my $ld = $compile_opts{extra_ldflags} // '';
$compile_opts{extra_ldflags} = $ld . ' -lws2_32'
unless $ld =~ /-lws2_32\b/;
}
# Emit a visible breadcrumb BEFORE the (potentially slow) gcc/cc
# invocation so smoker logs never show "(child wrote no output)"
# when wait_for_port times out mid-compile. Force a flush in case
# something upstream disabled autoflush on STDERR.
if ($ENV{HYPERSONIC_COMPILE_DIAG} || $ENV{AUTOMATED_TESTING}) {
local $| = 1;
print STDERR "# Hypersonic: compiling JIT module $module_name ...\n";
eval { STDERR->flush; };
}
# The JIT boot xsub installs Hypersonic::Stream::* xsubs into
# the same package that Hypersonic/Stream.pm already lives in,
# which Perl reports as "Subroutine ... redefined". This is
# expected (the .pm defines is_streaming_handler and the .so
# provides the rest at compile time, or - on a second compile()
# in the same process - it reinstalls them). Silence the noise.
my $ok;
{
no warnings 'redefine';
$ok = XS::JIT->compile(%compile_opts);
}
die "XS::JIT->compile failed for $module_name (check liburing/zlib/openssl "
. "are installed and linkable; extra_ldflags='"
. ($compile_opts{extra_ldflags} // '') . "')"
unless $ok;
# Store function references - confirm the boot xsub actually installed
# them. If we somehow got past compile() with the .so loaded but the
# symbols missing, fail loudly here rather than crash later in run().
{
no strict 'refs';
$self->{run_loop_fn} = \&{"${module_name}::run_event_loop"};
$self->{dispatch_fn} = \&{"${module_name}::dispatch"};
die "XS::JIT loaded $module_name but ::dispatch is not defined "
. "(stale cache_dir? link line was: ldflags='"
. ($compile_opts{extra_ldflags} // '') . "')"
unless defined &{"${module_name}::dispatch"};
}
# Mark Future/Pool as compiled if async pool is enabled
# (prevents them from trying to compile separately)
if ($self->{route_analysis}{needs_async_pool}) {
$Hypersonic::Future::COMPILED = 1;
$Hypersonic::Future::Pool::COMPILED = 1;
# Register custom ops for Future after compilation
Hypersonic::Future->_register_ops();
}
$self->{compiled} = 1;
return $self;
}
sub _generate_server_code {
my ($self, $full_responses) = @_;
# Load event backend module
require Hypersonic::Event;
my $backend_name = $self->{event_backend} // Hypersonic::Event->best_backend;
my $backend = Hypersonic::Event->backend($backend_name);
# Store backend for use in event loop generation
$self->{_event_backend} = $backend;
$self->{_event_backend_name} = $backend_name;
my $builder = XS::JIT::Builder->new;
# C99 detection for inline keyword
my $inline = Hypersonic::JIT::Util->inline_keyword;
# Check if we have any dynamic routes
my $has_dynamic = grep { $_->{dynamic} } @{$self->{routes}};
# Common includes - portable across POSIX and Windows. On Windows
# we substitute Winsock for netinet/socket/unistd, and define a
lib/Hypersonic.pm view on Meta::CPAN
->line(' return compressed_len;')
->line('}')
->blank;
}
# Connection tracking for keep-alive timeout - O(1) using fd as index
$builder->comment('Connection tracking - O(1) using fd as direct index')
->line('#define MAX_FD 65536')
->line('static time_t g_conn_time[MAX_FD];')
->line('static time_t g_current_time = 0;')
->blank
->line("static $inline void track_connection(int fd, time_t now) {")
->line(' if (fd >= 0 && fd < MAX_FD) {')
->line(' g_conn_time[fd] = now;')
->line(' g_active_connections++;')
->line(' }')
->line('}')
->blank
->line("static $inline void update_connection(int fd, time_t now) {")
->line(' if (fd >= 0 && fd < MAX_FD) {')
->line(' g_conn_time[fd] = now;')
->line(' }')
->line('}')
->blank
->line("static $inline void remove_connection(int fd) {")
->line(' if (fd >= 0 && fd < MAX_FD && g_conn_time[fd] > 0) {')
->line(' g_conn_time[fd] = 0;')
->line(' g_active_connections--;')
->line(' }')
->line('}')
->blank;
# TLS code generation - SSL context, accept, read/write wrappers
if ($self->{tls}) {
$builder->comment('TLS/HTTPS support via OpenSSL')
->raw(Hypersonic::TLS::gen_ssl_ctx_init(http2 => $self->{http2}))
->blank
->raw(Hypersonic::TLS::gen_ssl_accept())
->blank
->raw(Hypersonic::TLS::gen_ssl_io())
->blank
->raw(Hypersonic::TLS::gen_ssl_close())
->blank;
}
# HTTP/2 code generation - nghttp2 callbacks, session init, dispatchers
if ($self->{http2}) {
require Hypersonic::Protocol::HTTP2;
$builder->comment('HTTP/2 support via nghttp2');
Hypersonic::Protocol::HTTP2->gen_connection_struct($builder);
Hypersonic::Protocol::HTTP2->gen_connection_preface_check($builder);
Hypersonic::Protocol::HTTP2->gen_response_sender($builder);
Hypersonic::Protocol::HTTP2->gen_404_response($builder);
Hypersonic::Protocol::HTTP2->gen_callbacks($builder);
Hypersonic::Protocol::HTTP2->gen_session_init($builder);
Hypersonic::Protocol::HTTP2->gen_dispatcher($builder);
Hypersonic::Protocol::HTTP2->gen_input_processor($builder);
$builder->blank;
}
# Streaming support - JIT: only generate when streaming handlers detected
my $analysis = $self->{route_analysis};
if ($analysis->{needs_streaming}) {
require Hypersonic::Stream;
Hypersonic::Stream->generate_c_code($builder, {
max_streams => $self->{max_connections},
});
# SSE support - compile SSE methods when streaming is enabled
require Hypersonic::SSE;
Hypersonic::SSE->generate_c_code($builder, {
max_sse_instances => $self->{max_connections},
});
}
# WebSocket support - JIT: only generate when WebSocket routes exist
if ($analysis->{needs_websocket}) {
require Hypersonic::WebSocket;
require Hypersonic::Protocol::WebSocket;
require Hypersonic::Protocol::WebSocket::Frame;
# Generate WebSocket frame encoding functions
Hypersonic::Protocol::WebSocket::Frame->generate_c_code($builder, {
max_connections => $self->{max_connections},
});
# Generate WebSocket connection management
Hypersonic::WebSocket->generate_c_code($builder, {
max_websockets => $self->{max_connections},
});
}
# WebSocket Handler - JIT: connection registry, only when websocket routes exist
if ($analysis->{needs_websocket_handler}) {
require Hypersonic::WebSocket::Handler;
Hypersonic::WebSocket::Handler->generate_c_code($builder, {
max_connections => $self->{max_connections},
});
}
# WebSocket Rooms - JIT: broadcast groups, only when explicitly enabled
if ($analysis->{needs_websocket_rooms}) {
require Hypersonic::WebSocket::Room;
Hypersonic::WebSocket::Room->generate_c_code($builder, {
max_rooms => $self->{max_rooms},
max_clients_per_room => $self->{max_clients_per_room},
});
}
# Future/Pool - JIT: async thread pool for blocking operations
if ($analysis->{needs_async_pool}) {
require Hypersonic::Future;
require Hypersonic::Future::Pool;
my $async_config = $self->{_async_config} // {};
Hypersonic::Future->generate_c_code($builder, {
max_futures => $async_config->{max_futures} // 65536,
});
Hypersonic::Future::Pool->generate_c_code($builder, {
workers => $async_config->{workers} // 8,
queue_size => $async_config->{queue_size} // 4096,
});
}
# need_xs_builder routes - call handlers with fresh builder
if ($analysis->{needs_xs_builder}) {
my @xs_builder_routes;
for my $i (0 .. $#{$self->{routes}}) {
my $route = $self->{routes}[$i];
next unless $route->{need_xs_builder};
lib/Hypersonic.pm view on Meta::CPAN
->raw($xsr->{code})
->blank;
}
}
# Store for function merging later
$self->{_xs_builder_routes} = \@xs_builder_routes;
}
# JIT: WebSocket handler storage (independent of dynamic routes)
if ($analysis->{needs_websocket}) {
$builder->comment('WebSocket handler storage')
->line('static SV* g_websocket_handlers = NULL;')
->blank;
}
# Global storage for dynamic handler dispatch (only if needed)
if ($has_dynamic) {
$builder->comment('Storage for dynamic handler callbacks')
->line('static SV* g_handler_array = NULL;')
->line('static SV* g_server_obj = NULL;');
# JIT: Only generate middleware storage if middleware is present
if ($analysis->{has_any_middleware}) {
$builder->line('static SV* g_before_middleware = NULL;')
->line('static SV* g_after_middleware = NULL;')
->line('static SV* g_route_before_middleware = NULL;')
->line('static SV* g_route_after_middleware = NULL;');
}
$builder->blank;
# Generate param info table for named path parameters
# Structure: { param_name, segment_position } per handler
$builder->comment('Path parameter info per dynamic handler')
->line('typedef struct { const char* name; int position; } ParamInfo;')
->line('typedef struct { int count; ParamInfo params[8]; } RouteParamInfo;')
->blank;
my @route_params = @{$self->{route_param_info} // []};
my $handler_count = scalar @route_params;
$builder->line("static RouteParamInfo g_route_params[$handler_count] = {");
for my $i (0 .. $#route_params) {
my $params = $route_params[$i] // [];
my $count = scalar @$params;
my @param_strs;
for my $p (@$params) {
push @param_strs, qq({ "$p->{name}", $p->{position} });
}
# Pad to 8 elements with {NULL, 0}
my $padding = 8 - scalar(@param_strs);
for (1 .. $padding) {
push @param_strs, '{NULL, 0}';
}
my $params_str = join(', ', @param_strs);
$builder->line(" { $count, { $params_str } },");
}
$builder->line('};')
->blank;
# JIT: Streaming handler flags array (only if streaming is enabled)
if ($analysis->{needs_streaming} && $self->{_streaming_flags}) {
my @flags = @{$self->{_streaming_flags}};
my $flags_str = join(', ', @flags);
$builder->comment('Streaming handler flags - 1 = streaming, 0 = normal')
->line("static int g_streaming_handlers[$handler_count] = { $flags_str };")
->blank;
}
}
# JIT: WebSocket route paths array (only if WebSocket routes exist)
if ($analysis->{needs_websocket} && $self->{_websocket_paths}) {
my @paths = @{$self->{_websocket_paths}};
my $ws_count = scalar @paths;
$builder->comment('WebSocket route paths');
for my $i (0 .. $#paths) {
my $escaped = _escape_c_string($paths[$i]);
$builder->line(qq{static const char WS_PATH_$i\[] = "$escaped";});
}
$builder->line("static const char* g_ws_paths[$ws_count] = {");
for my $i (0 .. $#paths) {
my $comma = ($i < $#paths) ? ',' : '';
$builder->line(" WS_PATH_$i$comma");
}
$builder->line('};')
->line("static const int g_ws_path_count = $ws_count;")
->blank;
}
# Emit FULL pre-computed HTTP responses (headers + body)
for my $i (0 .. $#$full_responses) {
my $resp = $full_responses->[$i];
my $escaped = _escape_c_string($resp);
my $len = length($resp);
$builder->line("static const char RESP_$i\[] = \"$escaped\";")
->line("static const int RESP_${i}_LEN = $len;");
}
$builder->blank;
# 404 response via Protocol module
my $security_hdrs_404 = $self->{enable_security_headers}
? $self->_get_security_headers_string()
: '';
my $resp_404 = $PROTOCOL->build_404_response(
security_headers => $security_hdrs_404,
);
my $escaped_404 = _escape_c_string($resp_404);
$builder->line("static const char RESP_404[] = \"$escaped_404\";")
->line("static const int RESP_404_LEN = " . length($resp_404) . ";")
->blank;
# Security headers constant for dynamic responses
if ($self->{enable_security_headers} && $has_dynamic) {
$builder->raw($self->_gen_security_headers_c_constant())
->blank;
}
# Generate dynamic handler caller if needed
if ($has_dynamic) {
$builder->raw($self->_gen_dynamic_handler_caller())
->blank;
}
# Generate XS builder route dispatcher if needed
if ($analysis->{needs_xs_builder} && $self->{_xs_builder_routes} && @{$self->{_xs_builder_routes}}) {
lib/Hypersonic.pm view on Meta::CPAN
->line('update_connection(fd, now);')
->blank
->line('recv_buf[len] = \'\\0\';')
->blank;
# WebSocket frame handling - JIT: only generate if WebSocket routes exist
if ($analysis->{needs_websocket}) {
$self->_gen_websocket_frame_handler($builder);
}
# Method parser - delegates to Protocol module
$self->_gen_method_parser($builder);
$builder->blank;
# Path parsing - delegates to Protocol module
$PROTOCOL->gen_path_parser($builder);
$builder->blank;
# WebSocket upgrade detection - JIT: only generate if WebSocket routes exist
if ($analysis->{needs_websocket}) {
$self->_gen_websocket_dispatch($builder);
}
# Dispatch
$builder->comment('Dispatch request')
->line('const char* resp;')
->line('int resp_len;')
->line('int handler_idx;')
->line('int dispatch_result = dispatch_request(method, method_len, path, path_len, &resp, &resp_len, &handler_idx);')
->blank;
# Dynamic dispatch
if ($has_dynamic) {
# Check for XS builder routes first (dispatch_result == 2)
if ($analysis->{needs_xs_builder} && $self->{_xs_builder_routes} && @{$self->{_xs_builder_routes}}) {
$builder->if('dispatch_result == 2')
->comment('XS builder route - call generated XS function directly');
# Body parsing for XS builder routes (they may need body access)
$PROTOCOL->gen_body_parser($builder, has_body_access => $has_body_access);
$builder->blank
->line('char* xs_resp;')
->line('int xs_resp_len;')
->line('call_xs_builder_handler(aTHX_ handler_idx, fd, method, method_len, path, path_len, body, body_len, &xs_resp, &xs_resp_len);')
->line('HYPERSONIC_SEND(fd, xs_resp, xs_resp_len);')
->elsif('dispatch_result == 1');
} else {
$builder->if('dispatch_result == 1');
}
$builder->comment('Dynamic route - call Perl handler');
# Body parsing - delegates to Protocol module
$PROTOCOL->gen_body_parser($builder, has_body_access => $has_body_access);
$builder->blank
->line('char* dyn_resp;')
->line('int dyn_resp_len;')
->line('call_dynamic_handler(aTHX_ handler_idx, fd, method, method_len, path, full_path_len, body, body_len, recv_buf, len, &dyn_resp, &dyn_resp_len);')
->comment('dyn_resp_len == -1 means streaming handler (response already sent)')
->line('if (dyn_resp_len >= 0) {')
->line(' HYPERSONIC_SEND(fd, dyn_resp, dyn_resp_len);')
->line('}')
->else
->line('HYPERSONIC_SEND(fd, resp, resp_len);')
->endif;
} else {
$builder->line('HYPERSONIC_SEND(fd, resp, resp_len);');
}
$builder->blank;
# Keep-alive check - delegates to Protocol module
$PROTOCOL->gen_keepalive_check($builder);
$builder->blank
->if('!keep_alive');
# Backend-specific: Remove from event loop on close
$backend->gen_del($builder, 'ev_fd', 'fd');
$builder->line('HYPERSONIC_CLOSE(fd);');
# Reset WebSocket state if WebSocket routes exist
if ($analysis->{needs_websocket}) {
$builder->line('ws_reset(fd);');
}
$builder->line('remove_connection(fd);')
->endif;
# Close event processing
$builder->endif # fd == listen_fd
->endfor # for i
->endwhile # main loop
->blank;
# Async Pool: Shutdown thread pool on server exit
if ($analysis->{needs_async_pool}) {
$builder->comment('Shutdown async thread pool')
->line('pool_shutdown();');
}
$builder->line('close(ev_fd);')
->xs_return('0')
->xs_end;
return $builder;
}
sub _gen_xs_builder_dispatcher {
my ($self) = @_;
my $builder = XS::JIT::Builder->new;
my $xs_routes = $self->{_xs_builder_routes} || [];
return '' unless @$xs_routes;
$builder->comment('XS Builder route dispatcher')
->comment('Dispatches to user-defined XS functions based on handler_idx')
->line('static void call_xs_builder_handler(pTHX_ int handler_idx, int fd,')
->line(' const char* method, int method_len,')
->line(' const char* path, int path_len,')
lib/Hypersonic.pm view on Meta::CPAN
->line(' dSP;')
->line(' SSize_t len = av_len(handlers) + 1;')
->line(' SSize_t i;')
->line(' for (i = 0; i < len; i++) {')
->line(' SV** handler_sv = av_fetch(handlers, i, 0);')
->line(' if (!handler_sv || !SvROK(*handler_sv)) continue;')
->line(' PUSHMARK(SP);')
->line(' XPUSHs(req_ref);')
->line(' PUTBACK;')
->line(' int count = call_sv(*handler_sv, G_SCALAR | G_EVAL);')
->line(' SPAGAIN;')
->line(' if (SvTRUE(ERRSV)) {')
->line(' POPs;')
->line(' continue;')
->line(' }')
->line(' if (count == 1) {')
->line(' SV* result = POPs;')
->line(' PUTBACK;')
->line(' if (SvOK(result)) {')
->line(' return SvREFCNT_inc(result);')
->line(' }')
->line(' }')
->line(' PUTBACK;')
->line(' }')
->line(' return NULL;')
->line('}')
->blank;
}
# Main handler function
$builder->comment('Call dynamic Perl handler and format HTTP response')
->line('static void call_dynamic_handler(pTHX_ int handler_idx, int client_fd,')
->line(' const char* method, int method_len,')
->line(' const char* path, int path_len,')
->line(' const char* body, int body_len,')
->line(' const char* raw_request, int raw_request_len,')
->line(' char** resp_out, int* resp_len_out) {')
->line(' dSP;')
->line(' int count;')
->line(' SV* result;')
->line(' STRLEN len;')
->line(' const char* body_str;')
->line(' int status = 200;')
->line(' const char* content_type = "text/plain";')
->line(' AV* handlers;')
->line(' SV** handler_sv;')
->line(' const char* query_start;')
->line(' int clean_path_len;')
->line(' AV* req;')
->line(' const char* segments[16];')
->line(' int seg_lens[16];')
->line(' int seg_count;')
->line(' AV* seg_av;')
->line(' int i;')
->line(' HV* params_hv;')
->line(' RouteParamInfo* param_info;')
->line(' SV* req_ref;')
->line(' SV* mw_result = NULL;')
->line(' int short_circuit = 0;')
->line(' HV* headers_hv = NULL;')
->line(' int is_streaming = 0;')
->line(' SV* stream_sv = NULL;')
->blank
->line(' if (!g_handler_array) {')
->line(' *resp_out = (char*)RESP_404;')
->line(' *resp_len_out = RESP_404_LEN;')
->line(' return;')
->line(' }')
->blank
->line(' handlers = (AV*)SvRV(g_handler_array);')
->line(' handler_sv = av_fetch(handlers, handler_idx, 0);')
->line(' if (!handler_sv || !SvROK(*handler_sv)) {')
->line(' *resp_out = (char*)RESP_404;')
->line(' *resp_len_out = RESP_404_LEN;')
->line(' return;')
->line(' }')
->blank;
# Query string separation
$builder->comment('Separate path from query string')
->line(' query_start = memchr(path, \'?\', path_len);')
->line(' clean_path_len = query_start ? (query_start - path) : path_len;')
->blank
->comment('Build array-based request object (JIT slots)')
->comment('Slot layout: METHOD=0, PATH=1, BODY=2, PARAMS=3, QUERY=4, QUERY_STRING=5,')
->comment(' HEADERS=6, COOKIES=7, JSON=8, FORM=9, SEGMENTS=10, ID=11')
->line(' req = newAV();')
->line(' av_extend(req, 11);') # Pre-allocate 12 slots (0-11)
->line(' av_store(req, 0, newSVpvn(method, method_len));') # SLOT_METHOD
->line(' av_store(req, 1, newSVpvn(path, clean_path_len));') # SLOT_PATH
->line(' av_store(req, 2, newSVpvn(body, body_len));') # SLOT_BODY
->blank;
# Path segments and params - always needed
$builder->comment('Parse path segments and named params')
->line(' seg_count = parse_path_segments(path, path_len, segments, seg_lens, 16);')
->line(' seg_av = newAV();')
->line(' for (i = 0; i < seg_count; i++) {')
->line(' av_push(seg_av, newSVpvn(segments[i], seg_lens[i]));')
->line(' }')
->line(' av_store(req, 10, newRV_noinc((SV*)seg_av));') # SLOT_SEGMENTS
->blank
->comment('Build named params from route_param_info table')
->line(' params_hv = newHV();')
->line(' param_info = &g_route_params[handler_idx];')
->line(' for (i = 0; i < param_info->count && i < seg_count; i++) {')
->line(' int pos = param_info->params[i].position;')
->line(' if (pos < seg_count) {')
->line(' hv_store(params_hv, param_info->params[i].name,')
->line(' strlen(param_info->params[i].name),')
->line(' newSVpvn(segments[pos], seg_lens[pos]), 0);')
->line(' }')
->line(' }')
->line(' av_store(req, 3, newRV_noinc((SV*)params_hv));') # SLOT_PARAMS
->line(' if (seg_count > 0) {')
->line(' av_store(req, 11, newSVpvn(segments[seg_count-1], seg_lens[seg_count-1]));') # SLOT_ID
->line(' } else {')
->line(' av_store(req, 11, newSVpvn("", 0));') # SLOT_ID (empty)
->line(' }')
->blank;
lib/Hypersonic.pm view on Meta::CPAN
$builder->comment('Bless array into Hypersonic::Request and call Perl handler')
->line(' req_ref = newRV_noinc((SV*)req);')
->line(' sv_bless(req_ref, gv_stashpv("Hypersonic::Request", GV_ADD));')
->line(' ENTER;')
->line(' SAVETMPS;');
# JIT: Add middleware short-circuit variable only if middleware present
# (mw_result and short_circuit already declared at function top)
# JIT: Builder-based before middleware (inline C - no Perl calls)
if ($analysis->{has_builder_before}) {
my $ctx = {
req_var => 'req',
req_ref_var => 'req_ref',
slots => $analysis->{middleware_slots},
};
$builder->blank
->comment('JIT: Builder before middleware (inline C - zero Perl overhead)');
for my $mw (@{$analysis->{builder_before}}) {
if ($mw->can('build_before')) {
$mw->build_before($builder, $ctx);
}
}
}
# JIT: Call global before middleware (Perl coderefs via call_sv)
if ($analysis->{has_global_before}) {
$builder->blank
->comment('JIT: Call global before middleware (Perl)')
->line(' if (g_before_middleware && SvROK(g_before_middleware)) {')
->line(' AV* before_arr = (AV*)SvRV(g_before_middleware);')
->line(' mw_result = call_middleware_chain(aTHX_ before_arr, req_ref);')
->line(' if (mw_result) {')
->line(' result = mw_result;')
->line(' short_circuit = 1;')
->line(' }')
->line(' }');
}
# JIT: Call per-route before middleware
if ($analysis->{has_route_middleware}) {
$builder->blank
->comment('JIT: Call per-route before middleware')
->line(' if (!short_circuit && g_route_before_middleware && SvROK(g_route_before_middleware)) {')
->line(' AV* route_before = (AV*)SvRV(g_route_before_middleware);')
->line(' SV** handler_arr_ref = av_fetch(route_before, handler_idx, 0);')
->line(' if (handler_arr_ref && SvROK(*handler_arr_ref)) {')
->line(' AV* handler_arr = (AV*)SvRV(*handler_arr_ref);')
->line(' if (av_len(handler_arr) >= 0) {')
->line(' mw_result = call_middleware_chain(aTHX_ handler_arr, req_ref);')
->line(' if (mw_result) {')
->line(' result = mw_result;')
->line(' short_circuit = 1;')
->line(' }')
->line(' }')
->line(' }')
->line(' }');
}
# JIT: Streaming handler support
if ($analysis->{needs_streaming}) {
$builder->blank
->comment('JIT: Check if this is a streaming handler')
->line(' is_streaming = g_streaming_handlers[handler_idx];')
->if('is_streaming')
->comment('Create Hypersonic::Stream object for streaming handler')
->line('dSP;')
->line('PUSHMARK(SP);')
->line('XPUSHs(sv_2mortal(newSVpv("Hypersonic::Stream", 0)));')
->line('XPUSHs(sv_2mortal(newSVpv("fd", 0)));')
->line('XPUSHs(sv_2mortal(newSViv(client_fd)));')
->line('PUTBACK;')
->line('int stream_count = call_method("new", G_SCALAR);')
->line('SPAGAIN;')
->if('stream_count > 0')
->line('stream_sv = POPs;')
->line('SvREFCNT_inc(stream_sv);')
->endif
->line('PUTBACK;')
->endif;
}
# Call the main handler (conditionally if middleware present)
if ($analysis->{has_any_middleware}) {
$builder->blank
->comment('Call main handler (unless middleware short-circuited)')
->line(' if (!short_circuit) {')
->line(' PUSHMARK(SP);')
->line(' XPUSHs(req_ref);');
# For streaming handlers with middleware
if ($analysis->{needs_streaming}) {
$builder->line(' if (is_streaming && stream_sv) XPUSHs(stream_sv);');
}
$builder->line(' PUTBACK;')
->line(' count = call_sv(*handler_sv, G_SCALAR | G_EVAL);')
->line(' SPAGAIN;')
->line(' if (count == 1) result = POPs;')
->line(' PUTBACK;')
->line(' }');
} else {
$builder->line(' PUSHMARK(SP);')
->line(' XPUSHs(sv_2mortal(req_ref));');
# For streaming handlers without middleware
if ($analysis->{needs_streaming}) {
$builder->line(' if (is_streaming && stream_sv) XPUSHs(stream_sv);');
}
$builder->line(' PUTBACK;')
->line(' count = call_sv(*handler_sv, G_SCALAR | G_EVAL);')
->line(' SPAGAIN;');
}
# JIT: Call per-route after middleware
if ($analysis->{has_route_middleware}) {
$builder->blank
->comment('JIT: Call per-route after middleware')
->line(' if (g_route_after_middleware && SvROK(g_route_after_middleware)) {')
->line(' AV* route_after = (AV*)SvRV(g_route_after_middleware);')
->line(' SV** handler_arr_ref = av_fetch(route_after, handler_idx, 0);')
->line(' if (handler_arr_ref && SvROK(*handler_arr_ref)) {')
->line(' AV* handler_arr = (AV*)SvRV(*handler_arr_ref);')
->line(' if (av_len(handler_arr) >= 0) {')
->line(' SV* after_result = call_middleware_chain(aTHX_ handler_arr, req_ref);')
->line(' if (after_result) {')
->line(' result = after_result;')
->line(' }')
->line(' }')
->line(' }')
->line(' }');
}
# JIT: Call global after middleware (Perl coderefs via call_sv)
if ($analysis->{has_global_after}) {
$builder->blank
->comment('JIT: Call global after middleware (Perl)')
->line(' if (g_after_middleware && SvROK(g_after_middleware)) {')
->line(' AV* after_arr = (AV*)SvRV(g_after_middleware);')
->line(' SV* after_result = call_middleware_chain(aTHX_ after_arr, req_ref);')
->line(' if (after_result) {')
->line(' result = after_result;')
->line(' }')
->line(' }');
}
# JIT: Builder-based after middleware (inline C - no Perl calls)
if ($analysis->{has_builder_after}) {
my $ctx = {
req_var => 'req',
req_ref_var => 'req_ref',
res_var => 'result',
slots => $analysis->{middleware_slots},
};
$builder->blank
->comment('JIT: Builder after middleware (inline C - zero Perl overhead)');
for my $mw (@{$analysis->{builder_after}}) {
if ($mw->can('build_after')) {
$mw->build_after($builder, $ctx);
}
}
}
# JIT: Streaming handlers - early return (response already sent via Stream)
if ($analysis->{needs_streaming}) {
$builder->blank
->comment('JIT: Streaming handlers return early - response sent via Stream object')
->if('is_streaming')
->line('if (stream_sv) SvREFCNT_dec(stream_sv);')
->line('FREETMPS;')
->line('LEAVE;')
->line('*resp_out = NULL;')
->line('*resp_len_out = -1;')
->comment('Signal streaming response - caller should not send')
->line('return;')
->endif;
}
$builder->blank
->line(' if (SvTRUE(ERRSV)) {')
->line(' static char error_resp[512];')
->line(' int err_len = snprintf(error_resp, sizeof(error_resp),')
->line(' "HTTP/1.1 500 Internal Server Error\\r\\n"')
->line(' "Content-Type: text/plain\\r\\n"')
->line(' "Content-Length: 21\\r\\n"')
->line(' "Connection: close\\r\\n\\r\\n"')
->line(' "Internal Server Error");')
->line(' *resp_out = error_resp;')
->line(' *resp_len_out = err_len;');
# Middleware version already has result set, no-middleware version needs POPs
if ($analysis->{has_any_middleware}) {
$builder->line(' } else if (SvOK(result)) {')
->line(' HV* custom_headers = NULL;');
} else {
$builder->line(' POPs;')
->line(' } else if (count == 1) {')
->line(' result = POPs;')
->line(' if (SvOK(result)) {')
->line(' HV* custom_headers = NULL;');
}
$builder
->comment(' Handle arrayref [status, headers, body]')
->line(' if (SvROK(result) && SvTYPE(SvRV(result)) == SVt_PVAV) {')
->line(' AV* arr = (AV*)SvRV(result);')
->line(' SV** status_sv = av_fetch(arr, 0, 0);')
->line(' SV** headers_sv = av_fetch(arr, 1, 0);')
->line(' SV** body_sv = av_fetch(arr, 2, 0);')
->line(' if (status_sv) status = (int)SvIV(*status_sv);')
->line(' if (body_sv) body_str = SvPV(*body_sv, len);')
->line(' else { body_str = ""; len = 0; }')
->line(' if (headers_sv && SvROK(*headers_sv) && SvTYPE(SvRV(*headers_sv)) == SVt_PVHV) {')
->line(' custom_headers = (HV*)SvRV(*headers_sv);')
->line(' SV** ct_sv = hv_fetch(custom_headers, "Content-Type", 12, 0);')
->line(' if (ct_sv && SvOK(*ct_sv)) {')
->line(' STRLEN ct_len;')
->line(' content_type = SvPV(*ct_sv, ct_len);')
->line(' }')
->line(' }')
->line(' }')
->comment(' Handle hashref {status, headers, body}')
->line(' else if (SvROK(result) && SvTYPE(SvRV(result)) == SVt_PVHV) {')
->line(' HV* hash = (HV*)SvRV(result);')
->line(' SV** status_sv = hv_fetch(hash, "status", 6, 0);')
->line(' SV** headers_sv = hv_fetch(hash, "headers", 7, 0);')
->line(' SV** body_sv = hv_fetch(hash, "body", 4, 0);')
->line(' if (status_sv) status = (int)SvIV(*status_sv);')
->line(' if (body_sv) body_str = SvPV(*body_sv, len);')
->line(' else { body_str = ""; len = 0; }')
->line(' if (headers_sv && SvROK(*headers_sv) && SvTYPE(SvRV(*headers_sv)) == SVt_PVHV) {')
->line(' custom_headers = (HV*)SvRV(*headers_sv);')
->line(' SV** ct_sv = hv_fetch(custom_headers, "Content-Type", 12, 0);')
->line(' if (ct_sv && SvOK(*ct_sv)) {')
lib/Hypersonic.pm view on Meta::CPAN
=item $room->has($ws)
Check if a connection is in the room.
=item $room->count
Get number of connections in the room.
=item $room->count_open
Get number of OPEN (not closing/closed) connections in the room.
=item $room->broadcast($message, $exclude)
Send text message to all room members. Optional C<$exclude> WebSocket
connection to skip (typically the sender).
=item $room->broadcast_binary($data, $exclude)
Send binary data to all room members.
=item $room->close_all($code, $reason)
Close all connections in the room with given code and reason.
=item $room->clear
Remove all connections from the room (without closing them).
=item $room->clients
Get list of all WebSocket connections in the room.
=back
B<Global Broadcast Pattern:>
To broadcast to ALL connected WebSocket clients, use a global room:
my $global = Hypersonic::WebSocket::Room->new('__global__');
$server->websocket('/ws' => sub {
my ($ws) = @_;
$ws->on(open => sub {
$global->join($ws);
$global->broadcast("A user joined! (" . $global->count . " online)");
});
$ws->on(message => sub {
my ($msg) = @_;
$global->broadcast($msg, $ws); # Send to all except sender
});
$ws->on(close => sub {
$global->leave($ws);
$global->broadcast("A user left");
});
});
=head2 streaming
$server->get('/events' => sub {
my ($stream) = @_;
# Send SSE events
my $sse = $stream->sse;
$sse->event(type => 'update', data => 'Hello');
$sse->keepalive;
$sse->close;
}, { streaming => 1 });
Enable streaming responses for a route. The handler receives a
L<Hypersonic::Stream> object instead of returning a static response.
B<Stream Object Methods:>
=over 4
=item $stream->write($data)
Write data to the response (chunked encoding).
=item $stream->end($data)
Write final data and close the stream.
=item $stream->sse
Get an L<Hypersonic::SSE> object for Server-Sent Events.
=back
B<SSE Object Methods:>
=over 4
=item $sse->event(type => $type, data => $data, id => $id)
Send an SSE event with optional type and id.
=item $sse->data($data)
Send a data-only event (no type field).
=item $sse->retry($ms)
Set client reconnection interval in milliseconds.
=item $sse->keepalive
Send a keepalive comment to prevent timeout.
=item $sse->comment($text)
Send an SSE comment.
=item $sse->close
Close the SSE stream.
=back
B<Example - Server-Sent Events:>
$server->get('/notifications' => sub {
my ($stream) = @_;
my $sse = $stream->sse;
$sse->retry(3000); # Reconnect after 3s
$sse->event(
type => 'notification',
data => '{"message":"New update!"}',
id => '12345',
);
# Keep connection alive...
$sse->keepalive;
}, { streaming => 1 });
=head2 Route Handler Options
All route methods accept an optional hashref as the third argument:
$server->get('/path' => sub { ... }, {
dynamic => 1, # Force dynamic handler
parse_query => 1, # Parse query string
parse_headers => 1, # Parse HTTP headers
parse_cookies => 1, # Parse Cookie header
parse_json => 1, # Parse JSON body
parse_form => 1, # Parse form-urlencoded body
before => [\&mw1], # Per-route before middleware
after => [\&mw2], # Per-route after middleware
need_xs_builder => 1, # Generate C code at compile time
});
=head2 need_xs_builder Routes
When C<need_xs_builder =E<gt> 1>, the route handler is called at compile time
with a fresh L<XS::JIT::Builder> object instead of a request object. The handler
must generate C code and return a hashref with the XS function name:
$server->get('/xs/counter' => sub {
my ($builder) = @_;
# Generate C code for this route
$builder->line('static int counter = 0;')
->line('static void handle_counter(pTHX_ int fd,')
->line(' const char* method, int method_len,')
->line(' const char* path, int path_len,')
->line(' const char* body, int body_len,')
->line(' char** resp_out, int* resp_len_out) {')
->line(' counter++;')
->line(' static char response[256];')
->line(' int n = snprintf(response, sizeof(response),')
->line(' "HTTP/1.1 200 OK\\r\\nContent-Type: application/json\\r\\n"')
->line(' "Content-Length: 14\\r\\n\\r\\n{\\"count\\":%d}", counter);')
->line(' *resp_out = response;')
->line(' *resp_len_out = n;')
->line('}');
return { xs_function => 'handle_counter' };
}, { need_xs_builder => 1 });
The XS function signature must match:
void func_name(pTHX_ int fd,
const char* method, int method_len,
const char* path, int path_len,
const char* body, int body_len,
char** resp_out, int* resp_len_out);
The function should write a complete HTTP response to C<*resp_out> and set
C<*resp_len_out> to the response length.
Use with C<c_helpers> to share utility functions:
my $server = Hypersonic->new(
c_helpers => sub {
lib/Hypersonic.pm view on Meta::CPAN
$f->done(@values);
$f->fail($message, $category);
# State checks
$f->is_ready; # True if done, failed, or cancelled
$f->is_done; # True if resolved with values
$f->is_failed; # True if rejected
$f->is_cancelled; # True if cancelled
# Get results
my @values = $f->result; # Returns result values
my ($msg, $cat) = $f->failure; # Returns error info
# Chaining
$f->then(sub { ... })
->catch(sub { ... })
->finally(sub { ... });
# Callbacks
$f->on_done(sub { my @vals = @_; ... });
$f->on_fail(sub { my ($msg, $cat) = @_; ... });
$f->on_ready(sub { ... }); # Called for any completion
# Convergent futures
Hypersonic::Future->needs_all($f1, $f2, $f3); # All must succeed
Hypersonic::Future->needs_any($f1, $f2); # First success wins
Hypersonic::Future->wait_all($f1, $f2, $f3); # Wait for all (success or fail)
Hypersonic::Future->wait_any($f1, $f2); # Wait for first completion
See L<Hypersonic::Future> and L<Hypersonic::Future::Pool> for full documentation.
=head2 compile
$server->compile();
Compile all registered routes into JIT'd native code. This:
=over 4
=item 1. Executes static handlers once to get response strings
=item 2. Analyzes which features each route needs (JIT philosophy)
=item 3. Generates C code with responses as static constants
=item 4. Generates dynamic handler caller with only needed parsing
=item 5. Compiles via XS::JIT
=back
Must be called after all routes are registered, before C<run()>.
=head2 JIT Feature Detection
Hypersonic uses a "JIT philosophy" - only code that's actually needed gets
compiled. The C<compile()> method analyzes your routes and sets these flags:
=over 4
=item needs_streaming
Set when any route has C<streaming =E<gt> 1>. Compiles L<Hypersonic::Stream>
and L<Hypersonic::SSE> XS code.
=item needs_websocket
Set when any C<websocket()> routes are registered. Compiles
L<Hypersonic::WebSocket> and L<Hypersonic::Protocol::WebSocket::Frame> XS code.
=item needs_websocket_handler
Automatically set when C<needs_websocket> is true. Compiles
L<Hypersonic::WebSocket::Handler> for connection registry.
=item needs_websocket_rooms
Set when C<websocket_rooms =E<gt> 1> is passed to C<new()>, or when any
WebSocket route has C<rooms =E<gt> 1> in its options. Compiles
L<Hypersonic::WebSocket::Room> for broadcast groups.
=item has_any_middleware
Set when global C<before()> or C<after()> middleware is registered.
=item has_route_middleware
Set when any route has per-route C<before> or C<after> options.
=item needs_async_pool
Set when C<async_pool()> is called. Compiles L<Hypersonic::Future> and
L<Hypersonic::Future::Pool> for async thread pool operations.
=back
You can inspect these flags after compile:
$server->compile();
my $analysis = $server->{route_analysis};
say "Has streaming: ", $analysis->{needs_streaming} ? "yes" : "no";
say "Has WebSocket: ", $analysis->{needs_websocket} ? "yes" : "no";
say "Has Rooms: ", $analysis->{needs_websocket_rooms} ? "yes" : "no";
=head2 dispatch
my $response = $server->dispatch($request_arrayref);
Dispatch a request and return the response. Primarily for testing.
Request is an arrayref: C<[method, path, body, keep_alive, fd]>
=head2 run
$server->run(port => 8080, workers => 4);
Start the HTTP server event loop.
B<Options:>
=over 4
=item port
Port to listen on. Default: C<8080>
=item workers
Number of worker processes. Default: C<1>
=back
=head1 FULL EXAMPLE
use Hypersonic;
use Hypersonic::Response 'res';
my $server = Hypersonic->new(
max_request_size => 16384,
enable_security_headers => 1,
);
# Global middleware
$server->before(sub {
my ($req) = @_;
# Log request
warn $req->method . ' ' . $req->path . "\n";
return; # Continue
});
# Static route (runs once at compile time)
$server->get('/health' => sub {
'{"status":"ok"}'
});
# Dynamic route with path parameter
$server->get('/users/:id' => sub {
my ($req) = @_;
my $id = $req->param('id');
return res->json({ id => $id, name => "User $id" });
});
( run in 2.116 seconds using v1.01-cache-2.11-cpan-140bd7fdf52 )