File Coverage

blib/lib/Hypersonic/JIT/Util.pm
Criterion Covered Total %
statement 109 241 45.2
branch 38 100 38.0
condition 13 74 17.5
subroutine 14 29 48.2
pod 10 21 47.6
total 184 465 39.5


line stmt bran cond sub pod time code
1             package Hypersonic::JIT::Util;
2 48     48   285 use strict;
  48         83  
  48         1520  
3 48     48   200 use warnings;
  48         154  
  48         1824  
4              
5 48     48   186 use Config;
  48         57  
  48         1718  
6 48     48   305 use Carp qw(croak);
  48         141  
  48         122983  
7              
8             our $VERSION = '0.18';
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 53     53 1 237 my ($class, $builder, @features) = @_;
150            
151 53         161 my %features = map { $_ => 1 } @features;
  203         599  
152            
153             # Always needed
154 53         565 $builder->line('#include ')
155             ->line('#include ')
156             ->line('#include ');
157            
158 53 100       231 if ($features{stdio}) {
159 35         97 $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 53 0 33     210 if ($features{unistd} || $features{fcntl} || $features{socket}) {
      0        
167 53         270 $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 53 50       279 $builder->line('#include ') if $features{unistd};
185 53 50       231 $builder->line('#include ') if $features{fcntl};
186 53 100       154 if ($features{socket}) {
187 35         3301 $builder->line('#include ')
188             ->line('#include ')
189             ->line('#include ')
190             ->line('#include ')
191             ->line('#include ')
192             ->line('#include ');
193             }
194 53         260 $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 53 100       254 if ($features{threading}) {
203 18         122 $builder->line('#ifndef _WIN32')
204             ->line('#include ')
205             ->line('#endif');
206             }
207            
208 53 100       147 if ($features{eventfd}) {
209 9         137 $builder->line('#ifdef __linux__')
210             ->line('#include ')
211             ->line('#endif');
212             }
213            
214 53 50       168 if ($features{time}) {
215 0         0 $builder->line('#include ');
216             }
217            
218 53 50       227 if ($features{signal}) {
219 0         0 $builder->line('#include ');
220             }
221            
222 53 50       163 if ($features{openssl}) {
223 0         0 $builder->line('#include ')
224             ->line('#include ')
225             ->line('#include ');
226             }
227            
228 53         207 $builder->blank;
229            
230 53         213 return $builder;
231             }
232              
233             # =============================================================================
234             # Platform Detection Helpers
235             # =============================================================================
236              
237             sub add_platform_eventfd {
238 9     9 0 26 my ($class, $builder) = @_;
239            
240 9         102 $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         15 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   18 return $HAS_DEVEL_CHECKLIB if defined $HAS_DEVEL_CHECKLIB;
279 1 50       1 $HAS_DEVEL_CHECKLIB = eval { require Devel::CheckLib; 1 } ? 1 : 0;
  1         520  
  1         30619  
280 1         6 return $HAS_DEVEL_CHECKLIB;
281             }
282              
283             # Check if ExtUtils::PkgConfig is available
284             my $HAS_EXTUTILS_PKGCONFIG;
285             sub _has_extutils_pkgconfig {
286 31 100   31   82 return $HAS_EXTUTILS_PKGCONFIG if defined $HAS_EXTUTILS_PKGCONFIG;
287 30 50       52 $HAS_EXTUTILS_PKGCONFIG = eval { require ExtUtils::PkgConfig; 1 } ? 1 : 0;
  30         2396  
  0         0  
288 30         199 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 12 my ($class, $cflags, $ldflags, $test_symbol, $extra_includes) = @_;
295 3   50     13 $cflags //= '';
296 3   50     10 $ldflags //= '';
297 3   100     23 $extra_includes //= '';
298              
299             # Extract header names from include directives for Devel::CheckLib
300             # e.g., '#include ' -> 'math.h'
301 3         5 my @headers;
302 3 100       12 if ($extra_includes) {
303 2         24 @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       18 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         11 my $function = "void *p = (void*)$test_symbol; volatile void *vp = p; return vp ? 0 : 1;";
312 3         32 my %args = (
313             INC => $cflags,
314             LIBS => $ldflags,
315             function => $function,
316             );
317 3 100       18 $args{header} = \@headers if @headers;
318 3         19 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 31     31 1 246 my ($class, $lib_name, %opts) = @_;
395              
396 31         108 my $result = {
397             available => 0,
398             cflags => '',
399             ldflags => '',
400             };
401              
402 31         76 my $test_symbol = $opts{test_symbol};
403 31   50     99 my $test_include = $opts{test_include} // '';
404              
405             # Try Alien module first
406 31   33     77 my $alien_module = $opts{alien} // "Alien::\u$lib_name";
407 31 50       1732 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 31   33     162 my $pkg_name = $opts{pkg_config} // $lib_name;
422 31         98 my ($cflags, $ldflags);
423              
424             # ExtUtils::PkgConfig only grew the ->exists() class method in
425             # later releases (it's not in the 1.16 shipped with some
426             # vendor perls). Older versions die with
427             # method 'exists' not implemented at ... line 424
428             # which is exactly what the CPAN tester report for Hypersonic
429             # 0.17 on perl 5.20 showed. Probe with ->find() inside an eval
430             # when the class can't ->exists, and treat any failure as
431             # "not available via pkg-config" so we fall through to the
432             # command-line pkg-config branch below.
433 31 50       360 if ($class->_has_extutils_pkgconfig()) {
434 0         0 my $is_available = eval {
435 0 0       0 if (ExtUtils::PkgConfig->can('exists')) {
436 0 0       0 ExtUtils::PkgConfig->exists($pkg_name) ? 1 : 0;
437             } else {
438             # Older API: ->find dies if the package is missing,
439             # returns a key/value list (cflags/libs/modversion) on
440             # success. Either outcome is fine here - we just want
441             # a 1/0 answer.
442 0         0 my %info = ExtUtils::PkgConfig->find($pkg_name);
443 0 0 0     0 ($info{cflags} || $info{libs} || $info{modversion}) ? 1 : 0;
444             }
445             };
446             # If the probe died OR returned a false value, fall back to
447             # the shell pkg-config below; never propagate the die.
448 0 0       0 if ($is_available) {
449 0         0 eval {
450 0         0 my %pkg_info = ExtUtils::PkgConfig->find($pkg_name);
451 0   0     0 $cflags = $pkg_info{cflags} // '';
452 0   0     0 $ldflags = $pkg_info{libs} // '';
453             };
454             # Same defensive eval - some old versions of
455             # ExtUtils::PkgConfig die on find() in odd corner cases
456             # (e.g. when the .pc file references an unresolvable
457             # Requires:). Swallow and fall through to shell pkg-config.
458             }
459             }
460              
461             # Fallback to command-line pkg-config
462 31 50       199 if (!$ldflags) {
463 31         143326 $cflags = `pkg-config --cflags $pkg_name 2>/dev/null`;
464 31         122014 $ldflags = `pkg-config --libs $pkg_name 2>/dev/null`;
465 31 50       1033 if ($? == 0) {
466 0         0 chomp($cflags);
467 0         0 chomp($ldflags);
468             } else {
469 31         399 $cflags = $ldflags = '';
470             }
471             }
472              
473 31 50       440 if ($ldflags) {
474             # Verify it actually links if test_symbol provided
475 0 0 0     0 if (!$test_symbol || $class->can_link($cflags, $ldflags, $test_symbol, $test_include)) {
476 0         0 $result->{available} = 1;
477 0         0 $result->{cflags} = $cflags;
478 0         0 $result->{ldflags} = $ldflags;
479 0         0 return $result;
480             }
481             }
482              
483             # Try common paths (with additional Homebrew versioned paths for OpenSSL)
484 31         228 my @search_paths = @{$opts{paths} // [
485             # macOS Homebrew (Apple Silicon) - versioned first
486 31   50     1412 '/opt/homebrew/opt/openssl@3',
487             '/opt/homebrew/opt/openssl',
488             '/opt/homebrew',
489             # macOS Homebrew (Intel) - versioned first
490             '/usr/local/opt/openssl@3',
491             '/usr/local/opt/openssl',
492             '/usr/local',
493             # Linux standard locations
494             '/usr',
495             '/opt/local',
496             ]};
497              
498 31         274 my $header = $opts{header};
499 31   33     350 my $lib = $opts{lib} // "lib$lib_name";
500              
501 31         198 for my $prefix (@search_paths) {
502 248         437 my $inc_dir = "$prefix/include";
503 248         407 my $lib_dir = "$prefix/lib";
504              
505             # Check for header if specified
506 248 100 66     4754 if ($header && !-f "$inc_dir/$header") {
507 217         429 next;
508             }
509              
510             # Check for library file
511 31         131 my $found_lib = 0;
512 31         154 for my $ext (qw(.dylib .so .a)) {
513 93 50       827 if (-f "$lib_dir/$lib$ext") {
514 0         0 $found_lib = 1;
515 0         0 last;
516             }
517             }
518              
519 31 50       217 if ($found_lib) {
520 0         0 my $try_cflags = "-I$inc_dir";
521 0         0 my $try_ldflags = "-L$lib_dir -l$lib_name";
522              
523             # Verify it actually links if test_symbol provided
524 0 0 0     0 if (!$test_symbol || $class->can_link($try_cflags, $try_ldflags, $test_symbol, $test_include)) {
525 0         0 $result->{available} = 1;
526 0         0 $result->{cflags} = $try_cflags;
527 0         0 $result->{ldflags} = $try_ldflags;
528 0         0 return $result;
529             }
530             }
531             }
532              
533 31         1547 return $result;
534             }
535              
536             # Convenience methods for common libraries
537             sub detect_openssl {
538 30     30 1 81 my ($class) = @_;
539 30         142 return $class->detect_library('ssl',
540             alien => 'Alien::OpenSSL',
541             pkg_config => 'openssl',
542             header => 'openssl/ssl.h',
543             lib => 'libssl',
544             test_symbol => 'SSL_new',
545             test_include => '#include ',
546             );
547             }
548              
549             sub detect_zlib {
550 1     1 1 2 my ($class) = @_;
551 1         5 return $class->detect_library('z',
552             alien => 'Alien::zlib',
553             pkg_config => 'zlib',
554             header => 'zlib.h',
555             lib => 'libz',
556             test_symbol => 'deflate',
557             test_include => '#include ',
558             );
559             }
560              
561             sub detect_nghttp2 {
562 0     0 1 0 my ($class) = @_;
563 0         0 return $class->detect_library('nghttp2',
564             pkg_config => 'libnghttp2',
565             header => 'nghttp2/nghttp2.h',
566             test_symbol => 'nghttp2_session_client_new',
567             test_include => '#include ',
568             );
569             }
570              
571             # =============================================================================
572             # C99 Detection
573             # =============================================================================
574              
575             my $C99_SUPPORTED;
576              
577             sub has_c99 {
578 44     44 1 84 my ($class) = @_;
579 44 100       209 return $C99_SUPPORTED if defined $C99_SUPPORTED;
580              
581 21         17455 require File::Temp;
582              
583             # Test C99 features: inline keyword and for-loop declarations
584 21         328609 my $test_code = <<'C';
585             static inline int test_inline(void) { return 1; }
586             int main(void) {
587             for (int i = 0; i < 1; i++) { }
588             return test_inline();
589             }
590             C
591              
592 21         141 my $src = File::Temp->new(SUFFIX => '.c', UNLINK => 1);
593 21         16306 my $src_path = $src->filename;
594 21         213 print $src $test_code;
595 21         1125 close $src;
596              
597 21         121 my $out = File::Temp->new(SUFFIX => '', UNLINK => 1);
598 21         9539 my $out_path = $out->filename;
599 21         364 close $out;
600              
601 21   50     667 my $cc = $ENV{CC} || $Config{cc} || 'cc';
602 21         1509599 my $result = system("$cc -o $out_path $src_path 2>/dev/null");
603              
604 21 50       4477 unlink $out_path if -f $out_path;
605              
606 21 50       691 $C99_SUPPORTED = ($result == 0) ? 1 : 0;
607 21         1438 return $C99_SUPPORTED;
608             }
609              
610             sub inline_keyword {
611 44     44 1 120 my ($class) = @_;
612 44 50       185 return $class->has_c99() ? 'inline' : '';
613             }
614              
615              
616             1;
617              
618             __END__