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   3543806 use strict;
  29         43  
  29         939  
4 29     29   130 use warnings;
  29         38  
  29         1166  
5 29     29   437 use 5.010;
  29         83  
6              
7             our $VERSION = '0.18';
8              
9 29     29   128 use Scalar::Util qw(blessed);
  29         87  
  29         1483  
10 29     29   167 use Config ();
  29         111  
  29         506  
11 29     29   10193 use XS::JIT;
  29         23236  
  29         968  
12 29     29   13704 use XS::JIT::Builder;
  29         32856  
  29         1417  
13 29     29   12663 use Hypersonic::Socket;
  29         67  
  29         144  
14 29     29   12832 use Hypersonic::Protocol::HTTP1;
  29         64  
  29         883  
15 29     29   174 use Hypersonic::JIT::Util;
  29         40  
  29         116895  
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 930310 my ($class, %opts) = @_;
33            
34             # Validate TLS options
35 42 50       172 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       149 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     258 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     3468 }, $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 549 sub get { shift->_add_route('GET', @_) }
105 5     5 1 36 sub post { shift->_add_route('POST', @_) }
106 1     1 1 5 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 430 my ($self, $path, $handler) = @_;
115 1   50     10 $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     7 };
121            
122 1         4 return $self->get($path => $handler);
123             }
124              
125             # Readiness check endpoint - separate from health for k8s
126             sub ready_check {
127 1     1 0 263 my ($self, $path, $handler) = @_;
128 1   50     10 $path //= '/ready';
129            
130             $handler //= sub {
131 0     0   0 return '{"ready":true}';
132 1   33     6 };
133            
134 1         2 return $self->get($path => $handler);
135             }
136              
137             # WebSocket route registration
138             sub websocket {
139 4     4 1 61 my ($self, $path, $handler) = @_;
140            
141 4 50       21 die "WebSocket path must start with /" unless $path =~ m{^/};
142 4 50       14 die "WebSocket handler must be a coderef" unless ref($handler) eq 'CODE';
143            
144 4   100     7 push @{$self->{websocket_routes} //= []}, {
  4         34  
145             path => $path,
146             handler => $handler,
147             pattern => $self->_compile_path_pattern($path),
148             };
149            
150 4         13 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     23 return scalar @{$self->{websocket_routes} // []};
  15         146  
157             }
158              
159             # Match a path against WebSocket routes
160             sub _match_websocket_route {
161 3     3   3524 my ($self, $path) = @_;
162            
163 3   50     6 for my $route (@{$self->{websocket_routes} // []}) {
  3         18  
164 5         9 my $pattern = $route->{pattern};
165 5 100       37 if ($path =~ $pattern) {
166             # Extract params
167 2         4 my %params;
168 2         12 my @captures = ($path =~ $pattern);
169 2         33 my @param_names = $route->{path} =~ /:(\w+)/g;
170 2         9 for my $i (0..$#param_names) {
171 1 50       8 $params{$param_names[$i]} = $captures[$i] if defined $captures[$i];
172             }
173 2         16 return ($route->{handler}, \%params);
174             }
175             }
176 1         5 return;
177             }
178              
179             # Compile path pattern (reuse for HTTP routes)
180             sub _compile_path_pattern {
181 6     6   1615 my ($self, $path) = @_;
182            
183 6         9 my $pattern = $path;
184 6         21 $pattern =~ s{:(\w+)}{([^/]+)}g; # :param -> capture group
185 6         14 $pattern =~ s{\*}{(.+)}g; # * -> greedy capture
186 6         223 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 257 my ($self, %opts) = @_;
294              
295 1         6 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         6 push @{$self->{before_middleware}}, Hypersonic::Middleware::RequestId::middleware(%opts);
  1         5  
300 1         1 push @{$self->{after_middleware}}, Hypersonic::Middleware::RequestId::after_middleware(%opts);
  1         3  
301              
302 1         1 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         6 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         7  
318            
319             # After: Save session to cookie/store
320 1         4 push @{$self->{after_middleware}}, Hypersonic::Session::after_middleware();
  1         5  
321            
322             # Mark that we need cookies parsed for all dynamic routes
323 1         3 $self->{_session_enabled} = 1;
324            
325 1         5 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   162 my ($self, $method, $path, $handler, $opts) = @_;
377              
378 49 100       201 die "Path must start with /" unless $path =~ m{^/};
379 48 100       164 die "Handler must be a code ref" unless ref($handler) eq 'CODE';
380              
381             # Check for dynamic option and feature flags
382 47         61 my $dynamic = 0;
383 47         254 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       111 if (ref($opts) eq 'HASH') {
395 13 100       27 $dynamic = $opts->{dynamic} ? 1 : 0;
396             # Copy feature flags from options
397 13         42 for my $feat (keys %features) {
398 104 50       158 $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         65 my @params;
404 47         142 my @segments = split '/', $path;
405 47         73 shift @segments; # Remove leading empty string
406            
407 47         122 for my $i (0 .. $#segments) {
408 58 100       147 if ($segments[$i] =~ /^:(\w+)$/) {
409 5         13 push @params, { name => $1, position => $i };
410 5         9 $dynamic = 1; # Path params imply dynamic
411             }
412             }
413              
414             # Streaming handlers are always dynamic
415 47 100       91 if ($features{streaming}) {
416 7         14 $dynamic = 1;
417             }
418              
419             # need_xs_builder handlers are always dynamic (but handled specially at compile time)
420 47 50       108 if ($features{need_xs_builder}) {
421 0         0 $dynamic = 1;
422             }
423              
424 47         647 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     57 after => $opts->{after} // [],
      50        
437             };
438              
439 47         130 return $self;
440             }
441              
442             sub compile {
443 17     17 1 978 my ($self) = @_;
444              
445 17 100 50     27 die "No routes defined" unless @{$self->{routes}} || @{$self->{static_dirs} // []};
  17   66     70  
  1         13  
446 16 100       65 die "Already compiled" if $self->{compiled};
447              
448             # ============================================================
449             # STATIC FILE PROCESSING - bake files into C at compile time
450             # ============================================================
451 15 50       100 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         47 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         33 has_global_before => scalar(@{$self->{before_middleware}}) > 0,
478 15         34 has_global_after => scalar(@{$self->{after_middleware}}) > 0,
  15         245  
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         49 for my $route (@{$self->{routes}}) {
  15         85  
485 30         76 $analysis{methods_used}{$route->{method}} = 1;
486              
487 30 100       63 if ($route->{dynamic}) {
488 6         14 $analysis{has_dynamic} = 1;
489             # Dynamic routes might need body access (POST/PUT/PATCH typically do)
490 6 100       27 if ($route->{method} =~ /^(POST|PUT|PATCH)$/) {
491 3         5 $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     20 my $f = $route->{features} // {};
497            
498             # Explicit flags take precedence
499 6 50       16 $analysis{needs_query} = 1 if $f->{parse_query};
500 6 50       11 $analysis{needs_headers} = 1 if $f->{parse_headers};
501 6 50       10 $analysis{needs_cookies} = 1 if $f->{parse_cookies};
502 6 100       14 $analysis{needs_json} = 1 if $f->{parse_json};
503 6 50       11 $analysis{needs_form} = 1 if $f->{parse_form};
504 6 50       12 $analysis{needs_response_helpers} = 1 if $f->{response_helpers};
505 6 100       16 $analysis{needs_streaming} = 1 if $f->{streaming};
506 6 50       11 $analysis{needs_xs_builder} = 1 if $f->{need_xs_builder};
507            
508             # Auto-detect by analyzing handler code
509 6         56 my $handler_code = _deparse_handler($route->{handler});
510 6 50       30 if ($handler_code) {
511             # Look for $req->{query} or ->{query} access patterns
512 6 50       28 $analysis{needs_query} = 1 if $handler_code =~ /\{['"]*query['"]*\}/;
513 6 50       21 $analysis{needs_headers} = 1 if $handler_code =~ /\{['"]*headers['"]*\}/;
514 6 50       18 $analysis{needs_cookies} = 1 if $handler_code =~ /\{['"]*cookies['"]*\}/;
515 6 50       41 $analysis{needs_json} = 1 if $handler_code =~ /\{['"]*json['"]*\}/;
516 6 50       24 $analysis{needs_form} = 1 if $handler_code =~ /\{['"]*form['"]*\}/;
517             }
518             } else {
519 24         39 $analysis{has_static} = 1;
520             }
521              
522 30 50       39 if (@{$route->{params}}) {
  30         56  
523 0         0 $analysis{has_path_params} = 1;
524             }
525            
526             # Check for per-route middleware
527 30 50 33     50 if (@{$route->{before}} || @{$route->{after}}) {
  30         176  
  30         100  
528 0         0 $analysis{has_route_middleware} = 1;
529             }
530             }
531            
532             # Session support: force cookie parsing when sessions are enabled
533 15 100       44 if ($self->{_session_enabled}) {
534 1         3 $analysis{needs_cookies} = 1;
535             }
536            
537             # Compression support: force header parsing to check Accept-Encoding
538 15 50       42 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       36 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         31 my (@builder_before, @perl_before, @builder_after, @perl_after);
558 15         26 for my $mw (@{$self->{before_middleware}}) {
  15         43  
559 1 50 0     3 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         32  
566 1 50 0     2 if (blessed($mw) && ($mw->can('build_before') || $mw->can('build_after'))) {
      33        
567 0         0 push @builder_after, $mw;
568             } else {
569 1         14 push @perl_after, $mw;
570             }
571             }
572              
573 15         37 $analysis{builder_before} = \@builder_before;
574 15         38 $analysis{builder_after} = \@builder_after;
575 15         34 $analysis{perl_before} = \@perl_before;
576 15         34 $analysis{perl_after} = \@perl_after;
577 15         35 $analysis{has_builder_before} = @builder_before > 0;
578 15         36 $analysis{has_builder_after} = @builder_after > 0;
579             # Update flags: has_global_* now refers to Perl middleware only (for call_sv)
580 15         33 $analysis{has_global_before} = @perl_before > 0;
581 15         24 $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         23 my $next_slot = 16;
587 15         25 my %middleware_slots;
588 15         47 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         58 $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     191 $analysis{has_route_middleware};
605              
606             # Check if only one method is used
607 15         25 my @methods = keys %{$analysis{methods_used}};
  15         48  
608 15 100       41 if (@methods == 1) {
609 13         27 $analysis{single_method} = $methods[0];
610             }
611              
612             # Check for common prefix (e.g., all routes start with /api)
613 15         35 my @paths = map { $_->{path} } @{$self->{routes}};
  30         77  
  15         45  
614 15 100       42 if (@paths > 1) {
615 7         47 my $prefix = _find_common_prefix(@paths);
616 7 50       22 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         60 $self->{route_analysis} = \%analysis;
623              
624             # Compile JIT request accessors (after analysis so we know what features are needed)
625 15         5222 require Hypersonic::Request;
626             Hypersonic::Request->compile_accessors(
627             cache_dir => $self->{cache_dir},
628             response_helpers => $analysis{needs_response_helpers},
629 15         125 );
630              
631             # Pre-evaluate all static handlers and build FULL HTTP responses
632 15         89 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         37 for my $i (0 .. $#{$self->{routes}}) {
  15         102  
637 30         73 my $route = $self->{routes}[$i];
638              
639 30 100       97 if (!$route->{dynamic}) {
640             # Check if this is a pre-built static file response
641 24 50       72 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       68 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         92 my $result = $route->{handler}->();
658            
659             # Support both string and [status, headers, body] format
660 24         175 my ($status, $headers, $body);
661 24 100       83 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     21 $status = $result->{status} // 200;
667 8   100     30 $headers = $result->{headers} // {};
668 8   50     17 $body = $result->{body} // '';
669             } else {
670 15         20 $status = 200;
671 15         27 $headers = {};
672 15         25 $body = $result;
673             }
674            
675 24 100 66     170 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       128 ? $self->_get_security_headers_string()
681             : '';
682            
683 23         196 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         43 push @full_responses, $full_response;
692 23         113 $route->{response_idx} = $#full_responses;
693             } else {
694             # Dynamic route - store handler index and param info
695 6         14 push @dynamic_handlers, $route->{handler};
696 6         10 push @route_param_info, $route->{params};
697 6         19 $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         32 $self->{route_param_info} = \@route_param_info;
704              
705             # JIT: Build streaming handlers lookup table (only if streaming is enabled)
706 14 100       53 if ($self->{route_analysis}{needs_streaming}) {
707 2         3 my @streaming_flags;
708 2         3 for my $route (@{$self->{routes}}) {
  2         5  
709 4 100       16 next unless $route->{dynamic};
710 2 50       8 push @streaming_flags, $route->{streaming} ? 1 : 0;
711             }
712 2         6 $self->{_streaming_flags} = \@streaming_flags;
713             }
714              
715             # JIT: Build WebSocket handlers lookup table
716 14 50       87 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         48 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     107 if ($analysis->{has_global_before} || $analysis->{has_global_after}) {
759 1         7 $self->{_perl_before_mw} = $analysis->{perl_before};
760 1         3 $self->{_perl_after_mw} = $analysis->{perl_after};
761             }
762              
763             # Generate C code with pure C event loop
764 14         77 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         185 my $module_id;
788             {
789 14         24 my $hash_input = join("\0",
790             $c_code,
791             $VERSION,
792             (defined $Config::Config{archname}
793 14 50 50     1121 ? $Config::Config{archname} : ''),
794             $] || '',
795             );
796 14 50       54 if (eval { require Digest::MD5; 1 }) {
  14         204  
  14         45  
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         907 $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         43 my $module_name = 'Hypersonic::_Server_' . $module_id;
808            
809             # Build compile options - add TLS flags if enabled
810 14         180 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       52 if ($self->{route_analysis}{needs_streaming}) {
823 2         6 %functions = (%functions, %{Hypersonic::Stream->get_xs_functions()});
  2         17  
824 2         13 %functions = (%functions, %{Hypersonic::SSE->get_xs_functions()});
  2         9  
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       47 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       51 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       54 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       61 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         221 functions => \%functions,
872             );
873            
874             # Add OpenSSL flags for TLS support
875 14 50       82 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       41 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       48 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       43 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       39 if ($self->{_event_backend}) {
905 14         36 my $backend = $self->{_event_backend};
906 14 50       315 if ($backend->can('extra_cflags')) {
907 14   50     71 my $ev_cflags = $backend->extra_cflags // '';
908 14 50 0     41 $compile_opts{extra_cflags} = ($compile_opts{extra_cflags} // '') . " $ev_cflags"
909             if $ev_cflags;
910             }
911 14 50       58 if ($backend->can('extra_ldflags')) {
912 14   50     65 my $ev_ldflags = $backend->extra_ldflags // '';
913 14 50 0     36 $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       84 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     173 if ($ENV{HYPERSONIC_COMPILE_DIAG} || $ENV{AUTOMATED_TESTING}) {
932 14         156 local $| = 1;
933 14         733 print STDERR "# Hypersonic: compiling JIT module $module_name ...\n";
934 14         44 eval { STDERR->flush; };
  14         274  
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   204 no warnings 'redefine';
  29         54  
  29         2463  
  14         23  
946 14         7539890 $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     225 . ($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   137 no strict 'refs';
  29         60  
  29         266782  
  14         30  
958 14         26 $self->{run_loop_fn} = \&{"${module_name}::run_event_loop"};
  14         166  
959 14         27 $self->{dispatch_fn} = \&{"${module_name}::dispatch"};
  14         75  
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     31 unless defined &{"${module_name}::dispatch"};
  14         65  
964             }
965              
966             # Mark Future/Pool as compiled if async pool is enabled
967             # (prevents them from trying to compile separately)
968 14 50       72 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         42 $self->{compiled} = 1;
976 14         444 return $self;
977             }
978              
979             sub _generate_server_code {
980 14     14   44 my ($self, $full_responses) = @_;
981              
982             # Load event backend module
983 14         5395 require Hypersonic::Event;
984 14   33     217 my $backend_name = $self->{event_backend} // Hypersonic::Event->best_backend;
985 14         156 my $backend = Hypersonic::Event->backend($backend_name);
986              
987             # Store backend for use in event loop generation
988 14         148 $self->{_event_backend} = $backend;
989 14         87 $self->{_event_backend_name} = $backend_name;
990              
991 14         319 my $builder = XS::JIT::Builder->new;
992              
993             # C99 detection for inline keyword
994 14         237 my $inline = Hypersonic::JIT::Util->inline_keyword;
995              
996             # Check if we have any dynamic routes
997 14         4206 my $has_dynamic = grep { $_->{dynamic} } @{$self->{routes}};
  29         144  
  14         367  
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         400 $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         283 $builder->line($backend->includes);
1033            
1034             # Add signal and time headers for graceful shutdown
1035 14         112 $builder->line('#include ')
1036             ->line('#include ');
1037            
1038             # Compression support - include zlib if compression is enabled
1039 14 50       111 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       149 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       101 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         62 my $max_connections = $self->{max_connections};
1058 14         62 my $max_request_size = $self->{max_request_size};
1059 14         50 my $keepalive_timeout = $self->{keepalive_timeout};
1060 14         50 my $recv_timeout = $self->{recv_timeout};
1061 14         45 my $drain_timeout = $self->{drain_timeout};
1062              
1063             # Backend-specific defines
1064 14         156 $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     294 if ($self->{enable_security_headers} && $has_dynamic) {
1071 3         21 $builder->line('#define HYPERSONIC_SECURITY_HEADERS 1');
1072             }
1073            
1074             $builder
1075 14         275 ->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         361 $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       103 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         235 $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('}')
1118             ->blank;
1119            
1120             # Compression support - JIT compiled zlib functions
1121 14 50       112 if ($self->{_compression_enabled}) {
1122 0         0 my $config = $self->{_compression_config};
1123 0   0     0 my $min_size = $config->{min_size} // 1024;
1124 0   0     0 my $level = $config->{level} // 6;
1125            
1126 0         0 $builder->comment('Gzip compression support - JIT compiled')
1127             ->line('static __thread unsigned char gzip_out_buf[131072];')
1128             ->blank
1129             ->line('static int accepts_gzip(const char* accept_encoding, size_t len) {')
1130             ->line(' if (!accept_encoding || len == 0) return 0;')
1131             ->line(' const char* p = accept_encoding;')
1132             ->line(' const char* end = accept_encoding + len;')
1133             ->line(' while (p < end - 3) {')
1134             ->line(' if (p[0] == \'g\' && p[1] == \'z\' && p[2] == \'i\' && p[3] == \'p\') return 1;')
1135             ->line(' p++;')
1136             ->line(' }')
1137             ->line(' return 0;')
1138             ->line('}')
1139             ->blank
1140             ->line('static size_t gzip_compress(const char* input, size_t input_len, unsigned char** output) {')
1141             ->line(' size_t max_out;')
1142             ->line(' z_stream strm;')
1143             ->line(' int ret;')
1144             ->line(' size_t compressed_len;')
1145             ->line(" if (input_len < $min_size) return 0;")
1146             ->line(' max_out = compressBound(input_len) + 18;')
1147             ->line(' if (max_out > sizeof(gzip_out_buf)) return 0;')
1148             ->line(' memset(&strm, 0, sizeof(strm));')
1149             ->line(" if (deflateInit2(&strm, $level, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) return 0;")
1150             ->line(' strm.next_in = (Bytef*)input;')
1151             ->line(' strm.avail_in = input_len;')
1152             ->line(' strm.next_out = gzip_out_buf;')
1153             ->line(' strm.avail_out = sizeof(gzip_out_buf);')
1154             ->line(' ret = deflate(&strm, Z_FINISH);')
1155             ->line(' compressed_len = strm.total_out;')
1156             ->line(' deflateEnd(&strm);')
1157             ->line(' if (ret != Z_STREAM_END || compressed_len >= input_len) return 0;')
1158             ->line(' *output = gzip_out_buf;')
1159             ->line(' return compressed_len;')
1160             ->line('}')
1161             ->blank;
1162             }
1163            
1164             # Connection tracking for keep-alive timeout - O(1) using fd as index
1165 14         523 $builder->comment('Connection tracking - O(1) using fd as direct index')
1166             ->line('#define MAX_FD 65536')
1167             ->line('static time_t g_conn_time[MAX_FD];')
1168             ->line('static time_t g_current_time = 0;')
1169             ->blank
1170             ->line("static $inline void track_connection(int fd, time_t now) {")
1171             ->line(' if (fd >= 0 && fd < MAX_FD) {')
1172             ->line(' g_conn_time[fd] = now;')
1173             ->line(' g_active_connections++;')
1174             ->line(' }')
1175             ->line('}')
1176             ->blank
1177             ->line("static $inline void update_connection(int fd, time_t now) {")
1178             ->line(' if (fd >= 0 && fd < MAX_FD) {')
1179             ->line(' g_conn_time[fd] = now;')
1180             ->line(' }')
1181             ->line('}')
1182             ->blank
1183             ->line("static $inline void remove_connection(int fd) {")
1184             ->line(' if (fd >= 0 && fd < MAX_FD && g_conn_time[fd] > 0) {')
1185             ->line(' g_conn_time[fd] = 0;')
1186             ->line(' g_active_connections--;')
1187             ->line(' }')
1188             ->line('}')
1189             ->blank;
1190              
1191             # TLS code generation - SSL context, accept, read/write wrappers
1192 14 50       64 if ($self->{tls}) {
1193             $builder->comment('TLS/HTTPS support via OpenSSL')
1194 0         0 ->raw(Hypersonic::TLS::gen_ssl_ctx_init(http2 => $self->{http2}))
1195             ->blank
1196             ->raw(Hypersonic::TLS::gen_ssl_accept())
1197             ->blank
1198             ->raw(Hypersonic::TLS::gen_ssl_io())
1199             ->blank
1200             ->raw(Hypersonic::TLS::gen_ssl_close())
1201             ->blank;
1202             }
1203            
1204             # HTTP/2 code generation - nghttp2 callbacks, session init, dispatchers
1205 14 50       60 if ($self->{http2}) {
1206 0         0 require Hypersonic::Protocol::HTTP2;
1207 0         0 $builder->comment('HTTP/2 support via nghttp2');
1208 0         0 Hypersonic::Protocol::HTTP2->gen_connection_struct($builder);
1209 0         0 Hypersonic::Protocol::HTTP2->gen_connection_preface_check($builder);
1210 0         0 Hypersonic::Protocol::HTTP2->gen_response_sender($builder);
1211 0         0 Hypersonic::Protocol::HTTP2->gen_404_response($builder);
1212 0         0 Hypersonic::Protocol::HTTP2->gen_callbacks($builder);
1213 0         0 Hypersonic::Protocol::HTTP2->gen_session_init($builder);
1214 0         0 Hypersonic::Protocol::HTTP2->gen_dispatcher($builder);
1215 0         0 Hypersonic::Protocol::HTTP2->gen_input_processor($builder);
1216 0         0 $builder->blank;
1217             }
1218              
1219             # Streaming support - JIT: only generate when streaming handlers detected
1220 14         71 my $analysis = $self->{route_analysis};
1221 14 100       54 if ($analysis->{needs_streaming}) {
1222 2         45 require Hypersonic::Stream;
1223             Hypersonic::Stream->generate_c_code($builder, {
1224             max_streams => $self->{max_connections},
1225 2         67 });
1226              
1227             # SSE support - compile SSE methods when streaming is enabled
1228 2         2111 require Hypersonic::SSE;
1229             Hypersonic::SSE->generate_c_code($builder, {
1230             max_sse_instances => $self->{max_connections},
1231 2         20 });
1232             }
1233              
1234             # WebSocket support - JIT: only generate when WebSocket routes exist
1235 14 50       62 if ($analysis->{needs_websocket}) {
1236 0         0 require Hypersonic::WebSocket;
1237 0         0 require Hypersonic::Protocol::WebSocket;
1238 0         0 require Hypersonic::Protocol::WebSocket::Frame;
1239              
1240             # Generate WebSocket frame encoding functions
1241             Hypersonic::Protocol::WebSocket::Frame->generate_c_code($builder, {
1242             max_connections => $self->{max_connections},
1243 0         0 });
1244              
1245             # Generate WebSocket connection management
1246             Hypersonic::WebSocket->generate_c_code($builder, {
1247             max_websockets => $self->{max_connections},
1248 0         0 });
1249             }
1250              
1251             # WebSocket Handler - JIT: connection registry, only when websocket routes exist
1252 14 50       76 if ($analysis->{needs_websocket_handler}) {
1253 0         0 require Hypersonic::WebSocket::Handler;
1254             Hypersonic::WebSocket::Handler->generate_c_code($builder, {
1255             max_connections => $self->{max_connections},
1256 0         0 });
1257             }
1258              
1259             # WebSocket Rooms - JIT: broadcast groups, only when explicitly enabled
1260 14 50       72 if ($analysis->{needs_websocket_rooms}) {
1261 0         0 require Hypersonic::WebSocket::Room;
1262             Hypersonic::WebSocket::Room->generate_c_code($builder, {
1263             max_rooms => $self->{max_rooms},
1264             max_clients_per_room => $self->{max_clients_per_room},
1265 0         0 });
1266             }
1267              
1268             # Future/Pool - JIT: async thread pool for blocking operations
1269 14 50       55 if ($analysis->{needs_async_pool}) {
1270 0         0 require Hypersonic::Future;
1271 0         0 require Hypersonic::Future::Pool;
1272 0   0     0 my $async_config = $self->{_async_config} // {};
1273             Hypersonic::Future->generate_c_code($builder, {
1274 0   0     0 max_futures => $async_config->{max_futures} // 65536,
1275             });
1276             Hypersonic::Future::Pool->generate_c_code($builder, {
1277             workers => $async_config->{workers} // 8,
1278 0   0     0 queue_size => $async_config->{queue_size} // 4096,
      0        
1279             });
1280             }
1281              
1282             # need_xs_builder routes - call handlers with fresh builder
1283 14 50       56 if ($analysis->{needs_xs_builder}) {
1284 0         0 my @xs_builder_routes;
1285 0         0 for my $i (0 .. $#{$self->{routes}}) {
  0         0  
1286 0         0 my $route = $self->{routes}[$i];
1287 0 0       0 next unless $route->{need_xs_builder};
1288            
1289             # Create fresh builder for this handler
1290 0         0 my $ext_builder = XS::JIT::Builder->new;
1291            
1292             # Call handler with clean builder
1293 0         0 my $result = $route->{handler}->($ext_builder);
1294            
1295             die "need_xs_builder handler for $route->{method} $route->{path} must return hashref with xs_function"
1296 0 0 0     0 unless ref($result) eq 'HASH' && $result->{xs_function};
1297            
1298             # Store result
1299 0         0 $route->{_xs_result} = $result;
1300 0         0 push @xs_builder_routes, {
1301             route_idx => $i,
1302             route => $route,
1303             result => $result,
1304             code => $ext_builder->code,
1305             };
1306             }
1307            
1308             # Emit generated code
1309 0 0       0 if (@xs_builder_routes) {
1310 0         0 $builder->comment('User XS builder routes');
1311 0         0 for my $xsr (@xs_builder_routes) {
1312             $builder->comment("Route: $xsr->{route}{method} $xsr->{route}{path}")
1313             ->raw($xsr->{code})
1314 0         0 ->blank;
1315             }
1316             }
1317            
1318             # Store for function merging later
1319 0         0 $self->{_xs_builder_routes} = \@xs_builder_routes;
1320             }
1321              
1322             # JIT: WebSocket handler storage (independent of dynamic routes)
1323 14 50       82 if ($analysis->{needs_websocket}) {
1324 0         0 $builder->comment('WebSocket handler storage')
1325             ->line('static SV* g_websocket_handlers = NULL;')
1326             ->blank;
1327             }
1328              
1329             # Global storage for dynamic handler dispatch (only if needed)
1330 14 100       51 if ($has_dynamic) {
1331 3         24 $builder->comment('Storage for dynamic handler callbacks')
1332             ->line('static SV* g_handler_array = NULL;')
1333             ->line('static SV* g_server_obj = NULL;');
1334              
1335             # JIT: Only generate middleware storage if middleware is present
1336 3 100       156 if ($analysis->{has_any_middleware}) {
1337 1         13 $builder->line('static SV* g_before_middleware = NULL;')
1338             ->line('static SV* g_after_middleware = NULL;')
1339             ->line('static SV* g_route_before_middleware = NULL;')
1340             ->line('static SV* g_route_after_middleware = NULL;');
1341             }
1342 3         10 $builder->blank;
1343              
1344             # Generate param info table for named path parameters
1345             # Structure: { param_name, segment_position } per handler
1346 3         17 $builder->comment('Path parameter info per dynamic handler')
1347             ->line('typedef struct { const char* name; int position; } ParamInfo;')
1348             ->line('typedef struct { int count; ParamInfo params[8]; } RouteParamInfo;')
1349             ->blank;
1350              
1351 3   50     4 my @route_params = @{$self->{route_param_info} // []};
  3         42  
1352 3         12 my $handler_count = scalar @route_params;
1353              
1354 3         18 $builder->line("static RouteParamInfo g_route_params[$handler_count] = {");
1355 3         25 for my $i (0 .. $#route_params) {
1356 6   50     19 my $params = $route_params[$i] // [];
1357 6         11 my $count = scalar @$params;
1358 6         8 my @param_strs;
1359 6         14 for my $p (@$params) {
1360 0         0 push @param_strs, qq({ "$p->{name}", $p->{position} });
1361             }
1362             # Pad to 8 elements with {NULL, 0}
1363 6         9 my $padding = 8 - scalar(@param_strs);
1364 6         14 for (1 .. $padding) {
1365 48         68 push @param_strs, '{NULL, 0}';
1366             }
1367 6         19 my $params_str = join(', ', @param_strs);
1368 6         42 $builder->line(" { $count, { $params_str } },");
1369             }
1370 3         11 $builder->line('};')
1371             ->blank;
1372              
1373             # JIT: Streaming handler flags array (only if streaming is enabled)
1374 3 50 66     21 if ($analysis->{needs_streaming} && $self->{_streaming_flags}) {
1375 2         3 my @flags = @{$self->{_streaming_flags}};
  2         10  
1376 2         8 my $flags_str = join(', ', @flags);
1377 2         14 $builder->comment('Streaming handler flags - 1 = streaming, 0 = normal')
1378             ->line("static int g_streaming_handlers[$handler_count] = { $flags_str };")
1379             ->blank;
1380             }
1381             }
1382              
1383             # JIT: WebSocket route paths array (only if WebSocket routes exist)
1384 14 0 33     48 if ($analysis->{needs_websocket} && $self->{_websocket_paths}) {
1385 0         0 my @paths = @{$self->{_websocket_paths}};
  0         0  
1386 0         0 my $ws_count = scalar @paths;
1387 0         0 $builder->comment('WebSocket route paths');
1388 0         0 for my $i (0 .. $#paths) {
1389 0         0 my $escaped = _escape_c_string($paths[$i]);
1390 0         0 $builder->line(qq{static const char WS_PATH_$i\[] = "$escaped";});
1391             }
1392 0         0 $builder->line("static const char* g_ws_paths[$ws_count] = {");
1393 0         0 for my $i (0 .. $#paths) {
1394 0 0       0 my $comma = ($i < $#paths) ? ',' : '';
1395 0         0 $builder->line(" WS_PATH_$i$comma");
1396             }
1397 0         0 $builder->line('};')
1398             ->line("static const int g_ws_path_count = $ws_count;")
1399             ->blank;
1400             }
1401              
1402             # Emit FULL pre-computed HTTP responses (headers + body)
1403 14         145 for my $i (0 .. $#$full_responses) {
1404 23         87 my $resp = $full_responses->[$i];
1405 23         184 my $escaped = _escape_c_string($resp);
1406 23         51 my $len = length($resp);
1407 23         268 $builder->line("static const char RESP_$i\[] = \"$escaped\";")
1408             ->line("static const int RESP_${i}_LEN = $len;");
1409             }
1410 14         47 $builder->blank;
1411              
1412             # 404 response via Protocol module
1413             my $security_hdrs_404 = $self->{enable_security_headers}
1414 14 50       260 ? $self->_get_security_headers_string()
1415             : '';
1416 14         252 my $resp_404 = $PROTOCOL->build_404_response(
1417             security_headers => $security_hdrs_404,
1418             );
1419            
1420 14         46 my $escaped_404 = _escape_c_string($resp_404);
1421 14         238 $builder->line("static const char RESP_404[] = \"$escaped_404\";")
1422             ->line("static const int RESP_404_LEN = " . length($resp_404) . ";")
1423             ->blank;
1424            
1425             # Security headers constant for dynamic responses
1426 14 100 66     129 if ($self->{enable_security_headers} && $has_dynamic) {
1427 3         17 $builder->raw($self->_gen_security_headers_c_constant())
1428             ->blank;
1429             }
1430              
1431             # Generate dynamic handler caller if needed
1432 14 100       50 if ($has_dynamic) {
1433 3         31 $builder->raw($self->_gen_dynamic_handler_caller())
1434             ->blank;
1435             }
1436              
1437             # Generate XS builder route dispatcher if needed
1438 14 0 33     51 if ($analysis->{needs_xs_builder} && $self->{_xs_builder_routes} && @{$self->{_xs_builder_routes}}) {
  0   0     0  
1439 0         0 $builder->raw($self->_gen_xs_builder_dispatcher())
1440             ->blank;
1441             }
1442              
1443             # Generate WebSocket handler caller if needed
1444 14 50       40 if ($analysis->{needs_websocket}) {
1445 0         0 $builder->raw($self->_gen_websocket_handler_caller())
1446             ->blank;
1447 0         0 $builder->raw($self->_gen_websocket_data_processor())
1448             ->blank;
1449             }
1450              
1451             # Group routes by method for dispatch generation
1452 14         26 my %methods;
1453 14         24 for my $route (@{$self->{routes}}) {
  14         51  
1454 29         36 push @{$methods{$route->{method}}}, $route;
  29         173  
1455             }
1456              
1457             # Generate inline C dispatch function
1458 14         266 $builder->comment('Inline dispatch - returns response pointer and length')
1459             ->comment('Returns: 0=static response, 1=dynamic route, 2=XS builder route, -1=404')
1460             ->comment('For dynamic/XS routes: returns handler_idx in *handler_idx_out')
1461             ->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) {")
1462             ->line(' *handler_idx_out = -1;');
1463              
1464 14         50 for my $method (sort keys %methods) {
1465 18         30 my $mlen = length($method);
1466 18         111 $builder->if("method_len == $mlen && memcmp(method, \"$method\", $mlen) == 0");
1467              
1468 18         30 for my $r (@{$methods{$method}}) {
  18         49  
1469 29         116 my $path = $r->{path};
1470 29         38 my $plen = length($path);
1471              
1472 29 100       79 if (!$r->{dynamic}) {
1473             # Static route - return pre-computed response
1474 23         48 my $escaped_path = _escape_c_string($path);
1475 23         61 my $idx = $r->{response_idx};
1476              
1477 23         254 $builder->if("path_len == $plen && memcmp(path, \"$escaped_path\", $plen) == 0")
1478             ->line("*resp_out = RESP_$idx;")
1479             ->line("*resp_len_out = RESP_${idx}_LEN;")
1480             ->line("return 0;")
1481             ->endif;
1482             } else {
1483             # Dynamic route - check for path params or exact match
1484 6         12 my $handler_idx = $r->{handler_idx};
1485              
1486             # Check if this is a need_xs_builder route - dispatch to XS directly
1487 6 50 33     24 if ($r->{need_xs_builder} && $r->{_xs_result}) {
    50          
1488 0         0 my $xs_func = $r->{_xs_result}{xs_function};
1489 0         0 my $escaped_path = _escape_c_string($path);
1490            
1491 0 0       0 if ($path =~ /:(\w+)/) {
1492             # Path has parameters
1493 0         0 my ($prefix) = $path =~ m{^([^:]+)};
1494 0         0 my $prefix_len = length($prefix);
1495 0         0 my $escaped_prefix = _escape_c_string($prefix);
1496            
1497 0         0 $builder->if("path_len >= $prefix_len && memcmp(path, \"$escaped_prefix\", $prefix_len) == 0")
1498             ->comment("XS builder route - call $xs_func directly")
1499             ->line("*handler_idx_out = $handler_idx;")
1500             ->line("return 2;") # Special return code for XS builder routes
1501             ->endif;
1502             } else {
1503             # Exact match
1504 0         0 $builder->if("path_len == $plen && memcmp(path, \"$escaped_path\", $plen) == 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             }
1510             } elsif ($path =~ /:(\w+)/) {
1511             # Path has parameters - generate pattern matching
1512 0         0 my ($prefix) = $path =~ m{^([^:]+)};
1513 0         0 my $prefix_len = length($prefix);
1514 0         0 my $escaped_prefix = _escape_c_string($prefix);
1515              
1516 0         0 $builder->if("path_len >= $prefix_len && memcmp(path, \"$escaped_prefix\", $prefix_len) == 0")
1517             ->line("*resp_out = NULL;")
1518             ->line("*handler_idx_out = $handler_idx;")
1519             ->line("return 1;")
1520             ->endif;
1521             } else {
1522             # Exact match dynamic route
1523 6         12 my $escaped_path = _escape_c_string($path);
1524 6         50 $builder->if("path_len == $plen && memcmp(path, \"$escaped_path\", $plen) == 0")
1525             ->line("*resp_out = NULL;")
1526             ->line("*handler_idx_out = $handler_idx;")
1527             ->line("return 1;")
1528             ->endif;
1529             }
1530             }
1531             }
1532              
1533 18         49 $builder->endif;
1534             }
1535              
1536 14         111 $builder->line('*resp_out = RESP_404;')
1537             ->line('*resp_len_out = RESP_404_LEN;')
1538             ->line('return -1;')
1539             ->line('}')
1540             ->blank;
1541              
1542             # Generate the pure C event loop using backend module
1543 14         93 $self->_gen_event_loop($builder, $backend);
1544 14         35 $builder->blank;
1545              
1546             # Keep the Perl-callable dispatch for testing/compatibility
1547 14         625 $builder->comment('Perl-callable dispatch wrapper')
1548             ->xs_function('hypersonic_dispatch')
1549             ->xs_preamble
1550             ->line('if (items != 1) croak_xs_usage(cv, "req_ref");')
1551             ->declare_sv('req_ref', 'ST(0)')
1552             ->if('!SvROK(req_ref) || SvTYPE(SvRV(req_ref)) != SVt_PVAV')
1553             ->line('ST(0) = &PL_sv_undef;')
1554             ->line('XSRETURN(1);')
1555             ->endif
1556             ->declare_av('req', '(AV*)SvRV(req_ref)')
1557             ->line('SV** ary = AvARRAY(req);')
1558             ->line('STRLEN method_len, path_len;')
1559             ->line('const char* method = SvPV(ary[0], method_len);')
1560             ->line('const char* path = SvPV(ary[1], path_len);')
1561             ->line('const char* resp;')
1562             ->line('int resp_len;')
1563             ->line('int handler_idx;')
1564             ->line('int rc = dispatch_request(method, (int)method_len, path, (int)path_len, &resp, &resp_len, &handler_idx);')
1565             ->if('rc == 0')
1566             ->comment('Static route')
1567             ->line('ST(0) = sv_2mortal(newSVpvn(resp, resp_len));')
1568             ->elsif('rc == 1')
1569             ->comment('Dynamic route - for testing, just return undef')
1570             ->line('ST(0) = &PL_sv_undef;')
1571             ->else
1572             ->line('ST(0) = &PL_sv_undef;')
1573             ->endif
1574             ->xs_return('1')
1575             ->xs_end;
1576              
1577 14         641 return $builder->code;
1578             }
1579              
1580             # Generate optimized method parser - delegates to Protocol module
1581             sub _gen_method_parser {
1582 14     14   35 my ($self, $builder) = @_;
1583 14         32 my $analysis = $self->{route_analysis};
1584              
1585             # Delegate to Protocol module for HTTP/1.1 parsing
1586             # This separation allows future HTTP/2 support without changing core logic
1587 14         114 return $PROTOCOL->gen_method_parser($builder, $analysis);
1588             }
1589              
1590             # Generate WebSocket frame handling for established connections
1591             sub _gen_websocket_frame_handler {
1592 0     0   0 my ($self, $builder) = @_;
1593              
1594 0         0 $builder->comment('Check if this is an established WebSocket connection')
1595             ->if('fd >= 0 && fd < WS_MAX && ws_registry[fd].state == WS_STATE_OPEN')
1596             ->comment('This is a WebSocket connection - process frames')
1597             ->line('process_websocket_data(aTHX_ fd, recv_buf, len);')
1598             ->line('continue;')
1599             ->endif
1600             ->blank;
1601             }
1602              
1603             # Generate WebSocket upgrade detection and dispatch
1604             sub _gen_websocket_dispatch {
1605 0     0   0 my ($self, $builder) = @_;
1606              
1607 0         0 $builder->comment('WebSocket upgrade detection')
1608             ->comment('Check for Upgrade: websocket header')
1609             ->line('int is_websocket = 0;')
1610             ->line('int ws_handler_idx = -1;')
1611             ->line('const char* ws_key = NULL;')
1612             ->line('int ws_key_len = 0;')
1613             ->blank
1614             ->comment('Look for Upgrade header in raw request')
1615             ->line('const char* upgrade_pos = strstr(recv_buf, "Upgrade:");')
1616             ->if('!upgrade_pos')
1617             ->line('upgrade_pos = strstr(recv_buf, "upgrade:");')
1618             ->endif
1619             ->if('upgrade_pos')
1620             ->line('const char* val_start = upgrade_pos + 8;')
1621             ->line('while (*val_start == \' \') val_start++;')
1622             ->comment('Check for websocket (case-insensitive)')
1623             ->if('strncasecmp(val_start, "websocket", 9) == 0')
1624             ->line('is_websocket = 1;')
1625             ->endif
1626             ->endif
1627             ->blank
1628             ->if('is_websocket')
1629             ->comment('Extract Sec-WebSocket-Key')
1630             ->line('const char* key_pos = strstr(recv_buf, "Sec-WebSocket-Key:");')
1631             ->if('!key_pos')
1632             ->line('key_pos = strstr(recv_buf, "sec-websocket-key:");')
1633             ->endif
1634             ->if('key_pos')
1635             ->line('const char* key_start = key_pos + 18;')
1636             ->line('while (*key_start == \' \') key_start++;')
1637             ->line('const char* key_end = key_start;')
1638             ->line('while (*key_end && *key_end != \'\\r\' && *key_end != \'\\n\') key_end++;')
1639             ->line('ws_key = key_start;')
1640             ->line('ws_key_len = key_end - key_start;')
1641             ->endif
1642             ->blank
1643             ->comment('Match path to WebSocket routes')
1644             ->line('int clean_path_len = path_len;')
1645             ->line('const char* qmark = memchr(path, \'?\', path_len);')
1646             ->if('qmark')
1647             ->line('clean_path_len = qmark - path;')
1648             ->endif
1649             ->for('int i = 0', 'i < g_ws_path_count', 'i++')
1650             ->line('int ws_path_len = strlen(g_ws_paths[i]);')
1651             ->if('clean_path_len == ws_path_len && memcmp(path, g_ws_paths[i], ws_path_len) == 0')
1652             ->line('ws_handler_idx = i;')
1653             ->line('break;')
1654             ->endif
1655             ->endfor
1656             ->blank
1657             ->if('ws_handler_idx >= 0 && ws_key')
1658             ->comment('WebSocket upgrade - call Perl handler')
1659             ->line('call_websocket_handler(aTHX_ ws_handler_idx, fd, path, path_len, ws_key, ws_key_len, recv_buf, len);')
1660             ->line('continue;') # Skip normal dispatch, connection stays open
1661             ->endif
1662             ->endif
1663             ->blank;
1664             }
1665              
1666             # Unified event loop generator - uses backend module for platform-specific code
1667             sub _gen_event_loop {
1668 14     14   49 my ($self, $builder, $backend) = @_;
1669 14         26 my $has_dynamic = grep { $_->{dynamic} } @{$self->{routes}};
  29         71  
  14         41  
1670 14         29 my $analysis = $self->{route_analysis};
1671 14   50     58 my $has_body_access = $analysis->{has_body_access} // 0;
1672 14         87 my $backend_name = $backend->name;
1673 14         51 my $event_struct = $backend->event_struct;
1674              
1675 14         311 $builder->comment("Pure C event loop using $backend_name backend - WITH SECURITY HARDENING")
1676             ->xs_function('hypersonic_run_event_loop')
1677             ->xs_preamble
1678             ->check_items(2, 2, 'listen_fd, server_obj')
1679             ->blank
1680             ->declare('int', 'listen_fd', '(int)SvIV(ST(0))')
1681             ->declare_sv('server_obj', 'ST(1)')
1682             ->blank;
1683              
1684             # Handler storage (only if dynamic routes)
1685 14 100       149 if ($has_dynamic) {
1686 3         44 $builder->comment('Store server object for dynamic handler access')
1687             ->if('SvROK(server_obj)')
1688             ->declare_hv('self', '(HV*)SvRV(server_obj)')
1689             ->line('SV** handlers_ref = hv_fetch(self, "dynamic_handlers", 16, 0);')
1690             ->if('handlers_ref && SvROK(*handlers_ref)')
1691             ->line('g_handler_array = *handlers_ref;')
1692             ->line('SvREFCNT_inc(g_handler_array);')
1693             ->endif
1694             ->line('g_server_obj = server_obj;')
1695             ->line('SvREFCNT_inc(g_server_obj);');
1696              
1697             # JIT: Only fetch middleware if any middleware is present
1698 3 100       9 if ($analysis->{has_any_middleware}) {
1699 1         4 $builder->blank
1700             ->comment('JIT: Middleware storage (Perl coderefs only - builders are inline C)');
1701              
1702             # Only fetch Perl middleware if we have any
1703 1 50       2 if ($analysis->{has_global_before}) {
1704 1         7 $builder->line('SV** before_ref = hv_fetch(self, "_perl_before_mw", 15, 0);')
1705             ->if('before_ref && SvROK(*before_ref)')
1706             ->line('g_before_middleware = *before_ref;')
1707             ->line('SvREFCNT_inc(g_before_middleware);')
1708             ->endif;
1709             }
1710 1 50       2 if ($analysis->{has_global_after}) {
1711 1         6 $builder->line('SV** after_ref = hv_fetch(self, "_perl_after_mw", 14, 0);')
1712             ->if('after_ref && SvROK(*after_ref)')
1713             ->line('g_after_middleware = *after_ref;')
1714             ->line('SvREFCNT_inc(g_after_middleware);')
1715             ->endif;
1716             }
1717              
1718             # Per-route middleware arrays
1719 1 50       5 if ($analysis->{has_route_middleware}) {
1720 0         0 $builder->line('SV** route_before_ref = hv_fetch(self, "_route_before_mw", 16, 0);')
1721             ->if('route_before_ref && SvROK(*route_before_ref)')
1722             ->line('g_route_before_middleware = *route_before_ref;')
1723             ->line('SvREFCNT_inc(g_route_before_middleware);')
1724             ->endif
1725             ->line('SV** route_after_ref = hv_fetch(self, "_route_after_mw", 15, 0);')
1726             ->if('route_after_ref && SvROK(*route_after_ref)')
1727             ->line('g_route_after_middleware = *route_after_ref;')
1728             ->line('SvREFCNT_inc(g_route_after_middleware);')
1729             ->endif;
1730             }
1731             }
1732              
1733 3         26 $builder->endif;
1734             } else {
1735 11         107 $builder->line('(void)server_obj; /* Not needed for static-only routes */');
1736             }
1737 14         44 $builder->blank;
1738              
1739             # WebSocket handler storage (only if WebSocket routes)
1740 14 50       41 if ($analysis->{needs_websocket}) {
1741 0         0 $builder->comment('Store WebSocket handlers for dispatch')
1742             ->if('SvROK(server_obj)')
1743             ->declare_hv('ws_self', '(HV*)SvRV(server_obj)')
1744             ->line('SV** ws_handlers_ref = hv_fetch(ws_self, "_websocket_handlers", 19, 0);')
1745             ->if('ws_handlers_ref && SvROK(*ws_handlers_ref)')
1746             ->line('g_websocket_handlers = *ws_handlers_ref;')
1747             ->line('SvREFCNT_inc(g_websocket_handlers);')
1748             ->endif
1749             ->endif
1750             ->blank;
1751             }
1752              
1753             # Signal handlers and initialization
1754 14         152 $builder->comment('Setup graceful shutdown signal handlers')
1755             ->line('signal(SIGTERM, handle_shutdown_signal);')
1756             ->line('signal(SIGINT, handle_shutdown_signal);')
1757             ->blank
1758             ->comment('Initialize connection tracking')
1759             ->line('memset(g_conn_time, 0, sizeof(g_conn_time));')
1760             ->line('g_active_connections = 0;')
1761             ->blank;
1762              
1763             # TLS initialization
1764 14 50       56 if ($self->{tls}) {
1765 0         0 my $cert_file = _escape_c_string($self->{cert_file});
1766 0         0 my $key_file = _escape_c_string($self->{key_file});
1767 0         0 $builder->comment('Initialize TLS/HTTPS')
1768             ->line('#ifdef HYPERSONIC_TLS')
1769             ->line("if (init_ssl_ctx(\"$cert_file\", \"$key_file\") != 0) {")
1770             ->line(' croak("Failed to initialize TLS context - check cert/key files");')
1771             ->line('}')
1772             ->line('memset(g_tls_connections, 0, sizeof(g_tls_connections));')
1773             ->line('#endif')
1774             ->blank;
1775             }
1776              
1777 14         111 $builder->comment('Thread-local receive buffer - each worker gets its own')
1778             ->line('static __thread char recv_buf[RECV_BUF_SIZE];')
1779             ->blank;
1780              
1781             # Backend-specific: Create event loop and add listen socket
1782 14         118 $backend->gen_create($builder, 'listen_fd');
1783              
1784             # Async Pool: Initialize thread pool and add notify fd to event loop
1785 14 50       50 if ($analysis->{needs_async_pool}) {
1786 0         0 $builder->blank
1787             ->comment('Initialize async thread pool')
1788             ->line('pool_init();')
1789             ->line('int pool_notify_fd = pool_get_notify_fd();');
1790 0         0 $backend->gen_add_pool_notify($builder, 'ev_fd', 'pool_notify_fd');
1791             }
1792              
1793             # Declare event structure variable based on backend
1794 14 50       60 if ($backend_name eq 'kqueue') {
    50          
    0          
1795             # kqueue's gen_create already declares 'ev', but we need events array
1796 0         0 $builder->line('struct kevent events[MAX_EVENTS];');
1797             } elsif ($backend_name eq 'epoll') {
1798             # epoll's gen_create already declares 'ev'
1799 14         80 $builder->line('struct epoll_event events[MAX_EVENTS];');
1800             } elsif ($backend_name eq 'io_uring') {
1801             # io_uring uses completion queue entries
1802 0         0 $builder->line('struct io_uring_cqe** events = NULL;');
1803             } else {
1804             # poll/select manage their own fd arrays internally
1805             # Declare a dummy events pointer to satisfy gen_wait signature
1806 0         0 $builder->line('void* events = NULL;');
1807             }
1808              
1809 14         70 $builder->line('time_t last_cleanup = time(NULL);')
1810             ->line('int accepting = 1; /* Flag to control accepting new connections */')
1811             ->blank;
1812              
1813             # Main event loop
1814 14         99 $builder->while('!g_shutdown || g_active_connections > 0')
1815             ->comment('Use timeout for keep-alive cleanup and shutdown check');
1816              
1817             # Backend-specific: Wait for events
1818 14         91 $backend->gen_wait($builder, 'ev_fd', 'events', 'n', '1000');
1819              
1820 14         81 $builder->blank
1821             ->comment('Check for graceful shutdown - stop accepting new connections')
1822             ->if('g_shutdown && accepting');
1823              
1824             # Backend-specific: Remove listen socket from event loop
1825 14         62 $backend->gen_del($builder, 'ev_fd', 'listen_fd');
1826 14         127 $builder->line('accepting = 0;')
1827             ->endif
1828             ->blank
1829             ->comment('Get time once per event batch')
1830             ->line('time_t now = time(NULL);')
1831             ->line('g_current_time = now;')
1832             ->blank;
1833              
1834             # Keep-alive cleanup
1835 14         197 $builder->comment('Periodic keep-alive timeout cleanup')
1836             ->if('now - last_cleanup >= 5')
1837             ->declare('int', 'cleanup_i', '0')
1838             ->for('cleanup_i = 0', 'cleanup_i < MAX_FD', 'cleanup_i++')
1839             ->if('g_conn_time[cleanup_i] > 0')
1840             ->if('now - g_conn_time[cleanup_i] > KEEPALIVE_TIMEOUT')
1841             ->comment('Close idle connection')
1842             ->line('int idle_fd = cleanup_i;');
1843              
1844             # Backend-specific: Remove idle connection
1845 14         57 $backend->gen_del($builder, 'ev_fd', 'idle_fd');
1846 14         187 $builder->line('HYPERSONIC_CLOSE(idle_fd);')
1847             ->line('remove_connection(idle_fd);')
1848             ->endif
1849             ->endif
1850             ->endfor
1851             ->line('last_cleanup = now;')
1852             ->endif
1853             ->blank;
1854              
1855             # Event processing loop
1856 14         92 $builder->declare('int', 'i', '0')
1857             ->for('i = 0', 'i < n', 'i++');
1858              
1859             # Backend-specific: Get fd from event
1860 14         87 $backend->gen_get_fd($builder, 'events', 'i', 'fd');
1861              
1862 14         79 $builder->blank
1863             ->if('fd == listen_fd && accepting');
1864              
1865             # Accept loop
1866 14         882 $builder->comment('Accept new connections with limit check')
1867             ->while('1')
1868             ->comment('Check connection limit before accepting')
1869             ->if('g_active_connections >= MAX_CONNECTIONS')
1870             ->line('break; /* At capacity, stop accepting */')
1871             ->endif
1872             ->blank
1873             ->line('struct sockaddr_in client_addr;')
1874             ->line('socklen_t client_len = sizeof(client_addr);')
1875             ->line('int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);')
1876             ->if('client_fd < 0')
1877             ->line('break;')
1878             ->endif
1879             ->blank
1880             ->comment('Set non-blocking (hs_set_nonblocking handles Win/POSIX divergence)')
1881             ->line('hs_set_nonblocking(client_fd);')
1882             ->blank
1883             ->comment('Disable Nagle')
1884             ->line('int one = 1;')
1885             ->line('setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, (const char*)&one, sizeof(one));')
1886             ->blank
1887             ->comment('Set receive timeout for security')
1888             ->line('struct timeval tv;')
1889             ->line('tv.tv_sec = RECV_TIMEOUT;')
1890             ->line('tv.tv_usec = 0;')
1891             ->line('setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv));')
1892             ->blank
1893             ->comment('Track connection for keep-alive timeout')
1894             ->line('track_connection(client_fd, now);')
1895             ->blank
1896             ->comment('TLS handshake if enabled')
1897             ->line('#ifdef HYPERSONIC_TLS')
1898             ->line('if (tls_accept(client_fd) < 0) {')
1899             ->line(' close(client_fd);')
1900             ->line(' remove_connection(client_fd);')
1901             ->line(' continue;')
1902             ->line('}')
1903             ->line('#endif')
1904             ->blank
1905             ->comment('Add to event loop');
1906              
1907             # Backend-specific: Add client to event loop
1908 14         97 $backend->gen_add($builder, 'ev_fd', 'client_fd');
1909 14         120 $builder->endwhile;
1910              
1911             # Async Pool: Handle pool notify fd - process completed futures
1912 14 50       136 if ($analysis->{needs_async_pool}) {
1913 0         0 $builder->elsif('fd == pool_notify_fd')
1914             ->comment('Thread pool notification - process completed async operations')
1915             ->line('pool_process_ready();');
1916             }
1917              
1918             # Handle client request
1919 14         247 $builder->elsif('fd != listen_fd')
1920             ->comment('Handle client request')
1921             ->line('#ifdef HYPERSONIC_TLS')
1922             ->line('TLSConnection* tls_conn = get_tls_connection(fd);')
1923             ->line('ssize_t len = tls_conn ? tls_recv(tls_conn, recv_buf, RECV_BUF_SIZE - 1) : -1;')
1924             ->line('#else')
1925             ->line('ssize_t len = recv(fd, recv_buf, RECV_BUF_SIZE - 1, 0);')
1926             ->line('#endif')
1927             ->blank
1928             ->if('len <= 0')
1929             ->comment('Connection closed or error');
1930              
1931             # Backend-specific: Remove from event loop
1932 14         61 $backend->gen_del($builder, 'ev_fd', 'fd');
1933 14         135 $builder->line('#ifdef HYPERSONIC_TLS')
1934             ->line('tls_close(fd);')
1935             ->line('#else')
1936             ->line('close(fd);')
1937             ->line('#endif');
1938              
1939             # Reset WebSocket state if WebSocket routes exist
1940 14 50       41 if ($analysis->{needs_websocket}) {
1941 0         0 $builder->line('ws_reset(fd);');
1942             }
1943              
1944 14         282 $builder->line('remove_connection(fd);')
1945             ->line('continue;')
1946             ->endif
1947             ->blank
1948             ->comment('Update connection activity for keep-alive timeout')
1949             ->line('update_connection(fd, now);')
1950             ->blank
1951             ->line('recv_buf[len] = \'\\0\';')
1952             ->blank;
1953              
1954             # WebSocket frame handling - JIT: only generate if WebSocket routes exist
1955 14 50       40 if ($analysis->{needs_websocket}) {
1956 0         0 $self->_gen_websocket_frame_handler($builder);
1957             }
1958              
1959             # Method parser - delegates to Protocol module
1960 14         102 $self->_gen_method_parser($builder);
1961 14         34 $builder->blank;
1962              
1963             # Path parsing - delegates to Protocol module
1964 14         89 $PROTOCOL->gen_path_parser($builder);
1965 14         33 $builder->blank;
1966              
1967             # WebSocket upgrade detection - JIT: only generate if WebSocket routes exist
1968 14 50       39 if ($analysis->{needs_websocket}) {
1969 0         0 $self->_gen_websocket_dispatch($builder);
1970             }
1971              
1972             # Dispatch
1973 14         121 $builder->comment('Dispatch request')
1974             ->line('const char* resp;')
1975             ->line('int resp_len;')
1976             ->line('int handler_idx;')
1977             ->line('int dispatch_result = dispatch_request(method, method_len, path, path_len, &resp, &resp_len, &handler_idx);')
1978             ->blank;
1979              
1980             # Dynamic dispatch
1981 14 100       31 if ($has_dynamic) {
1982             # Check for XS builder routes first (dispatch_result == 2)
1983 3 50 33     11 if ($analysis->{needs_xs_builder} && $self->{_xs_builder_routes} && @{$self->{_xs_builder_routes}}) {
  0   0     0  
1984 0         0 $builder->if('dispatch_result == 2')
1985             ->comment('XS builder route - call generated XS function directly');
1986            
1987             # Body parsing for XS builder routes (they may need body access)
1988 0         0 $PROTOCOL->gen_body_parser($builder, has_body_access => $has_body_access);
1989            
1990 0         0 $builder->blank
1991             ->line('char* xs_resp;')
1992             ->line('int xs_resp_len;')
1993             ->line('call_xs_builder_handler(aTHX_ handler_idx, fd, method, method_len, path, path_len, body, body_len, &xs_resp, &xs_resp_len);')
1994             ->line('HYPERSONIC_SEND(fd, xs_resp, xs_resp_len);')
1995             ->elsif('dispatch_result == 1');
1996             } else {
1997 3         10 $builder->if('dispatch_result == 1');
1998             }
1999            
2000 3         10 $builder->comment('Dynamic route - call Perl handler');
2001              
2002             # Body parsing - delegates to Protocol module
2003 3         18 $PROTOCOL->gen_body_parser($builder, has_body_access => $has_body_access);
2004              
2005 3         67 $builder->blank
2006             ->line('char* dyn_resp;')
2007             ->line('int dyn_resp_len;')
2008             ->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);')
2009             ->comment('dyn_resp_len == -1 means streaming handler (response already sent)')
2010             ->line('if (dyn_resp_len >= 0) {')
2011             ->line(' HYPERSONIC_SEND(fd, dyn_resp, dyn_resp_len);')
2012             ->line('}')
2013             ->else
2014             ->line('HYPERSONIC_SEND(fd, resp, resp_len);')
2015             ->endif;
2016             } else {
2017 11         44 $builder->line('HYPERSONIC_SEND(fd, resp, resp_len);');
2018             }
2019 14         35 $builder->blank;
2020              
2021             # Keep-alive check - delegates to Protocol module
2022 14         119 $PROTOCOL->gen_keepalive_check($builder);
2023 14         105 $builder->blank
2024             ->if('!keep_alive');
2025              
2026             # Backend-specific: Remove from event loop on close
2027 14         82 $backend->gen_del($builder, 'ev_fd', 'fd');
2028 14         63 $builder->line('HYPERSONIC_CLOSE(fd);');
2029              
2030             # Reset WebSocket state if WebSocket routes exist
2031 14 50       41 if ($analysis->{needs_websocket}) {
2032 0         0 $builder->line('ws_reset(fd);');
2033             }
2034              
2035 14         83 $builder->line('remove_connection(fd);')
2036             ->endif;
2037              
2038             # Close event processing
2039 14         74 $builder->endif # fd == listen_fd
2040             ->endfor # for i
2041             ->endwhile # main loop
2042             ->blank;
2043              
2044             # Async Pool: Shutdown thread pool on server exit
2045 14 50       41 if ($analysis->{needs_async_pool}) {
2046 0         0 $builder->comment('Shutdown async thread pool')
2047             ->line('pool_shutdown();');
2048             }
2049              
2050 14         110 $builder->line('close(ev_fd);')
2051             ->xs_return('0')
2052             ->xs_end;
2053              
2054 14         27 return $builder;
2055             }
2056              
2057             sub _gen_xs_builder_dispatcher {
2058 0     0   0 my ($self) = @_;
2059            
2060 0         0 my $builder = XS::JIT::Builder->new;
2061 0   0     0 my $xs_routes = $self->{_xs_builder_routes} || [];
2062            
2063 0 0       0 return '' unless @$xs_routes;
2064            
2065 0         0 $builder->comment('XS Builder route dispatcher')
2066             ->comment('Dispatches to user-defined XS functions based on handler_idx')
2067             ->line('static void call_xs_builder_handler(pTHX_ int handler_idx, int fd,')
2068             ->line(' const char* method, int method_len,')
2069             ->line(' const char* path, int path_len,')
2070             ->line(' const char* body, int body_len,')
2071             ->line(' char** resp_out, int* resp_len_out) {')
2072             ->line(' switch (handler_idx) {');
2073            
2074 0         0 for my $entry (@$xs_routes) {
2075 0         0 my $handler_idx = $entry->{route}{handler_idx};
2076 0         0 my $xs_func = $entry->{result}{xs_function};
2077            
2078 0         0 $builder->line(" case $handler_idx:")
2079             ->line(" $xs_func(aTHX_ fd, method, method_len, path, path_len, body, body_len, resp_out, resp_len_out);")
2080             ->line(" break;");
2081             }
2082            
2083 0         0 $builder->line(' default:')
2084             ->line(' *resp_out = (char*)RESP_404;')
2085             ->line(' *resp_len_out = RESP_404_LEN;')
2086             ->line(' break;')
2087             ->line(' }')
2088             ->line('}')
2089             ->blank;
2090            
2091 0         0 return $builder->code;
2092             }
2093              
2094             sub _gen_dynamic_handler_caller {
2095 3     3   15 my ($self) = @_;
2096 3         7 my $analysis = $self->{route_analysis};
2097 3         62 my $builder = XS::JIT::Builder->new;
2098            
2099             # ============================================================
2100             # JIT PHILOSOPHY: Only generate code for features actually used
2101             # ============================================================
2102            
2103             # Status code helper - always needed for dynamic routes
2104 3         140 $builder->comment('Status code to text mapping')
2105             ->line('static const char* get_status_text(int code) {')
2106             ->line(' switch(code) {')
2107             ->line(' case 200: return "OK";')
2108             ->line(' case 201: return "Created";')
2109             ->line(' case 202: return "Accepted";')
2110             ->line(' case 204: return "No Content";')
2111             ->line(' case 301: return "Moved Permanently";')
2112             ->line(' case 302: return "Found";')
2113             ->line(' case 303: return "See Other";')
2114             ->line(' case 304: return "Not Modified";')
2115             ->line(' case 400: return "Bad Request";')
2116             ->line(' case 401: return "Unauthorized";')
2117             ->line(' case 403: return "Forbidden";')
2118             ->line(' case 404: return "Not Found";')
2119             ->line(' case 405: return "Method Not Allowed";')
2120             ->line(' case 408: return "Request Timeout";')
2121             ->line(' case 409: return "Conflict";')
2122             ->line(' case 413: return "Payload Too Large";')
2123             ->line(' case 422: return "Unprocessable Entity";')
2124             ->line(' case 429: return "Too Many Requests";')
2125             ->line(' case 500: return "Internal Server Error";')
2126             ->line(' case 502: return "Bad Gateway";')
2127             ->line(' case 503: return "Service Unavailable";')
2128             ->line(' default: return "Unknown";')
2129             ->line(' }')
2130             ->line('}')
2131             ->blank;
2132            
2133             # Path segment parser - always needed for path params
2134 3         90 $builder->comment('Parse path segments into array - stops at ? for query string')
2135             ->line('static int parse_path_segments(const char* path, int path_len,')
2136             ->line(' const char** segments, int* seg_lens, int max_segs) {')
2137             ->line(' int count = 0;')
2138             ->line(' const char* start;')
2139             ->line(' const char* end;')
2140             ->line(' const char* query;')
2141             ->line(' const char* seg_end;')
2142             ->line(' start = path;')
2143             ->line(' end = path + path_len;')
2144             ->line(' query = memchr(path, \'?\', path_len);')
2145             ->line(' if (query) end = query;')
2146             ->line(' while (start < end && count < max_segs) {')
2147             ->line(' if (*start == \'/\') start++;')
2148             ->line(' if (start >= end) break;')
2149             ->line(' seg_end = start;')
2150             ->line(' while (seg_end < end && *seg_end != \'/\') seg_end++;')
2151             ->line(' segments[count] = start;')
2152             ->line(' seg_lens[count] = seg_end - start;')
2153             ->line(' count++;')
2154             ->line(' start = seg_end;')
2155             ->line(' }')
2156             ->line(' return count;')
2157             ->line('}')
2158             ->blank;
2159            
2160             # URL decoder - only if query/form parsing needed
2161 3 50 33     36 if ($analysis->{needs_query} || $analysis->{needs_form}) {
2162 0         0 $builder->comment('JIT: URL decode helper (query/form parsing enabled)')
2163             ->line('static int url_decode(char* str, int len) {')
2164             ->line(' char* dst = str;')
2165             ->line(' const char* src = str;')
2166             ->line(' const char* end = str + len;')
2167             ->line(' while (src < end) {')
2168             ->line(' if (*src == \'%\' && src + 2 < end) {')
2169             ->line(' int hi = src[1];')
2170             ->line(' int lo = src[2];')
2171             ->line(' hi = (hi >= \'0\' && hi <= \'9\') ? hi - \'0\' :')
2172             ->line(' (hi >= \'A\' && hi <= \'F\') ? hi - \'A\' + 10 :')
2173             ->line(' (hi >= \'a\' && hi <= \'f\') ? hi - \'a\' + 10 : -1;')
2174             ->line(' lo = (lo >= \'0\' && lo <= \'9\') ? lo - \'0\' :')
2175             ->line(' (lo >= \'A\' && lo <= \'F\') ? lo - \'A\' + 10 :')
2176             ->line(' (lo >= \'a\' && lo <= \'f\') ? lo - \'a\' + 10 : -1;')
2177             ->line(' if (hi >= 0 && lo >= 0) {')
2178             ->line(' *dst++ = (char)((hi << 4) | lo);')
2179             ->line(' src += 3;')
2180             ->line(' continue;')
2181             ->line(' }')
2182             ->line(' } else if (*src == \'+\') {')
2183             ->line(' *dst++ = \' \';')
2184             ->line(' src++;')
2185             ->line(' continue;')
2186             ->line(' }')
2187             ->line(' *dst++ = *src++;')
2188             ->line(' }')
2189             ->line(' return dst - str;')
2190             ->line('}')
2191             ->blank;
2192            
2193 0         0 $builder->comment('JIT: Query string parser (query/form parsing enabled)')
2194             ->line('static void parse_query_string(pTHX_ const char* query, int query_len, HV* hv) {')
2195             ->line(' const char* start = query;')
2196             ->line(' const char* end = query + query_len;')
2197             ->line(' while (start < end) {')
2198             ->line(' const char* eq = memchr(start, \'=\', end - start);')
2199             ->line(' const char* amp = memchr(start, \'&\', end - start);')
2200             ->line(' if (!amp) amp = end;')
2201             ->line(' if (eq && eq < amp) {')
2202             ->line(' int key_len = eq - start;')
2203             ->line(' const char* val = eq + 1;')
2204             ->line(' int val_len = amp - val;')
2205             ->line(' char val_buf[1024];')
2206             ->line(' if (val_len < (int)sizeof(val_buf)) {')
2207             ->line(' memcpy(val_buf, val, val_len);')
2208             ->line(' val_len = url_decode(val_buf, val_len);')
2209             ->line(' hv_store(hv, start, key_len, newSVpvn(val_buf, val_len), 0);')
2210             ->line(' }')
2211             ->line(' } else if (amp > start) {')
2212             ->line(' hv_store(hv, start, amp - start, newSVpvn("", 0), 0);')
2213             ->line(' }')
2214             ->line(' start = amp + 1;')
2215             ->line(' }')
2216             ->line('}')
2217             ->blank;
2218             }
2219            
2220             # Header parser - only if headers/cookies/json/form needed
2221 3 50 66     29 if ($analysis->{needs_headers} || $analysis->{needs_cookies} ||
      66        
      33        
2222             $analysis->{needs_json} || $analysis->{needs_form}) {
2223 1         34 $builder->comment('JIT: Header parser (headers/cookies/json/form enabled)')
2224             ->line('static void parse_headers(pTHX_ const char* raw, int raw_len, HV* hv) {')
2225             ->line(' const char* line = memchr(raw, \'\\n\', raw_len);')
2226             ->line(' if (!line) return;')
2227             ->line(' line++;')
2228             ->line(' const char* end = raw + raw_len;')
2229             ->line(' while (line < end) {')
2230             ->line(' const char* line_end = memchr(line, \'\\r\', end - line);')
2231             ->line(' if (!line_end) line_end = memchr(line, \'\\n\', end - line);')
2232             ->line(' if (!line_end || line_end == line) break;')
2233             ->line(' const char* colon = memchr(line, \':\', line_end - line);')
2234             ->line(' if (colon) {')
2235             ->line(' int name_len = colon - line;')
2236             ->line(' const char* value = colon + 1;')
2237             ->line(' while (value < line_end && (*value == \' \' || *value == \'\\t\')) value++;')
2238             ->line(' int value_len = line_end - value;')
2239             ->line(' char name_buf[128];')
2240             ->line(' if (name_len < (int)sizeof(name_buf)) {')
2241             ->line(' int i;')
2242             ->line(' for (i = 0; i < name_len; i++) {')
2243             ->line(' char c = line[i];')
2244             ->line(' name_buf[i] = (c >= \'A\' && c <= \'Z\') ? c + 32 : (c == \'-\') ? \'_\' : c;')
2245             ->line(' }')
2246             ->line(' hv_store(hv, name_buf, name_len, newSVpvn(value, value_len), 0);')
2247             ->line(' }')
2248             ->line(' }')
2249             ->line(' line = line_end;')
2250             ->line(' if (*line == \'\\r\') line++;')
2251             ->line(' if (line < end && *line == \'\\n\') line++;')
2252             ->line(' }')
2253             ->line('}')
2254             ->blank;
2255             }
2256            
2257             # JIT: Generate C helpers from builder middleware (static functions at file scope)
2258 3 50 33     54 if ($analysis->{has_builder_before} || $analysis->{has_builder_after}) {
2259 0         0 my %seen_classes;
2260 0         0 $builder->comment('JIT: Builder middleware C helpers (zero Perl in hot path)');
2261 0         0 for my $mw (@{$analysis->{builder_before}}, @{$analysis->{builder_after}}) {
  0         0  
  0         0  
2262 0         0 my $class = ref($mw);
2263 0 0       0 next if $seen_classes{$class}++; # Deduplicate by class
2264 0 0       0 if ($mw->can('build_helpers')) {
2265 0         0 $mw->build_helpers($builder);
2266 0         0 $builder->blank;
2267             }
2268             }
2269             }
2270              
2271             # JIT: Generate middleware helper function BEFORE call_dynamic_handler
2272 3 100       9 if ($analysis->{has_any_middleware}) {
2273 1         34 $builder->comment('JIT: Middleware helper - call array of handlers, short-circuit on defined return')
2274             ->line('static SV* call_middleware_chain(pTHX_ AV* handlers, SV* req_ref) {')
2275             ->line(' dSP;')
2276             ->line(' SSize_t len = av_len(handlers) + 1;')
2277             ->line(' SSize_t i;')
2278             ->line(' for (i = 0; i < len; i++) {')
2279             ->line(' SV** handler_sv = av_fetch(handlers, i, 0);')
2280             ->line(' if (!handler_sv || !SvROK(*handler_sv)) continue;')
2281             ->line(' PUSHMARK(SP);')
2282             ->line(' XPUSHs(req_ref);')
2283             ->line(' PUTBACK;')
2284             ->line(' int count = call_sv(*handler_sv, G_SCALAR | G_EVAL);')
2285             ->line(' SPAGAIN;')
2286             ->line(' if (SvTRUE(ERRSV)) {')
2287             ->line(' POPs;')
2288             ->line(' continue;')
2289             ->line(' }')
2290             ->line(' if (count == 1) {')
2291             ->line(' SV* result = POPs;')
2292             ->line(' PUTBACK;')
2293             ->line(' if (SvOK(result)) {')
2294             ->line(' return SvREFCNT_inc(result);')
2295             ->line(' }')
2296             ->line(' }')
2297             ->line(' PUTBACK;')
2298             ->line(' }')
2299             ->line(' return NULL;')
2300             ->line('}')
2301             ->blank;
2302             }
2303            
2304             # Main handler function
2305 3         216 $builder->comment('Call dynamic Perl handler and format HTTP response')
2306             ->line('static void call_dynamic_handler(pTHX_ int handler_idx, int client_fd,')
2307             ->line(' const char* method, int method_len,')
2308             ->line(' const char* path, int path_len,')
2309             ->line(' const char* body, int body_len,')
2310             ->line(' const char* raw_request, int raw_request_len,')
2311             ->line(' char** resp_out, int* resp_len_out) {')
2312             ->line(' dSP;')
2313             ->line(' int count;')
2314             ->line(' SV* result;')
2315             ->line(' STRLEN len;')
2316             ->line(' const char* body_str;')
2317             ->line(' int status = 200;')
2318             ->line(' const char* content_type = "text/plain";')
2319             ->line(' AV* handlers;')
2320             ->line(' SV** handler_sv;')
2321             ->line(' const char* query_start;')
2322             ->line(' int clean_path_len;')
2323             ->line(' AV* req;')
2324             ->line(' const char* segments[16];')
2325             ->line(' int seg_lens[16];')
2326             ->line(' int seg_count;')
2327             ->line(' AV* seg_av;')
2328             ->line(' int i;')
2329             ->line(' HV* params_hv;')
2330             ->line(' RouteParamInfo* param_info;')
2331             ->line(' SV* req_ref;')
2332             ->line(' SV* mw_result = NULL;')
2333             ->line(' int short_circuit = 0;')
2334             ->line(' HV* headers_hv = NULL;')
2335             ->line(' int is_streaming = 0;')
2336             ->line(' SV* stream_sv = NULL;')
2337             ->blank
2338             ->line(' if (!g_handler_array) {')
2339             ->line(' *resp_out = (char*)RESP_404;')
2340             ->line(' *resp_len_out = RESP_404_LEN;')
2341             ->line(' return;')
2342             ->line(' }')
2343             ->blank
2344             ->line(' handlers = (AV*)SvRV(g_handler_array);')
2345             ->line(' handler_sv = av_fetch(handlers, handler_idx, 0);')
2346             ->line(' if (!handler_sv || !SvROK(*handler_sv)) {')
2347             ->line(' *resp_out = (char*)RESP_404;')
2348             ->line(' *resp_len_out = RESP_404_LEN;')
2349             ->line(' return;')
2350             ->line(' }')
2351             ->blank;
2352            
2353             # Query string separation
2354 3         80 $builder->comment('Separate path from query string')
2355             ->line(' query_start = memchr(path, \'?\', path_len);')
2356             ->line(' clean_path_len = query_start ? (query_start - path) : path_len;')
2357             ->blank
2358             ->comment('Build array-based request object (JIT slots)')
2359             ->comment('Slot layout: METHOD=0, PATH=1, BODY=2, PARAMS=3, QUERY=4, QUERY_STRING=5,')
2360             ->comment(' HEADERS=6, COOKIES=7, JSON=8, FORM=9, SEGMENTS=10, ID=11')
2361             ->line(' req = newAV();')
2362             ->line(' av_extend(req, 11);') # Pre-allocate 12 slots (0-11)
2363             ->line(' av_store(req, 0, newSVpvn(method, method_len));') # SLOT_METHOD
2364             ->line(' av_store(req, 1, newSVpvn(path, clean_path_len));') # SLOT_PATH
2365             ->line(' av_store(req, 2, newSVpvn(body, body_len));') # SLOT_BODY
2366             ->blank;
2367              
2368             # Path segments and params - always needed
2369 3         99 $builder->comment('Parse path segments and named params')
2370             ->line(' seg_count = parse_path_segments(path, path_len, segments, seg_lens, 16);')
2371             ->line(' seg_av = newAV();')
2372             ->line(' for (i = 0; i < seg_count; i++) {')
2373             ->line(' av_push(seg_av, newSVpvn(segments[i], seg_lens[i]));')
2374             ->line(' }')
2375             ->line(' av_store(req, 10, newRV_noinc((SV*)seg_av));') # SLOT_SEGMENTS
2376             ->blank
2377             ->comment('Build named params from route_param_info table')
2378             ->line(' params_hv = newHV();')
2379             ->line(' param_info = &g_route_params[handler_idx];')
2380             ->line(' for (i = 0; i < param_info->count && i < seg_count; i++) {')
2381             ->line(' int pos = param_info->params[i].position;')
2382             ->line(' if (pos < seg_count) {')
2383             ->line(' hv_store(params_hv, param_info->params[i].name,')
2384             ->line(' strlen(param_info->params[i].name),')
2385             ->line(' newSVpvn(segments[pos], seg_lens[pos]), 0);')
2386             ->line(' }')
2387             ->line(' }')
2388             ->line(' av_store(req, 3, newRV_noinc((SV*)params_hv));') # SLOT_PARAMS
2389             ->line(' if (seg_count > 0) {')
2390             ->line(' av_store(req, 11, newSVpvn(segments[seg_count-1], seg_lens[seg_count-1]));') # SLOT_ID
2391             ->line(' } else {')
2392             ->line(' av_store(req, 11, newSVpvn("", 0));') # SLOT_ID (empty)
2393             ->line(' }')
2394             ->blank;
2395            
2396             # Query string parsing - JIT conditional
2397 3 50       11 if ($analysis->{needs_query}) {
2398 0         0 $builder->comment('JIT: Parse query string (parse_query enabled)')
2399             ->line(' if (query_start) {')
2400             ->line(' HV* query_hv = newHV();')
2401             ->line(' int query_len = path_len - (query_start - path) - 1;')
2402             ->line(' parse_query_string(aTHX_ query_start + 1, query_len, query_hv);')
2403             ->line(' av_store(req, 4, newRV_noinc((SV*)query_hv));') # SLOT_QUERY
2404             ->line(' av_store(req, 5, newSVpvn(query_start + 1, query_len));') # SLOT_QUERY_STRING
2405             ->line(' } else {')
2406             ->line(' av_store(req, 4, newRV_noinc((SV*)newHV()));') # SLOT_QUERY
2407             ->line(' av_store(req, 5, newSVpvn("", 0));') # SLOT_QUERY_STRING
2408             ->line(' }')
2409             ->blank;
2410             } else {
2411 3         21 $builder->comment('JIT: Query parsing SKIPPED (no routes use parse_query)')
2412             ->line(' av_store(req, 4, newRV_noinc((SV*)newHV()));') # SLOT_QUERY
2413             ->line(' av_store(req, 5, newSVpvn("", 0));') # SLOT_QUERY_STRING
2414             ->blank;
2415             }
2416            
2417             # Header parsing - JIT conditional
2418             my $needs_header_parse = $analysis->{needs_headers} || $analysis->{needs_cookies} ||
2419 3   33     39 $analysis->{needs_json} || $analysis->{needs_form};
2420            
2421 3 100       8 if ($needs_header_parse) {
2422 1         5 $builder->comment('JIT: Parse headers (headers/cookies/json/form enabled)')
2423             ->line(' headers_hv = newHV();')
2424             ->line(' parse_headers(aTHX_ raw_request, raw_request_len, headers_hv);')
2425             ->line(' av_store(req, 6, newRV_noinc((SV*)headers_hv));') # SLOT_HEADERS
2426             ->blank;
2427            
2428             # Cookie parsing
2429 1 50       5 if ($analysis->{needs_cookies}) {
2430 1         22 $builder->comment('JIT: Parse cookies (parse_cookies enabled)')
2431             ->line(' SV** cookie_sv = hv_fetch(headers_hv, "cookie", 6, 0);')
2432             ->line(' if (cookie_sv && SvOK(*cookie_sv)) {')
2433             ->line(' HV* cookies_hv = newHV();')
2434             ->line(' STRLEN cookie_len;')
2435             ->line(' const char* cookie_str = SvPV(*cookie_sv, cookie_len);')
2436             ->line(' const char* start = cookie_str;')
2437             ->line(' const char* end = cookie_str + cookie_len;')
2438             ->line(' while (start < end) {')
2439             ->line(' while (start < end && (*start == \' \' || *start == \';\')) start++;')
2440             ->line(' if (start >= end) break;')
2441             ->line(' const char* eq = memchr(start, \'=\', end - start);')
2442             ->line(' const char* semi = memchr(start, \';\', end - start);')
2443             ->line(' if (!semi) semi = end;')
2444             ->line(' if (eq && eq < semi) {')
2445             ->line(' int name_len = eq - start;')
2446             ->line(' const char* val = eq + 1;')
2447             ->line(' int val_len = semi - val;')
2448             ->line(' hv_store(cookies_hv, start, name_len, newSVpvn(val, val_len), 0);')
2449             ->line(' }')
2450             ->line(' start = semi + 1;')
2451             ->line(' }')
2452             ->line(' av_store(req, 7, newRV_noinc((SV*)cookies_hv));') # SLOT_COOKIES
2453             ->line(' } else {')
2454             ->line(' av_store(req, 7, newRV_noinc((SV*)newHV()));') # SLOT_COOKIES
2455             ->line(' }')
2456             ->blank;
2457             } else {
2458 0         0 $builder->comment('JIT: Cookie parsing SKIPPED')
2459             ->line(' av_store(req, 7, newRV_noinc((SV*)newHV()));') # SLOT_COOKIES
2460             ->blank;
2461             }
2462            
2463             # Form/JSON parsing
2464 1 50 33     23 if ($analysis->{needs_form} || $analysis->{needs_json}) {
2465 1         22 $builder->comment('JIT: Form/JSON body parsing')
2466             ->line(' SV** ct_sv = hv_fetch(headers_hv, "content_type", 12, 0);')
2467             ->line(' if (ct_sv && SvOK(*ct_sv) && body_len > 0) {')
2468             ->line(' STRLEN ct_len;')
2469             ->line(' const char* ct_str = SvPV(*ct_sv, ct_len);');
2470            
2471 1 50       3 if ($analysis->{needs_form}) {
2472 0         0 $builder->line(' if (ct_len >= 33 && memcmp(ct_str, "application/x-www-form-urlencoded", 33) == 0) {')
2473             ->line(' HV* form_hv = newHV();')
2474             ->line(' parse_query_string(aTHX_ body, body_len, form_hv);')
2475             ->line(' av_store(req, 9, newRV_noinc((SV*)form_hv));') # SLOT_FORM
2476             ->line(' } else {')
2477             ->line(' av_store(req, 9, newRV_noinc((SV*)newHV()));') # SLOT_FORM
2478             ->line(' }');
2479             } else {
2480 1         4 $builder->line(' av_store(req, 9, newRV_noinc((SV*)newHV()));'); # SLOT_FORM
2481             }
2482              
2483 1 50       2 if ($analysis->{needs_json}) {
2484 1         30 $builder->line(' if (ct_len >= 16 && memcmp(ct_str, "application/json", 16) == 0) {')
2485             ->line(' dSP;')
2486             ->line(' PUSHMARK(SP);')
2487             ->line(' XPUSHs(sv_2mortal(newSVpvn(body, body_len)));')
2488             ->line(' PUTBACK;')
2489             ->line(' int json_count = call_pv("Hypersonic::_decode_json", G_SCALAR | G_EVAL);')
2490             ->line(' SPAGAIN;')
2491             ->line(' if (json_count == 1 && !SvTRUE(ERRSV)) {')
2492             ->line(' SV* json_sv = POPs;')
2493             ->line(' av_store(req, 8, SvREFCNT_inc(json_sv));') # SLOT_JSON
2494             ->line(' } else {')
2495             ->line(' if (SvTRUE(ERRSV)) POPs;')
2496             ->line(' av_store(req, 8, &PL_sv_undef);') # SLOT_JSON
2497             ->line(' }')
2498             ->line(' PUTBACK;')
2499             ->line(' } else {')
2500             ->line(' av_store(req, 8, &PL_sv_undef);') # SLOT_JSON
2501             ->line(' }');
2502             } else {
2503 0         0 $builder->line(' av_store(req, 8, &PL_sv_undef);'); # SLOT_JSON
2504             }
2505              
2506 1         23 $builder->line(' } else {')
2507             ->line(' av_store(req, 9, newRV_noinc((SV*)newHV()));') # SLOT_FORM
2508             ->line(' av_store(req, 8, &PL_sv_undef);') # SLOT_JSON
2509             ->line(' }')
2510             ->blank;
2511             } else {
2512 0         0 $builder->comment('JIT: Form/JSON parsing SKIPPED')
2513             ->line(' av_store(req, 9, newRV_noinc((SV*)newHV()));') # SLOT_FORM
2514             ->line(' av_store(req, 8, &PL_sv_undef);') # SLOT_JSON
2515             ->blank;
2516             }
2517             } else {
2518 2         28 $builder->comment('JIT: All header-based parsing SKIPPED (no routes use these features)')
2519             ->line(' av_store(req, 6, newRV_noinc((SV*)newHV()));') # SLOT_HEADERS
2520             ->line(' av_store(req, 7, newRV_noinc((SV*)newHV()));') # SLOT_COOKIES
2521             ->line(' av_store(req, 8, &PL_sv_undef);') # SLOT_JSON
2522             ->line(' av_store(req, 9, newRV_noinc((SV*)newHV()));') # SLOT_FORM
2523             ->blank;
2524             }
2525              
2526             # Call handler and build response
2527 3         25 $builder->comment('Bless array into Hypersonic::Request and call Perl handler')
2528             ->line(' req_ref = newRV_noinc((SV*)req);')
2529             ->line(' sv_bless(req_ref, gv_stashpv("Hypersonic::Request", GV_ADD));')
2530             ->line(' ENTER;')
2531             ->line(' SAVETMPS;');
2532            
2533             # JIT: Add middleware short-circuit variable only if middleware present
2534             # (mw_result and short_circuit already declared at function top)
2535            
2536             # JIT: Builder-based before middleware (inline C - no Perl calls)
2537 3 50       9 if ($analysis->{has_builder_before}) {
2538             my $ctx = {
2539             req_var => 'req',
2540             req_ref_var => 'req_ref',
2541             slots => $analysis->{middleware_slots},
2542 0         0 };
2543 0         0 $builder->blank
2544             ->comment('JIT: Builder before middleware (inline C - zero Perl overhead)');
2545 0         0 for my $mw (@{$analysis->{builder_before}}) {
  0         0  
2546 0 0       0 if ($mw->can('build_before')) {
2547 0         0 $mw->build_before($builder, $ctx);
2548             }
2549             }
2550             }
2551              
2552             # JIT: Call global before middleware (Perl coderefs via call_sv)
2553 3 100       10 if ($analysis->{has_global_before}) {
2554 1         26 $builder->blank
2555             ->comment('JIT: Call global before middleware (Perl)')
2556             ->line(' if (g_before_middleware && SvROK(g_before_middleware)) {')
2557             ->line(' AV* before_arr = (AV*)SvRV(g_before_middleware);')
2558             ->line(' mw_result = call_middleware_chain(aTHX_ before_arr, req_ref);')
2559             ->line(' if (mw_result) {')
2560             ->line(' result = mw_result;')
2561             ->line(' short_circuit = 1;')
2562             ->line(' }')
2563             ->line(' }');
2564             }
2565            
2566             # JIT: Call per-route before middleware
2567 3 50       8 if ($analysis->{has_route_middleware}) {
2568 0         0 $builder->blank
2569             ->comment('JIT: Call per-route before middleware')
2570             ->line(' if (!short_circuit && g_route_before_middleware && SvROK(g_route_before_middleware)) {')
2571             ->line(' AV* route_before = (AV*)SvRV(g_route_before_middleware);')
2572             ->line(' SV** handler_arr_ref = av_fetch(route_before, handler_idx, 0);')
2573             ->line(' if (handler_arr_ref && SvROK(*handler_arr_ref)) {')
2574             ->line(' AV* handler_arr = (AV*)SvRV(*handler_arr_ref);')
2575             ->line(' if (av_len(handler_arr) >= 0) {')
2576             ->line(' mw_result = call_middleware_chain(aTHX_ handler_arr, req_ref);')
2577             ->line(' if (mw_result) {')
2578             ->line(' result = mw_result;')
2579             ->line(' short_circuit = 1;')
2580             ->line(' }')
2581             ->line(' }')
2582             ->line(' }')
2583             ->line(' }');
2584             }
2585            
2586             # JIT: Streaming handler support
2587 3 100       10 if ($analysis->{needs_streaming}) {
2588 2         44 $builder->blank
2589             ->comment('JIT: Check if this is a streaming handler')
2590             ->line(' is_streaming = g_streaming_handlers[handler_idx];')
2591             ->if('is_streaming')
2592             ->comment('Create Hypersonic::Stream object for streaming handler')
2593             ->line('dSP;')
2594             ->line('PUSHMARK(SP);')
2595             ->line('XPUSHs(sv_2mortal(newSVpv("Hypersonic::Stream", 0)));')
2596             ->line('XPUSHs(sv_2mortal(newSVpv("fd", 0)));')
2597             ->line('XPUSHs(sv_2mortal(newSViv(client_fd)));')
2598             ->line('PUTBACK;')
2599             ->line('int stream_count = call_method("new", G_SCALAR);')
2600             ->line('SPAGAIN;')
2601             ->if('stream_count > 0')
2602             ->line('stream_sv = POPs;')
2603             ->line('SvREFCNT_inc(stream_sv);')
2604             ->endif
2605             ->line('PUTBACK;')
2606             ->endif;
2607             }
2608              
2609             # Call the main handler (conditionally if middleware present)
2610 3 100       8 if ($analysis->{has_any_middleware}) {
2611 1         6 $builder->blank
2612             ->comment('Call main handler (unless middleware short-circuited)')
2613             ->line(' if (!short_circuit) {')
2614             ->line(' PUSHMARK(SP);')
2615             ->line(' XPUSHs(req_ref);');
2616              
2617             # For streaming handlers with middleware
2618 1 50       3 if ($analysis->{needs_streaming}) {
2619 0         0 $builder->line(' if (is_streaming && stream_sv) XPUSHs(stream_sv);');
2620             }
2621              
2622 1         7 $builder->line(' PUTBACK;')
2623             ->line(' count = call_sv(*handler_sv, G_SCALAR | G_EVAL);')
2624             ->line(' SPAGAIN;')
2625             ->line(' if (count == 1) result = POPs;')
2626             ->line(' PUTBACK;')
2627             ->line(' }');
2628             } else {
2629 2         8 $builder->line(' PUSHMARK(SP);')
2630             ->line(' XPUSHs(sv_2mortal(req_ref));');
2631              
2632             # For streaming handlers without middleware
2633 2 50       5 if ($analysis->{needs_streaming}) {
2634 2         6 $builder->line(' if (is_streaming && stream_sv) XPUSHs(stream_sv);');
2635             }
2636              
2637 2         14 $builder->line(' PUTBACK;')
2638             ->line(' count = call_sv(*handler_sv, G_SCALAR | G_EVAL);')
2639             ->line(' SPAGAIN;');
2640             }
2641            
2642             # JIT: Call per-route after middleware
2643 3 50       9 if ($analysis->{has_route_middleware}) {
2644 0         0 $builder->blank
2645             ->comment('JIT: Call per-route after middleware')
2646             ->line(' if (g_route_after_middleware && SvROK(g_route_after_middleware)) {')
2647             ->line(' AV* route_after = (AV*)SvRV(g_route_after_middleware);')
2648             ->line(' SV** handler_arr_ref = av_fetch(route_after, handler_idx, 0);')
2649             ->line(' if (handler_arr_ref && SvROK(*handler_arr_ref)) {')
2650             ->line(' AV* handler_arr = (AV*)SvRV(*handler_arr_ref);')
2651             ->line(' if (av_len(handler_arr) >= 0) {')
2652             ->line(' SV* after_result = call_middleware_chain(aTHX_ handler_arr, req_ref);')
2653             ->line(' if (after_result) {')
2654             ->line(' result = after_result;')
2655             ->line(' }')
2656             ->line(' }')
2657             ->line(' }')
2658             ->line(' }');
2659             }
2660            
2661             # JIT: Call global after middleware (Perl coderefs via call_sv)
2662 3 100       13 if ($analysis->{has_global_after}) {
2663 1         17 $builder->blank
2664             ->comment('JIT: Call global after middleware (Perl)')
2665             ->line(' if (g_after_middleware && SvROK(g_after_middleware)) {')
2666             ->line(' AV* after_arr = (AV*)SvRV(g_after_middleware);')
2667             ->line(' SV* after_result = call_middleware_chain(aTHX_ after_arr, req_ref);')
2668             ->line(' if (after_result) {')
2669             ->line(' result = after_result;')
2670             ->line(' }')
2671             ->line(' }');
2672             }
2673              
2674             # JIT: Builder-based after middleware (inline C - no Perl calls)
2675 3 50       12 if ($analysis->{has_builder_after}) {
2676             my $ctx = {
2677             req_var => 'req',
2678             req_ref_var => 'req_ref',
2679             res_var => 'result',
2680             slots => $analysis->{middleware_slots},
2681 0         0 };
2682 0         0 $builder->blank
2683             ->comment('JIT: Builder after middleware (inline C - zero Perl overhead)');
2684 0         0 for my $mw (@{$analysis->{builder_after}}) {
  0         0  
2685 0 0       0 if ($mw->can('build_after')) {
2686 0         0 $mw->build_after($builder, $ctx);
2687             }
2688             }
2689             }
2690              
2691             # JIT: Streaming handlers - early return (response already sent via Stream)
2692 3 100       10 if ($analysis->{needs_streaming}) {
2693 2         50 $builder->blank
2694             ->comment('JIT: Streaming handlers return early - response sent via Stream object')
2695             ->if('is_streaming')
2696             ->line('if (stream_sv) SvREFCNT_dec(stream_sv);')
2697             ->line('FREETMPS;')
2698             ->line('LEAVE;')
2699             ->line('*resp_out = NULL;')
2700             ->line('*resp_len_out = -1;')
2701             ->comment('Signal streaming response - caller should not send')
2702             ->line('return;')
2703             ->endif;
2704             }
2705              
2706             $builder->blank
2707 3         40 ->line(' if (SvTRUE(ERRSV)) {')
2708             ->line(' static char error_resp[512];')
2709             ->line(' int err_len = snprintf(error_resp, sizeof(error_resp),')
2710             ->line(' "HTTP/1.1 500 Internal Server Error\\r\\n"')
2711             ->line(' "Content-Type: text/plain\\r\\n"')
2712             ->line(' "Content-Length: 21\\r\\n"')
2713             ->line(' "Connection: close\\r\\n\\r\\n"')
2714             ->line(' "Internal Server Error");')
2715             ->line(' *resp_out = error_resp;')
2716             ->line(' *resp_len_out = err_len;');
2717            
2718             # Middleware version already has result set, no-middleware version needs POPs
2719 3 100       7 if ($analysis->{has_any_middleware}) {
2720 1         5 $builder->line(' } else if (SvOK(result)) {')
2721             ->line(' HV* custom_headers = NULL;');
2722             } else {
2723 2         15 $builder->line(' POPs;')
2724             ->line(' } else if (count == 1) {')
2725             ->line(' result = POPs;')
2726             ->line(' if (SvOK(result)) {')
2727             ->line(' HV* custom_headers = NULL;');
2728             }
2729              
2730 3         184 $builder
2731             ->comment(' Handle arrayref [status, headers, body]')
2732             ->line(' if (SvROK(result) && SvTYPE(SvRV(result)) == SVt_PVAV) {')
2733             ->line(' AV* arr = (AV*)SvRV(result);')
2734             ->line(' SV** status_sv = av_fetch(arr, 0, 0);')
2735             ->line(' SV** headers_sv = av_fetch(arr, 1, 0);')
2736             ->line(' SV** body_sv = av_fetch(arr, 2, 0);')
2737             ->line(' if (status_sv) status = (int)SvIV(*status_sv);')
2738             ->line(' if (body_sv) body_str = SvPV(*body_sv, len);')
2739             ->line(' else { body_str = ""; len = 0; }')
2740             ->line(' if (headers_sv && SvROK(*headers_sv) && SvTYPE(SvRV(*headers_sv)) == SVt_PVHV) {')
2741             ->line(' custom_headers = (HV*)SvRV(*headers_sv);')
2742             ->line(' SV** ct_sv = hv_fetch(custom_headers, "Content-Type", 12, 0);')
2743             ->line(' if (ct_sv && SvOK(*ct_sv)) {')
2744             ->line(' STRLEN ct_len;')
2745             ->line(' content_type = SvPV(*ct_sv, ct_len);')
2746             ->line(' }')
2747             ->line(' }')
2748             ->line(' }')
2749             ->comment(' Handle hashref {status, headers, body}')
2750             ->line(' else if (SvROK(result) && SvTYPE(SvRV(result)) == SVt_PVHV) {')
2751             ->line(' HV* hash = (HV*)SvRV(result);')
2752             ->line(' SV** status_sv = hv_fetch(hash, "status", 6, 0);')
2753             ->line(' SV** headers_sv = hv_fetch(hash, "headers", 7, 0);')
2754             ->line(' SV** body_sv = hv_fetch(hash, "body", 4, 0);')
2755             ->line(' if (status_sv) status = (int)SvIV(*status_sv);')
2756             ->line(' if (body_sv) body_str = SvPV(*body_sv, len);')
2757             ->line(' else { body_str = ""; len = 0; }')
2758             ->line(' if (headers_sv && SvROK(*headers_sv) && SvTYPE(SvRV(*headers_sv)) == SVt_PVHV) {')
2759             ->line(' custom_headers = (HV*)SvRV(*headers_sv);')
2760             ->line(' SV** ct_sv = hv_fetch(custom_headers, "Content-Type", 12, 0);')
2761             ->line(' if (ct_sv && SvOK(*ct_sv)) {')
2762             ->line(' STRLEN ct_len;')
2763             ->line(' content_type = SvPV(*ct_sv, ct_len);')
2764             ->line(' }')
2765             ->line(' }')
2766             ->line(' }')
2767             ->comment(' Plain string response')
2768             ->line(' else {')
2769             ->line(' body_str = SvPV(result, len);')
2770             ->line(' }')
2771             ->blank
2772             ->comment(' Auto-detect JSON content type')
2773             ->line(' if (strcmp(content_type, "text/plain") == 0 && len > 0 &&')
2774             ->line(' (body_str[0] == \'{\' || body_str[0] == \'[\')) {')
2775             ->line(' content_type = "application/json";')
2776             ->line(' }')
2777             ->blank;
2778            
2779             # JIT: Add compression logic only if compression is enabled
2780 3 50       10 if ($self->{_compression_enabled}) {
2781 0         0 my $config = $self->{_compression_config};
2782 0   0     0 my $min_size = $config->{min_size} // 1024;
2783            
2784 0         0 $builder
2785             ->comment(' Gzip compression - check Accept-Encoding')
2786             ->line('#ifdef HYPERSONIC_COMPRESSION')
2787             ->line(' int use_gzip = 0;')
2788             ->line(' unsigned char* compressed_body = NULL;')
2789             ->line(' size_t compressed_len = 0;')
2790             ->blank
2791             ->comment(' Get Accept-Encoding from request (from SLOT_HEADERS)')
2792             ->line(' SV** req_arr = AvARRAY(req);')
2793             ->line(' HV* hdrs = NULL;')
2794             ->line(' if (req_arr[6] && SvROK(req_arr[6])) {')
2795             ->line(' hdrs = (HV*)SvRV(req_arr[6]);')
2796             ->line(' }')
2797             ->line(' if (hdrs && len >= ' . $min_size . ') {')
2798             ->line(' SV** ae = hv_fetch(hdrs, "accept_encoding", 15, 0);')
2799             ->line(' if (ae && SvOK(*ae)) {')
2800             ->line(' STRLEN ae_len;')
2801             ->line(' const char* ae_str = SvPV(*ae, ae_len);')
2802             ->line(' if (accepts_gzip(ae_str, ae_len)) {')
2803             ->line(' compressed_len = gzip_compress(body_str, len, &compressed_body);')
2804             ->line(' if (compressed_len > 0) {')
2805             ->line(' use_gzip = 1;')
2806             ->line(' body_str = (const char*)compressed_body;')
2807             ->line(' len = compressed_len;')
2808             ->line(' }')
2809             ->line(' }')
2810             ->line(' }')
2811             ->line(' }')
2812             ->line('#endif')
2813             ->blank;
2814             }
2815            
2816             $builder
2817 3         348 ->comment(' Build response with custom headers support')
2818             ->line(' static __thread char resp_buf[65536];')
2819             ->line(' int hdr_len;')
2820             ->line('#ifdef HYPERSONIC_SECURITY_HEADERS')
2821             ->line(' hdr_len = snprintf(resp_buf, 2048,')
2822             ->line(' "HTTP/1.1 %d %s\\r\\n"')
2823             ->line(' "Content-Type: %s\\r\\n"')
2824             ->line(' "Content-Length: %zu\\r\\n"')
2825             ->line(' "Connection: keep-alive\\r\\n"')
2826             ->line(' "%s",')
2827             ->line(' status, get_status_text(status), content_type, len, SECURITY_HEADERS);')
2828             ->line('#else')
2829             ->line(' hdr_len = snprintf(resp_buf, 512,')
2830             ->line(' "HTTP/1.1 %d %s\\r\\n"')
2831             ->line(' "Content-Type: %s\\r\\n"')
2832             ->line(' "Content-Length: %zu\\r\\n"')
2833             ->line(' "Connection: keep-alive\\r\\n",')
2834             ->line(' status, get_status_text(status), content_type, len);')
2835             ->line('#endif')
2836             ->blank
2837             ->comment(' Add custom headers from response (Location, Set-Cookie, etc.)')
2838             ->line(' if (custom_headers) {')
2839             ->line(' HE* entry;')
2840             ->line(' hv_iterinit(custom_headers);')
2841             ->line(' while ((entry = hv_iternext(custom_headers))) {')
2842             ->line(' I32 klen;')
2843             ->line(' const char* key = hv_iterkey(entry, &klen);')
2844             ->comment(' Skip Content-Type/Content-Length (already added)')
2845             ->line(' if (klen == 12 && memcmp(key, "Content-Type", 12) == 0) continue;')
2846             ->line(' if (klen == 14 && memcmp(key, "Content-Length", 14) == 0) continue;')
2847             ->line(' SV* val = hv_iterval(custom_headers, entry);')
2848             ->comment(' Handle Set-Cookie array (multiple cookies)')
2849             ->line(' if (SvROK(val) && SvTYPE(SvRV(val)) == SVt_PVAV) {')
2850             ->line(' AV* arr = (AV*)SvRV(val);')
2851             ->line(' SSize_t arr_len = av_len(arr) + 1;')
2852             ->line(' SSize_t j;')
2853             ->line(' for (j = 0; j < arr_len; j++) {')
2854             ->line(' SV** item = av_fetch(arr, j, 0);')
2855             ->line(' if (item && SvOK(*item)) {')
2856             ->line(' STRLEN vlen;')
2857             ->line(' const char* vstr = SvPV(*item, vlen);')
2858             ->line(' hdr_len += snprintf(resp_buf + hdr_len, sizeof(resp_buf) - hdr_len,')
2859             ->line(' "%.*s: %.*s\\r\\n", (int)klen, key, (int)vlen, vstr);')
2860             ->line(' }')
2861             ->line(' }')
2862             ->line(' } else if (SvOK(val)) {')
2863             ->line(' STRLEN vlen;')
2864             ->line(' const char* vstr = SvPV(val, vlen);')
2865             ->line(' hdr_len += snprintf(resp_buf + hdr_len, sizeof(resp_buf) - hdr_len,')
2866             ->line(' "%.*s: %.*s\\r\\n", (int)klen, key, (int)vlen, vstr);')
2867             ->line(' }')
2868             ->line(' }')
2869             ->line(' }')
2870             ->blank
2871             ->comment(' Add Content-Encoding header if gzip was used')
2872             ->line('#ifdef HYPERSONIC_COMPRESSION')
2873             ->line(' if (use_gzip) {')
2874             ->line(' hdr_len += snprintf(resp_buf + hdr_len, sizeof(resp_buf) - hdr_len,')
2875             ->line(' "Content-Encoding: gzip\\r\\n");')
2876             ->line(' }')
2877             ->line('#endif')
2878             ->blank
2879             ->comment(' End headers')
2880             ->line(' memcpy(resp_buf + hdr_len, "\\r\\n", 2);')
2881             ->line(' hdr_len += 2;')
2882             ->blank
2883             ->line(' if (hdr_len + len < sizeof(resp_buf)) {')
2884             ->line(' memcpy(resp_buf + hdr_len, body_str, len);')
2885             ->line(' *resp_out = resp_buf;')
2886             ->line(' *resp_len_out = hdr_len + (int)len;')
2887             ->line(' } else {')
2888             ->line(' *resp_out = (char*)RESP_404;')
2889             ->line(' *resp_len_out = RESP_404_LEN;')
2890             ->line(' }');
2891            
2892             # Different closing braces based on middleware presence
2893 3 100       11 if ($analysis->{has_any_middleware}) {
2894             # Middleware: } else if (SvOK(result)) { ... } else { 404 }
2895 1         5 $builder->line(' } else {')
2896             ->line(' *resp_out = (char*)RESP_404;')
2897             ->line(' *resp_len_out = RESP_404_LEN;')
2898             ->line(' }');
2899             } else {
2900             # No middleware: } else if (count == 1) { result = POPs; if (SvOK(result)) { ... } } else { 404 }
2901 2         19 $builder->line(' } else {')
2902             ->line(' *resp_out = (char*)RESP_404;')
2903             ->line(' *resp_len_out = RESP_404_LEN;')
2904             ->line(' }')
2905             ->line(' } else {')
2906             ->line(' *resp_out = (char*)RESP_404;')
2907             ->line(' *resp_len_out = RESP_404_LEN;')
2908             ->line(' }');
2909             }
2910            
2911 3         16 $builder->blank
2912             ->line(' PUTBACK;')
2913             ->line(' FREETMPS;')
2914             ->line(' LEAVE;')
2915             ->line('}');
2916            
2917 3         196 return $builder->code;
2918             }
2919              
2920             # JSON decoder helper - called from C via call_pv
2921             sub _decode_json {
2922 0     0   0 my ($json_str) = @_;
2923 0         0 require Cpanel::JSON::XS;
2924 0         0 return Cpanel::JSON::XS::decode_json($json_str);
2925             }
2926              
2927             # Generate WebSocket handler caller function
2928             sub _gen_websocket_handler_caller {
2929 0     0   0 my ($self) = @_;
2930              
2931 0         0 my $builder = XS::JIT::Builder->new;
2932              
2933 0         0 $builder->comment('WebSocket handler caller - performs handshake and calls Perl handler')
2934             ->line('static void call_websocket_handler(pTHX_ int handler_idx, int fd,')
2935             ->line(' const char* path, int path_len,')
2936             ->line(' const char* ws_key, int ws_key_len,')
2937             ->line(' const char* raw_request, int raw_request_len) {')
2938             ->line(' dSP;')
2939             ->blank
2940             ->if('!g_websocket_handlers || !SvROK(g_websocket_handlers)')
2941             ->line('return;')
2942             ->endif
2943             ->blank
2944             ->line('AV* handlers = (AV*)SvRV(g_websocket_handlers);')
2945             ->line('SV** handler_sv = av_fetch(handlers, handler_idx, 0);')
2946             ->if('!handler_sv || !SvROK(*handler_sv)')
2947             ->line('return;')
2948             ->endif
2949             ->blank
2950             ->comment('Generate WebSocket accept key')
2951             ->line('ENTER;')
2952             ->line('SAVETMPS;')
2953             ->blank
2954             ->comment('Build handshake response via Protocol::WebSocket')
2955             ->line('PUSHMARK(SP);')
2956             ->line('XPUSHs(sv_2mortal(newSVpvs("Hypersonic::Protocol::WebSocket")));')
2957             ->line('XPUSHs(sv_2mortal(newSVpvs("key")));')
2958             ->line('XPUSHs(sv_2mortal(newSVpvn(ws_key, ws_key_len)));')
2959             ->line('PUTBACK;')
2960             ->line('int count = call_method("build_response", G_SCALAR);')
2961             ->line('SPAGAIN;')
2962             ->blank
2963             ->if('count != 1')
2964             ->line('FREETMPS;')
2965             ->line('LEAVE;')
2966             ->line('return;')
2967             ->endif
2968             ->blank
2969             ->line('SV* response_sv = POPs;')
2970             ->line('STRLEN resp_len;')
2971             ->line('const char* response = SvPV(response_sv, resp_len);')
2972             ->blank
2973             ->comment('Send WebSocket handshake response')
2974             ->line('send(fd, response, resp_len, 0);')
2975             ->blank
2976             ->comment('Create Stream object for WebSocket')
2977             ->line('PUSHMARK(SP);')
2978             ->line('XPUSHs(sv_2mortal(newSVpvs("Hypersonic::Stream")));')
2979             ->line('XPUSHs(sv_2mortal(newSVpvs("fd")));')
2980             ->line('XPUSHs(sv_2mortal(newSViv(fd)));')
2981             ->line('PUTBACK;')
2982             ->line('count = call_method("new", G_SCALAR);')
2983             ->line('SPAGAIN;')
2984             ->blank
2985             ->if('count != 1')
2986             ->line('FREETMPS;')
2987             ->line('LEAVE;')
2988             ->line('return;')
2989             ->endif
2990             ->blank
2991             ->line('SV* stream_sv = POPs;')
2992             ->line('SvREFCNT_inc(stream_sv);')
2993             ->blank
2994             ->comment('Create WebSocket object')
2995             ->line('PUSHMARK(SP);')
2996             ->line('XPUSHs(sv_2mortal(newSVpvs("Hypersonic::WebSocket")));')
2997             ->line('XPUSHs(stream_sv);')
2998             ->line('XPUSHs(sv_2mortal(newSVpvs("fd")));')
2999             ->line('XPUSHs(sv_2mortal(newSViv(fd)));')
3000             ->line('PUTBACK;')
3001             ->line('count = call_method("new", G_SCALAR);')
3002             ->line('SPAGAIN;')
3003             ->blank
3004             ->if('count != 1')
3005             ->line('SvREFCNT_dec(stream_sv);')
3006             ->line('FREETMPS;')
3007             ->line('LEAVE;')
3008             ->line('return;')
3009             ->endif
3010             ->blank
3011             ->line('SV* ws_sv = POPs;')
3012             ->line('SvREFCNT_inc(ws_sv);')
3013             ->blank
3014             ->comment('Set WebSocket state to OPEN and store object in registry')
3015             ->if('fd >= 0 && fd < WS_MAX')
3016             ->line('ws_registry[fd].state = WS_STATE_OPEN;')
3017             ->line('ws_registry[fd].ws_object = ws_sv;')
3018             ->line('SvREFCNT_inc(ws_sv);')
3019             ->endif
3020             ->blank
3021             ->comment('Register with Handler for global broadcast support')
3022             ->line('PUSHMARK(SP);')
3023             ->line('XPUSHs(sv_2mortal(newSVpvs("Hypersonic::WebSocket::Handler")));')
3024             ->line('XPUSHs(sv_2mortal(newSViv(fd)));')
3025             ->line('XPUSHs(ws_sv);')
3026             ->line('PUTBACK;')
3027             ->line('call_method("new", G_DISCARD);')
3028             ->blank
3029             ->comment('Call the Perl WebSocket handler with the WebSocket object')
3030             ->line('PUSHMARK(SP);')
3031             ->line('XPUSHs(ws_sv);')
3032             ->line('PUTBACK;')
3033             ->line('call_sv(*handler_sv, G_DISCARD | G_EVAL);')
3034             ->blank
3035             ->if('SvTRUE(ERRSV)')
3036             ->line('warn("WebSocket handler error: %s", SvPV_nolen(ERRSV));')
3037             ->endif
3038             ->blank
3039             ->comment('Emit open event')
3040             ->line('PUSHMARK(SP);')
3041             ->line('XPUSHs(ws_sv);')
3042             ->line('XPUSHs(sv_2mortal(newSVpvs("open")));')
3043             ->line('PUTBACK;')
3044             ->line('call_method("emit", G_DISCARD | G_EVAL);')
3045             ->blank
3046             ->line('SvREFCNT_dec(stream_sv);')
3047             ->line('SvREFCNT_dec(ws_sv);')
3048             ->line('FREETMPS;')
3049             ->line('LEAVE;')
3050             ->line('}')
3051             ->blank;
3052              
3053 0         0 return $builder->code;
3054             }
3055              
3056             # Generate WebSocket data processor for established connections
3057             sub _gen_websocket_data_processor {
3058 0     0   0 my ($self) = @_;
3059              
3060 0         0 my $builder = XS::JIT::Builder->new;
3061              
3062 0         0 $builder->comment('Process incoming WebSocket data on established connection')
3063             ->line('static void process_websocket_data(pTHX_ int fd, const char* data, ssize_t len) {')
3064             ->line(' dSP;')
3065             ->blank
3066             ->comment('Get WebSocket object from registry')
3067             ->if('fd < 0 || fd >= WS_MAX || !ws_registry[fd].ws_object')
3068             ->line('return;')
3069             ->endif
3070             ->blank
3071             ->line('SV* ws_sv = ws_registry[fd].ws_object;')
3072             ->blank
3073             ->comment('Parse WebSocket frame')
3074             ->line('WSFrame frame;')
3075             ->line('int result = ws_decode_frame((const uint8_t*)data, len, &frame);')
3076             ->blank
3077             ->if('result < 0')
3078             ->comment('Incomplete frame or error - wait for more data')
3079             ->line('return;')
3080             ->endif
3081             ->blank
3082             ->comment('Handle different opcodes')
3083             ->if('frame.opcode == WS_OP_TEXT || frame.opcode == WS_OP_BINARY')
3084             ->comment('Data frame - emit message event')
3085             ->line('ENTER;')
3086             ->line('SAVETMPS;')
3087             ->line('PUSHMARK(SP);')
3088             ->line('XPUSHs(ws_sv);')
3089             ->line('XPUSHs(sv_2mortal(newSVpvs("message")));')
3090             ->line('XPUSHs(sv_2mortal(newSVpvn((const char*)frame.payload, frame.payload_length)));')
3091             ->line('PUTBACK;')
3092             ->line('call_method("emit", G_DISCARD | G_EVAL);')
3093             ->line('FREETMPS;')
3094             ->line('LEAVE;')
3095             ->elsif('frame.opcode == WS_OP_CLOSE')
3096             ->comment('Close frame - emit close event and respond')
3097             ->line('int close_code = 1000;')
3098             ->if('frame.payload_length >= 2')
3099             ->line('close_code = (frame.payload[0] << 8) | frame.payload[1];')
3100             ->endif
3101             ->blank
3102             ->comment('Send close response')
3103             ->line('uint8_t close_frame[4];')
3104             ->line('close_frame[0] = 0x88;') # FIN + Close opcode
3105             ->line('close_frame[1] = 2;') # Length 2 (just the code)
3106             ->line('close_frame[2] = (close_code >> 8) & 0xFF;')
3107             ->line('close_frame[3] = close_code & 0xFF;')
3108             ->line('send(fd, close_frame, 4, 0);')
3109             ->blank
3110             ->comment('Emit close event')
3111             ->line('ENTER;')
3112             ->line('SAVETMPS;')
3113             ->line('PUSHMARK(SP);')
3114             ->line('XPUSHs(ws_sv);')
3115             ->line('XPUSHs(sv_2mortal(newSVpvs("close")));')
3116             ->line('XPUSHs(sv_2mortal(newSViv(close_code)));')
3117             ->line('PUTBACK;')
3118             ->line('call_method("emit", G_DISCARD | G_EVAL);')
3119             ->line('FREETMPS;')
3120             ->line('LEAVE;')
3121             ->blank
3122             ->comment('Unregister from Handler')
3123             ->line('ENTER;')
3124             ->line('SAVETMPS;')
3125             ->line('PUSHMARK(SP);')
3126             ->line('XPUSHs(sv_2mortal(newSVpvs("Hypersonic::WebSocket::Handler")));')
3127             ->line('XPUSHs(sv_2mortal(newSViv(fd)));')
3128             ->line('PUTBACK;')
3129             ->line('call_method("close", G_DISCARD);')
3130             ->line('FREETMPS;')
3131             ->line('LEAVE;')
3132             ->blank
3133             ->comment('Mark connection as closed')
3134             ->line('ws_registry[fd].state = WS_STATE_CLOSED;')
3135             ->elsif('frame.opcode == WS_OP_PING')
3136             ->comment('Ping - auto pong')
3137             ->line('uint8_t pong_frame[256];')
3138             ->line('size_t pong_len = ws_encode_pong(pong_frame, sizeof(pong_frame), frame.payload, frame.payload_length);')
3139             ->if('pong_len > 0')
3140             ->line('send(fd, pong_frame, pong_len, 0);')
3141             ->endif
3142             ->endif
3143             ->line('}')
3144             ->blank;
3145              
3146 0         0 return $builder->code;
3147             }
3148              
3149             sub _escape_c_string {
3150 69     69   140 my ($str) = @_;
3151 69         179 $str =~ s/\\/\\\\/g;
3152 69         158 $str =~ s/"/\\"/g;
3153 69         317 $str =~ s/\n/\\n/g;
3154 69         266 $str =~ s/\r/\\r/g;
3155 69         97 $str =~ s/\t/\\t/g;
3156 69         169 return $str;
3157             }
3158              
3159             # Deparse a handler coderef to analyze what request features it uses
3160             # Returns deparsed code as string, or undef on failure
3161             sub _deparse_handler {
3162 6     6   9 my ($coderef) = @_;
3163 6 50       20 return undef unless ref($coderef) eq 'CODE';
3164            
3165 6         10 my $code = eval {
3166 6 100       31 require B::Deparse unless $DEPARSER;
3167 6   66     252 $DEPARSER //= B::Deparse->new('-q'); # -q = don't quote simple strings
3168 6         11205 $DEPARSER->coderef2text($coderef);
3169             };
3170 6 50       36 return $@ ? undef : $code;
3171             }
3172              
3173             # Find common prefix of multiple paths
3174             sub _find_common_prefix {
3175 7     7   21 my @paths = @_;
3176 7 50       16 return '' unless @paths;
3177 7 50       23 return $paths[0] if @paths == 1;
3178              
3179 7         14 my $prefix = $paths[0];
3180 7         29 for my $path (@paths[1..$#paths]) {
3181 15         31 while (index($path, $prefix) != 0) {
3182 44         49 $prefix = substr($prefix, 0, -1);
3183 44 50       84 return '' if $prefix eq '';
3184             }
3185             }
3186             # Don't include trailing non-slash char as prefix
3187             # e.g., /api/hello and /api/health -> /api/ not /api/he
3188 7 50 33     49 if ($prefix !~ m{/$} && $prefix =~ m{^(.*/)[^/]+$}) {
3189 0         0 $prefix = $1;
3190             }
3191 7         19 return $prefix;
3192             }
3193              
3194             # HTTP status code to text mapping
3195             my %STATUS_TEXT = (
3196             200 => 'OK',
3197             201 => 'Created',
3198             202 => 'Accepted',
3199             204 => 'No Content',
3200             301 => 'Moved Permanently',
3201             302 => 'Found',
3202             303 => 'See Other',
3203             304 => 'Not Modified',
3204             307 => 'Temporary Redirect',
3205             308 => 'Permanent Redirect',
3206             400 => 'Bad Request',
3207             401 => 'Unauthorized',
3208             403 => 'Forbidden',
3209             404 => 'Not Found',
3210             405 => 'Method Not Allowed',
3211             408 => 'Request Timeout',
3212             409 => 'Conflict',
3213             410 => 'Gone',
3214             413 => 'Payload Too Large',
3215             415 => 'Unsupported Media Type',
3216             422 => 'Unprocessable Entity',
3217             429 => 'Too Many Requests',
3218             500 => 'Internal Server Error',
3219             501 => 'Not Implemented',
3220             502 => 'Bad Gateway',
3221             503 => 'Service Unavailable',
3222             504 => 'Gateway Timeout',
3223             );
3224              
3225             sub _status_text {
3226 0     0   0 my ($code) = @_;
3227 0   0     0 return $STATUS_TEXT{$code} // 'Unknown';
3228             }
3229              
3230             # ============================================================
3231             # STATIC FILE SERVING - JIT compiled for maximum performance
3232             # Files are read at compile time and baked into C string constants
3233             # ============================================================
3234              
3235             sub _compile_static_files {
3236 0     0   0 my ($self, $static_dirs) = @_;
3237            
3238 0         0 require File::Find;
3239 0         0 require Digest::MD5;
3240            
3241 0         0 my @static_files;
3242            
3243 0         0 for my $config (@$static_dirs) {
3244 0         0 my $prefix = $config->{prefix};
3245 0         0 my $dir = $config->{directory};
3246 0         0 my $max_age = $config->{max_age};
3247 0         0 my $gen_etag = $config->{etag};
3248            
3249             # Recursively find all files
3250             File::Find::find({
3251             no_chdir => 1,
3252             wanted => sub {
3253 0 0   0   0 return unless -f $_;
3254 0         0 my $file_path = $_;
3255 0         0 my $rel_path = $file_path;
3256 0         0 $rel_path =~ s{^\Q$dir\E/?}{};
3257            
3258             # URL path for this file
3259 0         0 my $url_path = "$prefix/$rel_path";
3260 0         0 $url_path =~ s{//+}{/}g;
3261            
3262             # Read file content
3263 0 0       0 open my $fh, '<:raw', $file_path or return;
3264 0         0 local $/;
3265 0         0 my $content = <$fh>;
3266 0         0 close $fh;
3267            
3268             # Get MIME type
3269 0         0 my $mime = _get_mime_type($file_path);
3270            
3271             # Generate ETag (MD5 of content)
3272 0         0 my $etag = '';
3273 0 0       0 if ($gen_etag) {
3274 0         0 $etag = Digest::MD5::md5_hex($content);
3275             }
3276            
3277             # Store file info
3278 0         0 push @static_files, {
3279             url_path => $url_path,
3280             content => $content,
3281             mime => $mime,
3282             etag => $etag,
3283             max_age => $max_age,
3284             length => length($content),
3285             };
3286             },
3287 0         0 }, $dir);
3288             }
3289            
3290             # Store for code generation
3291 0         0 $self->{_static_files} = \@static_files;
3292            
3293             # Create static routes - these are essentially pre-computed responses
3294 0         0 for my $file (@static_files) {
3295 0         0 my $url_path = $file->{url_path};
3296 0         0 my $content = $file->{content};
3297 0         0 my $mime = $file->{mime};
3298 0         0 my $etag = $file->{etag};
3299 0         0 my $max_age = $file->{max_age};
3300 0         0 my $len = $file->{length};
3301            
3302             # Build complete HTTP response at compile time
3303 0         0 my $response = "HTTP/1.1 200 OK\r\n"
3304             . "Content-Type: $mime\r\n"
3305             . "Content-Length: $len\r\n"
3306             . "Connection: keep-alive\r\n";
3307 0 0       0 $response .= "Cache-Control: public, max-age=$max_age\r\n" if $max_age;
3308 0 0       0 $response .= "ETag: \"$etag\"\r\n" if $etag;
3309            
3310             # Add security headers
3311 0 0       0 if ($self->{enable_security_headers}) {
3312 0         0 $response .= $self->_get_security_headers_string();
3313             }
3314            
3315 0         0 $response .= "\r\n" . $content;
3316            
3317             # Store as static route
3318 0         0 push @{$self->{routes}}, {
3319             method => 'GET',
3320             path => $url_path,
3321 0     0   0 handler => sub { $content }, # Dummy handler for static
3322 0         0 dynamic => 0,
3323             params => [],
3324             segments => [split('/', $url_path)],
3325             features => {},
3326             before => [],
3327             after => [],
3328             # Mark as static file with pre-built response
3329             _static_response => $response,
3330             _static_file => 1,
3331             };
3332             }
3333             }
3334              
3335             # Generate security headers string for HTTP responses
3336             # Pre-computed at compile time - zero runtime overhead
3337             sub _get_security_headers_string {
3338 40     40   159 my ($self) = @_;
3339 40         123 my $headers = '';
3340            
3341 40         68 for my $name (sort keys %{$self->{security_headers}}) {
  40         526  
3342 280         471 my $value = $self->{security_headers}{$name};
3343 280 100 66     723 next unless defined $value && length($value);
3344 160         318 $headers .= "$name: $value\r\n";
3345             }
3346            
3347 40         121 return $headers;
3348             }
3349              
3350             # Generate security headers as C string constant for dynamic routes
3351             sub _gen_security_headers_c_constant {
3352 3     3   7 my ($self) = @_;
3353 3 50       25 return '' unless $self->{enable_security_headers};
3354            
3355 3         7 my $headers = $self->_get_security_headers_string();
3356 3 50       8 return '' unless length($headers);
3357            
3358 3         8 my $escaped = _escape_c_string($headers);
3359 3         55 return "static const char SECURITY_HEADERS[] = \"$escaped\";\n"
3360             . "static const int SECURITY_HEADERS_LEN = " . length($headers) . ";\n";
3361             }
3362              
3363             sub dispatch {
3364 31     31 1 1022198 my ($self, $req) = @_;
3365 31 100       114 die "Must call compile() first" unless $self->{compiled};
3366 30         150 return $self->{dispatch_fn}->($req);
3367             }
3368              
3369             sub run {
3370 1     1 1 16 my ($self, %opts) = @_;
3371              
3372 1 50       12 die "Must call compile() before run()" unless $self->{compiled};
3373              
3374 0   0       my $host = $opts{host} // $self->{host};
3375 0   0       my $port = $opts{port} // $self->{port};
3376 0   0       my $workers = $opts{workers} // 1;
3377              
3378             # Windows lacks fork(); the multi-worker model assumes SO_REUSEPORT
3379             # + fork()-per-worker so the kernel can balance connections. With
3380             # neither primitive available the only safe behavior is single
3381             # process. Warn loudly so users know they're not getting the
3382             # workers they asked for.
3383 0 0 0       if ($^O eq 'MSWin32' && $workers > 1) {
3384 0           warn "Hypersonic: Windows lacks fork(); forcing workers=1.\n";
3385 0           $workers = 1;
3386             }
3387              
3388             # Protocol mode indication
3389 0 0         my $mode = $self->{http2} ? "HTTP/2" : ($self->{tls} ? "HTTPS/TLS" : "HTTP");
    0          
3390 0           print "Hypersonic listening on $host:$port ($mode, pure C event loop, $workers workers)\n";
3391              
3392             # Fork workers if requested
3393             # Each worker creates its OWN listening socket with SO_REUSEPORT
3394             # This avoids thundering herd and lets kernel distribute connections
3395 0 0         if ($workers > 1) {
3396 0           for my $i (1 .. $workers - 1) {
3397 0           my $pid = fork();
3398 0 0         if (!defined $pid) {
3399 0           die "Fork failed: $!";
3400             }
3401 0 0         if ($pid == 0) {
3402             # Child - create own listen socket and run event loop
3403 0           my $listen_fd = Hypersonic::Socket::create_listen_socket($port);
3404 0 0         die "Worker $i: Failed to create listen socket" if $listen_fd < 0;
3405 0           $self->{run_loop_fn}->($listen_fd, $self);
3406 0           exit(0);
3407             }
3408             }
3409             }
3410              
3411             # Parent (or single worker) - create listen socket and run event loop
3412 0           my $listen_fd = Hypersonic::Socket::create_listen_socket($port);
3413 0 0         die "Failed to create listen socket on port $port" if $listen_fd < 0;
3414 0           $self->{run_loop_fn}->($listen_fd, $self);
3415             }
3416              
3417              
3418             1;
3419              
3420             __END__