File Coverage

blib/lib/Hypersonic/JIT/Util.pm
Criterion Covered Total %
statement 109 235 46.3
branch 38 92 41.3
condition 14 74 18.9
subroutine 14 29 48.2
pod 10 21 47.6
total 185 451 41.0


line stmt bran cond sub pod time code
1             package Hypersonic::JIT::Util;
2 54     54   267 use strict;
  54         80  
  54         1547  
3 54     54   172 use warnings;
  54         76  
  54         2094  
4              
5 54     54   220 use Config;
  54         209  
  54         2137  
6 54     54   315 use Carp qw(croak);
  54         86  
  54         136698  
7              
8             our $VERSION = '0.15';
9              
10             # =============================================================================
11             # Cache Directory Management
12             # =============================================================================
13              
14             # Base cache directory
15             our $CACHE_BASE = '_hypersonic_cache';
16              
17             # Get cache directory for a module
18             sub cache_dir {
19 0     0 1 0 my ($class, $subdir) = @_;
20 0 0       0 return $subdir ? "$CACHE_BASE/$subdir" : $CACHE_BASE;
21             }
22              
23             # Subdirectory constants
24 0     0 0 0 sub cache_dir_core { shift->cache_dir('core') }
25 0     0 0 0 sub cache_dir_socket { shift->cache_dir('socket') }
26 0     0 0 0 sub cache_dir_request { shift->cache_dir('request') }
27 0     0 0 0 sub cache_dir_response { shift->cache_dir('response') }
28 0     0 0 0 sub cache_dir_session { shift->cache_dir('session') }
29 0     0 0 0 sub cache_dir_tls { shift->cache_dir('tls') }
30 0     0 0 0 sub cache_dir_future { shift->cache_dir('future') }
31              
32             # =============================================================================
33             # Fork Detection
34             # =============================================================================
35              
36             sub can_fork {
37 0   0 0 1 0 return $Config{d_fork} && eval {
38             my $pid = fork();
39             if (defined $pid && $pid == 0) {
40             exit(0); # Child exits immediately
41             }
42             waitpid($pid, 0) if defined $pid;
43             1;
44             };
45             }
46              
47             # =============================================================================
48             # Parallel Compilation
49             # =============================================================================
50              
51             # Standalone modules that can compile independently
52             our @STANDALONE_MODULES = qw(
53             Hypersonic::Socket
54             Hypersonic::Response
55             Hypersonic::Session
56             Hypersonic::TLS
57             );
58              
59             sub compile_standalone_modules {
60 0     0 1 0 my ($class, %opts) = @_;
61            
62 0   0     0 my @modules = $opts{modules} // @STANDALONE_MODULES;
63 0   0     0 my $parallel = $opts{parallel} // 1;
64            
65             # Load modules
66 0         0 for my $mod (@modules) {
67 0 0       0 eval "require $mod" or do {
68 0         0 warn "Failed to load $mod: $@";
69 0         0 return 0;
70             };
71             }
72            
73 0 0 0     0 if ($parallel && $class->can_fork) {
74 0         0 return $class->_compile_parallel(\@modules, \%opts);
75             } else {
76 0         0 return $class->_compile_sequential(\@modules, \%opts);
77             }
78             }
79              
80             sub _compile_parallel {
81 0     0   0 my ($class, $modules, $opts) = @_;
82            
83 0         0 my @pids;
84             my %child_to_mod;
85            
86 0         0 for my $mod (@$modules) {
87 0         0 my $pid = fork();
88            
89 0 0       0 if (!defined $pid) {
    0          
90 0         0 warn "Fork failed for $mod: $!";
91             # Fallback to sequential for this module
92 0         0 eval { $mod->compile(%$opts) };
  0         0  
93 0 0       0 warn "Compile failed for $mod: $@" if $@;
94             } elsif ($pid == 0) {
95             # Child process
96 0         0 eval { $mod->compile(%$opts) };
  0         0  
97 0 0       0 exit($@ ? 1 : 0);
98             } else {
99             # Parent
100 0         0 push @pids, $pid;
101 0         0 $child_to_mod{$pid} = $mod;
102             }
103             }
104            
105             # Wait for all children
106 0         0 my $all_ok = 1;
107 0         0 for my $pid (@pids) {
108 0         0 waitpid($pid, 0);
109 0 0       0 if ($? != 0) {
110 0         0 my $mod = $child_to_mod{$pid};
111 0         0 warn "Compilation failed for $mod (exit code: " . ($? >> 8) . ")";
112 0         0 $all_ok = 0;
113             }
114             }
115            
116             # After fork compilation, parent must load the compiled modules
117             # (children wrote to cache, but parent needs to load)
118 0         0 for my $mod (@$modules) {
119 0         0 eval { $mod->compile(%$opts) }; # Will load from cache
  0         0  
120 0 0       0 if ($@) {
121 0         0 warn "Failed to load compiled $mod: $@";
122 0         0 $all_ok = 0;
123             }
124             }
125            
126 0         0 return $all_ok;
127             }
128              
129             sub _compile_sequential {
130 0     0   0 my ($class, $modules, $opts) = @_;
131            
132 0         0 my $all_ok = 1;
133 0         0 for my $mod (@$modules) {
134 0         0 eval { $mod->compile(%$opts) };
  0         0  
135 0 0       0 if ($@) {
136 0         0 warn "Compile failed for $mod: $@";
137 0         0 $all_ok = 0;
138             }
139             }
140            
141 0         0 return $all_ok;
142             }
143              
144             # =============================================================================
145             # Common Include Patterns
146             # =============================================================================
147              
148             sub add_standard_includes {
149 59     59 1 262 my ($class, $builder, @features) = @_;
150            
151 59         208 my %features = map { $_ => 1 } @features;
  227         661  
152            
153             # Always needed
154 59         654 $builder->line('#include ')
155             ->line('#include ')
156             ->line('#include ');
157            
158 59 100       182 if ($features{stdio}) {
159 41         139 $builder->line('#include ');
160             }
161            
162             # On Windows, unistd / fcntl / netinet don't exist. Winsock provides
163             # the socket API but with different headers + a stub `close()`. We
164             # also define `hs_set_nonblocking(fd)` here so callers don't need
165             # platform-specific fcntl vs ioctlsocket dances.
166 59 0 33     253 if ($features{unistd} || $features{fcntl} || $features{socket}) {
      0        
167 59         256 $builder->raw(<<'C');
168             #ifdef _WIN32
169             # define WIN32_LEAN_AND_MEAN
170             # include
171             # include
172             # include
173             # ifndef close
174             # define close(fd) closesocket(fd)
175             # endif
176             # ifndef MSG_NOSIGNAL
177             # define MSG_NOSIGNAL 0
178             # endif
179             # ifndef hs_set_nonblocking
180             # define hs_set_nonblocking(fd) do { u_long _m = 1; ioctlsocket((fd), FIONBIO, &_m); } while(0)
181             # endif
182             #else
183             C
184 59 50       269 $builder->line('#include ') if $features{unistd};
185 59 50       366 $builder->line('#include ') if $features{fcntl};
186 59 100       195 if ($features{socket}) {
187 41         1900 $builder->line('#include ')
188             ->line('#include ')
189             ->line('#include ')
190             ->line('#include ')
191             ->line('#include ')
192             ->line('#include ');
193             }
194 59         1703 $builder->raw(<<'C');
195             # ifndef hs_set_nonblocking
196             # define hs_set_nonblocking(fd) do { int _f = fcntl((fd), F_GETFL, 0); fcntl((fd), F_SETFL, _f | O_NONBLOCK); } while(0)
197             # endif
198             #endif
199             C
200             }
201            
202 59 100       175 if ($features{threading}) {
203 18         151 $builder->line('#ifndef _WIN32')
204             ->line('#include ')
205             ->line('#endif');
206             }
207            
208 59 100       185 if ($features{eventfd}) {
209 9         106 $builder->line('#ifdef __linux__')
210             ->line('#include ')
211             ->line('#endif');
212             }
213            
214 59 50       6733 if ($features{time}) {
215 0         0 $builder->line('#include ');
216             }
217            
218 59 50       165 if ($features{signal}) {
219 0         0 $builder->line('#include ');
220             }
221            
222 59 50       374 if ($features{openssl}) {
223 0         0 $builder->line('#include ')
224             ->line('#include ')
225             ->line('#include ');
226             }
227            
228 59         319 $builder->blank;
229            
230 59         202 return $builder;
231             }
232              
233             # =============================================================================
234             # Platform Detection Helpers
235             # =============================================================================
236              
237             sub add_platform_eventfd {
238 9     9 0 20 my ($class, $builder) = @_;
239            
240 9         91 $builder->line('#ifndef _WIN32')
241             ->line('#ifdef __linux__')
242             ->line('#include ')
243             ->line('#define USE_EVENTFD 1')
244             ->line('#else')
245             ->line('#define USE_EVENTFD 0')
246             ->line('#endif')
247             ->line('#endif /* !_WIN32 */')
248             ->blank;
249            
250 9         13 return $builder;
251             }
252              
253             sub add_platform_detection {
254 0     0 0 0 my ($class, $builder) = @_;
255            
256 0         0 $builder->line('/* Platform detection */')
257             ->line('#if defined(__APPLE__)')
258             ->line('#define HYPERSONIC_MACOS 1')
259             ->line('#elif defined(__linux__)')
260             ->line('#define HYPERSONIC_LINUX 1')
261             ->line('#elif defined(_WIN32)')
262             ->line('#define HYPERSONIC_WINDOWS 1')
263             ->line('#elif defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)')
264             ->line('#define HYPERSONIC_BSD 1')
265             ->line('#endif')
266             ->blank;
267            
268 0         0 return $builder;
269             }
270              
271             # =============================================================================
272             # Library Detection
273             # =============================================================================
274              
275             # Check if Devel::CheckLib is available (CPAN standard for library detection)
276             my $HAS_DEVEL_CHECKLIB;
277             sub _has_devel_checklib {
278 3 100   3   14 return $HAS_DEVEL_CHECKLIB if defined $HAS_DEVEL_CHECKLIB;
279 1 50       2 $HAS_DEVEL_CHECKLIB = eval { require Devel::CheckLib; 1 } ? 1 : 0;
  1         494  
  1         22455  
280 1         5 return $HAS_DEVEL_CHECKLIB;
281             }
282              
283             # Check if ExtUtils::PkgConfig is available
284             my $HAS_EXTUTILS_PKGCONFIG;
285             sub _has_extutils_pkgconfig {
286 37 100   37   127 return $HAS_EXTUTILS_PKGCONFIG if defined $HAS_EXTUTILS_PKGCONFIG;
287 36 50       77 $HAS_EXTUTILS_PKGCONFIG = eval { require ExtUtils::PkgConfig; 1 } ? 1 : 0;
  36         3262  
  0         0  
288 36         152 return $HAS_EXTUTILS_PKGCONFIG;
289             }
290              
291             # Check if a library can actually be linked (compile+link test)
292             # Uses Devel::CheckLib when available (CPAN standard), falls back to manual test
293             sub can_link {
294 3     3 0 18 my ($class, $cflags, $ldflags, $test_symbol, $extra_includes) = @_;
295 3   50     16 $cflags //= '';
296 3   50     8 $ldflags //= '';
297 3   100     37 $extra_includes //= '';
298              
299             # Extract header names from include directives for Devel::CheckLib
300             # e.g., '#include ' -> 'math.h'
301 3         3 my @headers;
302 3 100       12 if ($extra_includes) {
303 2         17 @headers = ($extra_includes =~ /#include\s*[<"]([^>"]+)[>"]/g);
304             }
305              
306             # Use Devel::CheckLib if available - it's the CPAN standard and handles
307             # edge cases (different compilers, platforms, error handling) better
308 3 50       13 if ($class->_has_devel_checklib()) {
309             # Take address of symbol and store in volatile to force linker resolution
310             # The void* cast works for both functions and variables
311 3         7 my $function = "void *p = (void*)$test_symbol; volatile void *vp = p; return vp ? 0 : 1;";
312 3         21 my %args = (
313             INC => $cflags,
314             LIBS => $ldflags,
315             function => $function,
316             );
317 3 100       15 $args{header} = \@headers if @headers;
318 3         16 return Devel::CheckLib::check_lib(%args);
319             }
320              
321             # Fallback: manual compile+link test
322 0         0 require File::Temp;
323              
324             # Generate test code that forces the linker to resolve the symbol
325             # Take address and store in volatile to prevent optimization
326 0         0 my $test_code = <<"C";
327             $extra_includes
328             int main(void) {
329             void *p = (void*)$test_symbol;
330             volatile void *vp = p;
331             return vp ? 0 : 1;
332             }
333             C
334              
335 0         0 my $src = File::Temp->new(SUFFIX => '.c', UNLINK => 1);
336 0         0 my $src_path = $src->filename;
337 0         0 print $src $test_code;
338 0         0 close $src;
339              
340 0         0 my $out = File::Temp->new(SUFFIX => '', UNLINK => 1);
341 0         0 my $out_path = $out->filename;
342 0         0 close $out;
343              
344 0   0     0 my $cc = $ENV{CC} || $Config{cc} || 'cc';
345 0         0 my $result = system("$cc $cflags -o $out_path $src_path $ldflags 2>/dev/null");
346              
347 0 0       0 unlink $out_path if -f $out_path;
348              
349 0         0 return $result == 0;
350             }
351              
352             ## Compile a tiny C program and run it. Returns 1 if the program
353             ## compiles, links, AND exits 0; 0 otherwise. Use this when "linkable"
354             ## isn't enough — e.g. probing whether a syscall actually works on the
355             ## current kernel (io_uring on RHEL9 with kernel.io_uring_disabled=2
356             ## links fine but io_uring_setup() returns EINVAL at runtime).
357             sub can_run {
358 0     0 0 0 my ($class, $cflags, $ldflags, $body, $extra_includes) = @_;
359 0   0     0 $cflags //= '';
360 0   0     0 $ldflags //= '';
361 0   0     0 $extra_includes //= '';
362              
363 0         0 require File::Temp;
364              
365 0         0 my $src_code = <<"C";
366             $extra_includes
367             int main(void) {
368             $body
369             }
370             C
371              
372 0         0 my $src = File::Temp->new(SUFFIX => '.c', UNLINK => 1);
373 0         0 print $src $src_code;
374 0         0 close $src;
375              
376 0         0 my $out = File::Temp->new(SUFFIX => '', UNLINK => 1);
377 0         0 my $out_path = $out->filename;
378 0         0 close $out;
379              
380 0   0     0 my $cc = $ENV{CC} || $Config{cc} || 'cc';
381 0         0 my $compile_rc = system("$cc $cflags -o $out_path " . $src->filename
382             . " $ldflags >/dev/null 2>&1");
383 0 0 0     0 if ($compile_rc != 0 || !-x $out_path) {
384 0 0       0 unlink $out_path if -f $out_path;
385 0         0 return 0;
386             }
387              
388 0         0 my $run_rc = system("$out_path >/dev/null 2>&1");
389 0 0       0 unlink $out_path if -f $out_path;
390 0         0 return $run_rc == 0;
391             }
392              
393             sub detect_library {
394 37     37 1 341 my ($class, $lib_name, %opts) = @_;
395              
396 37         166 my $result = {
397             available => 0,
398             cflags => '',
399             ldflags => '',
400             };
401              
402 37         103 my $test_symbol = $opts{test_symbol};
403 37   50     158 my $test_include = $opts{test_include} // '';
404              
405             # Try Alien module first
406 37   33     111 my $alien_module = $opts{alien} // "Alien::\u$lib_name";
407 37 50       2606 if (eval "require $alien_module; 1") {
408 0   0     0 my $cflags = $alien_module->cflags // '';
409 0   0     0 my $ldflags = $alien_module->libs // '';
410              
411             # Verify it actually links if test_symbol provided
412 0 0 0     0 if (!$test_symbol || $class->can_link($cflags, $ldflags, $test_symbol, $test_include)) {
413 0         0 $result->{available} = 1;
414 0         0 $result->{cflags} = $cflags;
415 0         0 $result->{ldflags} = $ldflags;
416 0         0 return $result;
417             }
418             }
419              
420             # Try pkg-config (prefer ExtUtils::PkgConfig if available - CPAN standard)
421 37   33     232 my $pkg_name = $opts{pkg_config} // $lib_name;
422 37         87 my ($cflags, $ldflags);
423              
424 37 50 33     232 if ($class->_has_extutils_pkgconfig() && ExtUtils::PkgConfig->exists($pkg_name)) {
425 0         0 eval {
426 0         0 my %pkg_info = ExtUtils::PkgConfig->find($pkg_name);
427 0   0     0 $cflags = $pkg_info{cflags} // '';
428 0   0     0 $ldflags = $pkg_info{libs} // '';
429             };
430             }
431              
432             # Fallback to command-line pkg-config
433 37 50       164 if (!$ldflags) {
434 37         193862 $cflags = `pkg-config --cflags $pkg_name 2>/dev/null`;
435 37         153899 $ldflags = `pkg-config --libs $pkg_name 2>/dev/null`;
436 37 50       1407 if ($? == 0) {
437 0         0 chomp($cflags);
438 0         0 chomp($ldflags);
439             } else {
440 37         415 $cflags = $ldflags = '';
441             }
442             }
443              
444 37 50       1394 if ($ldflags) {
445             # Verify it actually links if test_symbol provided
446 0 0 0     0 if (!$test_symbol || $class->can_link($cflags, $ldflags, $test_symbol, $test_include)) {
447 0         0 $result->{available} = 1;
448 0         0 $result->{cflags} = $cflags;
449 0         0 $result->{ldflags} = $ldflags;
450 0         0 return $result;
451             }
452             }
453              
454             # Try common paths (with additional Homebrew versioned paths for OpenSSL)
455 37         786 my @search_paths = @{$opts{paths} // [
456             # macOS Homebrew (Apple Silicon) - versioned first
457 37   50     1712 '/opt/homebrew/opt/openssl@3',
458             '/opt/homebrew/opt/openssl',
459             '/opt/homebrew',
460             # macOS Homebrew (Intel) - versioned first
461             '/usr/local/opt/openssl@3',
462             '/usr/local/opt/openssl',
463             '/usr/local',
464             # Linux standard locations
465             '/usr',
466             '/opt/local',
467             ]};
468              
469 37         483 my $header = $opts{header};
470 37   33     362 my $lib = $opts{lib} // "lib$lib_name";
471              
472 37         227 for my $prefix (@search_paths) {
473 296         531 my $inc_dir = "$prefix/include";
474 296         521 my $lib_dir = "$prefix/lib";
475              
476             # Check for header if specified
477 296 100 66     4412 if ($header && !-f "$inc_dir/$header") {
478 259         488 next;
479             }
480              
481             # Check for library file
482 37         191 my $found_lib = 0;
483 37         167 for my $ext (qw(.dylib .so .a)) {
484 111 50       1321 if (-f "$lib_dir/$lib$ext") {
485 0         0 $found_lib = 1;
486 0         0 last;
487             }
488             }
489              
490 37 50       286 if ($found_lib) {
491 0         0 my $try_cflags = "-I$inc_dir";
492 0         0 my $try_ldflags = "-L$lib_dir -l$lib_name";
493              
494             # Verify it actually links if test_symbol provided
495 0 0 0     0 if (!$test_symbol || $class->can_link($try_cflags, $try_ldflags, $test_symbol, $test_include)) {
496 0         0 $result->{available} = 1;
497 0         0 $result->{cflags} = $try_cflags;
498 0         0 $result->{ldflags} = $try_ldflags;
499 0         0 return $result;
500             }
501             }
502             }
503              
504 37         1936 return $result;
505             }
506              
507             # Convenience methods for common libraries
508             sub detect_openssl {
509 36     36 1 147 my ($class) = @_;
510 36         183 return $class->detect_library('ssl',
511             alien => 'Alien::OpenSSL',
512             pkg_config => 'openssl',
513             header => 'openssl/ssl.h',
514             lib => 'libssl',
515             test_symbol => 'SSL_new',
516             test_include => '#include ',
517             );
518             }
519              
520             sub detect_zlib {
521 1     1 1 8 my ($class) = @_;
522 1         20 return $class->detect_library('z',
523             alien => 'Alien::zlib',
524             pkg_config => 'zlib',
525             header => 'zlib.h',
526             lib => 'libz',
527             test_symbol => 'deflate',
528             test_include => '#include ',
529             );
530             }
531              
532             sub detect_nghttp2 {
533 0     0 1 0 my ($class) = @_;
534 0         0 return $class->detect_library('nghttp2',
535             pkg_config => 'libnghttp2',
536             header => 'nghttp2/nghttp2.h',
537             test_symbol => 'nghttp2_session_client_new',
538             test_include => '#include ',
539             );
540             }
541              
542             # =============================================================================
543             # C99 Detection
544             # =============================================================================
545              
546             my $C99_SUPPORTED;
547              
548             sub has_c99 {
549 50     50 1 109 my ($class) = @_;
550 50 100       254 return $C99_SUPPORTED if defined $C99_SUPPORTED;
551              
552 27         23824 require File::Temp;
553              
554             # Test C99 features: inline keyword and for-loop declarations
555 27         422527 my $test_code = <<'C';
556             static inline int test_inline(void) { return 1; }
557             int main(void) {
558             for (int i = 0; i < 1; i++) { }
559             return test_inline();
560             }
561             C
562              
563 27         204 my $src = File::Temp->new(SUFFIX => '.c', UNLINK => 1);
564 27         20961 my $src_path = $src->filename;
565 27         276 print $src $test_code;
566 27         1484 close $src;
567              
568 27         178 my $out = File::Temp->new(SUFFIX => '', UNLINK => 1);
569 27         13303 my $out_path = $out->filename;
570 27         449 close $out;
571              
572 27   50     854 my $cc = $ENV{CC} || $Config{cc} || 'cc';
573 27         1977848 my $result = system("$cc -o $out_path $src_path 2>/dev/null");
574              
575 27 50       5354 unlink $out_path if -f $out_path;
576              
577 27 50       1047 $C99_SUPPORTED = ($result == 0) ? 1 : 0;
578 27         1587 return $C99_SUPPORTED;
579             }
580              
581             sub inline_keyword {
582 50     50 1 162 my ($class) = @_;
583 50 50       312 return $class->has_c99() ? 'inline' : '';
584             }
585              
586              
587             1;
588              
589             __END__