Hypersonic
view release on metacpan or search on metacpan
lib/Hypersonic.pm view on Meta::CPAN
use Hypersonic::Protocol::HTTP1;
use Hypersonic::JIT::Util;
# Cache deparser instance for handler analysis (B::Deparse lazy-loaded)
my $DEPARSER;
# Protocol module for HTTP/1.1 (extensible for HTTP/2 in future)
my $PROTOCOL = 'Hypersonic::Protocol::HTTP1';
# Optional TLS support
my $HAS_TLS = 0;
eval { require Hypersonic::TLS; $HAS_TLS = Hypersonic::TLS::check_openssl(); };
# Check for HTTP/2 support (nghttp2)
my $HAS_HTTP2 = 0;
eval { require Hypersonic::Protocol::HTTP2; $HAS_HTTP2 = Hypersonic::Protocol::HTTP2::check_nghttp2() ? 1 : 0; };
sub new {
my ($class, %opts) = @_;
# Validate TLS options
if ($opts{tls}) {
die "TLS support not available (OpenSSL not found)" unless $HAS_TLS;
die "cert_file required for TLS" unless $opts{cert_file};
die "key_file required for TLS" unless $opts{key_file};
die "cert_file not found: $opts{cert_file}" unless -f $opts{cert_file};
die "key_file not found: $opts{key_file}" unless -f $opts{key_file};
}
# Validate HTTP/2 options
if ($opts{http2}) {
die "HTTP/2 requires TLS (set tls => 1)" unless $opts{tls};
die "HTTP/2 not available (nghttp2 not found)" unless $HAS_HTTP2;
}
# Security headers configuration
my $security_headers = $opts{security_headers} // {};
return bless {
routes => [],
compiled => 0,
cache_dir => $opts{cache_dir} // '_hypersonic_cache',
# Legacy fallback id: only used by compile() when Digest::MD5
# isn't installable (extremely rare). The active code path uses
# a content hash of the generated C source so identical server
# configurations share the same JIT cache entry across fresh
# perl processes - see compile() for details.
id => int(rand(100000)),
# Server options
host => $opts{host} // '0.0.0.0',
port => $opts{port} // 8080,
# TLS options
tls => $opts{tls} // 0,
cert_file => $opts{cert_file},
key_file => $opts{key_file},
# HTTP/2 support
http2 => $opts{http2} // 0,
# Security hardening options
max_connections => $opts{max_connections} // 10000,
max_request_size => $opts{max_request_size} // 8192,
keepalive_timeout => $opts{keepalive_timeout} // 30,
recv_timeout => $opts{recv_timeout} // 30,
# WebSocket JIT options - granular control
websocket_rooms => $opts{websocket_rooms} // 0, # Enable Room support
max_rooms => $opts{max_rooms} // 1000,
max_clients_per_room => $opts{max_clients_per_room} // 10000,
# Graceful shutdown
drain_timeout => $opts{drain_timeout} // 5,
# JIT extension points
c_helpers => $opts{c_helpers}, # User C helper functions
# Security headers (JIT optimized - pre-computed at compile time)
security_headers => {
'X-Frame-Options' => $security_headers->{'X-Frame-Options'} // 'DENY',
'X-Content-Type-Options' => $security_headers->{'X-Content-Type-Options'} // 'nosniff',
'X-XSS-Protection' => $security_headers->{'X-XSS-Protection'} // '1; mode=block',
'Referrer-Policy' => $security_headers->{'Referrer-Policy'} // 'strict-origin-when-cross-origin',
'Content-Security-Policy' => $security_headers->{'Content-Security-Policy'}, # User must set this
'Strict-Transport-Security' => ($opts{tls} ? ($security_headers->{'Strict-Transport-Security'} // 'max-age=31536000; includeSubDomains') : undef),
'Permissions-Policy' => $security_headers->{'Permissions-Policy'}, # User can optionally set
},
enable_security_headers => $opts{enable_security_headers} // 1, # Enabled by default
# Middleware support
before_middleware => [], # Global before hooks
after_middleware => [], # Global after hooks
# Event backend (optional override)
event_backend => $opts{event_backend},
}, $class;
}
# Route registration methods
sub get { shift->_add_route('GET', @_) }
sub post { shift->_add_route('POST', @_) }
sub put { shift->_add_route('PUT', @_) }
sub del { shift->_add_route('DELETE', @_) }
sub patch { shift->_add_route('PATCH', @_) }
sub head { shift->_add_route('HEAD', @_) }
sub options { shift->_add_route('OPTIONS', @_) }
# Health check endpoint - built-in route for load balancer / k8s probes
sub health_check {
my ($self, $path, $handler) = @_;
$path //= '/health';
# Default handler returns JSON string (JIT compiled as constant)
$handler //= sub {
return '{"status":"ok"}';
};
return $self->get($path => $handler);
}
# Readiness check endpoint - separate from health for k8s
sub ready_check {
my ($self, $path, $handler) = @_;
$path //= '/ready';
$handler //= sub {
return '{"ready":true}';
};
return $self->get($path => $handler);
lib/Hypersonic.pm view on Meta::CPAN
cache_dir => $self->{cache_dir},
response_helpers => $analysis{needs_response_helpers},
);
# Pre-evaluate all static handlers and build FULL HTTP responses
my @full_responses;
my @dynamic_handlers; # Store CV refs for dynamic routes
my @route_param_info; # Store param info for dynamic routes
for my $i (0 .. $#{$self->{routes}}) {
my $route = $self->{routes}[$i];
if (!$route->{dynamic}) {
# Check if this is a pre-built static file response
if ($route->{_static_file}) {
push @full_responses, $route->{_static_response};
$route->{response_idx} = $#full_responses;
next;
}
# 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};
lib/Hypersonic.pm view on Meta::CPAN
# Common includes - portable across POSIX and Windows. On Windows
# we substitute Winsock for netinet/socket/unistd, and define a
# couple of compatibility macros so the rest of the codegen can
# stay POSIX-shaped.
$builder->line('#include <string.h>')
->line('#include <errno.h>')
->raw(<<'C');
#ifdef _WIN32
# define WIN32_LEAN_AND_MEAN
# include <winsock2.h>
# include <ws2tcpip.h>
# include <io.h>
# define close(fd) closesocket(fd)
/* Windows has no O_NONBLOCK / fcntl(F_SETFL, O_NONBLOCK); use
* ioctlsocket(FIONBIO) instead. Wrap both fcntl invocations the
* codegen emits so they vanish on Windows. */
# define hs_set_nonblocking(fd) do { u_long _m = 1; ioctlsocket((fd), FIONBIO, &_m); } while(0)
/* sockets don't raise SIGPIPE on Windows. */
# ifndef MSG_NOSIGNAL
# define MSG_NOSIGNAL 0
# endif
#else
# include <unistd.h>
# include <fcntl.h>
# include <sys/socket.h>
# include <sys/types.h>
# include <netinet/in.h>
# include <netinet/tcp.h>
# define hs_set_nonblocking(fd) do { int _f = fcntl((fd), F_GETFL, 0); fcntl((fd), F_SETFL, _f | O_NONBLOCK); } while(0)
#endif
C
# Backend-specific includes
$builder->line($backend->includes);
# Add signal and time headers for graceful shutdown
$builder->line('#include <signal.h>')
->line('#include <time.h>');
# Compression support - include zlib if compression is enabled
if ($self->{_compression_enabled}) {
$builder->line('#include <zlib.h>')
->line('#define HYPERSONIC_COMPRESSION 1');
}
# TLS support - include OpenSSL headers if TLS is enabled
if ($self->{tls}) {
$builder->raw(Hypersonic::TLS::gen_includes())
->line('#define HYPERSONIC_TLS 1');
}
# HTTP/2 support - include nghttp2 if enabled
if ($self->{http2}) {
require Hypersonic::Protocol::HTTP2;
Hypersonic::Protocol::HTTP2->gen_includes($builder);
}
# Security hardening configuration
my $max_connections = $self->{max_connections};
my $max_request_size = $self->{max_request_size};
my $keepalive_timeout = $self->{keepalive_timeout};
my $recv_timeout = $self->{recv_timeout};
my $drain_timeout = $self->{drain_timeout};
# Backend-specific defines
$builder->blank
->line($backend->defines)
->line("#define RECV_BUF_SIZE $max_request_size")
->line("#define MAX_CONNECTIONS $max_connections");
# Enable security headers macro if configured
if ($self->{enable_security_headers} && $has_dynamic) {
$builder->line('#define HYPERSONIC_SECURITY_HEADERS 1');
}
$builder
->line("#define KEEPALIVE_TIMEOUT $keepalive_timeout")
->line("#define RECV_TIMEOUT $recv_timeout")
->line("#define DRAIN_TIMEOUT $drain_timeout")
->blank;
# TLS-aware I/O macros - compile-time decision, zero runtime overhead
$builder->comment('TLS-aware I/O wrappers - compile-time branching')
->line('#ifdef HYPERSONIC_TLS')
->line('#define HYPERSONIC_SEND(fd, buf, len) do { \\')
->line(' TLSConnection* _tc = get_tls_connection(fd); \\')
->line(' if (_tc) tls_send(_tc, buf, len); \\')
->line(' else send(fd, buf, len, 0); \\')
->line('} while(0)')
->line('#define HYPERSONIC_CLOSE(fd) tls_close(fd)')
->line('#else')
->line('#define HYPERSONIC_SEND(fd, buf, len) send(fd, buf, len, 0)')
->line('#define HYPERSONIC_CLOSE(fd) close(fd)')
->line('#endif')
->blank;
# User-defined C helpers (early, so they're available to all routes)
if (my $helpers = $self->{c_helpers}) {
$builder->comment('User-defined C helpers');
if (ref $helpers eq 'CODE') {
my $helper_builder = XS::JIT::Builder->new;
$helpers->($helper_builder);
$builder->raw($helper_builder->code);
} else {
# Raw C string
$builder->raw($helpers);
}
$builder->blank;
}
# Graceful shutdown support
$builder->comment('Graceful shutdown support')
->line('static volatile sig_atomic_t g_shutdown = 0;')
->line('static volatile int g_active_connections = 0;')
->blank
->line('static void handle_shutdown_signal(int sig) {')
->line(' (void)sig;')
->line(' g_shutdown = 1;')
->line('}')
->blank;
# Compression support - JIT compiled zlib functions
if ($self->{_compression_enabled}) {
my $config = $self->{_compression_config};
my $min_size = $config->{min_size} // 1024;
my $level = $config->{level} // 6;
$builder->comment('Gzip compression support - JIT compiled')
->line('static __thread unsigned char gzip_out_buf[131072];')
->blank
->line('static int accepts_gzip(const char* accept_encoding, size_t len) {')
->line(' if (!accept_encoding || len == 0) return 0;')
->line(' const char* p = accept_encoding;')
->line(' const char* end = accept_encoding + len;')
->line(' while (p < end - 3) {')
->line(' if (p[0] == \'g\' && p[1] == \'z\' && p[2] == \'i\' && p[3] == \'p\') return 1;')
->line(' p++;')
->line(' }')
->line(' return 0;')
->line('}')
->blank
->line('static size_t gzip_compress(const char* input, size_t input_len, unsigned char** output) {')
->line(' size_t max_out;')
->line(' z_stream strm;')
->line(' int ret;')
->line(' size_t compressed_len;')
->line(" if (input_len < $min_size) return 0;")
->line(' max_out = compressBound(input_len) + 18;')
->line(' if (max_out > sizeof(gzip_out_buf)) return 0;')
->line(' memset(&strm, 0, sizeof(strm));')
->line(" if (deflateInit2(&strm, $level, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) return 0;")
->line(' strm.next_in = (Bytef*)input;')
->line(' strm.avail_in = input_len;')
->line(' strm.next_out = gzip_out_buf;')
->line(' strm.avail_out = sizeof(gzip_out_buf);')
->line(' ret = deflate(&strm, Z_FINISH);')
->line(' compressed_len = strm.total_out;')
->line(' deflateEnd(&strm);')
->line(' if (ret != Z_STREAM_END || compressed_len >= input_len) return 0;')
->line(' *output = gzip_out_buf;')
->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},
lib/Hypersonic.pm view on Meta::CPAN
->line('signal(SIGTERM, handle_shutdown_signal);')
->line('signal(SIGINT, handle_shutdown_signal);')
->blank
->comment('Initialize connection tracking')
->line('memset(g_conn_time, 0, sizeof(g_conn_time));')
->line('g_active_connections = 0;')
->blank;
# TLS initialization
if ($self->{tls}) {
my $cert_file = _escape_c_string($self->{cert_file});
my $key_file = _escape_c_string($self->{key_file});
$builder->comment('Initialize TLS/HTTPS')
->line('#ifdef HYPERSONIC_TLS')
->line("if (init_ssl_ctx(\"$cert_file\", \"$key_file\") != 0) {")
->line(' croak("Failed to initialize TLS context - check cert/key files");')
->line('}')
->line('memset(g_tls_connections, 0, sizeof(g_tls_connections));')
->line('#endif')
->blank;
}
$builder->comment('Thread-local receive buffer - each worker gets its own')
->line('static __thread char recv_buf[RECV_BUF_SIZE];')
->blank;
# Backend-specific: Create event loop and add listen socket
$backend->gen_create($builder, 'listen_fd');
# Async Pool: Initialize thread pool and add notify fd to event loop
if ($analysis->{needs_async_pool}) {
$builder->blank
->comment('Initialize async thread pool')
->line('pool_init();')
->line('int pool_notify_fd = pool_get_notify_fd();');
$backend->gen_add_pool_notify($builder, 'ev_fd', 'pool_notify_fd');
}
# Declare event structure variable based on backend
if ($backend_name eq 'kqueue') {
# kqueue's gen_create already declares 'ev', but we need events array
$builder->line('struct kevent events[MAX_EVENTS];');
} elsif ($backend_name eq 'epoll') {
# epoll's gen_create already declares 'ev'
$builder->line('struct epoll_event events[MAX_EVENTS];');
} elsif ($backend_name eq 'io_uring') {
# io_uring uses completion queue entries
$builder->line('struct io_uring_cqe** events = NULL;');
} else {
# poll/select manage their own fd arrays internally
# Declare a dummy events pointer to satisfy gen_wait signature
$builder->line('void* events = NULL;');
}
$builder->line('time_t last_cleanup = time(NULL);')
->line('int accepting = 1; /* Flag to control accepting new connections */')
->blank;
# Main event loop
$builder->while('!g_shutdown || g_active_connections > 0')
->comment('Use timeout for keep-alive cleanup and shutdown check');
# Backend-specific: Wait for events
$backend->gen_wait($builder, 'ev_fd', 'events', 'n', '1000');
$builder->blank
->comment('Check for graceful shutdown - stop accepting new connections')
->if('g_shutdown && accepting');
# Backend-specific: Remove listen socket from event loop
$backend->gen_del($builder, 'ev_fd', 'listen_fd');
$builder->line('accepting = 0;')
->endif
->blank
->comment('Get time once per event batch')
->line('time_t now = time(NULL);')
->line('g_current_time = now;')
->blank;
# Keep-alive cleanup
$builder->comment('Periodic keep-alive timeout cleanup')
->if('now - last_cleanup >= 5')
->declare('int', 'cleanup_i', '0')
->for('cleanup_i = 0', 'cleanup_i < MAX_FD', 'cleanup_i++')
->if('g_conn_time[cleanup_i] > 0')
->if('now - g_conn_time[cleanup_i] > KEEPALIVE_TIMEOUT')
->comment('Close idle connection')
->line('int idle_fd = cleanup_i;');
# Backend-specific: Remove idle connection
$backend->gen_del($builder, 'ev_fd', 'idle_fd');
$builder->line('HYPERSONIC_CLOSE(idle_fd);')
->line('remove_connection(idle_fd);')
->endif
->endif
->endfor
->line('last_cleanup = now;')
->endif
->blank;
# Event processing loop
$builder->declare('int', 'i', '0')
->for('i = 0', 'i < n', 'i++');
# Backend-specific: Get fd from event
$backend->gen_get_fd($builder, 'events', 'i', 'fd');
$builder->blank
->if('fd == listen_fd && accepting');
# Accept loop
$builder->comment('Accept new connections with limit check')
->while('1')
->comment('Check connection limit before accepting')
->if('g_active_connections >= MAX_CONNECTIONS')
->line('break; /* At capacity, stop accepting */')
->endif
->blank
->line('struct sockaddr_in client_addr;')
->line('socklen_t client_len = sizeof(client_addr);')
->line('int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);')
->if('client_fd < 0')
->line('break;')
->endif
->blank
->comment('Set non-blocking (hs_set_nonblocking handles Win/POSIX divergence)')
->line('hs_set_nonblocking(client_fd);')
->blank
->comment('Disable Nagle')
->line('int one = 1;')
->line('setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, (const char*)&one, sizeof(one));')
->blank
->comment('Set receive timeout for security')
->line('struct timeval tv;')
->line('tv.tv_sec = RECV_TIMEOUT;')
->line('tv.tv_usec = 0;')
->line('setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));')
->blank
->comment('Track connection for keep-alive timeout')
->line('track_connection(client_fd, now);')
->blank
->comment('TLS handshake if enabled')
->line('#ifdef HYPERSONIC_TLS')
->line('if (tls_accept(client_fd) < 0) {')
->line(' close(client_fd);')
->line(' remove_connection(client_fd);')
->line(' continue;')
->line('}')
->line('#endif')
->blank
->comment('Add to event loop');
# Backend-specific: Add client to event loop
$backend->gen_add($builder, 'ev_fd', 'client_fd');
$builder->endwhile;
# Async Pool: Handle pool notify fd - process completed futures
if ($analysis->{needs_async_pool}) {
$builder->elsif('fd == pool_notify_fd')
->comment('Thread pool notification - process completed async operations')
->line('pool_process_ready();');
}
# Handle client request
$builder->elsif('fd != listen_fd')
->comment('Handle client request')
->line('#ifdef HYPERSONIC_TLS')
->line('TLSConnection* tls_conn = get_tls_connection(fd);')
->line('ssize_t len = tls_conn ? tls_recv(tls_conn, recv_buf, RECV_BUF_SIZE - 1) : -1;')
->line('#else')
->line('ssize_t len = recv(fd, recv_buf, RECV_BUF_SIZE - 1, 0);')
->line('#endif')
->blank
->if('len <= 0')
->comment('Connection closed or error');
# Backend-specific: Remove from event loop
$backend->gen_del($builder, 'ev_fd', 'fd');
$builder->line('#ifdef HYPERSONIC_TLS')
->line('tls_close(fd);')
->line('#else')
->line('close(fd);')
->line('#endif');
# Reset WebSocket state if WebSocket routes exist
if ($analysis->{needs_websocket}) {
$builder->line('ws_reset(fd);');
}
$builder->line('remove_connection(fd);')
->line('continue;')
->endif
->blank
->comment('Update connection activity for keep-alive timeout')
->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,')
->line(' const char* body, int body_len,')
->line(' char** resp_out, int* resp_len_out) {')
->line(' switch (handler_idx) {');
for my $entry (@$xs_routes) {
my $handler_idx = $entry->{route}{handler_idx};
my $xs_func = $entry->{result}{xs_function};
$builder->line(" case $handler_idx:")
->line(" $xs_func(aTHX_ fd, method, method_len, path, path_len, body, body_len, resp_out, resp_len_out);")
->line(" break;");
}
$builder->line(' default:')
->line(' *resp_out = (char*)RESP_404;')
lib/Hypersonic.pm view on Meta::CPAN
->line(' }')
->line(' }')
->comment(' Plain string response')
->line(' else {')
->line(' body_str = SvPV(result, len);')
->line(' }')
->blank
->comment(' Auto-detect JSON content type')
->line(' if (strcmp(content_type, "text/plain") == 0 && len > 0 &&')
->line(' (body_str[0] == \'{\' || body_str[0] == \'[\')) {')
->line(' content_type = "application/json";')
->line(' }')
->blank;
# JIT: Add compression logic only if compression is enabled
if ($self->{_compression_enabled}) {
my $config = $self->{_compression_config};
my $min_size = $config->{min_size} // 1024;
$builder
->comment(' Gzip compression - check Accept-Encoding')
->line('#ifdef HYPERSONIC_COMPRESSION')
->line(' int use_gzip = 0;')
->line(' unsigned char* compressed_body = NULL;')
->line(' size_t compressed_len = 0;')
->blank
->comment(' Get Accept-Encoding from request (from SLOT_HEADERS)')
->line(' SV** req_arr = AvARRAY(req);')
->line(' HV* hdrs = NULL;')
->line(' if (req_arr[6] && SvROK(req_arr[6])) {')
->line(' hdrs = (HV*)SvRV(req_arr[6]);')
->line(' }')
->line(' if (hdrs && len >= ' . $min_size . ') {')
->line(' SV** ae = hv_fetch(hdrs, "accept_encoding", 15, 0);')
->line(' if (ae && SvOK(*ae)) {')
->line(' STRLEN ae_len;')
->line(' const char* ae_str = SvPV(*ae, ae_len);')
->line(' if (accepts_gzip(ae_str, ae_len)) {')
->line(' compressed_len = gzip_compress(body_str, len, &compressed_body);')
->line(' if (compressed_len > 0) {')
->line(' use_gzip = 1;')
->line(' body_str = (const char*)compressed_body;')
->line(' len = compressed_len;')
->line(' }')
->line(' }')
->line(' }')
->line(' }')
->line('#endif')
->blank;
}
$builder
->comment(' Build response with custom headers support')
->line(' static __thread char resp_buf[65536];')
->line(' int hdr_len;')
->line('#ifdef HYPERSONIC_SECURITY_HEADERS')
->line(' hdr_len = snprintf(resp_buf, 2048,')
->line(' "HTTP/1.1 %d %s\\r\\n"')
->line(' "Content-Type: %s\\r\\n"')
->line(' "Content-Length: %zu\\r\\n"')
->line(' "Connection: keep-alive\\r\\n"')
->line(' "%s",')
->line(' status, get_status_text(status), content_type, len, SECURITY_HEADERS);')
->line('#else')
->line(' hdr_len = snprintf(resp_buf, 512,')
->line(' "HTTP/1.1 %d %s\\r\\n"')
->line(' "Content-Type: %s\\r\\n"')
->line(' "Content-Length: %zu\\r\\n"')
->line(' "Connection: keep-alive\\r\\n",')
->line(' status, get_status_text(status), content_type, len);')
->line('#endif')
->blank
->comment(' Add custom headers from response (Location, Set-Cookie, etc.)')
->line(' if (custom_headers) {')
->line(' HE* entry;')
->line(' hv_iterinit(custom_headers);')
->line(' while ((entry = hv_iternext(custom_headers))) {')
->line(' I32 klen;')
->line(' const char* key = hv_iterkey(entry, &klen);')
->comment(' Skip Content-Type/Content-Length (already added)')
->line(' if (klen == 12 && memcmp(key, "Content-Type", 12) == 0) continue;')
->line(' if (klen == 14 && memcmp(key, "Content-Length", 14) == 0) continue;')
->line(' SV* val = hv_iterval(custom_headers, entry);')
->comment(' Handle Set-Cookie array (multiple cookies)')
->line(' if (SvROK(val) && SvTYPE(SvRV(val)) == SVt_PVAV) {')
->line(' AV* arr = (AV*)SvRV(val);')
->line(' SSize_t arr_len = av_len(arr) + 1;')
->line(' SSize_t j;')
->line(' for (j = 0; j < arr_len; j++) {')
->line(' SV** item = av_fetch(arr, j, 0);')
->line(' if (item && SvOK(*item)) {')
->line(' STRLEN vlen;')
->line(' const char* vstr = SvPV(*item, vlen);')
->line(' hdr_len += snprintf(resp_buf + hdr_len, sizeof(resp_buf) - hdr_len,')
->line(' "%.*s: %.*s\\r\\n", (int)klen, key, (int)vlen, vstr);')
->line(' }')
->line(' }')
->line(' } else if (SvOK(val)) {')
->line(' STRLEN vlen;')
->line(' const char* vstr = SvPV(val, vlen);')
->line(' hdr_len += snprintf(resp_buf + hdr_len, sizeof(resp_buf) - hdr_len,')
->line(' "%.*s: %.*s\\r\\n", (int)klen, key, (int)vlen, vstr);')
->line(' }')
->line(' }')
->line(' }')
->blank
->comment(' Add Content-Encoding header if gzip was used')
->line('#ifdef HYPERSONIC_COMPRESSION')
->line(' if (use_gzip) {')
->line(' hdr_len += snprintf(resp_buf + hdr_len, sizeof(resp_buf) - hdr_len,')
->line(' "Content-Encoding: gzip\\r\\n");')
->line(' }')
->line('#endif')
->blank
->comment(' End headers')
->line(' memcpy(resp_buf + hdr_len, "\\r\\n", 2);')
->line(' hdr_len += 2;')
->blank
->line(' if (hdr_len + len < sizeof(resp_buf)) {')
->line(' memcpy(resp_buf + hdr_len, body_str, len);')
->line(' *resp_out = resp_buf;')
->line(' *resp_len_out = hdr_len + (int)len;')
->line(' } else {')
->line(' *resp_out = (char*)RESP_404;')
->line(' *resp_len_out = RESP_404_LEN;')
->line(' }');
# Different closing braces based on middleware presence
if ($analysis->{has_any_middleware}) {
lib/Hypersonic.pm view on Meta::CPAN
my $max_age = $config->{max_age};
my $gen_etag = $config->{etag};
# Recursively find all files
File::Find::find({
no_chdir => 1,
wanted => sub {
return unless -f $_;
my $file_path = $_;
my $rel_path = $file_path;
$rel_path =~ s{^\Q$dir\E/?}{};
# URL path for this file
my $url_path = "$prefix/$rel_path";
$url_path =~ s{//+}{/}g;
# Read file content
open my $fh, '<:raw', $file_path or return;
local $/;
my $content = <$fh>;
close $fh;
# Get MIME type
my $mime = _get_mime_type($file_path);
# Generate ETag (MD5 of content)
my $etag = '';
if ($gen_etag) {
$etag = Digest::MD5::md5_hex($content);
}
# Store file info
push @static_files, {
url_path => $url_path,
content => $content,
mime => $mime,
etag => $etag,
max_age => $max_age,
length => length($content),
};
},
}, $dir);
}
# Store for code generation
$self->{_static_files} = \@static_files;
# Create static routes - these are essentially pre-computed responses
for my $file (@static_files) {
my $url_path = $file->{url_path};
my $content = $file->{content};
my $mime = $file->{mime};
my $etag = $file->{etag};
my $max_age = $file->{max_age};
my $len = $file->{length};
# Build complete HTTP response at compile time
my $response = "HTTP/1.1 200 OK\r\n"
. "Content-Type: $mime\r\n"
. "Content-Length: $len\r\n"
. "Connection: keep-alive\r\n";
$response .= "Cache-Control: public, max-age=$max_age\r\n" if $max_age;
$response .= "ETag: \"$etag\"\r\n" if $etag;
# Add security headers
if ($self->{enable_security_headers}) {
$response .= $self->_get_security_headers_string();
}
$response .= "\r\n" . $content;
# Store as static route
push @{$self->{routes}}, {
method => 'GET',
path => $url_path,
handler => sub { $content }, # Dummy handler for static
dynamic => 0,
params => [],
segments => [split('/', $url_path)],
features => {},
before => [],
after => [],
# Mark as static file with pre-built response
_static_response => $response,
_static_file => 1,
};
}
}
# Generate security headers string for HTTP responses
# Pre-computed at compile time - zero runtime overhead
sub _get_security_headers_string {
my ($self) = @_;
my $headers = '';
for my $name (sort keys %{$self->{security_headers}}) {
my $value = $self->{security_headers}{$name};
next unless defined $value && length($value);
$headers .= "$name: $value\r\n";
}
return $headers;
}
# Generate security headers as C string constant for dynamic routes
sub _gen_security_headers_c_constant {
my ($self) = @_;
return '' unless $self->{enable_security_headers};
my $headers = $self->_get_security_headers_string();
return '' unless length($headers);
my $escaped = _escape_c_string($headers);
return "static const char SECURITY_HEADERS[] = \"$escaped\";\n"
. "static const int SECURITY_HEADERS_LEN = " . length($headers) . ";\n";
}
sub dispatch {
my ($self, $req) = @_;
die "Must call compile() first" unless $self->{compiled};
return $self->{dispatch_fn}->($req);
lib/Hypersonic.pm view on Meta::CPAN
Hypersonic is a benchmark-focused micro HTTP server that uses XS::JIT to
generate and compile C code at runtime. The entire event loop runs in C
with no Perl in the hot path.
B<What it does:>
=over 4
=item 1. Static route handlers run ONCE at C<compile()> time
=item 2. Response strings (including HTTP headers) are baked into C as static constants
=item 3. Dynamic routes run Perl handlers per-request with JIT-compiled request objects
=item 4. A pure C event loop (kqueue/epoll) is generated and compiled
=item 5. Security headers are pre-computed and baked into responses at compile time
=item 6. Optional TLS/HTTPS support via OpenSSL with JIT-compiled wrappers
=back
B<Performance:> ~290,000 requests/second on a single core (macOS/kqueue).
=head1 METHODS
=head2 new
my $server = Hypersonic->new(%options);
Create a new Hypersonic server instance.
B<Options:>
=over 4
=item cache_dir
Directory for caching compiled XS modules. Default: C<_hypersonic_cache>
=item tls
Enable TLS/HTTPS support. Requires C<cert_file> and C<key_file>. Default: C<0>
=item cert_file
Path to TLS certificate file (PEM format). Required if C<tls> is enabled.
=item key_file
Path to TLS private key file (PEM format). Required if C<tls> is enabled.
=item max_connections
Maximum number of concurrent connections. Default: C<10000>
=item max_request_size
Maximum request size in bytes. Default: C<8192>
=item keepalive_timeout
Keep-alive connection timeout in seconds. Default: C<30>
=item recv_timeout
Receive timeout in seconds. Default: C<30>
=item drain_timeout
Graceful shutdown drain timeout in seconds. Default: C<5>
=item enable_security_headers
Enable security headers (X-Frame-Options, X-Content-Type-Options, etc.). Default: C<1>
=item security_headers
HashRef of custom security header values. Example:
security_headers => {
'X-Frame-Options' => 'SAMEORIGIN',
'Content-Security-Policy' => "default-src 'self'",
}
=item websocket_rooms
Enable WebSocket Room support for broadcast groups. Default: C<0>
Only set to C<1> if you need L<Hypersonic::WebSocket::Room> for broadcasting
to groups of connections. This adds Room-specific XS code to your compiled server.
=item max_rooms
Maximum number of rooms when C<websocket_rooms> is enabled. Default: C<1000>
=item max_clients_per_room
Maximum clients per room when C<websocket_rooms> is enabled. Default: C<10000>
=item c_helpers
C helper functions to inject early in the generated code, making them available
to all routes. Can be a coderef that receives an L<XS::JIT::Builder>, or a raw
C string.
# Using a coderef
c_helpers => sub {
my ($builder) = @_;
$builder->line('static int double_value(int x) { return x * 2; }');
}
# Or raw C string
c_helpers => 'static int double_value(int x) { return x * 2; }'
These helpers are available to C<need_xs_builder> routes.
=back
B<Example with TLS:>
my $server = Hypersonic->new(
tls => 1,
lib/Hypersonic.pm view on Meta::CPAN
=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(
lib/Hypersonic.pm view on Meta::CPAN
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" });
});
# POST with JSON body
$server->post('/users' => sub {
my ($req) = @_;
my $data = $req->json;
return res->status(201)->json({ created => $data->{name} });
}, { parse_json => 1 });
# Query parameters
$server->get('/search' => sub {
( run in 0.538 second using v1.01-cache-2.11-cpan-56fb94df46f )