File Coverage

blib/lib/Hypersonic.pm
Criterion Covered Total %
statement 589 960 61.3
branch 207 414 50.0
condition 91 264 34.4
subroutine 38 59 64.4
pod 18 21 85.7
total 943 1718 54.8


line stmt bran cond sub pod time code
1             package Hypersonic;
2              
3 29     29   4043329 use strict;
  29         52  
  29         1016  
4 29     29   146 use warnings;
  29         41  
  29         1434  
5 29     29   504 use 5.010;
  29         91  
6              
7             our $VERSION = '0.19';
8              
9 29     29   136 use Scalar::Util qw(blessed);
  29         70  
  29         1616  
10 29     29   195 use Config ();
  29         93  
  29         563  
11 29     29   11777 use XS::JIT;
  29         25560  
  29         1047  
12 29     29   15357 use XS::JIT::Builder;
  29         37475  
  29         1540  
13 29     29   14042 use Hypersonic::Socket;
  29         75  
  29         168  
14 29     29   14052 use Hypersonic::Protocol::HTTP1;
  29         85  
  29         945  
15 29     29   150 use Hypersonic::JIT::Util;
  29         41  
  29         138081  
16              
17             # Cache deparser instance for handler analysis (B::Deparse lazy-loaded)
18             my $DEPARSER;
19              
20             # Protocol module for HTTP/1.1 (extensible for HTTP/2 in future)
21             my $PROTOCOL = 'Hypersonic::Protocol::HTTP1';
22              
23             # Optional TLS support
24             my $HAS_TLS = 0;
25             eval { require Hypersonic::TLS; $HAS_TLS = Hypersonic::TLS::check_openssl(); };
26              
27             # Check for HTTP/2 support (nghttp2)
28             my $HAS_HTTP2 = 0;
29             eval { require Hypersonic::Protocol::HTTP2; $HAS_HTTP2 = Hypersonic::Protocol::HTTP2::check_nghttp2() ? 1 : 0; };
30              
31             sub new {
32 42     42 1 948782 my ($class, %opts) = @_;
33            
34             # Validate TLS options
35 42 50       279 if ($opts{tls}) {
36 0 0       0 die "TLS support not available (OpenSSL not found)" unless $HAS_TLS;
37 0 0       0 die "cert_file required for TLS" unless $opts{cert_file};
38 0 0       0 die "key_file required for TLS" unless $opts{key_file};
39 0 0       0 die "cert_file not found: $opts{cert_file}" unless -f $opts{cert_file};
40 0 0       0 die "key_file not found: $opts{key_file}" unless -f $opts{key_file};
41             }
42            
43             # Validate HTTP/2 options
44 42 50       174 if ($opts{http2}) {
45 0 0       0 die "HTTP/2 requires TLS (set tls => 1)" unless $opts{tls};
46 0 0       0 die "HTTP/2 not available (nghttp2 not found)" unless $HAS_HTTP2;
47             }
48            
49             # Security headers configuration
50 42   100     283 my $security_headers = $opts{security_headers} // {};
51            
52             return bless {
53             routes => [],
54             compiled => 0,
55             cache_dir => $opts{cache_dir} // '_hypersonic_cache',
56             # Legacy fallback id: only used by compile() when Digest::MD5
57             # isn't installable (extremely rare). The active code path uses
58             # a content hash of the generated C source so identical server
59             # configurations share the same JIT cache entry across fresh
60             # perl processes - see compile() for details.
61             id => int(rand(100000)),
62             # Server options
63             host => $opts{host} // '0.0.0.0',
64             port => $opts{port} // 8080,
65             # TLS options
66             tls => $opts{tls} // 0,
67             cert_file => $opts{cert_file},
68             key_file => $opts{key_file},
69             # HTTP/2 support
70             http2 => $opts{http2} // 0,
71             # Security hardening options
72             max_connections => $opts{max_connections} // 10000,
73             max_request_size => $opts{max_request_size} // 8192,
74             keepalive_timeout => $opts{keepalive_timeout} // 30,
75             recv_timeout => $opts{recv_timeout} // 30,
76             # WebSocket JIT options - granular control
77             websocket_rooms => $opts{websocket_rooms} // 0, # Enable Room support
78             max_rooms => $opts{max_rooms} // 1000,
79             max_clients_per_room => $opts{max_clients_per_room} // 10000,
80             # Graceful shutdown
81             drain_timeout => $opts{drain_timeout} // 5,
82             # JIT extension points
83             c_helpers => $opts{c_helpers}, # User C helper functions
84             # Security headers (JIT optimized - pre-computed at compile time)
85             security_headers => {
86             'X-Frame-Options' => $security_headers->{'X-Frame-Options'} // 'DENY',
87             'X-Content-Type-Options' => $security_headers->{'X-Content-Type-Options'} // 'nosniff',
88             'X-XSS-Protection' => $security_headers->{'X-XSS-Protection'} // '1; mode=block',
89             'Referrer-Policy' => $security_headers->{'Referrer-Policy'} // 'strict-origin-when-cross-origin',
90             'Content-Security-Policy' => $security_headers->{'Content-Security-Policy'}, # User must set this
91             'Strict-Transport-Security' => ($opts{tls} ? ($security_headers->{'Strict-Transport-Security'} // 'max-age=31536000; includeSubDomains') : undef),
92             'Permissions-Policy' => $security_headers->{'Permissions-Policy'}, # User can optionally set
93             },
94             enable_security_headers => $opts{enable_security_headers} // 1, # Enabled by default
95             # Middleware support
96             before_middleware => [], # Global before hooks
97             after_middleware => [], # Global after hooks
98             # Event backend (optional override)
99             event_backend => $opts{event_backend},
100 42 50 100     3576 }, $class;
      50        
      100        
      50        
      50        
      100        
      100        
      100        
      100        
      50        
      50        
      50        
      100        
      100        
      50        
      50        
      50        
      0        
      100        
101             }
102              
103             # Route registration methods
104 42     42 1 603 sub get { shift->_add_route('GET', @_) }
105 5     5 1 34 sub post { shift->_add_route('POST', @_) }
106 1     1 1 9 sub put { shift->_add_route('PUT', @_) }
107 1     1 1 6 sub del { shift->_add_route('DELETE', @_) }
108 0     0 1 0 sub patch { shift->_add_route('PATCH', @_) }
109 0     0 1 0 sub head { shift->_add_route('HEAD', @_) }
110 0     0 1 0 sub options { shift->_add_route('OPTIONS', @_) }
111              
112             # Health check endpoint - built-in route for load balancer / k8s probes
113             sub health_check {
114 1     1 0 897 my ($self, $path, $handler) = @_;
115 1   50     17 $path //= '/health';
116            
117             # Default handler returns JSON string (JIT compiled as constant)
118             $handler //= sub {
119 0     0   0 return '{"status":"ok"}';
120 1   33     18 };
121            
122 1         11 return $self->get($path => $handler);
123             }
124              
125             # Readiness check endpoint - separate from health for k8s
126             sub ready_check {
127 1     1 0 510 my ($self, $path, $handler) = @_;
128 1   50     10 $path //= '/ready';
129            
130             $handler //= sub {
131 0     0   0 return '{"ready":true}';
132 1   33     11 };
133            
134 1         4 return $self->get($path => $handler);
135             }
136              
137             # WebSocket route registration
138             sub websocket {
139 4     4 1 62 my ($self, $path, $handler) = @_;
140            
141 4 50       19 die "WebSocket path must start with /" unless $path =~ m{^/};
142 4 50       12 die "WebSocket handler must be a coderef" unless ref($handler) eq 'CODE';
143            
144 4   100     6 push @{$self->{websocket_routes} //= []}, {
  4         36  
145             path => $path,
146             handler => $handler,
147             pattern => $self->_compile_path_pattern($path),
148             };
149            
150 4         10 return $self;
151             }
152              
153             # Check if any WebSocket routes are registered
154             sub _has_websocket_routes {
155 15     15   36 my ($self) = @_;
156 15   100     24 return scalar @{$self->{websocket_routes} // []};
  15         113  
157             }
158              
159             # Match a path against WebSocket routes
160             sub _match_websocket_route {
161 3     3   2513 my ($self, $path) = @_;
162            
163 3   50     8 for my $route (@{$self->{websocket_routes} // []}) {
  3         13  
164 5         9 my $pattern = $route->{pattern};
165 5 100       37 if ($path =~ $pattern) {
166             # Extract params
167 2         3 my %params;
168 2         12 my @captures = ($path =~ $pattern);
169 2         20 my @param_names = $route->{path} =~ /:(\w+)/g;
170 2         11 for my $i (0..$#param_names) {
171 1 50       8 $params{$param_names[$i]} = $captures[$i] if defined $captures[$i];
172             }
173 2         12 return ($route->{handler}, \%params);
174             }
175             }
176 1         6 return;
177             }
178              
179             # Compile path pattern (reuse for HTTP routes)
180             sub _compile_path_pattern {
181 6     6   1431 my ($self, $path) = @_;
182            
183 6         9 my $pattern = $path;
184 6         25 $pattern =~ s{:(\w+)}{([^/]+)}g; # :param -> capture group
185 6         13 $pattern =~ s{\*}{(.+)}g; # * -> greedy capture
186 6         210 return qr{^$pattern$};
187             }
188              
189             # Static file serving - JIT-compiled for maximum performance
190             # Files are read at compile time and baked into C constants
191             sub static {
192 0     0 1 0 my ($self, $url_prefix, $directory, $opts) = @_;
193            
194 0   0     0 $opts //= {};
195 0 0       0 die "URL prefix must start with /" unless $url_prefix =~ m{^/};
196 0 0       0 die "Directory does not exist: $directory" unless -d $directory;
197            
198             # Normalize paths
199 0         0 $url_prefix =~ s{/$}{}; # Remove trailing slash
200 0         0 $directory =~ s{/$}{};
201            
202             # Store static config for compile-time processing
203 0   0     0 push @{$self->{static_dirs} //= []}, {
204             prefix => $url_prefix,
205             directory => $directory,
206             # Options
207             max_age => $opts->{max_age} // 3600, # Cache-Control max-age
208             index => $opts->{index} // 'index.html', # Directory index file
209             etag => $opts->{etag} // 1, # Generate ETags
210 0   0     0 gzip => $opts->{gzip} // 0, # Serve .gz files if available
      0        
      0        
      0        
211             };
212            
213 0         0 return $self;
214             }
215              
216             # MIME type lookup - JIT baked into C at compile time
217             my %MIME_TYPES = (
218             # Text
219             html => 'text/html; charset=utf-8',
220             htm => 'text/html; charset=utf-8',
221             css => 'text/css; charset=utf-8',
222             js => 'application/javascript; charset=utf-8',
223             mjs => 'application/javascript; charset=utf-8',
224             json => 'application/json; charset=utf-8',
225             xml => 'application/xml; charset=utf-8',
226             txt => 'text/plain; charset=utf-8',
227             csv => 'text/csv; charset=utf-8',
228             md => 'text/markdown; charset=utf-8',
229             # Images
230             png => 'image/png',
231             jpg => 'image/jpeg',
232             jpeg => 'image/jpeg',
233             gif => 'image/gif',
234             svg => 'image/svg+xml',
235             ico => 'image/x-icon',
236             webp => 'image/webp',
237             avif => 'image/avif',
238             # Fonts
239             woff => 'font/woff',
240             woff2 => 'font/woff2',
241             ttf => 'font/ttf',
242             otf => 'font/otf',
243             eot => 'application/vnd.ms-fontobject',
244             # Media
245             mp3 => 'audio/mpeg',
246             mp4 => 'video/mp4',
247             webm => 'video/webm',
248             ogg => 'audio/ogg',
249             wav => 'audio/wav',
250             # Documents
251             pdf => 'application/pdf',
252             zip => 'application/zip',
253             gz => 'application/gzip',
254             tar => 'application/x-tar',
255             # Web
256             wasm => 'application/wasm',
257             map => 'application/json',
258             );
259              
260             sub _get_mime_type {
261 0     0   0 my ($path) = @_;
262 0         0 my ($ext) = $path =~ /\.(\w+)$/;
263 0   0     0 return $MIME_TYPES{lc($ext // '')} // 'application/octet-stream';
      0        
264             }
265              
266             # Middleware registration methods
267             # before() - runs before route handler, can short-circuit by returning a response
268             # after() - runs after route handler, can modify response
269             # Accepts either:
270             # - CODE ref (traditional Perl middleware, called via call_sv at runtime)
271             # - Builder object (has build_before/build_after methods, generates inline C at compile time)
272             sub before {
273 0     0 1 0 my ($self, $handler) = @_;
274 0   0     0 my $is_builder = blessed($handler) && ($handler->can('build_before') || $handler->can('build_after'));
275 0 0 0     0 die "Middleware must be a code ref or builder object"
276             unless ref($handler) eq 'CODE' || $is_builder;
277 0         0 push @{$self->{before_middleware}}, $handler;
  0         0  
278 0         0 return $self;
279             }
280              
281             sub after {
282 0     0 1 0 my ($self, $handler) = @_;
283 0   0     0 my $is_builder = blessed($handler) && ($handler->can('build_before') || $handler->can('build_after'));
284 0 0 0     0 die "Middleware must be a code ref or builder object"
285             unless ref($handler) eq 'CODE' || $is_builder;
286 0         0 push @{$self->{after_middleware}}, $handler;
  0         0  
287 0         0 return $self;
288             }
289              
290             # Enable request ID tracing - adds X-Request-ID header
291             # Only loads middleware module when called (JIT philosophy)
292             sub enable_request_id {
293 1     1 0 659 my ($self, %opts) = @_;
294              
295 1         16 require Hypersonic::Middleware::RequestId;
296              
297             # Builder pattern: middleware() returns a builder object, not a coderef
298             # The builder generates inline C at compile time - zero Perl in hot path
299 1         2 push @{$self->{before_middleware}}, Hypersonic::Middleware::RequestId::middleware(%opts);
  1         20  
300 1         3 push @{$self->{after_middleware}}, Hypersonic::Middleware::RequestId::after_middleware(%opts);
  1         6  
301              
302 1         5 return $self;
303             }
304              
305             # Session configuration - enables session support with signed cookies
306             # Only loads session module when called (JIT philosophy)
307             sub session_config {
308 1     1 1 9 my ($self, %opts) = @_;
309            
310 1         5 require Hypersonic::Session;
311            
312             # Configure the session module
313 1         5 my $config = Hypersonic::Session->configure(%opts);
314            
315             # Inject session middleware
316             # Before: Load session from cookie/store
317 1         1 unshift @{$self->{before_middleware}}, Hypersonic::Session::before_middleware();
  1         6  
318            
319             # After: Save session to cookie/store
320 1         1 push @{$self->{after_middleware}}, Hypersonic::Session::after_middleware();
  1         4  
321            
322             # Mark that we need cookies parsed for all dynamic routes
323 1         2 $self->{_session_enabled} = 1;
324            
325 1         3 return $self;
326             }
327              
328             # Async/Future Pool configuration - JIT-compiled thread pool for async operations
329             # Only loads Future/Pool when called (JIT philosophy)
330             sub async_pool {
331 0     0 1 0 my ($self, %opts) = @_;
332              
333 0         0 require Hypersonic::Future;
334 0         0 require Hypersonic::Future::Pool;
335              
336             # Create a Pool instance (OO - allows multiple pools)
337             my $pool = Hypersonic::Future::Pool->new(
338             workers => $opts{workers} // 8,
339 0   0     0 queue_size => $opts{queue_size} // 4096,
      0        
340             );
341              
342             # Store in array for event loop registration
343 0   0     0 push @{$self->{_async_pools} //= []}, $pool;
  0         0  
344              
345             # Mark that async pool is enabled - JIT code gen will include thread pool
346 0         0 $self->{_async_enabled} = 1;
347              
348             # Return the Pool object (not $self anymore)
349 0         0 return $pool;
350             }
351              
352             # Compression configuration - JIT-compiled gzip compression in C
353             # Only loads compression module when called (JIT philosophy)
354             sub compress {
355 0     0 1 0 my ($self, %opts) = @_;
356            
357 0         0 require Hypersonic::Compress;
358            
359             # Check for zlib
360 0 0       0 unless (Hypersonic::Compress::check_zlib()) {
361 0         0 warn "Warning: zlib not found, compression disabled\n";
362 0         0 return $self;
363             }
364            
365             # Configure compression
366 0         0 my $config = Hypersonic::Compress->configure(%opts);
367            
368             # Mark that compression is enabled - JIT code gen will include zlib code
369 0         0 $self->{_compression_enabled} = 1;
370 0         0 $self->{_compression_config} = $config;
371            
372 0         0 return $self;
373             }
374              
375             sub _add_route {
376 49     49   168 my ($self, $method, $path, $handler, $opts) = @_;
377              
378 49 100       198 die "Path must start with /" unless $path =~ m{^/};
379 48 100       136 die "Handler must be a code ref" unless ref($handler) eq 'CODE';
380              
381             # Check for dynamic option and feature flags
382 47         84 my $dynamic = 0;
383 47         276 my %features = (
384             parse_query => 0, # Parse ?key=value query strings
385             parse_headers => 0, # Parse HTTP headers
386             parse_cookies => 0, # Parse Cookie header
387             parse_json => 0, # Parse JSON body (requires Cpanel::JSON::XS)
388             parse_form => 0, # Parse form-urlencoded body
389             response_helpers => 0, # JIT compile response helper methods
390             streaming => 0, # Streaming response handler
391             need_xs_builder => 0, # Handler receives XS::JIT::Builder
392             );
393            
394 47 100       349 if (ref($opts) eq 'HASH') {
395 13 100       48 $dynamic = $opts->{dynamic} ? 1 : 0;
396             # Copy feature flags from options
397 13         47 for my $feat (keys %features) {
398 104 50       168 $features{$feat} = $opts->{$feat} ? 1 : 0 if exists $opts->{$feat};
    100          
399             }
400             }
401              
402             # Parse path parameters (supports multiple: /users/:user_id/posts/:post_id)
403 47         72 my @params;
404 47         631 my @segments = split '/', $path;
405 47         75 shift @segments; # Remove leading empty string
406            
407 47         129 for my $i (0 .. $#segments) {
408 58 100       174 if ($segments[$i] =~ /^:(\w+)$/) {
409 5         12 push @params, { name => $1, position => $i };
410 5         7 $dynamic = 1; # Path params imply dynamic
411             }
412             }
413              
414             # Streaming handlers are always dynamic
415 47 100       99 if ($features{streaming}) {
416 7         11 $dynamic = 1;
417             }
418              
419             # need_xs_builder handlers are always dynamic (but handled specially at compile time)
420 47 50       112 if ($features{need_xs_builder}) {
421 0         0 $dynamic = 1;
422             }
423              
424 47         682 push @{$self->{routes}}, {
425             method => $method,
426             path => $path,
427             handler => $handler,
428             dynamic => $dynamic,
429             streaming => $features{streaming},
430             need_xs_builder => $features{need_xs_builder},
431             params => \@params,
432             segments => \@segments,
433             features => \%features,
434             # Per-route middleware (optional)
435             before => $opts->{before} // [],
436 47   50     59 after => $opts->{after} // [],
      50        
437             };
438              
439 47         139 return $self;
440             }
441              
442             sub compile {
443 17     17 1 954 my ($self) = @_;
444              
445 17 100 50     35 die "No routes defined" unless @{$self->{routes}} || @{$self->{static_dirs} // []};
  17   66     78  
  1         17  
446 16 100       72 die "Already compiled" if $self->{compiled};
447              
448             # ============================================================
449             # STATIC FILE PROCESSING - bake files into C at compile time
450             # ============================================================
451 15 50       54 if (my $static_dirs = $self->{static_dirs}) {
452 0         0 $self->_compile_static_files($static_dirs);
453             }
454              
455             # ============================================================
456             # ROUTE ANALYSIS - determine what code to generate (JIT philosophy)
457             # ============================================================
458             my %analysis = (
459             methods_used => {}, # GET => 1, POST => 1, etc.
460             has_dynamic => 0, # Any dynamic routes?
461             has_static => 0, # Any static routes?
462             has_path_params => 0, # Any routes with :param?
463             has_body_access => 0, # Any routes that need body?
464 15         41 route_count => scalar(@{$self->{routes}}),
465             all_same_prefix => undef, # Common prefix like /api/*
466             single_method => undef, # Only one HTTP method used?
467             # JIT feature flags - only generate code for features actually used
468             needs_query => 0, # Any route needs query string parsing?
469             needs_headers => 0, # Any route needs header access?
470             needs_cookies => 0, # Any route needs cookie parsing?
471             needs_json => 0, # Any route needs JSON body parsing?
472             needs_form => 0, # Any route needs form data parsing?
473             needs_response_helpers => 0, # Any route needs response helper methods?
474             needs_streaming => 0, # Any route uses streaming responses?
475             needs_xs_builder => 0, # Any route uses need_xs_builder?
476             # Middleware flags - JIT: only generate middleware code if actually used
477 15         32 has_global_before => scalar(@{$self->{before_middleware}}) > 0,
478 15         41 has_global_after => scalar(@{$self->{after_middleware}}) > 0,
  15         266  
479             has_route_middleware => 0, # Any route has before/after hooks?
480             has_any_middleware => 0, # Global OR per-route middleware?
481             );
482              
483             # First pass: collect method usage and route characteristics
484 15         31 for my $route (@{$self->{routes}}) {
  15         46  
485 30         76 $analysis{methods_used}{$route->{method}} = 1;
486              
487 30 100       100 if ($route->{dynamic}) {
488 6         9 $analysis{has_dynamic} = 1;
489             # Dynamic routes might need body access (POST/PUT/PATCH typically do)
490 6 100       22 if ($route->{method} =~ /^(POST|PUT|PATCH)$/) {
491 3         4 $analysis{has_body_access} = 1;
492             }
493            
494             # JIT FEATURE DETECTION: Analyze handler code to detect what request
495             # features it actually uses. This avoids generating unused parsing code.
496 6   50     14 my $f = $route->{features} // {};
497            
498             # Explicit flags take precedence
499 6 50       14 $analysis{needs_query} = 1 if $f->{parse_query};
500 6 50       12 $analysis{needs_headers} = 1 if $f->{parse_headers};
501 6 50       14 $analysis{needs_cookies} = 1 if $f->{parse_cookies};
502 6 100       9 $analysis{needs_json} = 1 if $f->{parse_json};
503 6 50       12 $analysis{needs_form} = 1 if $f->{parse_form};
504 6 50       15 $analysis{needs_response_helpers} = 1 if $f->{response_helpers};
505 6 100       16 $analysis{needs_streaming} = 1 if $f->{streaming};
506 6 50       13 $analysis{needs_xs_builder} = 1 if $f->{need_xs_builder};
507            
508             # Auto-detect by analyzing handler code
509 6         24 my $handler_code = _deparse_handler($route->{handler});
510 6 50       17 if ($handler_code) {
511             # Look for $req->{query} or ->{query} access patterns
512 6 50       37 $analysis{needs_query} = 1 if $handler_code =~ /\{['"]*query['"]*\}/;
513 6 50       20 $analysis{needs_headers} = 1 if $handler_code =~ /\{['"]*headers['"]*\}/;
514 6 50       27 $analysis{needs_cookies} = 1 if $handler_code =~ /\{['"]*cookies['"]*\}/;
515 6 50       33 $analysis{needs_json} = 1 if $handler_code =~ /\{['"]*json['"]*\}/;
516 6 50       30 $analysis{needs_form} = 1 if $handler_code =~ /\{['"]*form['"]*\}/;
517             }
518             } else {
519 24         45 $analysis{has_static} = 1;
520             }
521              
522 30 50       48 if (@{$route->{params}}) {
  30         68  
523 0         0 $analysis{has_path_params} = 1;
524             }
525            
526             # Check for per-route middleware
527 30 50 33     130 if (@{$route->{before}} || @{$route->{after}}) {
  30         158  
  30         93  
528 0         0 $analysis{has_route_middleware} = 1;
529             }
530             }
531            
532             # Session support: force cookie parsing when sessions are enabled
533 15 100       42 if ($self->{_session_enabled}) {
534 1         2 $analysis{needs_cookies} = 1;
535             }
536            
537             # Compression support: force header parsing to check Accept-Encoding
538 15 50       41 if ($self->{_compression_enabled}) {
539 0         0 $analysis{needs_headers} = 1;
540             }
541              
542             # Async Pool support: enable thread pool integration.
543             # Disabled on Windows - Pool uses pthread + eventfd, neither of
544             # which exists there. (Future/Pool deserve a Win32 port; not in
545             # the 0.14 minimum-viable Windows scope.)
546 15 50       50 if ($self->{_async_enabled}) {
547 0 0       0 if ($^O eq 'MSWin32') {
548 0         0 warn "Hypersonic: async pool not supported on Windows; "
549             . "ignoring.\n";
550             } else {
551 0         0 $analysis{needs_async_pool} = 1;
552             }
553             }
554              
555             # Classify middleware as builder (inline C) or Perl (call_sv)
556             # Builder middleware has build_before/build_after methods and generates C at compile time
557 15         35 my (@builder_before, @perl_before, @builder_after, @perl_after);
558 15         28 for my $mw (@{$self->{before_middleware}}) {
  15         35  
559 1 50 0     18 if (blessed($mw) && ($mw->can('build_before') || $mw->can('build_after'))) {
      33        
560 0         0 push @builder_before, $mw;
561             } else {
562 1         2 push @perl_before, $mw;
563             }
564             }
565 15         25 for my $mw (@{$self->{after_middleware}}) {
  15         33  
566 1 50 0     3 if (blessed($mw) && ($mw->can('build_before') || $mw->can('build_after'))) {
      33        
567 0         0 push @builder_after, $mw;
568             } else {
569 1         2 push @perl_after, $mw;
570             }
571             }
572              
573 15         46 $analysis{builder_before} = \@builder_before;
574 15         43 $analysis{builder_after} = \@builder_after;
575 15         31 $analysis{perl_before} = \@perl_before;
576 15         30 $analysis{perl_after} = \@perl_after;
577 15         40 $analysis{has_builder_before} = @builder_before > 0;
578 15         31 $analysis{has_builder_after} = @builder_after > 0;
579             # Update flags: has_global_* now refers to Perl middleware only (for call_sv)
580 15         31 $analysis{has_global_before} = @perl_before > 0;
581 15         29 $analysis{has_global_after} = @perl_after > 0;
582              
583             # Allocate slots for builder middleware
584             # Request slots 0-15 are reserved for core fields (see Hypersonic::Request)
585             # Middleware slots start at 16
586 15         29 my $next_slot = 16;
587 15         23 my %middleware_slots;
588 15         55 for my $mw (@builder_before, @builder_after) {
589 0 0       0 next unless $mw->can('slot_requirements');
590 0         0 my $reqs = $mw->slot_requirements;
591 0         0 for my $name (keys %$reqs) {
592 0 0       0 next if exists $middleware_slots{$name}; # Deduplicate
593 0         0 $middleware_slots{$name} = $next_slot;
594 0         0 $next_slot += $reqs->{$name};
595             }
596             }
597 15         38 $analysis{middleware_slots} = \%middleware_slots;
598              
599             # Determine if any middleware is present (global or per-route)
600             $analysis{has_any_middleware} = $analysis{has_global_before} ||
601             $analysis{has_global_after} ||
602             $analysis{has_builder_before} ||
603             $analysis{has_builder_after} ||
604 15   33     187 $analysis{has_route_middleware};
605              
606             # Check if only one method is used
607 15         21 my @methods = keys %{$analysis{methods_used}};
  15         62  
608 15 100       49 if (@methods == 1) {
609 13         31 $analysis{single_method} = $methods[0];
610             }
611              
612             # Check for common prefix (e.g., all routes start with /api)
613 15         24 my @paths = map { $_->{path} } @{$self->{routes}};
  30         86  
  15         48  
614 15 100       68 if (@paths > 1) {
615 7         37 my $prefix = _find_common_prefix(@paths);
616 7 50       23 if (length($prefix) > 1) { # More than just "/"
617 0         0 $analysis{all_same_prefix} = $prefix;
618             }
619             }
620              
621             # Store analysis for use in code generation
622 15         52 $self->{route_analysis} = \%analysis;
623              
624             # Compile JIT request accessors (after analysis so we know what features are needed)
625 15         5471 require Hypersonic::Request;
626             Hypersonic::Request->compile_accessors(
627             cache_dir => $self->{cache_dir},
628             response_helpers => $analysis{needs_response_helpers},
629 15         130 );
630              
631             # Pre-evaluate all static handlers and build FULL HTTP responses
632 15         63 my @full_responses;
633             my @dynamic_handlers; # Store CV refs for dynamic routes
634 15         0 my @route_param_info; # Store param info for dynamic routes
635              
636 15         29 for my $i (0 .. $#{$self->{routes}}) {
  15         147  
637 30         188 my $route = $self->{routes}[$i];
638              
639 30 100       104 if (!$route->{dynamic}) {
640             # Check if this is a pre-built static file response
641 24 50       87 if ($route->{_static_file}) {
642 0         0 push @full_responses, $route->{_static_response};
643 0         0 $route->{response_idx} = $#full_responses;
644 0         0 next;
645             }
646            
647             # need_xs_builder routes are handled later in code generation
648 24 50       102 if ($route->{need_xs_builder}) {
649 0         0 $route->{dynamic} = 1; # Treat as dynamic for dispatch
650 0         0 push @dynamic_handlers, $route->{handler};
651 0         0 push @route_param_info, $route->{params};
652 0         0 $route->{handler_idx} = $#dynamic_handlers;
653 0         0 next;
654             }
655            
656             # RUN THE HANDLER ONCE - this is the magic
657 24         105 my $result = $route->{handler}->();
658            
659             # Support both string and [status, headers, body] format
660 24         152 my ($status, $headers, $body);
661 24 100       822 if (ref($result) eq 'ARRAY') {
    100          
662 1         3 ($status, $headers, $body) = @$result;
663 1   50     4 $status //= 200;
664 1   50     3 $headers //= {};
665             } elsif (ref($result) eq 'HASH') {
666 8   100     31 $status = $result->{status} // 200;
667 8   100     35 $headers = $result->{headers} // {};
668 8   50     20 $body = $result->{body} // '';
669             } else {
670 15         23 $status = 200;
671 15         36 $headers = {};
672 15         21 $body = $result;
673             }
674            
675 24 100 66     261 die "Handler for $route->{method} $route->{path} must return a string or response structure"
676             unless defined $body && !ref($body);
677              
678             # Build COMPLETE HTTP response via Protocol module (JIT at compile time)
679             my $security_hdrs = $self->{enable_security_headers}
680 23 50       136 ? $self->_get_security_headers_string()
681             : '';
682            
683 23         190 my $full_response = $PROTOCOL->build_response(
684             status => $status,
685             headers => $headers,
686             body => $body,
687             keep_alive => 1,
688             security_headers => $security_hdrs,
689             );
690              
691 23         41 push @full_responses, $full_response;
692 23         95 $route->{response_idx} = $#full_responses;
693             } else {
694             # Dynamic route - store handler index and param info
695 6         11 push @dynamic_handlers, $route->{handler};
696 6         21 push @route_param_info, $route->{params};
697 6         20 $route->{handler_idx} = $#dynamic_handlers;
698             }
699             }
700              
701             # Store dynamic handlers and param info for runtime access
702 14         40 $self->{dynamic_handlers} = \@dynamic_handlers;
703 14         28 $self->{route_param_info} = \@route_param_info;
704              
705             # JIT: Build streaming handlers lookup table (only if streaming is enabled)
706 14 100       54 if ($self->{route_analysis}{needs_streaming}) {
707 2         3 my @streaming_flags;
708 2         3 for my $route (@{$self->{routes}}) {
  2         4  
709 4 100       10 next unless $route->{dynamic};
710 2 50       7 push @streaming_flags, $route->{streaming} ? 1 : 0;
711             }
712 2         7 $self->{_streaming_flags} = \@streaming_flags;
713             }
714              
715             # JIT: Build WebSocket handlers lookup table
716 14 50       93 if ($self->_has_websocket_routes()) {
717 0         0 my @ws_handlers;
718             my @ws_paths;
719 0         0 for my $route (@{$self->{websocket_routes}}) {
  0         0  
720 0         0 push @ws_handlers, $route->{handler};
721 0         0 push @ws_paths, $route->{path};
722             # Check for Room usage in route options
723 0 0       0 if ($route->{opts}{rooms}) {
724 0         0 $self->{route_analysis}{needs_websocket_rooms} = 1;
725             }
726             }
727 0         0 $self->{_websocket_handlers} = \@ws_handlers;
728 0         0 $self->{_websocket_paths} = \@ws_paths;
729 0         0 $self->{route_analysis}{needs_websocket} = 1;
730             # Handler is needed if we have websocket routes
731 0         0 $self->{route_analysis}{needs_websocket_handler} = 1;
732             # WebSocket uses Stream for connection handling
733 0         0 $self->{route_analysis}{needs_streaming} = 1;
734             }
735              
736             # JIT: Explicit opt-in for Room support (can also be set in new())
737 14 50       42 if ($self->{websocket_rooms}) {
738 0         0 $self->{route_analysis}{needs_websocket_rooms} = 1;
739             }
740              
741             # JIT: Build per-route middleware arrays (only if route middleware is present)
742 14         34 my $analysis = $self->{route_analysis};
743 14 50       53 if ($analysis->{has_route_middleware}) {
744 0         0 my @route_before_mw;
745             my @route_after_mw;
746 0         0 for my $route (@{$self->{routes}}) {
  0         0  
747 0 0       0 next unless $route->{dynamic};
748             # Store arrays of middleware handlers per route (by handler_idx)
749 0         0 push @route_before_mw, $route->{before};
750 0         0 push @route_after_mw, $route->{after};
751             }
752 0         0 $self->{_route_before_mw} = \@route_before_mw;
753 0         0 $self->{_route_after_mw} = \@route_after_mw;
754             }
755              
756             # JIT: Store Perl-only middleware for runtime call_sv dispatch
757             # Builder middleware is handled at compile time (inline C), not runtime
758 14 100 66     137 if ($analysis->{has_global_before} || $analysis->{has_global_after}) {
759 1         8 $self->{_perl_before_mw} = $analysis->{perl_before};
760 1         2 $self->{_perl_after_mw} = $analysis->{perl_after};
761             }
762              
763             # Generate C code with pure C event loop
764 14         70 my $c_code = $self->_generate_server_code(\@full_responses);
765              
766             # Compile via XS::JIT
767             #
768             # Derive a STABLE module id from a hash of the generated C source
769             # (plus this dist's $VERSION and the target perl's archname/version
770             # so a cache built for one perl is never loaded into another).
771             #
772             # Pre-0.18 we used 'Hypersonic::_Server_' . int(rand(100000)) which
773             # gave a different module name on every fresh perl process; because
774             # XS::JIT's on-disk cache lives at
775             # /lib/auto//.
776             # (see XS-JIT/lib/XS/JIT/xs_jit.c xs_jit_cache_path()), a different
777             # $name on every run forced a full gcc/cc re-invocation EVERY time
778             # the test suite or a user re-ran their server. On slow CPAN smoker
779             # boxes that gcc invocation takes 30-60+ seconds per server, which
780             # is what caused the SIGKILL cascade in CPAN tester reports for
781             # 0.17 (t/0035-e2e-streaming.t, t/2012..t/2017, t/2102).
782             #
783             # Using a content hash means: identical route+option configurations
784             # produce the same module name -> warm cache hit -> dlopen() of a
785             # 100ms .so instead of a 30s gcc rebuild. The random fallback id is
786             # retained for the degenerate case where Digest::MD5 isn't available.
787 14         69 my $module_id;
788             {
789 14         21 my $hash_input = join("\0",
790             $c_code,
791             $VERSION,
792             (defined $Config::Config{archname}
793 14 50 50     1336 ? $Config::Config{archname} : ''),
794             $] || '',
795             );
796 14 50       58 if (eval { require Digest::MD5; 1 }) {
  14         180  
  14         49  
797             # 16 hex chars (64 bits) is plenty of namespace for one
798             # process and short enough to keep the on-disk file names
799             # readable. md5_hex is core since perl 5.7.3.
800 14         941 $module_id = substr(Digest::MD5::md5_hex($hash_input), 0, 16);
801             } else {
802             # No Digest::MD5? Fall back to the legacy random id. Cache
803             # won't be reused across runs but at least nothing breaks.
804 0         0 $module_id = $self->{id};
805             }
806             }
807 14         51 my $module_name = 'Hypersonic::_Server_' . $module_id;
808            
809             # Build compile options - add TLS flags if enabled
810 14         193 my %functions = (
811             "${module_name}::run_event_loop" => {
812             source => 'hypersonic_run_event_loop',
813             is_xs_native => 1,
814             },
815             "${module_name}::dispatch" => {
816             source => 'hypersonic_dispatch',
817             is_xs_native => 1,
818             },
819             );
820              
821             # Add Stream and SSE XS functions if streaming is enabled
822 14 100       57 if ($self->{route_analysis}{needs_streaming}) {
823 2         5 %functions = (%functions, %{Hypersonic::Stream->get_xs_functions()});
  2         19  
824 2         14 %functions = (%functions, %{Hypersonic::SSE->get_xs_functions()});
  2         8  
825             }
826              
827             # Add WebSocket XS functions if WebSocket routes are registered
828 14 50       45 if ($self->{route_analysis}{needs_websocket}) {
829 0         0 require Hypersonic::WebSocket;
830 0         0 %functions = (%functions, %{Hypersonic::WebSocket->get_xs_functions()});
  0         0  
831             }
832              
833             # Add WebSocket Handler XS functions
834 14 50       40 if ($self->{route_analysis}{needs_websocket_handler}) {
835 0         0 require Hypersonic::WebSocket::Handler;
836 0         0 %functions = (%functions, %{Hypersonic::WebSocket::Handler->get_xs_functions()});
  0         0  
837             }
838              
839             # Add WebSocket Room XS functions
840 14 50       60 if ($self->{route_analysis}{needs_websocket_rooms}) {
841 0         0 require Hypersonic::WebSocket::Room;
842 0         0 %functions = (%functions, %{Hypersonic::WebSocket::Room->get_xs_functions()});
  0         0  
843             }
844              
845             # Add Future/Pool XS functions if async pool is enabled
846 14 50       42 if ($self->{route_analysis}{needs_async_pool}) {
847 0         0 require Hypersonic::Future;
848 0         0 require Hypersonic::Future::Pool;
849 0         0 %functions = (%functions, %{Hypersonic::Future->get_xs_functions()});
  0         0  
850 0         0 %functions = (%functions, %{Hypersonic::Future::Pool->get_xs_functions()});
  0         0  
851             }
852              
853             # Add need_xs_builder route additional XS functions (if any)
854             # Note: The main handler functions are C functions called by call_xs_builder_handler,
855             # NOT XS functions callable from Perl, so we don't register them.
856 14 50       108 if (my $xsr = $self->{_xs_builder_routes}) {
857 0         0 for my $entry (@$xsr) {
858 0         0 my $result = $entry->{result};
859            
860             # Add any additional XS functions the handler defined
861 0 0       0 if ($result->{xs_functions}) {
862 0         0 %functions = (%functions, %{$result->{xs_functions}});
  0         0  
863             }
864             }
865             }
866              
867             my %compile_opts = (
868             code => $c_code,
869             name => $module_name,
870             cache_dir => $self->{cache_dir},
871 14         297 functions => \%functions,
872             );
873            
874             # Add OpenSSL flags for TLS support
875 14 50       92 if ($self->{tls}) {
876 0         0 $compile_opts{extra_cflags} = Hypersonic::TLS::get_extra_cflags();
877 0         0 $compile_opts{extra_ldflags} = Hypersonic::TLS::get_extra_ldflags();
878             }
879            
880             # Add nghttp2 flags for HTTP/2 support
881 14 50       58 if ($self->{http2}) {
882 0         0 require Hypersonic::Protocol::HTTP2;
883 0         0 my $h2_cflags = Hypersonic::Protocol::HTTP2::get_extra_cflags();
884 0         0 my $h2_ldflags = Hypersonic::Protocol::HTTP2::get_extra_ldflags();
885 0   0     0 $compile_opts{extra_cflags} = ($compile_opts{extra_cflags} // '') . " $h2_cflags";
886 0   0     0 $compile_opts{extra_ldflags} = ($compile_opts{extra_ldflags} // '') . " $h2_ldflags";
887             }
888            
889             # Add zlib flags for compression support
890 14 50       36 if ($self->{_compression_enabled}) {
891 0         0 require Hypersonic::Compress;
892 0         0 my ($cflags, $ldflags) = Hypersonic::Compress::get_zlib_flags();
893 0   0     0 $compile_opts{extra_cflags} = ($compile_opts{extra_cflags} // '') . " $cflags";
894 0   0     0 $compile_opts{extra_ldflags} = ($compile_opts{extra_ldflags} // '') . " $ldflags";
895             }
896              
897             # Add pthread flags for async pool (thread pool)
898 14 50       41 if ($self->{route_analysis}{needs_async_pool}) {
899 0   0     0 $compile_opts{extra_cflags} = ($compile_opts{extra_cflags} // '') . " -pthread";
900 0   0     0 $compile_opts{extra_ldflags} = ($compile_opts{extra_ldflags} // '') . " -lpthread";
901             }
902              
903             # Add event backend flags (e.g., io_uring needs -luring)
904 14 50       56 if ($self->{_event_backend}) {
905 14         36 my $backend = $self->{_event_backend};
906 14 50       311 if ($backend->can('extra_cflags')) {
907 14   50     59 my $ev_cflags = $backend->extra_cflags // '';
908 14 50 0     46 $compile_opts{extra_cflags} = ($compile_opts{extra_cflags} // '') . " $ev_cflags"
909             if $ev_cflags;
910             }
911 14 50       63 if ($backend->can('extra_ldflags')) {
912 14   50     40 my $ev_ldflags = $backend->extra_ldflags // '';
913 14 50 0     33 $compile_opts{extra_ldflags} = ($compile_opts{extra_ldflags} // '') . " $ev_ldflags"
914             if $ev_ldflags;
915             }
916             }
917              
918             # Windows: link Winsock so socket()/recv()/send()/etc. resolve in
919             # the JIT-compiled .so. The select backend already adds this, but
920             # belt-and-braces - the main compile uses these symbols too.
921 14 50       123 if ($^O eq 'MSWin32') {
922 0   0     0 my $ld = $compile_opts{extra_ldflags} // '';
923 0 0       0 $compile_opts{extra_ldflags} = $ld . ' -lws2_32'
924             unless $ld =~ /-lws2_32\b/;
925             }
926              
927             # Emit a visible breadcrumb BEFORE the (potentially slow) gcc/cc
928             # invocation so smoker logs never show "(child wrote no output)"
929             # when wait_for_port times out mid-compile. Force a flush in case
930             # something upstream disabled autoflush on STDERR.
931 14 50 33     181 if ($ENV{HYPERSONIC_COMPILE_DIAG} || $ENV{AUTOMATED_TESTING}) {
932 14         164 local $| = 1;
933 14         1214 print STDERR "# Hypersonic: compiling JIT module $module_name ...\n";
934 14         55 eval { STDERR->flush; };
  14         317  
935             }
936              
937             # The JIT boot xsub installs Hypersonic::Stream::* xsubs into
938             # the same package that Hypersonic/Stream.pm already lives in,
939             # which Perl reports as "Subroutine ... redefined". This is
940             # expected (the .pm defines is_streaming_handler and the .so
941             # provides the rest at compile time, or - on a second compile()
942             # in the same process - it reinstalls them). Silence the noise.
943 14         31 my $ok;
944             {
945 29     29   264 no warnings 'redefine';
  29         43  
  29         2929  
  14         29  
946 14         8136851 $ok = XS::JIT->compile(%compile_opts);
947             }
948             die "XS::JIT->compile failed for $module_name (check liburing/zlib/openssl "
949             . "are installed and linkable; extra_ldflags='"
950 14 50 0     238 . ($compile_opts{extra_ldflags} // '') . "')"
951             unless $ok;
952              
953             # Store function references - confirm the boot xsub actually installed
954             # them. If we somehow got past compile() with the .so loaded but the
955             # symbols missing, fail loudly here rather than crash later in run().
956             {
957 29     29   166 no strict 'refs';
  29         54  
  29         303003  
  14         33  
958 14         33 $self->{run_loop_fn} = \&{"${module_name}::run_event_loop"};
  14         213  
959 14         33 $self->{dispatch_fn} = \&{"${module_name}::dispatch"};
  14         72  
960             die "XS::JIT loaded $module_name but ::dispatch is not defined "
961             . "(stale cache_dir? link line was: ldflags='"
962             . ($compile_opts{extra_ldflags} // '') . "')"
963 14 50 0     30 unless defined &{"${module_name}::dispatch"};
  14         64  
964             }
965              
966             # Mark Future/Pool as compiled if async pool is enabled
967             # (prevents them from trying to compile separately)
968 14 50       67 if ($self->{route_analysis}{needs_async_pool}) {
969 0         0 $Hypersonic::Future::COMPILED = 1;
970 0         0 $Hypersonic::Future::Pool::COMPILED = 1;
971             # Register custom ops for Future after compilation
972 0         0 Hypersonic::Future->_register_ops();
973             }
974              
975 14         51 $self->{compiled} = 1;
976 14         498 return $self;
977             }
978              
979             sub _generate_server_code {
980 14     14   50 my ($self, $full_responses) = @_;
981              
982             # Load event backend module
983 14         5433 require Hypersonic::Event;
984 14   33     285 my $backend_name = $self->{event_backend} // Hypersonic::Event->best_backend;
985 14         117 my $backend = Hypersonic::Event->backend($backend_name);
986              
987             # Store backend for use in event loop generation
988 14         140 $self->{_event_backend} = $backend;
989 14         134 $self->{_event_backend_name} = $backend_name;
990              
991 14         293 my $builder = XS::JIT::Builder->new;
992              
993             # C99 detection for inline keyword
994 14         225 my $inline = Hypersonic::JIT::Util->inline_keyword;
995              
996             # Check if we have any dynamic routes
997 14         4104 my $has_dynamic = grep { $_->{dynamic} } @{$self->{routes}};
  29         145  
  14         365  
998              
999             # Common includes - portable across POSIX and Windows. On Windows
1000             # we substitute Winsock for netinet/socket/unistd, and define a
1001             # couple of compatibility macros so the rest of the codegen can
1002             # stay POSIX-shaped.
1003 14         440 $builder->line('#include ')
1004             ->line('#include ')
1005             ->raw(<<'C');
1006             #ifdef _WIN32
1007             # define WIN32_LEAN_AND_MEAN
1008             # include
1009             # include
1010             # include
1011             # define close(fd) closesocket(fd)
1012             /* Windows has no O_NONBLOCK / fcntl(F_SETFL, O_NONBLOCK); use
1013             * ioctlsocket(FIONBIO) instead. Wrap both fcntl invocations the
1014             * codegen emits so they vanish on Windows. */
1015             # define hs_set_nonblocking(fd) do { u_long _m = 1; ioctlsocket((fd), FIONBIO, &_m); } while(0)
1016             /* sockets don't raise SIGPIPE on Windows. */
1017             # ifndef MSG_NOSIGNAL
1018             # define MSG_NOSIGNAL 0
1019             # endif
1020             #else
1021             # include
1022             # include
1023             # include
1024             # include
1025             # include
1026             # include
1027             # define hs_set_nonblocking(fd) do { int _f = fcntl((fd), F_GETFL, 0); fcntl((fd), F_SETFL, _f | O_NONBLOCK); } while(0)
1028             #endif
1029             C
1030              
1031             # Backend-specific includes
1032 14         369 $builder->line($backend->includes);
1033            
1034             # Add signal and time headers for graceful shutdown
1035 14         98 $builder->line('#include ')
1036             ->line('#include ');
1037            
1038             # Compression support - include zlib if compression is enabled
1039 14 50       124 if ($self->{_compression_enabled}) {
1040 0         0 $builder->line('#include ')
1041             ->line('#define HYPERSONIC_COMPRESSION 1');
1042             }
1043            
1044             # TLS support - include OpenSSL headers if TLS is enabled
1045 14 50       109 if ($self->{tls}) {
1046 0         0 $builder->raw(Hypersonic::TLS::gen_includes())
1047             ->line('#define HYPERSONIC_TLS 1');
1048             }
1049            
1050             # HTTP/2 support - include nghttp2 if enabled
1051 14 50       215 if ($self->{http2}) {
1052 0         0 require Hypersonic::Protocol::HTTP2;
1053 0         0 Hypersonic::Protocol::HTTP2->gen_includes($builder);
1054             }
1055              
1056             # Security hardening configuration
1057 14         66 my $max_connections = $self->{max_connections};
1058 14         58 my $max_request_size = $self->{max_request_size};
1059 14         53 my $keepalive_timeout = $self->{keepalive_timeout};
1060 14         49 my $recv_timeout = $self->{recv_timeout};
1061 14         32 my $drain_timeout = $self->{drain_timeout};
1062              
1063             # Backend-specific defines
1064 14         177 $builder->blank
1065             ->line($backend->defines)
1066             ->line("#define RECV_BUF_SIZE $max_request_size")
1067             ->line("#define MAX_CONNECTIONS $max_connections");
1068            
1069             # Enable security headers macro if configured
1070 14 100 66     229 if ($self->{enable_security_headers} && $has_dynamic) {
1071 3         26 $builder->line('#define HYPERSONIC_SECURITY_HEADERS 1');
1072             }
1073            
1074             $builder
1075 14         352 ->line("#define KEEPALIVE_TIMEOUT $keepalive_timeout")
1076             ->line("#define RECV_TIMEOUT $recv_timeout")
1077             ->line("#define DRAIN_TIMEOUT $drain_timeout")
1078             ->blank;
1079            
1080             # TLS-aware I/O macros - compile-time decision, zero runtime overhead
1081 14         353 $builder->comment('TLS-aware I/O wrappers - compile-time branching')
1082             ->line('#ifdef HYPERSONIC_TLS')
1083             ->line('#define HYPERSONIC_SEND(fd, buf, len) do { \\')
1084             ->line(' TLSConnection* _tc = get_tls_connection(fd); \\')
1085             ->line(' if (_tc) tls_send(_tc, buf, len); \\')
1086             ->line(' else send(fd, buf, len, 0); \\')
1087             ->line('} while(0)')
1088             ->line('#define HYPERSONIC_CLOSE(fd) tls_close(fd)')
1089             ->line('#else')
1090             ->line('#define HYPERSONIC_SEND(fd, buf, len) send(fd, buf, len, 0)')
1091             ->line('#define HYPERSONIC_CLOSE(fd) close(fd)')
1092             ->line('#endif')
1093             ->blank;
1094              
1095             # User-defined C helpers (early, so they're available to all routes)
1096 14 50       82 if (my $helpers = $self->{c_helpers}) {
1097 0         0 $builder->comment('User-defined C helpers');
1098 0 0       0 if (ref $helpers eq 'CODE') {
1099 0         0 my $helper_builder = XS::JIT::Builder->new;
1100 0         0 $helpers->($helper_builder);
1101 0         0 $builder->raw($helper_builder->code);
1102             } else {
1103             # Raw C string
1104 0         0 $builder->raw($helpers);
1105             }
1106 0         0 $builder->blank;
1107             }
1108              
1109             # Graceful shutdown support
1110 14         464 $builder->comment('Graceful shutdown support')
1111             ->line('static volatile sig_atomic_t g_shutdown = 0;')
1112             ->line('static volatile int g_active_connections = 0;')
1113             ->blank
1114             ->line('static void handle_shutdown_signal(int sig) {')
1115             ->line(' (void)sig;')
1116             ->line(' g_shutdown = 1;')
1117             ->line(' if (getenv("HYPERSONIC_SHUTDOWN_DIAG")) {')
1118             ->line(' char msg[128];')
1119             ->line(' int n = snprintf(msg, sizeof(msg),')
1120             ->line(' "[hypersonic %d] SIGTERM received; g_active_connections=%d\n",')
1121             ->line(' (int)getpid(), g_active_connections);')
1122             ->line(' (void)!write(2, msg, n);')
1123             ->line(' }')
1124             ->line('}')
1125             ->blank;
1126            
1127             # Compression support - JIT compiled zlib functions
1128 14 50       83 if ($self->{_compression_enabled}) {
1129 0         0 my $config = $self->{_compression_config};
1130 0   0     0 my $min_size = $config->{min_size} // 1024;
1131 0   0     0 my $level = $config->{level} // 6;
1132            
1133 0         0 $builder->comment('Gzip compression support - JIT compiled')
1134             ->line('static __thread unsigned char gzip_out_buf[131072];')
1135             ->blank
1136             ->line('static int accepts_gzip(const char* accept_encoding, size_t len) {')
1137             ->line(' if (!accept_encoding || len == 0) return 0;')
1138             ->line(' const char* p = accept_encoding;')
1139             ->line(' const char* end = accept_encoding + len;')
1140             ->line(' while (p < end - 3) {')
1141             ->line(' if (p[0] == \'g\' && p[1] == \'z\' && p[2] == \'i\' && p[3] == \'p\') return 1;')
1142             ->line(' p++;')
1143             ->line(' }')
1144             ->line(' return 0;')
1145             ->line('}')
1146             ->blank
1147             ->line('static size_t gzip_compress(const char* input, size_t input_len, unsigned char** output) {')
1148             ->line(' size_t max_out;')
1149             ->line(' z_stream strm;')
1150             ->line(' int ret;')
1151             ->line(' size_t compressed_len;')
1152             ->line(" if (input_len < $min_size) return 0;")
1153             ->line(' max_out = compressBound(input_len) + 18;')
1154             ->line(' if (max_out > sizeof(gzip_out_buf)) return 0;')
1155             ->line(' memset(&strm, 0, sizeof(strm));')
1156             ->line(" if (deflateInit2(&strm, $level, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) return 0;")
1157             ->line(' strm.next_in = (Bytef*)input;')
1158             ->line(' strm.avail_in = input_len;')
1159             ->line(' strm.next_out = gzip_out_buf;')
1160             ->line(' strm.avail_out = sizeof(gzip_out_buf);')
1161             ->line(' ret = deflate(&strm, Z_FINISH);')
1162             ->line(' compressed_len = strm.total_out;')
1163             ->line(' deflateEnd(&strm);')
1164             ->line(' if (ret != Z_STREAM_END || compressed_len >= input_len) return 0;')
1165             ->line(' *output = gzip_out_buf;')
1166             ->line(' return compressed_len;')
1167             ->line('}')
1168             ->blank;
1169             }
1170            
1171             # Connection tracking for keep-alive timeout - O(1) using fd as index
1172 14         616 $builder->comment('Connection tracking - O(1) using fd as direct index')
1173             ->line('#define MAX_FD 65536')
1174             ->line('static time_t g_conn_time[MAX_FD];')
1175             ->line('static time_t g_current_time = 0;')
1176             ->blank
1177             ->line("static $inline void track_connection(int fd, time_t now) {")
1178             ->line(' if (fd >= 0 && fd < MAX_FD) {')
1179             ->line(' g_conn_time[fd] = now;')
1180             ->line(' g_active_connections++;')
1181             ->line(' }')
1182             ->line('}')
1183             ->blank
1184             ->line("static $inline void update_connection(int fd, time_t now) {")
1185             ->line(' if (fd >= 0 && fd < MAX_FD) {')
1186             ->line(' g_conn_time[fd] = now;')
1187             ->line(' }')
1188             ->line('}')
1189             ->blank
1190             ->line("static $inline void remove_connection(int fd) {")
1191             ->line(' if (fd >= 0 && fd < MAX_FD && g_conn_time[fd] > 0) {')
1192             ->line(' g_conn_time[fd] = 0;')
1193             ->line(' g_active_connections--;')
1194             ->line(' }')
1195             ->line('}')
1196             ->blank;
1197              
1198             # TLS code generation - SSL context, accept, read/write wrappers
1199 14 50       70 if ($self->{tls}) {
1200             $builder->comment('TLS/HTTPS support via OpenSSL')
1201 0         0 ->raw(Hypersonic::TLS::gen_ssl_ctx_init(http2 => $self->{http2}))
1202             ->blank
1203             ->raw(Hypersonic::TLS::gen_ssl_accept())
1204             ->blank
1205             ->raw(Hypersonic::TLS::gen_ssl_io())
1206             ->blank
1207             ->raw(Hypersonic::TLS::gen_ssl_close())
1208             ->blank;
1209             }
1210            
1211             # HTTP/2 code generation - nghttp2 callbacks, session init, dispatchers
1212 14 50       73 if ($self->{http2}) {
1213 0         0 require Hypersonic::Protocol::HTTP2;
1214 0         0 $builder->comment('HTTP/2 support via nghttp2');
1215 0         0 Hypersonic::Protocol::HTTP2->gen_connection_struct($builder);
1216 0         0 Hypersonic::Protocol::HTTP2->gen_connection_preface_check($builder);
1217 0         0 Hypersonic::Protocol::HTTP2->gen_response_sender($builder);
1218 0         0 Hypersonic::Protocol::HTTP2->gen_404_response($builder);
1219 0         0 Hypersonic::Protocol::HTTP2->gen_callbacks($builder);
1220 0         0 Hypersonic::Protocol::HTTP2->gen_session_init($builder);
1221 0         0 Hypersonic::Protocol::HTTP2->gen_dispatcher($builder);
1222 0         0 Hypersonic::Protocol::HTTP2->gen_input_processor($builder);
1223 0         0 $builder->blank;
1224             }
1225              
1226             # Streaming support - JIT: only generate when streaming handlers detected
1227 14         81 my $analysis = $self->{route_analysis};
1228 14 100       61 if ($analysis->{needs_streaming}) {
1229 2         49 require Hypersonic::Stream;
1230             Hypersonic::Stream->generate_c_code($builder, {
1231             max_streams => $self->{max_connections},
1232 2         51 });
1233              
1234             # SSE support - compile SSE methods when streaming is enabled
1235 2         2023 require Hypersonic::SSE;
1236             Hypersonic::SSE->generate_c_code($builder, {
1237             max_sse_instances => $self->{max_connections},
1238 2         21 });
1239             }
1240              
1241             # WebSocket support - JIT: only generate when WebSocket routes exist
1242 14 50       79 if ($analysis->{needs_websocket}) {
1243 0         0 require Hypersonic::WebSocket;
1244 0         0 require Hypersonic::Protocol::WebSocket;
1245 0         0 require Hypersonic::Protocol::WebSocket::Frame;
1246              
1247             # Generate WebSocket frame encoding functions
1248             Hypersonic::Protocol::WebSocket::Frame->generate_c_code($builder, {
1249             max_connections => $self->{max_connections},
1250 0         0 });
1251              
1252             # Generate WebSocket connection management
1253             Hypersonic::WebSocket->generate_c_code($builder, {
1254             max_websockets => $self->{max_connections},
1255 0         0 });
1256             }
1257              
1258             # WebSocket Handler - JIT: connection registry, only when websocket routes exist
1259 14 50       57 if ($analysis->{needs_websocket_handler}) {
1260 0         0 require Hypersonic::WebSocket::Handler;
1261             Hypersonic::WebSocket::Handler->generate_c_code($builder, {
1262             max_connections => $self->{max_connections},
1263 0         0 });
1264             }
1265              
1266             # WebSocket Rooms - JIT: broadcast groups, only when explicitly enabled
1267 14 50       55 if ($analysis->{needs_websocket_rooms}) {
1268 0         0 require Hypersonic::WebSocket::Room;
1269             Hypersonic::WebSocket::Room->generate_c_code($builder, {
1270             max_rooms => $self->{max_rooms},
1271             max_clients_per_room => $self->{max_clients_per_room},
1272 0         0 });
1273             }
1274              
1275             # Future/Pool - JIT: async thread pool for blocking operations
1276 14 50       45 if ($analysis->{needs_async_pool}) {
1277 0         0 require Hypersonic::Future;
1278 0         0 require Hypersonic::Future::Pool;
1279 0   0     0 my $async_config = $self->{_async_config} // {};
1280             Hypersonic::Future->generate_c_code($builder, {
1281 0   0     0 max_futures => $async_config->{max_futures} // 65536,
1282             });
1283             Hypersonic::Future::Pool->generate_c_code($builder, {
1284             workers => $async_config->{workers} // 8,
1285 0   0     0 queue_size => $async_config->{queue_size} // 4096,
      0        
1286             });
1287             }
1288              
1289             # need_xs_builder routes - call handlers with fresh builder
1290 14 50       64 if ($analysis->{needs_xs_builder}) {
1291 0         0 my @xs_builder_routes;
1292 0         0 for my $i (0 .. $#{$self->{routes}}) {
  0         0  
1293 0         0 my $route = $self->{routes}[$i];
1294 0 0       0 next unless $route->{need_xs_builder};
1295            
1296             # Create fresh builder for this handler
1297 0         0 my $ext_builder = XS::JIT::Builder->new;
1298            
1299             # Call handler with clean builder
1300 0         0 my $result = $route->{handler}->($ext_builder);
1301            
1302             die "need_xs_builder handler for $route->{method} $route->{path} must return hashref with xs_function"
1303 0 0 0     0 unless ref($result) eq 'HASH' && $result->{xs_function};
1304            
1305             # Store result
1306 0         0 $route->{_xs_result} = $result;
1307 0         0 push @xs_builder_routes, {
1308             route_idx => $i,
1309             route => $route,
1310             result => $result,
1311             code => $ext_builder->code,
1312             };
1313             }
1314            
1315             # Emit generated code
1316 0 0       0 if (@xs_builder_routes) {
1317 0         0 $builder->comment('User XS builder routes');
1318 0         0 for my $xsr (@xs_builder_routes) {
1319             $builder->comment("Route: $xsr->{route}{method} $xsr->{route}{path}")
1320             ->raw($xsr->{code})
1321 0         0 ->blank;
1322             }
1323             }
1324            
1325             # Store for function merging later
1326 0         0 $self->{_xs_builder_routes} = \@xs_builder_routes;
1327             }
1328              
1329             # JIT: WebSocket handler storage (independent of dynamic routes)
1330 14 50       58 if ($analysis->{needs_websocket}) {
1331 0         0 $builder->comment('WebSocket handler storage')
1332             ->line('static SV* g_websocket_handlers = NULL;')
1333             ->blank;
1334             }
1335              
1336             # Global storage for dynamic handler dispatch (only if needed)
1337 14 100       126 if ($has_dynamic) {
1338 3         19 $builder->comment('Storage for dynamic handler callbacks')
1339             ->line('static SV* g_handler_array = NULL;')
1340             ->line('static SV* g_server_obj = NULL;');
1341              
1342             # JIT: Only generate middleware storage if middleware is present
1343 3 100       9 if ($analysis->{has_any_middleware}) {
1344 1         8 $builder->line('static SV* g_before_middleware = NULL;')
1345             ->line('static SV* g_after_middleware = NULL;')
1346             ->line('static SV* g_route_before_middleware = NULL;')
1347             ->line('static SV* g_route_after_middleware = NULL;');
1348             }
1349 3         15 $builder->blank;
1350              
1351             # Generate param info table for named path parameters
1352             # Structure: { param_name, segment_position } per handler
1353 3         17 $builder->comment('Path parameter info per dynamic handler')
1354             ->line('typedef struct { const char* name; int position; } ParamInfo;')
1355             ->line('typedef struct { int count; ParamInfo params[8]; } RouteParamInfo;')
1356             ->blank;
1357              
1358 3   50     5 my @route_params = @{$self->{route_param_info} // []};
  3         88  
1359 3         10 my $handler_count = scalar @route_params;
1360              
1361 3         22 $builder->line("static RouteParamInfo g_route_params[$handler_count] = {");
1362 3         23 for my $i (0 .. $#route_params) {
1363 6   50     14 my $params = $route_params[$i] // [];
1364 6         10 my $count = scalar @$params;
1365 6         4 my @param_strs;
1366 6         10 for my $p (@$params) {
1367 0         0 push @param_strs, qq({ "$p->{name}", $p->{position} });
1368             }
1369             # Pad to 8 elements with {NULL, 0}
1370 6         9 my $padding = 8 - scalar(@param_strs);
1371 6         13 for (1 .. $padding) {
1372 48         58 push @param_strs, '{NULL, 0}';
1373             }
1374 6         14 my $params_str = join(', ', @param_strs);
1375 6         22 $builder->line(" { $count, { $params_str } },");
1376             }
1377 3         12 $builder->line('};')
1378             ->blank;
1379              
1380             # JIT: Streaming handler flags array (only if streaming is enabled)
1381 3 50 66     21 if ($analysis->{needs_streaming} && $self->{_streaming_flags}) {
1382 2         4 my @flags = @{$self->{_streaming_flags}};
  2         8  
1383 2         4 my $flags_str = join(', ', @flags);
1384 2         12 $builder->comment('Streaming handler flags - 1 = streaming, 0 = normal')
1385             ->line("static int g_streaming_handlers[$handler_count] = { $flags_str };")
1386             ->blank;
1387             }
1388             }
1389              
1390             # JIT: WebSocket route paths array (only if WebSocket routes exist)
1391 14 0 33     69 if ($analysis->{needs_websocket} && $self->{_websocket_paths}) {
1392 0         0 my @paths = @{$self->{_websocket_paths}};
  0         0  
1393 0         0 my $ws_count = scalar @paths;
1394 0         0 $builder->comment('WebSocket route paths');
1395 0         0 for my $i (0 .. $#paths) {
1396 0         0 my $escaped = _escape_c_string($paths[$i]);
1397 0         0 $builder->line(qq{static const char WS_PATH_$i\[] = "$escaped";});
1398             }
1399 0         0 $builder->line("static const char* g_ws_paths[$ws_count] = {");
1400 0         0 for my $i (0 .. $#paths) {
1401 0 0       0 my $comma = ($i < $#paths) ? ',' : '';
1402 0         0 $builder->line(" WS_PATH_$i$comma");
1403             }
1404 0         0 $builder->line('};')
1405             ->line("static const int g_ws_path_count = $ws_count;")
1406             ->blank;
1407             }
1408              
1409             # Emit FULL pre-computed HTTP responses (headers + body)
1410 14         156 for my $i (0 .. $#$full_responses) {
1411 23         96 my $resp = $full_responses->[$i];
1412 23         146 my $escaped = _escape_c_string($resp);
1413 23         46 my $len = length($resp);
1414 23         252 $builder->line("static const char RESP_$i\[] = \"$escaped\";")
1415             ->line("static const int RESP_${i}_LEN = $len;");
1416             }
1417 14         53 $builder->blank;
1418              
1419             # 404 response via Protocol module
1420             my $security_hdrs_404 = $self->{enable_security_headers}
1421 14 50       295 ? $self->_get_security_headers_string()
1422             : '';
1423 14         256 my $resp_404 = $PROTOCOL->build_404_response(
1424             security_headers => $security_hdrs_404,
1425             );
1426            
1427 14         50 my $escaped_404 = _escape_c_string($resp_404);
1428 14         227 $builder->line("static const char RESP_404[] = \"$escaped_404\";")
1429             ->line("static const int RESP_404_LEN = " . length($resp_404) . ";")
1430             ->blank;
1431            
1432             # Security headers constant for dynamic responses
1433 14 100 66     161 if ($self->{enable_security_headers} && $has_dynamic) {
1434 3         11 $builder->raw($self->_gen_security_headers_c_constant())
1435             ->blank;
1436             }
1437              
1438             # Generate dynamic handler caller if needed
1439 14 100       39 if ($has_dynamic) {
1440 3         25 $builder->raw($self->_gen_dynamic_handler_caller())
1441             ->blank;
1442             }
1443              
1444             # Generate XS builder route dispatcher if needed
1445 14 0 33     70 if ($analysis->{needs_xs_builder} && $self->{_xs_builder_routes} && @{$self->{_xs_builder_routes}}) {
  0   0     0  
1446 0         0 $builder->raw($self->_gen_xs_builder_dispatcher())
1447             ->blank;
1448             }
1449              
1450             # Generate WebSocket handler caller if needed
1451 14 50       40 if ($analysis->{needs_websocket}) {
1452 0         0 $builder->raw($self->_gen_websocket_handler_caller())
1453             ->blank;
1454 0         0 $builder->raw($self->_gen_websocket_data_processor())
1455             ->blank;
1456             }
1457              
1458             # Group routes by method for dispatch generation
1459 14         30 my %methods;
1460 14         24 for my $route (@{$self->{routes}}) {
  14         71  
1461 29         42 push @{$methods{$route->{method}}}, $route;
  29         152  
1462             }
1463              
1464             # Generate inline C dispatch function
1465 14         197 $builder->comment('Inline dispatch - returns response pointer and length')
1466             ->comment('Returns: 0=static response, 1=dynamic route, 2=XS builder route, -1=404')
1467             ->comment('For dynamic/XS routes: returns handler_idx in *handler_idx_out')
1468             ->line("static $inline int dispatch_request(const char* method, int method_len, const char* path, int path_len, const char** resp_out, int* resp_len_out, int* handler_idx_out) {")
1469             ->line(' *handler_idx_out = -1;');
1470              
1471 14         56 for my $method (sort keys %methods) {
1472 18         34 my $mlen = length($method);
1473 18         129 $builder->if("method_len == $mlen && memcmp(method, \"$method\", $mlen) == 0");
1474              
1475 18         29 for my $r (@{$methods{$method}}) {
  18         43  
1476 29         113 my $path = $r->{path};
1477 29         47 my $plen = length($path);
1478              
1479 29 100       68 if (!$r->{dynamic}) {
1480             # Static route - return pre-computed response
1481 23         90 my $escaped_path = _escape_c_string($path);
1482 23         46 my $idx = $r->{response_idx};
1483              
1484 23         300 $builder->if("path_len == $plen && memcmp(path, \"$escaped_path\", $plen) == 0")
1485             ->line("*resp_out = RESP_$idx;")
1486             ->line("*resp_len_out = RESP_${idx}_LEN;")
1487             ->line("return 0;")
1488             ->endif;
1489             } else {
1490             # Dynamic route - check for path params or exact match
1491 6         10 my $handler_idx = $r->{handler_idx};
1492              
1493             # Check if this is a need_xs_builder route - dispatch to XS directly
1494 6 50 33     28 if ($r->{need_xs_builder} && $r->{_xs_result}) {
    50          
1495 0         0 my $xs_func = $r->{_xs_result}{xs_function};
1496 0         0 my $escaped_path = _escape_c_string($path);
1497            
1498 0 0       0 if ($path =~ /:(\w+)/) {
1499             # Path has parameters
1500 0         0 my ($prefix) = $path =~ m{^([^:]+)};
1501 0         0 my $prefix_len = length($prefix);
1502 0         0 my $escaped_prefix = _escape_c_string($prefix);
1503            
1504 0         0 $builder->if("path_len >= $prefix_len && memcmp(path, \"$escaped_prefix\", $prefix_len) == 0")
1505             ->comment("XS builder route - call $xs_func directly")
1506             ->line("*handler_idx_out = $handler_idx;")
1507             ->line("return 2;") # Special return code for XS builder routes
1508             ->endif;
1509             } else {
1510             # Exact match
1511 0         0 $builder->if("path_len == $plen && memcmp(path, \"$escaped_path\", $plen) == 0")
1512             ->comment("XS builder route - call $xs_func directly")
1513             ->line("*handler_idx_out = $handler_idx;")
1514             ->line("return 2;") # Special return code for XS builder routes
1515             ->endif;
1516             }
1517             } elsif ($path =~ /:(\w+)/) {
1518             # Path has parameters - generate pattern matching
1519 0         0 my ($prefix) = $path =~ m{^([^:]+)};
1520 0         0 my $prefix_len = length($prefix);
1521 0         0 my $escaped_prefix = _escape_c_string($prefix);
1522              
1523 0         0 $builder->if("path_len >= $prefix_len && memcmp(path, \"$escaped_prefix\", $prefix_len) == 0")
1524             ->line("*resp_out = NULL;")
1525             ->line("*handler_idx_out = $handler_idx;")
1526             ->line("return 1;")
1527             ->endif;
1528             } else {
1529             # Exact match dynamic route
1530 6         12 my $escaped_path = _escape_c_string($path);
1531 6         46 $builder->if("path_len == $plen && memcmp(path, \"$escaped_path\", $plen) == 0")
1532             ->line("*resp_out = NULL;")
1533             ->line("*handler_idx_out = $handler_idx;")
1534             ->line("return 1;")
1535             ->endif;
1536             }
1537             }
1538             }
1539              
1540 18         70 $builder->endif;
1541             }
1542              
1543 14         101 $builder->line('*resp_out = RESP_404;')
1544             ->line('*resp_len_out = RESP_404_LEN;')
1545             ->line('return -1;')
1546             ->line('}')
1547             ->blank;
1548              
1549             # Generate the pure C event loop using backend module
1550 14         116 $self->_gen_event_loop($builder, $backend);
1551 14         36 $builder->blank;
1552              
1553             # Keep the Perl-callable dispatch for testing/compatibility
1554 14         672 $builder->comment('Perl-callable dispatch wrapper')
1555             ->xs_function('hypersonic_dispatch')
1556             ->xs_preamble
1557             ->line('if (items != 1) croak_xs_usage(cv, "req_ref");')
1558             ->declare_sv('req_ref', 'ST(0)')
1559             ->if('!SvROK(req_ref) || SvTYPE(SvRV(req_ref)) != SVt_PVAV')
1560             ->line('ST(0) = &PL_sv_undef;')
1561             ->line('XSRETURN(1);')
1562             ->endif
1563             ->declare_av('req', '(AV*)SvRV(req_ref)')
1564             ->line('SV** ary = AvARRAY(req);')
1565             ->line('STRLEN method_len, path_len;')
1566             ->line('const char* method = SvPV(ary[0], method_len);')
1567             ->line('const char* path = SvPV(ary[1], path_len);')
1568             ->line('const char* resp;')
1569             ->line('int resp_len;')
1570             ->line('int handler_idx;')
1571             ->line('int rc = dispatch_request(method, (int)method_len, path, (int)path_len, &resp, &resp_len, &handler_idx);')
1572             ->if('rc == 0')
1573             ->comment('Static route')
1574             ->line('ST(0) = sv_2mortal(newSVpvn(resp, resp_len));')
1575             ->elsif('rc == 1')
1576             ->comment('Dynamic route - for testing, just return undef')
1577             ->line('ST(0) = &PL_sv_undef;')
1578             ->else
1579             ->line('ST(0) = &PL_sv_undef;')
1580             ->endif
1581             ->xs_return('1')
1582             ->xs_end;
1583              
1584 14         839 return $builder->code;
1585             }
1586              
1587             # Generate optimized method parser - delegates to Protocol module
1588             sub _gen_method_parser {
1589 14     14   31 my ($self, $builder) = @_;
1590 14         34 my $analysis = $self->{route_analysis};
1591              
1592             # Delegate to Protocol module for HTTP/1.1 parsing
1593             # This separation allows future HTTP/2 support without changing core logic
1594 14         78 return $PROTOCOL->gen_method_parser($builder, $analysis);
1595             }
1596              
1597             # Generate WebSocket frame handling for established connections
1598             sub _gen_websocket_frame_handler {
1599 0     0   0 my ($self, $builder) = @_;
1600              
1601 0         0 $builder->comment('Check if this is an established WebSocket connection')
1602             ->if('fd >= 0 && fd < WS_MAX && ws_registry[fd].state == WS_STATE_OPEN')
1603             ->comment('This is a WebSocket connection - process frames')
1604             ->line('process_websocket_data(aTHX_ fd, recv_buf, len);')
1605             ->line('continue;')
1606             ->endif
1607             ->blank;
1608             }
1609              
1610             # Generate WebSocket upgrade detection and dispatch
1611             sub _gen_websocket_dispatch {
1612 0     0   0 my ($self, $builder) = @_;
1613              
1614 0         0 $builder->comment('WebSocket upgrade detection')
1615             ->comment('Check for Upgrade: websocket header')
1616             ->line('int is_websocket = 0;')
1617             ->line('int ws_handler_idx = -1;')
1618             ->line('const char* ws_key = NULL;')
1619             ->line('int ws_key_len = 0;')
1620             ->blank
1621             ->comment('Look for Upgrade header in raw request')
1622             ->line('const char* upgrade_pos = strstr(recv_buf, "Upgrade:");')
1623             ->if('!upgrade_pos')
1624             ->line('upgrade_pos = strstr(recv_buf, "upgrade:");')
1625             ->endif
1626             ->if('upgrade_pos')
1627             ->line('const char* val_start = upgrade_pos + 8;')
1628             ->line('while (*val_start == \' \') val_start++;')
1629             ->comment('Check for websocket (case-insensitive)')
1630             ->if('strncasecmp(val_start, "websocket", 9) == 0')
1631             ->line('is_websocket = 1;')
1632             ->endif
1633             ->endif
1634             ->blank
1635             ->if('is_websocket')
1636             ->comment('Extract Sec-WebSocket-Key')
1637             ->line('const char* key_pos = strstr(recv_buf, "Sec-WebSocket-Key:");')
1638             ->if('!key_pos')
1639             ->line('key_pos = strstr(recv_buf, "sec-websocket-key:");')
1640             ->endif
1641             ->if('key_pos')
1642             ->line('const char* key_start = key_pos + 18;')
1643             ->line('while (*key_start == \' \') key_start++;')
1644             ->line('const char* key_end = key_start;')
1645             ->line('while (*key_end && *key_end != \'\\r\' && *key_end != \'\\n\') key_end++;')
1646             ->line('ws_key = key_start;')
1647             ->line('ws_key_len = key_end - key_start;')
1648             ->endif
1649             ->blank
1650             ->comment('Match path to WebSocket routes')
1651             ->line('int clean_path_len = path_len;')
1652             ->line('const char* qmark = memchr(path, \'?\', path_len);')
1653             ->if('qmark')
1654             ->line('clean_path_len = qmark - path;')
1655             ->endif
1656             ->for('int i = 0', 'i < g_ws_path_count', 'i++')
1657             ->line('int ws_path_len = strlen(g_ws_paths[i]);')
1658             ->if('clean_path_len == ws_path_len && memcmp(path, g_ws_paths[i], ws_path_len) == 0')
1659             ->line('ws_handler_idx = i;')
1660             ->line('break;')
1661             ->endif
1662             ->endfor
1663             ->blank
1664             ->if('ws_handler_idx >= 0 && ws_key')
1665             ->comment('WebSocket upgrade - call Perl handler')
1666             ->line('call_websocket_handler(aTHX_ ws_handler_idx, fd, path, path_len, ws_key, ws_key_len, recv_buf, len);')
1667             ->line('continue;') # Skip normal dispatch, connection stays open
1668             ->endif
1669             ->endif
1670             ->blank;
1671             }
1672              
1673             # Unified event loop generator - uses backend module for platform-specific code
1674             sub _gen_event_loop {
1675 14     14   42 my ($self, $builder, $backend) = @_;
1676 14         23 my $has_dynamic = grep { $_->{dynamic} } @{$self->{routes}};
  29         104  
  14         44  
1677 14         43 my $analysis = $self->{route_analysis};
1678 14   50     74 my $has_body_access = $analysis->{has_body_access} // 0;
1679 14         90 my $backend_name = $backend->name;
1680 14         63 my $event_struct = $backend->event_struct;
1681              
1682 14         459 $builder->comment("Pure C event loop using $backend_name backend - WITH SECURITY HARDENING")
1683             ->xs_function('hypersonic_run_event_loop')
1684             ->xs_preamble
1685             ->check_items(2, 2, 'listen_fd, server_obj')
1686             ->blank
1687             ->declare('int', 'listen_fd', '(int)SvIV(ST(0))')
1688             ->declare_sv('server_obj', 'ST(1)')
1689             ->blank;
1690              
1691             # Handler storage (only if dynamic routes)
1692 14 100       44 if ($has_dynamic) {
1693 3         42 $builder->comment('Store server object for dynamic handler access')
1694             ->if('SvROK(server_obj)')
1695             ->declare_hv('self', '(HV*)SvRV(server_obj)')
1696             ->line('SV** handlers_ref = hv_fetch(self, "dynamic_handlers", 16, 0);')
1697             ->if('handlers_ref && SvROK(*handlers_ref)')
1698             ->line('g_handler_array = *handlers_ref;')
1699             ->line('SvREFCNT_inc(g_handler_array);')
1700             ->endif
1701             ->line('g_server_obj = server_obj;')
1702             ->line('SvREFCNT_inc(g_server_obj);');
1703              
1704             # JIT: Only fetch middleware if any middleware is present
1705 3 100       7 if ($analysis->{has_any_middleware}) {
1706 1         4 $builder->blank
1707             ->comment('JIT: Middleware storage (Perl coderefs only - builders are inline C)');
1708              
1709             # Only fetch Perl middleware if we have any
1710 1 50       2 if ($analysis->{has_global_before}) {
1711 1         6 $builder->line('SV** before_ref = hv_fetch(self, "_perl_before_mw", 15, 0);')
1712             ->if('before_ref && SvROK(*before_ref)')
1713             ->line('g_before_middleware = *before_ref;')
1714             ->line('SvREFCNT_inc(g_before_middleware);')
1715             ->endif;
1716             }
1717 1 50       2 if ($analysis->{has_global_after}) {
1718 1         7 $builder->line('SV** after_ref = hv_fetch(self, "_perl_after_mw", 14, 0);')
1719             ->if('after_ref && SvROK(*after_ref)')
1720             ->line('g_after_middleware = *after_ref;')
1721             ->line('SvREFCNT_inc(g_after_middleware);')
1722             ->endif;
1723             }
1724              
1725             # Per-route middleware arrays
1726 1 50       3 if ($analysis->{has_route_middleware}) {
1727 0         0 $builder->line('SV** route_before_ref = hv_fetch(self, "_route_before_mw", 16, 0);')
1728             ->if('route_before_ref && SvROK(*route_before_ref)')
1729             ->line('g_route_before_middleware = *route_before_ref;')
1730             ->line('SvREFCNT_inc(g_route_before_middleware);')
1731             ->endif
1732             ->line('SV** route_after_ref = hv_fetch(self, "_route_after_mw", 15, 0);')
1733             ->if('route_after_ref && SvROK(*route_after_ref)')
1734             ->line('g_route_after_middleware = *route_after_ref;')
1735             ->line('SvREFCNT_inc(g_route_after_middleware);')
1736             ->endif;
1737             }
1738             }
1739              
1740 3         8 $builder->endif;
1741             } else {
1742 11         49 $builder->line('(void)server_obj; /* Not needed for static-only routes */');
1743             }
1744 14         40 $builder->blank;
1745              
1746             # WebSocket handler storage (only if WebSocket routes)
1747 14 50       50 if ($analysis->{needs_websocket}) {
1748 0         0 $builder->comment('Store WebSocket handlers for dispatch')
1749             ->if('SvROK(server_obj)')
1750             ->declare_hv('ws_self', '(HV*)SvRV(server_obj)')
1751             ->line('SV** ws_handlers_ref = hv_fetch(ws_self, "_websocket_handlers", 19, 0);')
1752             ->if('ws_handlers_ref && SvROK(*ws_handlers_ref)')
1753             ->line('g_websocket_handlers = *ws_handlers_ref;')
1754             ->line('SvREFCNT_inc(g_websocket_handlers);')
1755             ->endif
1756             ->endif
1757             ->blank;
1758             }
1759              
1760             # Signal handlers and initialization
1761 14         182 $builder->comment('Setup graceful shutdown signal handlers')
1762             ->line('signal(SIGTERM, handle_shutdown_signal);')
1763             ->line('signal(SIGINT, handle_shutdown_signal);')
1764             ->blank
1765             ->comment('Initialize connection tracking')
1766             ->line('memset(g_conn_time, 0, sizeof(g_conn_time));')
1767             ->line('g_active_connections = 0;')
1768             ->blank;
1769              
1770             # TLS initialization
1771 14 50       49 if ($self->{tls}) {
1772 0         0 my $cert_file = _escape_c_string($self->{cert_file});
1773 0         0 my $key_file = _escape_c_string($self->{key_file});
1774 0         0 $builder->comment('Initialize TLS/HTTPS')
1775             ->line('#ifdef HYPERSONIC_TLS')
1776             ->line("if (init_ssl_ctx(\"$cert_file\", \"$key_file\") != 0) {")
1777             ->line(' croak("Failed to initialize TLS context - check cert/key files");')
1778             ->line('}')
1779             ->line('memset(g_tls_connections, 0, sizeof(g_tls_connections));')
1780             ->line('#endif')
1781             ->blank;
1782             }
1783              
1784 14         106 $builder->comment('Thread-local receive buffer - each worker gets its own')
1785             ->line('static __thread char recv_buf[RECV_BUF_SIZE];')
1786             ->blank;
1787              
1788             # Backend-specific: Create event loop and add listen socket
1789 14         106 $backend->gen_create($builder, 'listen_fd');
1790              
1791             # Async Pool: Initialize thread pool and add notify fd to event loop
1792 14 50       51 if ($analysis->{needs_async_pool}) {
1793 0         0 $builder->blank
1794             ->comment('Initialize async thread pool')
1795             ->line('pool_init();')
1796             ->line('int pool_notify_fd = pool_get_notify_fd();');
1797 0         0 $backend->gen_add_pool_notify($builder, 'ev_fd', 'pool_notify_fd');
1798             }
1799              
1800             # Declare event structure variable based on backend
1801 14 50       59 if ($backend_name eq 'kqueue') {
    50          
    0          
1802             # kqueue's gen_create already declares 'ev', but we need events array
1803 0         0 $builder->line('struct kevent events[MAX_EVENTS];');
1804             } elsif ($backend_name eq 'epoll') {
1805             # epoll's gen_create already declares 'ev'
1806 14         40 $builder->line('struct epoll_event events[MAX_EVENTS];');
1807             } elsif ($backend_name eq 'io_uring') {
1808             # io_uring backend uses a value-copy ring buffer (hs_iouring_event_t
1809             # is defined in IOUring->defines). gen_wait drains all available
1810             # CQEs into a static array of these values and assigns it here.
1811 0         0 $builder->line('hs_iouring_event_t* events = NULL;');
1812             } else {
1813             # poll/select manage their own fd arrays internally
1814             # Declare a dummy events pointer to satisfy gen_wait signature
1815 0         0 $builder->line('void* events = NULL;');
1816             }
1817              
1818 14         177 $builder->line('time_t last_cleanup = time(NULL);')
1819             ->line('int accepting = 1; /* Flag to control accepting new connections */')
1820             ->blank;
1821              
1822             # Main event loop
1823 14         85 $builder->while('!g_shutdown || g_active_connections > 0')
1824             ->comment('Use timeout for keep-alive cleanup and shutdown check');
1825              
1826             # Backend-specific: Wait for events
1827 14         120 $backend->gen_wait($builder, 'ev_fd', 'events', 'n', '1000');
1828              
1829 14         87 $builder->blank
1830             ->comment('Check for graceful shutdown - stop accepting new connections')
1831             ->if('g_shutdown && accepting');
1832              
1833             # Backend-specific: Remove listen socket from event loop
1834 14         59 $backend->gen_del($builder, 'ev_fd', 'listen_fd');
1835 14         157 $builder->line('accepting = 0;')
1836             ->endif
1837             ->blank
1838             ->comment('Get time once per event batch')
1839             ->line('time_t now = time(NULL);')
1840             ->line('g_current_time = now;')
1841             ->blank;
1842              
1843             # Keep-alive cleanup. During graceful shutdown the
1844             # `now - last_cleanup >= 5` throttle is bypassed AND the per-conn
1845             # idle-time threshold is dropped to 0, so ALL connections are
1846             # drained in the next loop iteration after SIGTERM. Without this,
1847             # a server with an idle keep-alive connection from a client whose
1848             # process is hung in waitpid() on the server's exit would itself
1849             # hang for the full KEEPALIVE_TIMEOUT (30s default) before the
1850             # main loop's exit condition (!g_shutdown || g_active_connections > 0)
1851             # becomes false. This bit users of t/2100..t/2103 hard with io_uring
1852             # specifically, but the latent bug is in the cleanup loop itself
1853             # and affects every backend equally.
1854 14         236 $builder->comment('Periodic keep-alive timeout cleanup (forced during shutdown)')
1855             ->if('g_shutdown || now - last_cleanup >= 5')
1856             ->declare('int', 'cleanup_i', '0')
1857             ->declare('time_t', 'idle_threshold', 'g_shutdown ? 0 : KEEPALIVE_TIMEOUT')
1858             ->for('cleanup_i = 0', 'cleanup_i < MAX_FD', 'cleanup_i++')
1859             ->if('g_conn_time[cleanup_i] > 0')
1860             ->if('now - g_conn_time[cleanup_i] >= idle_threshold')
1861             ->comment('Close idle (or shutdown-forced) connection')
1862             ->line('int idle_fd = cleanup_i;');
1863              
1864             # Backend-specific: Remove idle connection
1865 14         73 $backend->gen_del($builder, 'ev_fd', 'idle_fd');
1866 14         182 $builder->line('HYPERSONIC_CLOSE(idle_fd);')
1867             ->line('remove_connection(idle_fd);')
1868             ->endif
1869             ->endif
1870             ->endfor
1871             ->line('last_cleanup = now;')
1872             ->endif
1873             ->blank;
1874              
1875             # Event processing loop
1876 14         89 $builder->declare('int', 'i', '0')
1877             ->for('i = 0', 'i < n', 'i++');
1878              
1879             # Backend-specific: Get fd from event
1880 14         73 $backend->gen_get_fd($builder, 'events', 'i', 'fd');
1881              
1882 14         73 $builder->blank
1883             ->if('fd == listen_fd && accepting');
1884              
1885             # Accept loop
1886 14         996 $builder->comment('Accept new connections with limit check')
1887             ->while('1')
1888             ->comment('Check connection limit before accepting')
1889             ->if('g_active_connections >= MAX_CONNECTIONS')
1890             ->line('break; /* At capacity, stop accepting */')
1891             ->endif
1892             ->blank
1893             ->line('struct sockaddr_in client_addr;')
1894             ->line('socklen_t client_len = sizeof(client_addr);')
1895             ->line('int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);')
1896             ->if('client_fd < 0')
1897             ->line('break;')
1898             ->endif
1899             ->blank
1900             ->comment('Set non-blocking (hs_set_nonblocking handles Win/POSIX divergence)')
1901             ->line('hs_set_nonblocking(client_fd);')
1902             ->blank
1903             ->comment('Disable Nagle')
1904             ->line('int one = 1;')
1905             ->line('setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, (const char*)&one, sizeof(one));')
1906             ->blank
1907             ->comment('Set receive timeout for security')
1908             ->line('struct timeval tv;')
1909             ->line('tv.tv_sec = RECV_TIMEOUT;')
1910             ->line('tv.tv_usec = 0;')
1911             ->line('setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));')
1912             ->blank
1913             ->comment('Track connection for keep-alive timeout')
1914             ->line('track_connection(client_fd, now);')
1915             ->blank
1916             ->comment('TLS handshake if enabled')
1917             ->line('#ifdef HYPERSONIC_TLS')
1918             ->line('if (tls_accept(client_fd) < 0) {')
1919             ->line(' close(client_fd);')
1920             ->line(' remove_connection(client_fd);')
1921             ->line(' continue;')
1922             ->line('}')
1923             ->line('#endif')
1924             ->blank
1925             ->comment('Add to event loop');
1926              
1927             # Backend-specific: Add client to event loop
1928 14         122 $backend->gen_add($builder, 'ev_fd', 'client_fd');
1929 14         59 $builder->endwhile;
1930              
1931             # Async Pool: Handle pool notify fd - process completed futures
1932 14 50       127 if ($analysis->{needs_async_pool}) {
1933 0         0 $builder->elsif('fd == pool_notify_fd')
1934             ->comment('Thread pool notification - process completed async operations')
1935             ->line('pool_process_ready();');
1936             }
1937              
1938             # Handle client request
1939 14         286 $builder->elsif('fd != listen_fd')
1940             ->comment('Handle client request')
1941             ->line('#ifdef HYPERSONIC_TLS')
1942             ->line('TLSConnection* tls_conn = get_tls_connection(fd);')
1943             ->line('ssize_t len = tls_conn ? tls_recv(tls_conn, recv_buf, RECV_BUF_SIZE - 1) : -1;')
1944             ->line('#else')
1945             ->line('ssize_t len = recv(fd, recv_buf, RECV_BUF_SIZE - 1, 0);')
1946             ->line('#endif')
1947             ->blank
1948             ->if('len <= 0')
1949             ->comment('Connection closed or error');
1950              
1951             # Backend-specific: Remove from event loop
1952 14         48 $backend->gen_del($builder, 'ev_fd', 'fd');
1953 14         108 $builder->line('#ifdef HYPERSONIC_TLS')
1954             ->line('tls_close(fd);')
1955             ->line('#else')
1956             ->line('close(fd);')
1957             ->line('#endif');
1958              
1959             # Reset WebSocket state if WebSocket routes exist
1960 14 50       85 if ($analysis->{needs_websocket}) {
1961 0         0 $builder->line('ws_reset(fd);');
1962             }
1963              
1964 14         263 $builder->line('remove_connection(fd);')
1965             ->line('continue;')
1966             ->endif
1967             ->blank
1968             ->comment('Update connection activity for keep-alive timeout')
1969             ->line('update_connection(fd, now);')
1970             ->blank
1971             ->line('recv_buf[len] = \'\\0\';')
1972             ->blank;
1973              
1974             # WebSocket frame handling - JIT: only generate if WebSocket routes exist
1975 14 50       40 if ($analysis->{needs_websocket}) {
1976 0         0 $self->_gen_websocket_frame_handler($builder);
1977             }
1978              
1979             # Method parser - delegates to Protocol module
1980 14         95 $self->_gen_method_parser($builder);
1981 14         33 $builder->blank;
1982              
1983             # Path parsing - delegates to Protocol module
1984 14         103 $PROTOCOL->gen_path_parser($builder);
1985 14         37 $builder->blank;
1986              
1987             # WebSocket upgrade detection - JIT: only generate if WebSocket routes exist
1988 14 50       45 if ($analysis->{needs_websocket}) {
1989 0         0 $self->_gen_websocket_dispatch($builder);
1990             }
1991              
1992             # Dispatch
1993 14         176 $builder->comment('Dispatch request')
1994             ->line('const char* resp;')
1995             ->line('int resp_len;')
1996             ->line('int handler_idx;')
1997             ->line('int dispatch_result = dispatch_request(method, method_len, path, path_len, &resp, &resp_len, &handler_idx);')
1998             ->blank;
1999              
2000             # Dynamic dispatch
2001 14 100       36 if ($has_dynamic) {
2002             # Check for XS builder routes first (dispatch_result == 2)
2003 3 50 33     12 if ($analysis->{needs_xs_builder} && $self->{_xs_builder_routes} && @{$self->{_xs_builder_routes}}) {
  0   0     0  
2004 0         0 $builder->if('dispatch_result == 2')
2005             ->comment('XS builder route - call generated XS function directly');
2006            
2007             # Body parsing for XS builder routes (they may need body access)
2008 0         0 $PROTOCOL->gen_body_parser($builder, has_body_access => $has_body_access);
2009            
2010 0         0 $builder->blank
2011             ->line('char* xs_resp;')
2012             ->line('int xs_resp_len;')
2013             ->line('call_xs_builder_handler(aTHX_ handler_idx, fd, method, method_len, path, path_len, body, body_len, &xs_resp, &xs_resp_len);')
2014             ->line('HYPERSONIC_SEND(fd, xs_resp, xs_resp_len);')
2015             ->elsif('dispatch_result == 1');
2016             } else {
2017 3         14 $builder->if('dispatch_result == 1');
2018             }
2019            
2020 3         9 $builder->comment('Dynamic route - call Perl handler');
2021              
2022             # Body parsing - delegates to Protocol module
2023 3         17 $PROTOCOL->gen_body_parser($builder, has_body_access => $has_body_access);
2024              
2025 3         104 $builder->blank
2026             ->line('char* dyn_resp;')
2027             ->line('int dyn_resp_len;')
2028             ->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);')
2029             ->comment('dyn_resp_len == -1 means streaming handler (response already sent)')
2030             ->line('if (dyn_resp_len >= 0) {')
2031             ->line(' HYPERSONIC_SEND(fd, dyn_resp, dyn_resp_len);')
2032             ->line('}')
2033             ->else
2034             ->line('HYPERSONIC_SEND(fd, resp, resp_len);')
2035             ->endif;
2036             } else {
2037 11         45 $builder->line('HYPERSONIC_SEND(fd, resp, resp_len);');
2038             }
2039 14         36 $builder->blank;
2040              
2041             # Keep-alive check - delegates to Protocol module
2042 14         129 $PROTOCOL->gen_keepalive_check($builder);
2043 14         51 $builder->blank
2044             ->if('!keep_alive');
2045              
2046             # Backend-specific: Remove from event loop on close
2047 14         77 $backend->gen_del($builder, 'ev_fd', 'fd');
2048 14         47 $builder->line('HYPERSONIC_CLOSE(fd);');
2049              
2050             # Reset WebSocket state if WebSocket routes exist
2051 14 50       42 if ($analysis->{needs_websocket}) {
2052 0         0 $builder->line('ws_reset(fd);');
2053             }
2054              
2055 14         71 $builder->line('remove_connection(fd);')
2056             ->endif;
2057              
2058             # Close event processing
2059 14         78 $builder->endif # fd == listen_fd
2060             ->endfor # for i
2061             ->endwhile # main loop
2062             ->blank;
2063              
2064             # Async Pool: Shutdown thread pool on server exit
2065 14 50       49 if ($analysis->{needs_async_pool}) {
2066 0         0 $builder->comment('Shutdown async thread pool')
2067             ->line('pool_shutdown();');
2068             }
2069              
2070 14         121 $builder->line('close(ev_fd);')
2071             ->xs_return('0')
2072             ->xs_end;
2073              
2074 14         27 return $builder;
2075             }
2076              
2077             sub _gen_xs_builder_dispatcher {
2078 0     0   0 my ($self) = @_;
2079            
2080 0         0 my $builder = XS::JIT::Builder->new;
2081 0   0     0 my $xs_routes = $self->{_xs_builder_routes} || [];
2082            
2083 0 0       0 return '' unless @$xs_routes;
2084            
2085 0         0 $builder->comment('XS Builder route dispatcher')
2086             ->comment('Dispatches to user-defined XS functions based on handler_idx')
2087             ->line('static void call_xs_builder_handler(pTHX_ int handler_idx, int fd,')
2088             ->line(' const char* method, int method_len,')
2089             ->line(' const char* path, int path_len,')
2090             ->line(' const char* body, int body_len,')
2091             ->line(' char** resp_out, int* resp_len_out) {')
2092             ->line(' switch (handler_idx) {');
2093            
2094 0         0 for my $entry (@$xs_routes) {
2095 0         0 my $handler_idx = $entry->{route}{handler_idx};
2096 0         0 my $xs_func = $entry->{result}{xs_function};
2097            
2098 0         0 $builder->line(" case $handler_idx:")
2099             ->line(" $xs_func(aTHX_ fd, method, method_len, path, path_len, body, body_len, resp_out, resp_len_out);")
2100             ->line(" break;");
2101             }
2102            
2103 0         0 $builder->line(' default:')
2104             ->line(' *resp_out = (char*)RESP_404;')
2105             ->line(' *resp_len_out = RESP_404_LEN;')
2106             ->line(' break;')
2107             ->line(' }')
2108             ->line('}')
2109             ->blank;
2110            
2111 0         0 return $builder->code;
2112             }
2113              
2114             sub _gen_dynamic_handler_caller {
2115 3     3   9 my ($self) = @_;
2116 3         7 my $analysis = $self->{route_analysis};
2117 3         82 my $builder = XS::JIT::Builder->new;
2118            
2119             # ============================================================
2120             # JIT PHILOSOPHY: Only generate code for features actually used
2121             # ============================================================
2122            
2123             # Status code helper - always needed for dynamic routes
2124 3         132 $builder->comment('Status code to text mapping')
2125             ->line('static const char* get_status_text(int code) {')
2126             ->line(' switch(code) {')
2127             ->line(' case 200: return "OK";')
2128             ->line(' case 201: return "Created";')
2129             ->line(' case 202: return "Accepted";')
2130             ->line(' case 204: return "No Content";')
2131             ->line(' case 301: return "Moved Permanently";')
2132             ->line(' case 302: return "Found";')
2133             ->line(' case 303: return "See Other";')
2134             ->line(' case 304: return "Not Modified";')
2135             ->line(' case 400: return "Bad Request";')
2136             ->line(' case 401: return "Unauthorized";')
2137             ->line(' case 403: return "Forbidden";')
2138             ->line(' case 404: return "Not Found";')
2139             ->line(' case 405: return "Method Not Allowed";')
2140             ->line(' case 408: return "Request Timeout";')
2141             ->line(' case 409: return "Conflict";')
2142             ->line(' case 413: return "Payload Too Large";')
2143             ->line(' case 422: return "Unprocessable Entity";')
2144             ->line(' case 429: return "Too Many Requests";')
2145             ->line(' case 500: return "Internal Server Error";')
2146             ->line(' case 502: return "Bad Gateway";')
2147             ->line(' case 503: return "Service Unavailable";')
2148             ->line(' default: return "Unknown";')
2149             ->line(' }')
2150             ->line('}')
2151             ->blank;
2152            
2153             # Path segment parser - always needed for path params
2154 3         86 $builder->comment('Parse path segments into array - stops at ? for query string')
2155             ->line('static int parse_path_segments(const char* path, int path_len,')
2156             ->line(' const char** segments, int* seg_lens, int max_segs) {')
2157             ->line(' int count = 0;')
2158             ->line(' const char* start;')
2159             ->line(' const char* end;')
2160             ->line(' const char* query;')
2161             ->line(' const char* seg_end;')
2162             ->line(' start = path;')
2163             ->line(' end = path + path_len;')
2164             ->line(' query = memchr(path, \'?\', path_len);')
2165             ->line(' if (query) end = query;')
2166             ->line(' while (start < end && count < max_segs) {')
2167             ->line(' if (*start == \'/\') start++;')
2168             ->line(' if (start >= end) break;')
2169             ->line(' seg_end = start;')
2170             ->line(' while (seg_end < end && *seg_end != \'/\') seg_end++;')
2171             ->line(' segments[count] = start;')
2172             ->line(' seg_lens[count] = seg_end - start;')
2173             ->line(' count++;')
2174             ->line(' start = seg_end;')
2175             ->line(' }')
2176             ->line(' return count;')
2177             ->line('}')
2178             ->blank;
2179            
2180             # URL decoder - only if query/form parsing needed
2181 3 50 33     26 if ($analysis->{needs_query} || $analysis->{needs_form}) {
2182 0         0 $builder->comment('JIT: URL decode helper (query/form parsing enabled)')
2183             ->line('static int url_decode(char* str, int len) {')
2184             ->line(' char* dst = str;')
2185             ->line(' const char* src = str;')
2186             ->line(' const char* end = str + len;')
2187             ->line(' while (src < end) {')
2188             ->line(' if (*src == \'%\' && src + 2 < end) {')
2189             ->line(' int hi = src[1];')
2190             ->line(' int lo = src[2];')
2191             ->line(' hi = (hi >= \'0\' && hi <= \'9\') ? hi - \'0\' :')
2192             ->line(' (hi >= \'A\' && hi <= \'F\') ? hi - \'A\' + 10 :')
2193             ->line(' (hi >= \'a\' && hi <= \'f\') ? hi - \'a\' + 10 : -1;')
2194             ->line(' lo = (lo >= \'0\' && lo <= \'9\') ? lo - \'0\' :')
2195             ->line(' (lo >= \'A\' && lo <= \'F\') ? lo - \'A\' + 10 :')
2196             ->line(' (lo >= \'a\' && lo <= \'f\') ? lo - \'a\' + 10 : -1;')
2197             ->line(' if (hi >= 0 && lo >= 0) {')
2198             ->line(' *dst++ = (char)((hi << 4) | lo);')
2199             ->line(' src += 3;')
2200             ->line(' continue;')
2201             ->line(' }')
2202             ->line(' } else if (*src == \'+\') {')
2203             ->line(' *dst++ = \' \';')
2204             ->line(' src++;')
2205             ->line(' continue;')
2206             ->line(' }')
2207             ->line(' *dst++ = *src++;')
2208             ->line(' }')
2209             ->line(' return dst - str;')
2210             ->line('}')
2211             ->blank;
2212            
2213 0         0 $builder->comment('JIT: Query string parser (query/form parsing enabled)')
2214             ->line('static void parse_query_string(pTHX_ const char* query, int query_len, HV* hv) {')
2215             ->line(' const char* start = query;')
2216             ->line(' const char* end = query + query_len;')
2217             ->line(' while (start < end) {')
2218             ->line(' const char* eq = memchr(start, \'=\', end - start);')
2219             ->line(' const char* amp = memchr(start, \'&\', end - start);')
2220             ->line(' if (!amp) amp = end;')
2221             ->line(' if (eq && eq < amp) {')
2222             ->line(' int key_len = eq - start;')
2223             ->line(' const char* val = eq + 1;')
2224             ->line(' int val_len = amp - val;')
2225             ->line(' char val_buf[1024];')
2226             ->line(' if (val_len < (int)sizeof(val_buf)) {')
2227             ->line(' memcpy(val_buf, val, val_len);')
2228             ->line(' val_len = url_decode(val_buf, val_len);')
2229             ->line(' hv_store(hv, start, key_len, newSVpvn(val_buf, val_len), 0);')
2230             ->line(' }')
2231             ->line(' } else if (amp > start) {')
2232             ->line(' hv_store(hv, start, amp - start, newSVpvn("", 0), 0);')
2233             ->line(' }')
2234             ->line(' start = amp + 1;')
2235             ->line(' }')
2236             ->line('}')
2237             ->blank;
2238             }
2239            
2240             # Header parser - only if headers/cookies/json/form needed
2241 3 50 66     36 if ($analysis->{needs_headers} || $analysis->{needs_cookies} ||
      66        
      33        
2242             $analysis->{needs_json} || $analysis->{needs_form}) {
2243 1         39 $builder->comment('JIT: Header parser (headers/cookies/json/form enabled)')
2244             ->line('static void parse_headers(pTHX_ const char* raw, int raw_len, HV* hv) {')
2245             ->line(' const char* line = memchr(raw, \'\\n\', raw_len);')
2246             ->line(' if (!line) return;')
2247             ->line(' line++;')
2248             ->line(' const char* end = raw + raw_len;')
2249             ->line(' while (line < end) {')
2250             ->line(' const char* line_end = memchr(line, \'\\r\', end - line);')
2251             ->line(' if (!line_end) line_end = memchr(line, \'\\n\', end - line);')
2252             ->line(' if (!line_end || line_end == line) break;')
2253             ->line(' const char* colon = memchr(line, \':\', line_end - line);')
2254             ->line(' if (colon) {')
2255             ->line(' int name_len = colon - line;')
2256             ->line(' const char* value = colon + 1;')
2257             ->line(' while (value < line_end && (*value == \' \' || *value == \'\\t\')) value++;')
2258             ->line(' int value_len = line_end - value;')
2259             ->line(' char name_buf[128];')
2260             ->line(' if (name_len < (int)sizeof(name_buf)) {')
2261             ->line(' int i;')
2262             ->line(' for (i = 0; i < name_len; i++) {')
2263             ->line(' char c = line[i];')
2264             ->line(' name_buf[i] = (c >= \'A\' && c <= \'Z\') ? c + 32 : (c == \'-\') ? \'_\' : c;')
2265             ->line(' }')
2266             ->line(' hv_store(hv, name_buf, name_len, newSVpvn(value, value_len), 0);')
2267             ->line(' }')
2268             ->line(' }')
2269             ->line(' line = line_end;')
2270             ->line(' if (*line == \'\\r\') line++;')
2271             ->line(' if (line < end && *line == \'\\n\') line++;')
2272             ->line(' }')
2273             ->line('}')
2274             ->blank;
2275             }
2276            
2277             # JIT: Generate C helpers from builder middleware (static functions at file scope)
2278 3 50 33     47 if ($analysis->{has_builder_before} || $analysis->{has_builder_after}) {
2279 0         0 my %seen_classes;
2280 0         0 $builder->comment('JIT: Builder middleware C helpers (zero Perl in hot path)');
2281 0         0 for my $mw (@{$analysis->{builder_before}}, @{$analysis->{builder_after}}) {
  0         0  
  0         0  
2282 0         0 my $class = ref($mw);
2283 0 0       0 next if $seen_classes{$class}++; # Deduplicate by class
2284 0 0       0 if ($mw->can('build_helpers')) {
2285 0         0 $mw->build_helpers($builder);
2286 0         0 $builder->blank;
2287             }
2288             }
2289             }
2290              
2291             # JIT: Generate middleware helper function BEFORE call_dynamic_handler
2292 3 100       6 if ($analysis->{has_any_middleware}) {
2293 1         32 $builder->comment('JIT: Middleware helper - call array of handlers, short-circuit on defined return')
2294             ->line('static SV* call_middleware_chain(pTHX_ AV* handlers, SV* req_ref) {')
2295             ->line(' dSP;')
2296             ->line(' SSize_t len = av_len(handlers) + 1;')
2297             ->line(' SSize_t i;')
2298             ->line(' for (i = 0; i < len; i++) {')
2299             ->line(' SV** handler_sv = av_fetch(handlers, i, 0);')
2300             ->line(' if (!handler_sv || !SvROK(*handler_sv)) continue;')
2301             ->line(' PUSHMARK(SP);')
2302             ->line(' XPUSHs(req_ref);')
2303             ->line(' PUTBACK;')
2304             ->line(' int count = call_sv(*handler_sv, G_SCALAR | G_EVAL);')
2305             ->line(' SPAGAIN;')
2306             ->line(' if (SvTRUE(ERRSV)) {')
2307             ->line(' POPs;')
2308             ->line(' continue;')
2309             ->line(' }')
2310             ->line(' if (count == 1) {')
2311             ->line(' SV* result = POPs;')
2312             ->line(' PUTBACK;')
2313             ->line(' if (SvOK(result)) {')
2314             ->line(' return SvREFCNT_inc(result);')
2315             ->line(' }')
2316             ->line(' }')
2317             ->line(' PUTBACK;')
2318             ->line(' }')
2319             ->line(' return NULL;')
2320             ->line('}')
2321             ->blank;
2322             }
2323            
2324             # Main handler function
2325 3         199 $builder->comment('Call dynamic Perl handler and format HTTP response')
2326             ->line('static void call_dynamic_handler(pTHX_ int handler_idx, int client_fd,')
2327             ->line(' const char* method, int method_len,')
2328             ->line(' const char* path, int path_len,')
2329             ->line(' const char* body, int body_len,')
2330             ->line(' const char* raw_request, int raw_request_len,')
2331             ->line(' char** resp_out, int* resp_len_out) {')
2332             ->line(' dSP;')
2333             ->line(' int count;')
2334             ->line(' SV* result;')
2335             ->line(' STRLEN len;')
2336             ->line(' const char* body_str;')
2337             ->line(' int status = 200;')
2338             ->line(' const char* content_type = "text/plain";')
2339             ->line(' AV* handlers;')
2340             ->line(' SV** handler_sv;')
2341             ->line(' const char* query_start;')
2342             ->line(' int clean_path_len;')
2343             ->line(' AV* req;')
2344             ->line(' const char* segments[16];')
2345             ->line(' int seg_lens[16];')
2346             ->line(' int seg_count;')
2347             ->line(' AV* seg_av;')
2348             ->line(' int i;')
2349             ->line(' HV* params_hv;')
2350             ->line(' RouteParamInfo* param_info;')
2351             ->line(' SV* req_ref;')
2352             ->line(' SV* mw_result = NULL;')
2353             ->line(' int short_circuit = 0;')
2354             ->line(' HV* headers_hv = NULL;')
2355             ->line(' int is_streaming = 0;')
2356             ->line(' SV* stream_sv = NULL;')
2357             ->blank
2358             ->line(' if (!g_handler_array) {')
2359             ->line(' *resp_out = (char*)RESP_404;')
2360             ->line(' *resp_len_out = RESP_404_LEN;')
2361             ->line(' return;')
2362             ->line(' }')
2363             ->blank
2364             ->line(' handlers = (AV*)SvRV(g_handler_array);')
2365             ->line(' handler_sv = av_fetch(handlers, handler_idx, 0);')
2366             ->line(' if (!handler_sv || !SvROK(*handler_sv)) {')
2367             ->line(' *resp_out = (char*)RESP_404;')
2368             ->line(' *resp_len_out = RESP_404_LEN;')
2369             ->line(' return;')
2370             ->line(' }')
2371             ->blank;
2372            
2373             # Query string separation
2374 3         73 $builder->comment('Separate path from query string')
2375             ->line(' query_start = memchr(path, \'?\', path_len);')
2376             ->line(' clean_path_len = query_start ? (query_start - path) : path_len;')
2377             ->blank
2378             ->comment('Build array-based request object (JIT slots)')
2379             ->comment('Slot layout: METHOD=0, PATH=1, BODY=2, PARAMS=3, QUERY=4, QUERY_STRING=5,')
2380             ->comment(' HEADERS=6, COOKIES=7, JSON=8, FORM=9, SEGMENTS=10, ID=11')
2381             ->line(' req = newAV();')
2382             ->line(' av_extend(req, 11);') # Pre-allocate 12 slots (0-11)
2383             ->line(' av_store(req, 0, newSVpvn(method, method_len));') # SLOT_METHOD
2384             ->line(' av_store(req, 1, newSVpvn(path, clean_path_len));') # SLOT_PATH
2385             ->line(' av_store(req, 2, newSVpvn(body, body_len));') # SLOT_BODY
2386             ->blank;
2387              
2388             # Path segments and params - always needed
2389 3         108 $builder->comment('Parse path segments and named params')
2390             ->line(' seg_count = parse_path_segments(path, path_len, segments, seg_lens, 16);')
2391             ->line(' seg_av = newAV();')
2392             ->line(' for (i = 0; i < seg_count; i++) {')
2393             ->line(' av_push(seg_av, newSVpvn(segments[i], seg_lens[i]));')
2394             ->line(' }')
2395             ->line(' av_store(req, 10, newRV_noinc((SV*)seg_av));') # SLOT_SEGMENTS
2396             ->blank
2397             ->comment('Build named params from route_param_info table')
2398             ->line(' params_hv = newHV();')
2399             ->line(' param_info = &g_route_params[handler_idx];')
2400             ->line(' for (i = 0; i < param_info->count && i < seg_count; i++) {')
2401             ->line(' int pos = param_info->params[i].position;')
2402             ->line(' if (pos < seg_count) {')
2403             ->line(' hv_store(params_hv, param_info->params[i].name,')
2404             ->line(' strlen(param_info->params[i].name),')
2405             ->line(' newSVpvn(segments[pos], seg_lens[pos]), 0);')
2406             ->line(' }')
2407             ->line(' }')
2408             ->line(' av_store(req, 3, newRV_noinc((SV*)params_hv));') # SLOT_PARAMS
2409             ->line(' if (seg_count > 0) {')
2410             ->line(' av_store(req, 11, newSVpvn(segments[seg_count-1], seg_lens[seg_count-1]));') # SLOT_ID
2411             ->line(' } else {')
2412             ->line(' av_store(req, 11, newSVpvn("", 0));') # SLOT_ID (empty)
2413             ->line(' }')
2414             ->blank;
2415            
2416             # Query string parsing - JIT conditional
2417 3 50       10 if ($analysis->{needs_query}) {
2418 0         0 $builder->comment('JIT: Parse query string (parse_query enabled)')
2419             ->line(' if (query_start) {')
2420             ->line(' HV* query_hv = newHV();')
2421             ->line(' int query_len = path_len - (query_start - path) - 1;')
2422             ->line(' parse_query_string(aTHX_ query_start + 1, query_len, query_hv);')
2423             ->line(' av_store(req, 4, newRV_noinc((SV*)query_hv));') # SLOT_QUERY
2424             ->line(' av_store(req, 5, newSVpvn(query_start + 1, query_len));') # SLOT_QUERY_STRING
2425             ->line(' } else {')
2426             ->line(' av_store(req, 4, newRV_noinc((SV*)newHV()));') # SLOT_QUERY
2427             ->line(' av_store(req, 5, newSVpvn("", 0));') # SLOT_QUERY_STRING
2428             ->line(' }')
2429             ->blank;
2430             } else {
2431 3         16 $builder->comment('JIT: Query parsing SKIPPED (no routes use parse_query)')
2432             ->line(' av_store(req, 4, newRV_noinc((SV*)newHV()));') # SLOT_QUERY
2433             ->line(' av_store(req, 5, newSVpvn("", 0));') # SLOT_QUERY_STRING
2434             ->blank;
2435             }
2436            
2437             # Header parsing - JIT conditional
2438             my $needs_header_parse = $analysis->{needs_headers} || $analysis->{needs_cookies} ||
2439 3   33     37 $analysis->{needs_json} || $analysis->{needs_form};
2440            
2441 3 100       8 if ($needs_header_parse) {
2442 1         7 $builder->comment('JIT: Parse headers (headers/cookies/json/form enabled)')
2443             ->line(' headers_hv = newHV();')
2444             ->line(' parse_headers(aTHX_ raw_request, raw_request_len, headers_hv);')
2445             ->line(' av_store(req, 6, newRV_noinc((SV*)headers_hv));') # SLOT_HEADERS
2446             ->blank;
2447            
2448             # Cookie parsing
2449 1 50       4 if ($analysis->{needs_cookies}) {
2450 1         37 $builder->comment('JIT: Parse cookies (parse_cookies enabled)')
2451             ->line(' SV** cookie_sv = hv_fetch(headers_hv, "cookie", 6, 0);')
2452             ->line(' if (cookie_sv && SvOK(*cookie_sv)) {')
2453             ->line(' HV* cookies_hv = newHV();')
2454             ->line(' STRLEN cookie_len;')
2455             ->line(' const char* cookie_str = SvPV(*cookie_sv, cookie_len);')
2456             ->line(' const char* start = cookie_str;')
2457             ->line(' const char* end = cookie_str + cookie_len;')
2458             ->line(' while (start < end) {')
2459             ->line(' while (start < end && (*start == \' \' || *start == \';\')) start++;')
2460             ->line(' if (start >= end) break;')
2461             ->line(' const char* eq = memchr(start, \'=\', end - start);')
2462             ->line(' const char* semi = memchr(start, \';\', end - start);')
2463             ->line(' if (!semi) semi = end;')
2464             ->line(' if (eq && eq < semi) {')
2465             ->line(' int name_len = eq - start;')
2466             ->line(' const char* val = eq + 1;')
2467             ->line(' int val_len = semi - val;')
2468             ->line(' hv_store(cookies_hv, start, name_len, newSVpvn(val, val_len), 0);')
2469             ->line(' }')
2470             ->line(' start = semi + 1;')
2471             ->line(' }')
2472             ->line(' av_store(req, 7, newRV_noinc((SV*)cookies_hv));') # SLOT_COOKIES
2473             ->line(' } else {')
2474             ->line(' av_store(req, 7, newRV_noinc((SV*)newHV()));') # SLOT_COOKIES
2475             ->line(' }')
2476             ->blank;
2477             } else {
2478 0         0 $builder->comment('JIT: Cookie parsing SKIPPED')
2479             ->line(' av_store(req, 7, newRV_noinc((SV*)newHV()));') # SLOT_COOKIES
2480             ->blank;
2481             }
2482            
2483             # Form/JSON parsing
2484 1 50 33     8 if ($analysis->{needs_form} || $analysis->{needs_json}) {
2485 1         5 $builder->comment('JIT: Form/JSON body parsing')
2486             ->line(' SV** ct_sv = hv_fetch(headers_hv, "content_type", 12, 0);')
2487             ->line(' if (ct_sv && SvOK(*ct_sv) && body_len > 0) {')
2488             ->line(' STRLEN ct_len;')
2489             ->line(' const char* ct_str = SvPV(*ct_sv, ct_len);');
2490            
2491 1 50       2 if ($analysis->{needs_form}) {
2492 0         0 $builder->line(' if (ct_len >= 33 && memcmp(ct_str, "application/x-www-form-urlencoded", 33) == 0) {')
2493             ->line(' HV* form_hv = newHV();')
2494             ->line(' parse_query_string(aTHX_ body, body_len, form_hv);')
2495             ->line(' av_store(req, 9, newRV_noinc((SV*)form_hv));') # SLOT_FORM
2496             ->line(' } else {')
2497             ->line(' av_store(req, 9, newRV_noinc((SV*)newHV()));') # SLOT_FORM
2498             ->line(' }');
2499             } else {
2500 1         2 $builder->line(' av_store(req, 9, newRV_noinc((SV*)newHV()));'); # SLOT_FORM
2501             }
2502              
2503 1 50       2 if ($analysis->{needs_json}) {
2504 1         66 $builder->line(' if (ct_len >= 16 && memcmp(ct_str, "application/json", 16) == 0) {')
2505             ->line(' dSP;')
2506             ->line(' PUSHMARK(SP);')
2507             ->line(' XPUSHs(sv_2mortal(newSVpvn(body, body_len)));')
2508             ->line(' PUTBACK;')
2509             ->line(' int json_count = call_pv("Hypersonic::_decode_json", G_SCALAR | G_EVAL);')
2510             ->line(' SPAGAIN;')
2511             ->line(' if (json_count == 1 && !SvTRUE(ERRSV)) {')
2512             ->line(' SV* json_sv = POPs;')
2513             ->line(' av_store(req, 8, SvREFCNT_inc(json_sv));') # SLOT_JSON
2514             ->line(' } else {')
2515             ->line(' if (SvTRUE(ERRSV)) POPs;')
2516             ->line(' av_store(req, 8, &PL_sv_undef);') # SLOT_JSON
2517             ->line(' }')
2518             ->line(' PUTBACK;')
2519             ->line(' } else {')
2520             ->line(' av_store(req, 8, &PL_sv_undef);') # SLOT_JSON
2521             ->line(' }');
2522             } else {
2523 0         0 $builder->line(' av_store(req, 8, &PL_sv_undef);'); # SLOT_JSON
2524             }
2525              
2526 1         7 $builder->line(' } else {')
2527             ->line(' av_store(req, 9, newRV_noinc((SV*)newHV()));') # SLOT_FORM
2528             ->line(' av_store(req, 8, &PL_sv_undef);') # SLOT_JSON
2529             ->line(' }')
2530             ->blank;
2531             } else {
2532 0         0 $builder->comment('JIT: Form/JSON parsing SKIPPED')
2533             ->line(' av_store(req, 9, newRV_noinc((SV*)newHV()));') # SLOT_FORM
2534             ->line(' av_store(req, 8, &PL_sv_undef);') # SLOT_JSON
2535             ->blank;
2536             }
2537             } else {
2538 2         26 $builder->comment('JIT: All header-based parsing SKIPPED (no routes use these features)')
2539             ->line(' av_store(req, 6, newRV_noinc((SV*)newHV()));') # SLOT_HEADERS
2540             ->line(' av_store(req, 7, newRV_noinc((SV*)newHV()));') # SLOT_COOKIES
2541             ->line(' av_store(req, 8, &PL_sv_undef);') # SLOT_JSON
2542             ->line(' av_store(req, 9, newRV_noinc((SV*)newHV()));') # SLOT_FORM
2543             ->blank;
2544             }
2545              
2546             # Call handler and build response
2547 3         31 $builder->comment('Bless array into Hypersonic::Request and call Perl handler')
2548             ->line(' req_ref = newRV_noinc((SV*)req);')
2549             ->line(' sv_bless(req_ref, gv_stashpv("Hypersonic::Request", GV_ADD));')
2550             ->line(' ENTER;')
2551             ->line(' SAVETMPS;');
2552            
2553             # JIT: Add middleware short-circuit variable only if middleware present
2554             # (mw_result and short_circuit already declared at function top)
2555            
2556             # JIT: Builder-based before middleware (inline C - no Perl calls)
2557 3 50       11 if ($analysis->{has_builder_before}) {
2558             my $ctx = {
2559             req_var => 'req',
2560             req_ref_var => 'req_ref',
2561             slots => $analysis->{middleware_slots},
2562 0         0 };
2563 0         0 $builder->blank
2564             ->comment('JIT: Builder before middleware (inline C - zero Perl overhead)');
2565 0         0 for my $mw (@{$analysis->{builder_before}}) {
  0         0  
2566 0 0       0 if ($mw->can('build_before')) {
2567 0         0 $mw->build_before($builder, $ctx);
2568             }
2569             }
2570             }
2571              
2572             # JIT: Call global before middleware (Perl coderefs via call_sv)
2573 3 100       10 if ($analysis->{has_global_before}) {
2574 1         27 $builder->blank
2575             ->comment('JIT: Call global before middleware (Perl)')
2576             ->line(' if (g_before_middleware && SvROK(g_before_middleware)) {')
2577             ->line(' AV* before_arr = (AV*)SvRV(g_before_middleware);')
2578             ->line(' mw_result = call_middleware_chain(aTHX_ before_arr, req_ref);')
2579             ->line(' if (mw_result) {')
2580             ->line(' result = mw_result;')
2581             ->line(' short_circuit = 1;')
2582             ->line(' }')
2583             ->line(' }');
2584             }
2585            
2586             # JIT: Call per-route before middleware
2587 3 50       11 if ($analysis->{has_route_middleware}) {
2588 0         0 $builder->blank
2589             ->comment('JIT: Call per-route before middleware')
2590             ->line(' if (!short_circuit && g_route_before_middleware && SvROK(g_route_before_middleware)) {')
2591             ->line(' AV* route_before = (AV*)SvRV(g_route_before_middleware);')
2592             ->line(' SV** handler_arr_ref = av_fetch(route_before, handler_idx, 0);')
2593             ->line(' if (handler_arr_ref && SvROK(*handler_arr_ref)) {')
2594             ->line(' AV* handler_arr = (AV*)SvRV(*handler_arr_ref);')
2595             ->line(' if (av_len(handler_arr) >= 0) {')
2596             ->line(' mw_result = call_middleware_chain(aTHX_ handler_arr, req_ref);')
2597             ->line(' if (mw_result) {')
2598             ->line(' result = mw_result;')
2599             ->line(' short_circuit = 1;')
2600             ->line(' }')
2601             ->line(' }')
2602             ->line(' }')
2603             ->line(' }');
2604             }
2605            
2606             # JIT: Streaming handler support
2607 3 100       9 if ($analysis->{needs_streaming}) {
2608 2         70 $builder->blank
2609             ->comment('JIT: Check if this is a streaming handler')
2610             ->line(' is_streaming = g_streaming_handlers[handler_idx];')
2611             ->if('is_streaming')
2612             ->comment('Create Hypersonic::Stream object for streaming handler')
2613             ->line('dSP;')
2614             ->line('PUSHMARK(SP);')
2615             ->line('XPUSHs(sv_2mortal(newSVpv("Hypersonic::Stream", 0)));')
2616             ->line('XPUSHs(sv_2mortal(newSVpv("fd", 0)));')
2617             ->line('XPUSHs(sv_2mortal(newSViv(client_fd)));')
2618             ->line('PUTBACK;')
2619             ->line('int stream_count = call_method("new", G_SCALAR);')
2620             ->line('SPAGAIN;')
2621             ->if('stream_count > 0')
2622             ->line('stream_sv = POPs;')
2623             ->line('SvREFCNT_inc(stream_sv);')
2624             ->endif
2625             ->line('PUTBACK;')
2626             ->endif;
2627             }
2628              
2629             # Call the main handler (conditionally if middleware present)
2630 3 100       12 if ($analysis->{has_any_middleware}) {
2631 1         8 $builder->blank
2632             ->comment('Call main handler (unless middleware short-circuited)')
2633             ->line(' if (!short_circuit) {')
2634             ->line(' PUSHMARK(SP);')
2635             ->line(' XPUSHs(req_ref);');
2636              
2637             # For streaming handlers with middleware
2638 1 50       2 if ($analysis->{needs_streaming}) {
2639 0         0 $builder->line(' if (is_streaming && stream_sv) XPUSHs(stream_sv);');
2640             }
2641              
2642 1         6 $builder->line(' PUTBACK;')
2643             ->line(' count = call_sv(*handler_sv, G_SCALAR | G_EVAL);')
2644             ->line(' SPAGAIN;')
2645             ->line(' if (count == 1) result = POPs;')
2646             ->line(' PUTBACK;')
2647             ->line(' }');
2648             } else {
2649 2         25 $builder->line(' PUSHMARK(SP);')
2650             ->line(' XPUSHs(sv_2mortal(req_ref));');
2651              
2652             # For streaming handlers without middleware
2653 2 50       10 if ($analysis->{needs_streaming}) {
2654 2         4 $builder->line(' if (is_streaming && stream_sv) XPUSHs(stream_sv);');
2655             }
2656              
2657 2         8 $builder->line(' PUTBACK;')
2658             ->line(' count = call_sv(*handler_sv, G_SCALAR | G_EVAL);')
2659             ->line(' SPAGAIN;');
2660             }
2661            
2662             # JIT: Call per-route after middleware
2663 3 50       10 if ($analysis->{has_route_middleware}) {
2664 0         0 $builder->blank
2665             ->comment('JIT: Call per-route after middleware')
2666             ->line(' if (g_route_after_middleware && SvROK(g_route_after_middleware)) {')
2667             ->line(' AV* route_after = (AV*)SvRV(g_route_after_middleware);')
2668             ->line(' SV** handler_arr_ref = av_fetch(route_after, handler_idx, 0);')
2669             ->line(' if (handler_arr_ref && SvROK(*handler_arr_ref)) {')
2670             ->line(' AV* handler_arr = (AV*)SvRV(*handler_arr_ref);')
2671             ->line(' if (av_len(handler_arr) >= 0) {')
2672             ->line(' SV* after_result = call_middleware_chain(aTHX_ handler_arr, req_ref);')
2673             ->line(' if (after_result) {')
2674             ->line(' result = after_result;')
2675             ->line(' }')
2676             ->line(' }')
2677             ->line(' }')
2678             ->line(' }');
2679             }
2680            
2681             # JIT: Call global after middleware (Perl coderefs via call_sv)
2682 3 100       8 if ($analysis->{has_global_after}) {
2683 1         9 $builder->blank
2684             ->comment('JIT: Call global after middleware (Perl)')
2685             ->line(' if (g_after_middleware && SvROK(g_after_middleware)) {')
2686             ->line(' AV* after_arr = (AV*)SvRV(g_after_middleware);')
2687             ->line(' SV* after_result = call_middleware_chain(aTHX_ after_arr, req_ref);')
2688             ->line(' if (after_result) {')
2689             ->line(' result = after_result;')
2690             ->line(' }')
2691             ->line(' }');
2692             }
2693              
2694             # JIT: Builder-based after middleware (inline C - no Perl calls)
2695 3 50       9 if ($analysis->{has_builder_after}) {
2696             my $ctx = {
2697             req_var => 'req',
2698             req_ref_var => 'req_ref',
2699             res_var => 'result',
2700             slots => $analysis->{middleware_slots},
2701 0         0 };
2702 0         0 $builder->blank
2703             ->comment('JIT: Builder after middleware (inline C - zero Perl overhead)');
2704 0         0 for my $mw (@{$analysis->{builder_after}}) {
  0         0  
2705 0 0       0 if ($mw->can('build_after')) {
2706 0         0 $mw->build_after($builder, $ctx);
2707             }
2708             }
2709             }
2710              
2711             # JIT: Streaming handlers - early return (response already sent via Stream)
2712 3 100       7 if ($analysis->{needs_streaming}) {
2713 2         40 $builder->blank
2714             ->comment('JIT: Streaming handlers return early - response sent via Stream object')
2715             ->if('is_streaming')
2716             ->line('if (stream_sv) SvREFCNT_dec(stream_sv);')
2717             ->line('FREETMPS;')
2718             ->line('LEAVE;')
2719             ->line('*resp_out = NULL;')
2720             ->line('*resp_len_out = -1;')
2721             ->comment('Signal streaming response - caller should not send')
2722             ->line('return;')
2723             ->endif;
2724             }
2725              
2726             $builder->blank
2727 3         33 ->line(' if (SvTRUE(ERRSV)) {')
2728             ->line(' static char error_resp[512];')
2729             ->line(' int err_len = snprintf(error_resp, sizeof(error_resp),')
2730             ->line(' "HTTP/1.1 500 Internal Server Error\\r\\n"')
2731             ->line(' "Content-Type: text/plain\\r\\n"')
2732             ->line(' "Content-Length: 21\\r\\n"')
2733             ->line(' "Connection: close\\r\\n\\r\\n"')
2734             ->line(' "Internal Server Error");')
2735             ->line(' *resp_out = error_resp;')
2736             ->line(' *resp_len_out = err_len;');
2737            
2738             # Middleware version already has result set, no-middleware version needs POPs
2739 3 100       8 if ($analysis->{has_any_middleware}) {
2740 1         3 $builder->line(' } else if (SvOK(result)) {')
2741             ->line(' HV* custom_headers = NULL;');
2742             } else {
2743 2         13 $builder->line(' POPs;')
2744             ->line(' } else if (count == 1) {')
2745             ->line(' result = POPs;')
2746             ->line(' if (SvOK(result)) {')
2747             ->line(' HV* custom_headers = NULL;');
2748             }
2749              
2750 3         247 $builder
2751             ->comment(' Handle arrayref [status, headers, body]')
2752             ->line(' if (SvROK(result) && SvTYPE(SvRV(result)) == SVt_PVAV) {')
2753             ->line(' AV* arr = (AV*)SvRV(result);')
2754             ->line(' SV** status_sv = av_fetch(arr, 0, 0);')
2755             ->line(' SV** headers_sv = av_fetch(arr, 1, 0);')
2756             ->line(' SV** body_sv = av_fetch(arr, 2, 0);')
2757             ->line(' if (status_sv) status = (int)SvIV(*status_sv);')
2758             ->line(' if (body_sv) body_str = SvPV(*body_sv, len);')
2759             ->line(' else { body_str = ""; len = 0; }')
2760             ->line(' if (headers_sv && SvROK(*headers_sv) && SvTYPE(SvRV(*headers_sv)) == SVt_PVHV) {')
2761             ->line(' custom_headers = (HV*)SvRV(*headers_sv);')
2762             ->line(' SV** ct_sv = hv_fetch(custom_headers, "Content-Type", 12, 0);')
2763             ->line(' if (ct_sv && SvOK(*ct_sv)) {')
2764             ->line(' STRLEN ct_len;')
2765             ->line(' content_type = SvPV(*ct_sv, ct_len);')
2766             ->line(' }')
2767             ->line(' }')
2768             ->line(' }')
2769             ->comment(' Handle hashref {status, headers, body}')
2770             ->line(' else if (SvROK(result) && SvTYPE(SvRV(result)) == SVt_PVHV) {')
2771             ->line(' HV* hash = (HV*)SvRV(result);')
2772             ->line(' SV** status_sv = hv_fetch(hash, "status", 6, 0);')
2773             ->line(' SV** headers_sv = hv_fetch(hash, "headers", 7, 0);')
2774             ->line(' SV** body_sv = hv_fetch(hash, "body", 4, 0);')
2775             ->line(' if (status_sv) status = (int)SvIV(*status_sv);')
2776             ->line(' if (body_sv) body_str = SvPV(*body_sv, len);')
2777             ->line(' else { body_str = ""; len = 0; }')
2778             ->line(' if (headers_sv && SvROK(*headers_sv) && SvTYPE(SvRV(*headers_sv)) == SVt_PVHV) {')
2779             ->line(' custom_headers = (HV*)SvRV(*headers_sv);')
2780             ->line(' SV** ct_sv = hv_fetch(custom_headers, "Content-Type", 12, 0);')
2781             ->line(' if (ct_sv && SvOK(*ct_sv)) {')
2782             ->line(' STRLEN ct_len;')
2783             ->line(' content_type = SvPV(*ct_sv, ct_len);')
2784             ->line(' }')
2785             ->line(' }')
2786             ->line(' }')
2787             ->comment(' Plain string response')
2788             ->line(' else {')
2789             ->line(' body_str = SvPV(result, len);')
2790             ->line(' }')
2791             ->blank
2792             ->comment(' Auto-detect JSON content type')
2793             ->line(' if (strcmp(content_type, "text/plain") == 0 && len > 0 &&')
2794             ->line(' (body_str[0] == \'{\' || body_str[0] == \'[\')) {')
2795             ->line(' content_type = "application/json";')
2796             ->line(' }')
2797             ->blank;
2798            
2799             # JIT: Add compression logic only if compression is enabled
2800 3 50       17 if ($self->{_compression_enabled}) {
2801 0         0 my $config = $self->{_compression_config};
2802 0   0     0 my $min_size = $config->{min_size} // 1024;
2803            
2804 0         0 $builder
2805             ->comment(' Gzip compression - check Accept-Encoding')
2806             ->line('#ifdef HYPERSONIC_COMPRESSION')
2807             ->line(' int use_gzip = 0;')
2808             ->line(' unsigned char* compressed_body = NULL;')
2809             ->line(' size_t compressed_len = 0;')
2810             ->blank
2811             ->comment(' Get Accept-Encoding from request (from SLOT_HEADERS)')
2812             ->line(' SV** req_arr = AvARRAY(req);')
2813             ->line(' HV* hdrs = NULL;')
2814             ->line(' if (req_arr[6] && SvROK(req_arr[6])) {')
2815             ->line(' hdrs = (HV*)SvRV(req_arr[6]);')
2816             ->line(' }')
2817             ->line(' if (hdrs && len >= ' . $min_size . ') {')
2818             ->line(' SV** ae = hv_fetch(hdrs, "accept_encoding", 15, 0);')
2819             ->line(' if (ae && SvOK(*ae)) {')
2820             ->line(' STRLEN ae_len;')
2821             ->line(' const char* ae_str = SvPV(*ae, ae_len);')
2822             ->line(' if (accepts_gzip(ae_str, ae_len)) {')
2823             ->line(' compressed_len = gzip_compress(body_str, len, &compressed_body);')
2824             ->line(' if (compressed_len > 0) {')
2825             ->line(' use_gzip = 1;')
2826             ->line(' body_str = (const char*)compressed_body;')
2827             ->line(' len = compressed_len;')
2828             ->line(' }')
2829             ->line(' }')
2830             ->line(' }')
2831             ->line(' }')
2832             ->line('#endif')
2833             ->blank;
2834             }
2835            
2836             $builder
2837 3         308 ->comment(' Build response with custom headers support')
2838             ->line(' static __thread char resp_buf[65536];')
2839             ->line(' int hdr_len;')
2840             ->line('#ifdef HYPERSONIC_SECURITY_HEADERS')
2841             ->line(' hdr_len = snprintf(resp_buf, 2048,')
2842             ->line(' "HTTP/1.1 %d %s\\r\\n"')
2843             ->line(' "Content-Type: %s\\r\\n"')
2844             ->line(' "Content-Length: %zu\\r\\n"')
2845             ->line(' "Connection: keep-alive\\r\\n"')
2846             ->line(' "%s",')
2847             ->line(' status, get_status_text(status), content_type, len, SECURITY_HEADERS);')
2848             ->line('#else')
2849             ->line(' hdr_len = snprintf(resp_buf, 512,')
2850             ->line(' "HTTP/1.1 %d %s\\r\\n"')
2851             ->line(' "Content-Type: %s\\r\\n"')
2852             ->line(' "Content-Length: %zu\\r\\n"')
2853             ->line(' "Connection: keep-alive\\r\\n",')
2854             ->line(' status, get_status_text(status), content_type, len);')
2855             ->line('#endif')
2856             ->blank
2857             ->comment(' Add custom headers from response (Location, Set-Cookie, etc.)')
2858             ->line(' if (custom_headers) {')
2859             ->line(' HE* entry;')
2860             ->line(' hv_iterinit(custom_headers);')
2861             ->line(' while ((entry = hv_iternext(custom_headers))) {')
2862             ->line(' I32 klen;')
2863             ->line(' const char* key = hv_iterkey(entry, &klen);')
2864             ->comment(' Skip Content-Type/Content-Length (already added)')
2865             ->line(' if (klen == 12 && memcmp(key, "Content-Type", 12) == 0) continue;')
2866             ->line(' if (klen == 14 && memcmp(key, "Content-Length", 14) == 0) continue;')
2867             ->line(' SV* val = hv_iterval(custom_headers, entry);')
2868             ->comment(' Handle Set-Cookie array (multiple cookies)')
2869             ->line(' if (SvROK(val) && SvTYPE(SvRV(val)) == SVt_PVAV) {')
2870             ->line(' AV* arr = (AV*)SvRV(val);')
2871             ->line(' SSize_t arr_len = av_len(arr) + 1;')
2872             ->line(' SSize_t j;')
2873             ->line(' for (j = 0; j < arr_len; j++) {')
2874             ->line(' SV** item = av_fetch(arr, j, 0);')
2875             ->line(' if (item && SvOK(*item)) {')
2876             ->line(' STRLEN vlen;')
2877             ->line(' const char* vstr = SvPV(*item, vlen);')
2878             ->line(' hdr_len += snprintf(resp_buf + hdr_len, sizeof(resp_buf) - hdr_len,')
2879             ->line(' "%.*s: %.*s\\r\\n", (int)klen, key, (int)vlen, vstr);')
2880             ->line(' }')
2881             ->line(' }')
2882             ->line(' } else if (SvOK(val)) {')
2883             ->line(' STRLEN vlen;')
2884             ->line(' const char* vstr = SvPV(val, vlen);')
2885             ->line(' hdr_len += snprintf(resp_buf + hdr_len, sizeof(resp_buf) - hdr_len,')
2886             ->line(' "%.*s: %.*s\\r\\n", (int)klen, key, (int)vlen, vstr);')
2887             ->line(' }')
2888             ->line(' }')
2889             ->line(' }')
2890             ->blank
2891             ->comment(' Add Content-Encoding header if gzip was used')
2892             ->line('#ifdef HYPERSONIC_COMPRESSION')
2893             ->line(' if (use_gzip) {')
2894             ->line(' hdr_len += snprintf(resp_buf + hdr_len, sizeof(resp_buf) - hdr_len,')
2895             ->line(' "Content-Encoding: gzip\\r\\n");')
2896             ->line(' }')
2897             ->line('#endif')
2898             ->blank
2899             ->comment(' End headers')
2900             ->line(' memcpy(resp_buf + hdr_len, "\\r\\n", 2);')
2901             ->line(' hdr_len += 2;')
2902             ->blank
2903             ->line(' if (hdr_len + len < sizeof(resp_buf)) {')
2904             ->line(' memcpy(resp_buf + hdr_len, body_str, len);')
2905             ->line(' *resp_out = resp_buf;')
2906             ->line(' *resp_len_out = hdr_len + (int)len;')
2907             ->line(' } else {')
2908             ->line(' *resp_out = (char*)RESP_404;')
2909             ->line(' *resp_len_out = RESP_404_LEN;')
2910             ->line(' }');
2911            
2912             # Different closing braces based on middleware presence
2913 3 100       12 if ($analysis->{has_any_middleware}) {
2914             # Middleware: } else if (SvOK(result)) { ... } else { 404 }
2915 1         7 $builder->line(' } else {')
2916             ->line(' *resp_out = (char*)RESP_404;')
2917             ->line(' *resp_len_out = RESP_404_LEN;')
2918             ->line(' }');
2919             } else {
2920             # No middleware: } else if (count == 1) { result = POPs; if (SvOK(result)) { ... } } else { 404 }
2921 2         18 $builder->line(' } else {')
2922             ->line(' *resp_out = (char*)RESP_404;')
2923             ->line(' *resp_len_out = RESP_404_LEN;')
2924             ->line(' }')
2925             ->line(' } else {')
2926             ->line(' *resp_out = (char*)RESP_404;')
2927             ->line(' *resp_len_out = RESP_404_LEN;')
2928             ->line(' }');
2929             }
2930            
2931 3         31 $builder->blank
2932             ->line(' PUTBACK;')
2933             ->line(' FREETMPS;')
2934             ->line(' LEAVE;')
2935             ->line('}');
2936            
2937 3         237 return $builder->code;
2938             }
2939              
2940             # JSON decoder helper - called from C via call_pv
2941             sub _decode_json {
2942 0     0   0 my ($json_str) = @_;
2943 0         0 require Cpanel::JSON::XS;
2944 0         0 return Cpanel::JSON::XS::decode_json($json_str);
2945             }
2946              
2947             # Generate WebSocket handler caller function
2948             sub _gen_websocket_handler_caller {
2949 0     0   0 my ($self) = @_;
2950              
2951 0         0 my $builder = XS::JIT::Builder->new;
2952              
2953 0         0 $builder->comment('WebSocket handler caller - performs handshake and calls Perl handler')
2954             ->line('static void call_websocket_handler(pTHX_ int handler_idx, int fd,')
2955             ->line(' const char* path, int path_len,')
2956             ->line(' const char* ws_key, int ws_key_len,')
2957             ->line(' const char* raw_request, int raw_request_len) {')
2958             ->line(' dSP;')
2959             ->blank
2960             ->if('!g_websocket_handlers || !SvROK(g_websocket_handlers)')
2961             ->line('return;')
2962             ->endif
2963             ->blank
2964             ->line('AV* handlers = (AV*)SvRV(g_websocket_handlers);')
2965             ->line('SV** handler_sv = av_fetch(handlers, handler_idx, 0);')
2966             ->if('!handler_sv || !SvROK(*handler_sv)')
2967             ->line('return;')
2968             ->endif
2969             ->blank
2970             ->comment('Generate WebSocket accept key')
2971             ->line('ENTER;')
2972             ->line('SAVETMPS;')
2973             ->blank
2974             ->comment('Build handshake response via Protocol::WebSocket')
2975             ->line('PUSHMARK(SP);')
2976             ->line('XPUSHs(sv_2mortal(newSVpvs("Hypersonic::Protocol::WebSocket")));')
2977             ->line('XPUSHs(sv_2mortal(newSVpvs("key")));')
2978             ->line('XPUSHs(sv_2mortal(newSVpvn(ws_key, ws_key_len)));')
2979             ->line('PUTBACK;')
2980             ->line('int count = call_method("build_response", G_SCALAR);')
2981             ->line('SPAGAIN;')
2982             ->blank
2983             ->if('count != 1')
2984             ->line('FREETMPS;')
2985             ->line('LEAVE;')
2986             ->line('return;')
2987             ->endif
2988             ->blank
2989             ->line('SV* response_sv = POPs;')
2990             ->line('STRLEN resp_len;')
2991             ->line('const char* response = SvPV(response_sv, resp_len);')
2992             ->blank
2993             ->comment('Send WebSocket handshake response')
2994             ->line('send(fd, response, resp_len, 0);')
2995             ->blank
2996             ->comment('Create Stream object for WebSocket')
2997             ->line('PUSHMARK(SP);')
2998             ->line('XPUSHs(sv_2mortal(newSVpvs("Hypersonic::Stream")));')
2999             ->line('XPUSHs(sv_2mortal(newSVpvs("fd")));')
3000             ->line('XPUSHs(sv_2mortal(newSViv(fd)));')
3001             ->line('PUTBACK;')
3002             ->line('count = call_method("new", G_SCALAR);')
3003             ->line('SPAGAIN;')
3004             ->blank
3005             ->if('count != 1')
3006             ->line('FREETMPS;')
3007             ->line('LEAVE;')
3008             ->line('return;')
3009             ->endif
3010             ->blank
3011             ->line('SV* stream_sv = POPs;')
3012             ->line('SvREFCNT_inc(stream_sv);')
3013             ->blank
3014             ->comment('Create WebSocket object')
3015             ->line('PUSHMARK(SP);')
3016             ->line('XPUSHs(sv_2mortal(newSVpvs("Hypersonic::WebSocket")));')
3017             ->line('XPUSHs(stream_sv);')
3018             ->line('XPUSHs(sv_2mortal(newSVpvs("fd")));')
3019             ->line('XPUSHs(sv_2mortal(newSViv(fd)));')
3020             ->line('PUTBACK;')
3021             ->line('count = call_method("new", G_SCALAR);')
3022             ->line('SPAGAIN;')
3023             ->blank
3024             ->if('count != 1')
3025             ->line('SvREFCNT_dec(stream_sv);')
3026             ->line('FREETMPS;')
3027             ->line('LEAVE;')
3028             ->line('return;')
3029             ->endif
3030             ->blank
3031             ->line('SV* ws_sv = POPs;')
3032             ->line('SvREFCNT_inc(ws_sv);')
3033             ->blank
3034             ->comment('Set WebSocket state to OPEN and store object in registry')
3035             ->if('fd >= 0 && fd < WS_MAX')
3036             ->line('ws_registry[fd].state = WS_STATE_OPEN;')
3037             ->line('ws_registry[fd].ws_object = ws_sv;')
3038             ->line('SvREFCNT_inc(ws_sv);')
3039             ->endif
3040             ->blank
3041             ->comment('Register with Handler for global broadcast support')
3042             ->line('PUSHMARK(SP);')
3043             ->line('XPUSHs(sv_2mortal(newSVpvs("Hypersonic::WebSocket::Handler")));')
3044             ->line('XPUSHs(sv_2mortal(newSViv(fd)));')
3045             ->line('XPUSHs(ws_sv);')
3046             ->line('PUTBACK;')
3047             ->line('call_method("new", G_DISCARD);')
3048             ->blank
3049             ->comment('Call the Perl WebSocket handler with the WebSocket object')
3050             ->line('PUSHMARK(SP);')
3051             ->line('XPUSHs(ws_sv);')
3052             ->line('PUTBACK;')
3053             ->line('call_sv(*handler_sv, G_DISCARD | G_EVAL);')
3054             ->blank
3055             ->if('SvTRUE(ERRSV)')
3056             ->line('warn("WebSocket handler error: %s", SvPV_nolen(ERRSV));')
3057             ->endif
3058             ->blank
3059             ->comment('Emit open event')
3060             ->line('PUSHMARK(SP);')
3061             ->line('XPUSHs(ws_sv);')
3062             ->line('XPUSHs(sv_2mortal(newSVpvs("open")));')
3063             ->line('PUTBACK;')
3064             ->line('call_method("emit", G_DISCARD | G_EVAL);')
3065             ->blank
3066             ->line('SvREFCNT_dec(stream_sv);')
3067             ->line('SvREFCNT_dec(ws_sv);')
3068             ->line('FREETMPS;')
3069             ->line('LEAVE;')
3070             ->line('}')
3071             ->blank;
3072              
3073 0         0 return $builder->code;
3074             }
3075              
3076             # Generate WebSocket data processor for established connections
3077             sub _gen_websocket_data_processor {
3078 0     0   0 my ($self) = @_;
3079              
3080 0         0 my $builder = XS::JIT::Builder->new;
3081              
3082 0         0 $builder->comment('Process incoming WebSocket data on established connection')
3083             ->line('static void process_websocket_data(pTHX_ int fd, const char* data, ssize_t len) {')
3084             ->line(' dSP;')
3085             ->blank
3086             ->comment('Get WebSocket object from registry')
3087             ->if('fd < 0 || fd >= WS_MAX || !ws_registry[fd].ws_object')
3088             ->line('return;')
3089             ->endif
3090             ->blank
3091             ->line('SV* ws_sv = ws_registry[fd].ws_object;')
3092             ->blank
3093             ->comment('Parse WebSocket frame')
3094             ->line('WSFrame frame;')
3095             ->line('int result = ws_decode_frame((const uint8_t*)data, len, &frame);')
3096             ->blank
3097             ->if('result < 0')
3098             ->comment('Incomplete frame or error - wait for more data')
3099             ->line('return;')
3100             ->endif
3101             ->blank
3102             ->comment('Handle different opcodes')
3103             ->if('frame.opcode == WS_OP_TEXT || frame.opcode == WS_OP_BINARY')
3104             ->comment('Data frame - emit message event')
3105             ->line('ENTER;')
3106             ->line('SAVETMPS;')
3107             ->line('PUSHMARK(SP);')
3108             ->line('XPUSHs(ws_sv);')
3109             ->line('XPUSHs(sv_2mortal(newSVpvs("message")));')
3110             ->line('XPUSHs(sv_2mortal(newSVpvn((const char*)frame.payload, frame.payload_length)));')
3111             ->line('PUTBACK;')
3112             ->line('call_method("emit", G_DISCARD | G_EVAL);')
3113             ->line('FREETMPS;')
3114             ->line('LEAVE;')
3115             ->elsif('frame.opcode == WS_OP_CLOSE')
3116             ->comment('Close frame - emit close event and respond')
3117             ->line('int close_code = 1000;')
3118             ->if('frame.payload_length >= 2')
3119             ->line('close_code = (frame.payload[0] << 8) | frame.payload[1];')
3120             ->endif
3121             ->blank
3122             ->comment('Send close response')
3123             ->line('uint8_t close_frame[4];')
3124             ->line('close_frame[0] = 0x88;') # FIN + Close opcode
3125             ->line('close_frame[1] = 2;') # Length 2 (just the code)
3126             ->line('close_frame[2] = (close_code >> 8) & 0xFF;')
3127             ->line('close_frame[3] = close_code & 0xFF;')
3128             ->line('send(fd, close_frame, 4, 0);')
3129             ->blank
3130             ->comment('Emit close event')
3131             ->line('ENTER;')
3132             ->line('SAVETMPS;')
3133             ->line('PUSHMARK(SP);')
3134             ->line('XPUSHs(ws_sv);')
3135             ->line('XPUSHs(sv_2mortal(newSVpvs("close")));')
3136             ->line('XPUSHs(sv_2mortal(newSViv(close_code)));')
3137             ->line('PUTBACK;')
3138             ->line('call_method("emit", G_DISCARD | G_EVAL);')
3139             ->line('FREETMPS;')
3140             ->line('LEAVE;')
3141             ->blank
3142             ->comment('Unregister from Handler')
3143             ->line('ENTER;')
3144             ->line('SAVETMPS;')
3145             ->line('PUSHMARK(SP);')
3146             ->line('XPUSHs(sv_2mortal(newSVpvs("Hypersonic::WebSocket::Handler")));')
3147             ->line('XPUSHs(sv_2mortal(newSViv(fd)));')
3148             ->line('PUTBACK;')
3149             ->line('call_method("close", G_DISCARD);')
3150             ->line('FREETMPS;')
3151             ->line('LEAVE;')
3152             ->blank
3153             ->comment('Mark connection as closed')
3154             ->line('ws_registry[fd].state = WS_STATE_CLOSED;')
3155             ->elsif('frame.opcode == WS_OP_PING')
3156             ->comment('Ping - auto pong')
3157             ->line('uint8_t pong_frame[256];')
3158             ->line('size_t pong_len = ws_encode_pong(pong_frame, sizeof(pong_frame), frame.payload, frame.payload_length);')
3159             ->if('pong_len > 0')
3160             ->line('send(fd, pong_frame, pong_len, 0);')
3161             ->endif
3162             ->endif
3163             ->line('}')
3164             ->blank;
3165              
3166 0         0 return $builder->code;
3167             }
3168              
3169             sub _escape_c_string {
3170 69     69   129 my ($str) = @_;
3171 69         181 $str =~ s/\\/\\\\/g;
3172 69         163 $str =~ s/"/\\"/g;
3173 69         307 $str =~ s/\n/\\n/g;
3174 69         261 $str =~ s/\r/\\r/g;
3175 69         95 $str =~ s/\t/\\t/g;
3176 69         134 return $str;
3177             }
3178              
3179             # Deparse a handler coderef to analyze what request features it uses
3180             # Returns deparsed code as string, or undef on failure
3181             sub _deparse_handler {
3182 6     6   40 my ($coderef) = @_;
3183 6 50       16 return undef unless ref($coderef) eq 'CODE';
3184            
3185 6         7 my $code = eval {
3186 6 100       31 require B::Deparse unless $DEPARSER;
3187 6   66     205 $DEPARSER //= B::Deparse->new('-q'); # -q = don't quote simple strings
3188 6         10395 $DEPARSER->coderef2text($coderef);
3189             };
3190 6 50       35 return $@ ? undef : $code;
3191             }
3192              
3193             # Find common prefix of multiple paths
3194             sub _find_common_prefix {
3195 7     7   26 my @paths = @_;
3196 7 50       17 return '' unless @paths;
3197 7 50       23 return $paths[0] if @paths == 1;
3198              
3199 7         15 my $prefix = $paths[0];
3200 7         34 for my $path (@paths[1..$#paths]) {
3201 15         49 while (index($path, $prefix) != 0) {
3202 44         67 $prefix = substr($prefix, 0, -1);
3203 44 50       97 return '' if $prefix eq '';
3204             }
3205             }
3206             # Don't include trailing non-slash char as prefix
3207             # e.g., /api/hello and /api/health -> /api/ not /api/he
3208 7 50 33     51 if ($prefix !~ m{/$} && $prefix =~ m{^(.*/)[^/]+$}) {
3209 0         0 $prefix = $1;
3210             }
3211 7         52 return $prefix;
3212             }
3213              
3214             # HTTP status code to text mapping
3215             my %STATUS_TEXT = (
3216             200 => 'OK',
3217             201 => 'Created',
3218             202 => 'Accepted',
3219             204 => 'No Content',
3220             301 => 'Moved Permanently',
3221             302 => 'Found',
3222             303 => 'See Other',
3223             304 => 'Not Modified',
3224             307 => 'Temporary Redirect',
3225             308 => 'Permanent Redirect',
3226             400 => 'Bad Request',
3227             401 => 'Unauthorized',
3228             403 => 'Forbidden',
3229             404 => 'Not Found',
3230             405 => 'Method Not Allowed',
3231             408 => 'Request Timeout',
3232             409 => 'Conflict',
3233             410 => 'Gone',
3234             413 => 'Payload Too Large',
3235             415 => 'Unsupported Media Type',
3236             422 => 'Unprocessable Entity',
3237             429 => 'Too Many Requests',
3238             500 => 'Internal Server Error',
3239             501 => 'Not Implemented',
3240             502 => 'Bad Gateway',
3241             503 => 'Service Unavailable',
3242             504 => 'Gateway Timeout',
3243             );
3244              
3245             sub _status_text {
3246 0     0   0 my ($code) = @_;
3247 0   0     0 return $STATUS_TEXT{$code} // 'Unknown';
3248             }
3249              
3250             # ============================================================
3251             # STATIC FILE SERVING - JIT compiled for maximum performance
3252             # Files are read at compile time and baked into C string constants
3253             # ============================================================
3254              
3255             sub _compile_static_files {
3256 0     0   0 my ($self, $static_dirs) = @_;
3257            
3258 0         0 require File::Find;
3259 0         0 require Digest::MD5;
3260            
3261 0         0 my @static_files;
3262            
3263 0         0 for my $config (@$static_dirs) {
3264 0         0 my $prefix = $config->{prefix};
3265 0         0 my $dir = $config->{directory};
3266 0         0 my $max_age = $config->{max_age};
3267 0         0 my $gen_etag = $config->{etag};
3268            
3269             # Recursively find all files
3270             File::Find::find({
3271             no_chdir => 1,
3272             wanted => sub {
3273 0 0   0   0 return unless -f $_;
3274 0         0 my $file_path = $_;
3275 0         0 my $rel_path = $file_path;
3276 0         0 $rel_path =~ s{^\Q$dir\E/?}{};
3277            
3278             # URL path for this file
3279 0         0 my $url_path = "$prefix/$rel_path";
3280 0         0 $url_path =~ s{//+}{/}g;
3281            
3282             # Read file content
3283 0 0       0 open my $fh, '<:raw', $file_path or return;
3284 0         0 local $/;
3285 0         0 my $content = <$fh>;
3286 0         0 close $fh;
3287            
3288             # Get MIME type
3289 0         0 my $mime = _get_mime_type($file_path);
3290            
3291             # Generate ETag (MD5 of content)
3292 0         0 my $etag = '';
3293 0 0       0 if ($gen_etag) {
3294 0         0 $etag = Digest::MD5::md5_hex($content);
3295             }
3296            
3297             # Store file info
3298 0         0 push @static_files, {
3299             url_path => $url_path,
3300             content => $content,
3301             mime => $mime,
3302             etag => $etag,
3303             max_age => $max_age,
3304             length => length($content),
3305             };
3306             },
3307 0         0 }, $dir);
3308             }
3309            
3310             # Store for code generation
3311 0         0 $self->{_static_files} = \@static_files;
3312            
3313             # Create static routes - these are essentially pre-computed responses
3314 0         0 for my $file (@static_files) {
3315 0         0 my $url_path = $file->{url_path};
3316 0         0 my $content = $file->{content};
3317 0         0 my $mime = $file->{mime};
3318 0         0 my $etag = $file->{etag};
3319 0         0 my $max_age = $file->{max_age};
3320 0         0 my $len = $file->{length};
3321            
3322             # Build complete HTTP response at compile time
3323 0         0 my $response = "HTTP/1.1 200 OK\r\n"
3324             . "Content-Type: $mime\r\n"
3325             . "Content-Length: $len\r\n"
3326             . "Connection: keep-alive\r\n";
3327 0 0       0 $response .= "Cache-Control: public, max-age=$max_age\r\n" if $max_age;
3328 0 0       0 $response .= "ETag: \"$etag\"\r\n" if $etag;
3329            
3330             # Add security headers
3331 0 0       0 if ($self->{enable_security_headers}) {
3332 0         0 $response .= $self->_get_security_headers_string();
3333             }
3334            
3335 0         0 $response .= "\r\n" . $content;
3336            
3337             # Store as static route
3338 0         0 push @{$self->{routes}}, {
3339             method => 'GET',
3340             path => $url_path,
3341 0     0   0 handler => sub { $content }, # Dummy handler for static
3342 0         0 dynamic => 0,
3343             params => [],
3344             segments => [split('/', $url_path)],
3345             features => {},
3346             before => [],
3347             after => [],
3348             # Mark as static file with pre-built response
3349             _static_response => $response,
3350             _static_file => 1,
3351             };
3352             }
3353             }
3354              
3355             # Generate security headers string for HTTP responses
3356             # Pre-computed at compile time - zero runtime overhead
3357             sub _get_security_headers_string {
3358 40     40   125 my ($self) = @_;
3359 40         145 my $headers = '';
3360            
3361 40         65 for my $name (sort keys %{$self->{security_headers}}) {
  40         463  
3362 280         466 my $value = $self->{security_headers}{$name};
3363 280 100 66     741 next unless defined $value && length($value);
3364 160         313 $headers .= "$name: $value\r\n";
3365             }
3366            
3367 40         139 return $headers;
3368             }
3369              
3370             # Generate security headers as C string constant for dynamic routes
3371             sub _gen_security_headers_c_constant {
3372 3     3   6 my ($self) = @_;
3373 3 50       11 return '' unless $self->{enable_security_headers};
3374            
3375 3         13 my $headers = $self->_get_security_headers_string();
3376 3 50       14 return '' unless length($headers);
3377            
3378 3         10 my $escaped = _escape_c_string($headers);
3379 3         39 return "static const char SECURITY_HEADERS[] = \"$escaped\";\n"
3380             . "static const int SECURITY_HEADERS_LEN = " . length($headers) . ";\n";
3381             }
3382              
3383             sub dispatch {
3384 31     31 1 1030249 my ($self, $req) = @_;
3385 31 100       120 die "Must call compile() first" unless $self->{compiled};
3386 30         177 return $self->{dispatch_fn}->($req);
3387             }
3388              
3389             sub run {
3390 1     1 1 10 my ($self, %opts) = @_;
3391              
3392 1 50       25 die "Must call compile() before run()" unless $self->{compiled};
3393              
3394 0   0       my $host = $opts{host} // $self->{host};
3395 0   0       my $port = $opts{port} // $self->{port};
3396 0   0       my $workers = $opts{workers} // 1;
3397              
3398             # Windows lacks fork(); the multi-worker model assumes SO_REUSEPORT
3399             # + fork()-per-worker so the kernel can balance connections. With
3400             # neither primitive available the only safe behavior is single
3401             # process. Warn loudly so users know they're not getting the
3402             # workers they asked for.
3403 0 0 0       if ($^O eq 'MSWin32' && $workers > 1) {
3404 0           warn "Hypersonic: Windows lacks fork(); forcing workers=1.\n";
3405 0           $workers = 1;
3406             }
3407              
3408             # Protocol mode indication
3409 0 0         my $mode = $self->{http2} ? "HTTP/2" : ($self->{tls} ? "HTTPS/TLS" : "HTTP");
    0          
3410 0           print "Hypersonic listening on $host:$port ($mode, pure C event loop, $workers workers)\n";
3411              
3412             # Fork workers if requested
3413             # Each worker creates its OWN listening socket with SO_REUSEPORT
3414             # This avoids thundering herd and lets kernel distribute connections
3415 0 0         if ($workers > 1) {
3416 0           for my $i (1 .. $workers - 1) {
3417 0           my $pid = fork();
3418 0 0         if (!defined $pid) {
3419 0           die "Fork failed: $!";
3420             }
3421 0 0         if ($pid == 0) {
3422             # Child - create own listen socket and run event loop
3423 0           my $listen_fd = Hypersonic::Socket::create_listen_socket($port);
3424 0 0         die "Worker $i: Failed to create listen socket" if $listen_fd < 0;
3425 0           $self->{run_loop_fn}->($listen_fd, $self);
3426 0           exit(0);
3427             }
3428             }
3429             }
3430              
3431             # Parent (or single worker) - create listen socket and run event loop
3432 0           my $listen_fd = Hypersonic::Socket::create_listen_socket($port);
3433 0 0         die "Failed to create listen socket on port $port" if $listen_fd < 0;
3434 0           $self->{run_loop_fn}->($listen_fd, $self);
3435             }
3436              
3437              
3438             1;
3439              
3440             __END__