File Coverage

blib/lib/XS/JIT/Header.pm
Criterion Covered Total %
statement 223 310 71.9
branch 51 140 36.4
condition 26 64 40.6
subroutine 30 33 90.9
pod 10 11 90.9
total 340 558 60.9


line stmt bran cond sub pod time code
1             package XS::JIT::Header;
2              
3 3     3   923193 use strict;
  3         8  
  3         152  
4 3     3   20 use warnings;
  3         8  
  3         270  
5 3     3   133 use 5.010;
  3         17  
6              
7             our $VERSION = '0.22';
8              
9 3     3   2057 use XS::JIT;
  3         12  
  3         137  
10 3     3   2303 use XS::JIT::Builder;
  3         10  
  3         268  
11 3     3   1918 use XS::JIT::Header::Parser;
  3         10  
  3         148  
12 3     3   1970 use XS::JIT::Header::TypeMap;
  3         13  
  3         216  
13 3     3   22 use File::Spec;
  3         6  
  3         128  
14 3     3   17 use Digest::MD5 qw(md5_hex);
  3         5  
  3         308  
15 3     3   21 use Config;
  3         18  
  3         16441  
16              
17             sub new {
18 14     14 1 79788 my ($class, %opts) = @_;
19              
20             my $self = bless {
21             header => $opts{header},
22             lib => $opts{lib},
23             include => $opts{include} || [],
24             define => $opts{define} || {},
25             package => $opts{package} || caller(),
26             cache_dir => $opts{cache_dir} || '_CACHED_XS',
27             prefix => $opts{prefix} || '',
28 14   50     541 force => $opts{force} || 0,
      50        
      66        
      100        
      50        
      50        
29              
30             # Internal state
31             _parser => undef,
32             _functions => [], # Functions to compile
33             _compiled => 0,
34             _module_id => undef,
35             }, $class;
36              
37             # Parse header if provided
38 14 50       69 if ($self->{header}) {
39 14         50 $self->_parse_header();
40             }
41              
42 13         280 return $self;
43             }
44              
45             # Parse the header file
46             sub _parse_header {
47 14     14   35 my ($self) = @_;
48              
49             $self->{_parser} = XS::JIT::Header::Parser->new(
50             include => $self->{include},
51             define => $self->{define},
52 14         159 );
53              
54 14         30 my $header = $self->{header};
55              
56             # Handle system headers (e.g., 'math.h')
57 14 100       98 if ($header !~ m{^[/.]}) {
58 5         21 $header = $self->_find_header($header);
59             }
60              
61 14 100       358 if (-f $header) {
62 13         76 $self->{_parser}->parse_file($header);
63 13         65 $self->{_header_path} = $header;
64             }
65             else {
66 1         18 die "Cannot find header file: $self->{header}";
67             }
68              
69             # Generate unique module ID based on header content and options
70 13         121 $self->{_module_id} = $self->_generate_module_id();
71              
72 13         35 return $self;
73             }
74              
75             # Get system include paths - uses Perl's Config which knows where headers are
76             sub system_include_paths {
77 5     5 0 12 my ($class_or_self) = @_;
78              
79 5         10 my @paths;
80              
81             # Primary: Perl's configured system include (most reliable)
82 5 50       375 push @paths, $Config{usrinc} if $Config{usrinc};
83              
84             # From Perl's library paths (may contain include dirs)
85 5 50       116 if ($Config{locincpth}) {
86 5         43 push @paths, split(/\s+/, $Config{locincpth});
87             }
88              
89             # Standard fallbacks
90 5         19 push @paths, '/usr/include', '/usr/local/include';
91              
92             # macOS: use xcrun to find SDK (cached)
93 5 50       90 if ($^O eq 'darwin') {
94 0         0 state $sdk_path;
95 0 0       0 unless (defined $sdk_path) {
96 0         0 $sdk_path = `xcrun --show-sdk-path 2>/dev/null`;
97 0 0       0 chomp $sdk_path if $sdk_path;
98 0   0     0 $sdk_path //= '';
99             }
100 0 0 0     0 push @paths, "$sdk_path/usr/include" if $sdk_path && -d "$sdk_path/usr/include";
101             }
102              
103             # Filter to existing directories and dedupe
104 5         29 my %seen;
105 5 100 66     14 return grep { defined $_ && -d $_ && !$seen{$_}++ } @paths;
  45         954  
106             }
107              
108             # Find a header file in system include paths
109             sub _find_header {
110 5     5   16 my ($self, $header) = @_;
111              
112             # User-specified include paths first
113             my @search_paths = (
114 5         8 @{$self->{include}},
  5         23  
115             $self->system_include_paths,
116             );
117              
118 5         23 for my $dir (@search_paths) {
119 5         123 my $path = File::Spec->catfile($dir, $header);
120 5 50       95 return $path if -f $path;
121             }
122              
123             # Not found - return original (will fail later with clear error)
124 0         0 return $header;
125             }
126              
127             # Generate unique module ID for caching
128             sub _generate_module_id {
129 13     13   36 my ($self) = @_;
130              
131             my $sig = join("\0",
132             $self->{header} || '',
133             $self->{lib} || '',
134             $self->{package},
135             $self->{prefix},
136 13   50     147 map { "$_=$self->{define}{$_}" } sort keys %{$self->{define}},
  0   50     0  
  13         142  
137             );
138              
139 13         110 my $hash = substr(md5_hex($sig), 0, 8);
140 13         100 my $pkg = $self->{package};
141 13         104 $pkg =~ s/::/_/g;
142              
143 13         63 return "${pkg}_Header_${hash}";
144             }
145              
146             # Attach a single function
147             sub attach {
148 15     15 1 1220 my ($self, $c_name, @args) = @_;
149              
150 15 50       54 die "Function name required" unless defined $c_name;
151              
152             # Parse arguments: attach('c_name') or attach('c_name' => 'perl_name')
153             # or attach('c_name' => ['arg_types'] => 'return_type')
154 15         39 my ($perl_name, $arg_types, $return_type);
155              
156 15 50 0     43 if (@args == 0) {
    0 0        
    0          
    0          
157             # attach('func') - use same name, strip prefix
158 15         20 $perl_name = $c_name;
159 15 50       39 $perl_name =~ s/^\Q$self->{prefix}\E// if $self->{prefix};
160             }
161             elsif (@args == 1 && !ref $args[0]) {
162             # attach('c_func' => 'perl_func')
163 0         0 $perl_name = $args[0];
164             }
165             elsif (@args == 2 && ref $args[0] eq 'ARRAY') {
166             # attach('c_func' => ['int', 'char*'] => 'int')
167 0         0 $perl_name = $c_name;
168 0 0       0 $perl_name =~ s/^\Q$self->{prefix}\E// if $self->{prefix};
169 0         0 $arg_types = $args[0];
170 0         0 $return_type = $args[1];
171             }
172             elsif (@args == 3) {
173             # attach('c_func' => 'perl_func' => ['int'] => 'int')
174 0         0 $perl_name = $args[0];
175 0         0 $arg_types = $args[1];
176 0         0 $return_type = $args[2];
177             }
178              
179             # Get function info from parser if not explicitly provided
180 15         72 my $func_info = $self->{_parser}->function($c_name);
181              
182 15 0 33     37 if (!$func_info && !$arg_types) {
183 0         0 die "Unknown function '$c_name' and no type information provided";
184             }
185              
186             # Use parsed info if not explicitly provided
187 15 50 33     123 $arg_types //= $func_info->{param_types} if $func_info;
188 15 50 33     75 $return_type //= $func_info->{return_type} if $func_info;
189              
190             # Build full Perl name
191 15 50       70 my $full_perl_name = $perl_name =~ /::/ ? $perl_name : "$self->{package}::$perl_name";
192              
193 15         173 push @{$self->{_functions}}, {
194             c_name => $c_name,
195             perl_name => $full_perl_name,
196             arg_types => $arg_types || [],
197             return_type => $return_type || 'void',
198 15 50 50     25 is_variadic => $func_info ? $func_info->{is_variadic} : 0,
      50        
199             };
200              
201 15         50 return $self;
202             }
203              
204             # Attach multiple functions
205             sub attach_all {
206 1     1 1 22 my ($self, $filter) = @_;
207              
208 1         26 my @names = $self->{_parser}->function_names;
209              
210 1         7 for my $name (@names) {
211             # Apply filter if provided
212 4 50       20 if (defined $filter) {
213 0 0       0 if (ref $filter eq 'Regexp') {
    0          
214 0 0       0 next unless $name =~ $filter;
215             }
216             elsif (ref $filter eq 'CODE') {
217 0 0       0 next unless $filter->($name);
218             }
219             }
220              
221 4         19 $self->attach($name);
222             }
223              
224 1         6 return $self;
225             }
226              
227             # Get list of function names from parsed header
228             sub functions {
229 2     2 1 1962 my ($self) = @_;
230 2         13 return $self->{_parser}->function_names;
231             }
232              
233             # Get function info
234             sub function {
235 4     4 1 60 my ($self, $name) = @_;
236 4         24 return $self->{_parser}->function($name);
237             }
238              
239             # Get list of constant names
240             sub constants {
241 1     1 1 13 my ($self) = @_;
242 1         6 return $self->{_parser}->constant_names;
243             }
244              
245             # Get constant value
246             sub constant {
247 2     2 1 1996 my ($self, $name) = @_;
248 2         10 my $info = $self->{_parser}->constant($name);
249 2 50       9 return $info ? $info->{value} : undef;
250             }
251              
252             # Generate XS wrapper code for a function
253             sub _generate_wrapper {
254 14     14   27 my ($self, $func) = @_;
255              
256 14         279 my $b = XS::JIT::Builder->new;
257 14         34 my $safe_name = $func->{c_name};
258 14         75 $safe_name =~ s/\W/_/g;
259              
260 14         20 my @arg_types = @{$func->{arg_types}};
  14         43  
261 14         59 my $return_type = $func->{return_type};
262              
263 14         154 $b->xs_function("xs_$safe_name")
264             ->xs_preamble;
265              
266             # Check argument count
267 14         28 my $num_args = scalar @arg_types;
268 14 100       41 if ($num_args > 0) {
269 10         34 my $usage = join(', ', map { "\$arg$_" } 0 .. $#arg_types);
  20         105  
270 10         116 $b->check_items($num_args, $num_args, $usage);
271             }
272              
273             # Convert arguments from Perl to C
274 14         28 my @c_args;
275 14         85 for my $i (0 .. $#arg_types) {
276 20         39 my $type = $arg_types[$i];
277 20         82 my $type_info = XS::JIT::Header::TypeMap::resolve($type);
278 20         52 my $arg_name = "arg$i";
279              
280 20 100       87 if ($type_info->{perl} eq 'IV') {
    50          
    50          
    0          
    0          
    0          
    0          
    0          
281 12         98 $b->line("$type_info->{c} $arg_name = ($type_info->{c})SvIV(ST($i));");
282             }
283             elsif ($type_info->{perl} eq 'UV') {
284 0 0       0 if ($type_info->{is_ptr}) {
285 0         0 $b->line("$type_info->{c} $arg_name = INT2PTR($type_info->{c}, SvUV(ST($i)));");
286             }
287             else {
288 0         0 $b->line("$type_info->{c} $arg_name = ($type_info->{c})SvUV(ST($i));");
289             }
290             }
291             elsif ($type_info->{perl} eq 'NV') {
292 8         1694 $b->line("$type_info->{c} $arg_name = ($type_info->{c})SvNV(ST($i));");
293             }
294             elsif ($type_info->{perl} eq 'PV') {
295 0         0 $b->line("$type_info->{c} $arg_name = SvPV_nolen(ST($i));");
296             }
297             elsif ($type_info->{perl} eq 'SV') {
298             # Pass SV* directly - no conversion needed
299 0         0 $b->line("SV* $arg_name = ST($i);");
300             }
301             elsif ($type_info->{perl} eq 'HV') {
302             # Dereference to get HV* from hashref
303 0         0 $b->line("if (!SvROK(ST($i)) || SvTYPE(SvRV(ST($i))) != SVt_PVHV)");
304 0         0 $b->line(" croak(\"Argument $i must be a hash reference\");");
305 0         0 $b->line("HV* $arg_name = (HV*)SvRV(ST($i));");
306             }
307             elsif ($type_info->{perl} eq 'AV') {
308             # Dereference to get AV* from arrayref
309 0         0 $b->line("if (!SvROK(ST($i)) || SvTYPE(SvRV(ST($i))) != SVt_PVAV)");
310 0         0 $b->line(" croak(\"Argument $i must be an array reference\");");
311 0         0 $b->line("AV* $arg_name = (AV*)SvRV(ST($i));");
312             }
313             elsif ($type_info->{perl} eq 'CV') {
314             # Dereference to get CV* from coderef
315 0         0 $b->line("if (!SvROK(ST($i)) || SvTYPE(SvRV(ST($i))) != SVt_PVCV)");
316 0         0 $b->line(" croak(\"Argument $i must be a code reference\");");
317 0         0 $b->line("CV* $arg_name = (CV*)SvRV(ST($i));");
318             }
319             else {
320             # Unknown type - try to cast
321 0         0 $b->line("$type_info->{c} $arg_name = ($type_info->{c})SvIV(ST($i));");
322             }
323              
324 20         98 push @c_args, $arg_name;
325             }
326              
327             # Call the C function
328 14         56 my $ret_info = XS::JIT::Header::TypeMap::resolve($return_type);
329 14         54 my $call = "$func->{c_name}(" . join(', ', @c_args) . ")";
330              
331 14 100       38 if ($return_type eq 'void') {
332 2         35 $b->line("$call;");
333 2         18 $b->xs_return_undef;
334             }
335             else {
336 12         75 $b->line("$ret_info->{c} retval = $call;");
337              
338 12 100       51 if ($ret_info->{perl} eq 'IV') {
    50          
    100          
    50          
    0          
    0          
    0          
    0          
339 6         29 $b->line("ST(0) = sv_2mortal(newSViv((IV)retval));");
340             }
341             elsif ($ret_info->{perl} eq 'UV') {
342 0 0       0 if ($ret_info->{is_ptr}) {
343 0         0 $b->line("ST(0) = sv_2mortal(newSVuv(PTR2UV(retval)));");
344             }
345             else {
346 0         0 $b->line("ST(0) = sv_2mortal(newSVuv((UV)retval));");
347             }
348             }
349             elsif ($ret_info->{perl} eq 'NV') {
350 4         16 $b->line("ST(0) = sv_2mortal(newSVnv((NV)retval));");
351             }
352             elsif ($ret_info->{perl} eq 'PV') {
353 2         11 $b->line("ST(0) = sv_2mortal(newSVpv(retval, 0));");
354             }
355             elsif ($ret_info->{perl} eq 'SV') {
356             # Return SV* directly (mortalized)
357 0         0 $b->line("ST(0) = sv_2mortal(retval);");
358             }
359             elsif ($ret_info->{perl} eq 'HV') {
360             # Return HV* as hashref (mortalized)
361 0         0 $b->line("ST(0) = sv_2mortal(newRV_noinc((SV*)retval));");
362             }
363             elsif ($ret_info->{perl} eq 'AV') {
364             # Return AV* as arrayref (mortalized)
365 0         0 $b->line("ST(0) = sv_2mortal(newRV_noinc((SV*)retval));");
366             }
367             elsif ($ret_info->{perl} eq 'CV') {
368             # Return CV* as coderef (mortalized)
369 0         0 $b->line("ST(0) = sv_2mortal(newRV_noinc((SV*)retval));");
370             }
371             else {
372 0         0 $b->line("ST(0) = sv_2mortal(newSViv((IV)retval));");
373             }
374              
375 12         58 $b->xs_return(1);
376             }
377              
378 14         49 $b->xs_end;
379              
380             return {
381 14         243 code => $b->code,
382             xs_name => "xs_$safe_name",
383             };
384             }
385              
386             # Build compiler/linker flags
387             sub _build_cflags {
388 0     0   0 my ($self) = @_;
389              
390 0         0 my @flags;
391              
392             # Include paths
393 0         0 push @flags, map { "-I$_" } @{$self->{include}};
  0         0  
  0         0  
394              
395             # Header directory
396 0 0       0 if ($self->{_header_path}) {
397             my $dir = File::Spec->rel2abs(
398 0         0 (File::Spec->splitpath($self->{_header_path}))[1]
399             );
400 0 0       0 push @flags, "-I$dir" if $dir;
401             }
402              
403             # Defines
404 0         0 push @flags, map { "-D$_=$self->{define}{$_}" } keys %{$self->{define}};
  0         0  
  0         0  
405              
406 0         0 return join(' ', @flags);
407             }
408              
409             sub _build_ldflags {
410 0     0   0 my ($self) = @_;
411              
412 0 0       0 return '' unless defined $self->{lib};
413              
414 0         0 my $lib = $self->{lib};
415              
416             # If it's a path to a .so/.dylib/.dll, use it directly
417 0 0 0     0 if ($lib =~ m{[/\\]} || $lib =~ /\.(?:so|dylib|dll|a)(?:\.\d+)*$/) {
418 0         0 return $lib;
419             }
420              
421             # Otherwise, use -l flag
422 0         0 return "-l$lib";
423             }
424              
425             # Compile all attached functions
426             sub compile {
427 0     0 1 0 my ($self) = @_;
428              
429 0 0 0     0 return 1 if $self->{_compiled} && !$self->{force};
430              
431 0 0       0 die "No functions attached" unless @{$self->{_functions}};
  0         0  
432              
433             # Generate the include directive
434 0         0 my $code = "";
435              
436             # Add standard includes
437 0         0 $code .= "#include \n";
438 0         0 $code .= "#include \n";
439              
440             # Add the user's header
441 0 0       0 if ($self->{_header_path}) {
442 0         0 $code .= qq{#include "$self->{_header_path}"\n};
443             }
444              
445 0         0 $code .= "\n";
446              
447             # Generate wrapper code for each function
448 0         0 my %functions;
449 0         0 for my $func (@{$self->{_functions}}) {
  0         0  
450 0         0 my $wrapper = $self->_generate_wrapper($func);
451 0         0 $code .= $wrapper->{code} . "\n";
452              
453             $functions{$func->{perl_name}} = {
454             source => $wrapper->{xs_name},
455 0         0 is_xs_native => 1,
456             };
457             }
458              
459             # Compile with XS::JIT
460             # Note: We pass extra flags through environment since XS::JIT
461             # doesn't currently support extra cflags/ldflags directly
462 0         0 local $ENV{XS_JIT_EXTRA_CFLAGS} = $self->_build_cflags;
463 0         0 local $ENV{XS_JIT_EXTRA_LDFLAGS} = $self->_build_ldflags;
464              
465             my $result = XS::JIT->compile(
466             code => $code,
467             name => $self->{_module_id},
468             cache_dir => $self->{cache_dir},
469             functions => \%functions,
470             force => $self->{force},
471 0         0 );
472              
473 0 0       0 unless ($result) {
474 0         0 die "XS::JIT compilation failed";
475             }
476              
477 0         0 $self->{_compiled} = 1;
478 0         0 return 1;
479             }
480              
481             # Write module files without JIT compilation
482             # This generates static files that can be distributed and compiled normally
483             sub write_module {
484 6     6 1 246 my ($self, %opts) = @_;
485              
486 6 100       13 die "No functions attached" unless @{$self->{_functions}};
  6         52  
487              
488 5   50     24 my $dir = $opts{dir} || '.';
489 5   66     59 my $package = $opts{package} || $self->{package};
490              
491             # Create directory structure based on package name
492 5         24 my @parts = split /::/, $package;
493 5         21 my $module_name = pop @parts; # Last part is the module name
494              
495             # Build directory path
496 5         48 my $lib_dir = File::Spec->catdir($dir, 'lib', @parts);
497 5         52 _mkpath($lib_dir);
498              
499             # File paths
500 5         111 my $pm_file = File::Spec->catfile($lib_dir, "$module_name.pm");
501 5         59 my $xs_file = File::Spec->catfile($lib_dir, "$module_name.xs");
502 5         70 my $c_file = File::Spec->catfile($lib_dir, "${module_name}_funcs.c");
503              
504             # Generate the C code (function wrappers)
505 5         73 my $c_code = $self->_generate_module_c_code();
506              
507             # Generate the XS file (boot section and function registration)
508 5         23 my $xs_code = $self->_generate_module_xs_code($package, $module_name);
509              
510             # Generate the .pm file
511 5         22 my $pm_code = $self->_generate_module_pm_code($package);
512              
513             # Write files
514 5         20 _write_file($c_file, $c_code);
515 5         25 _write_file($xs_file, $xs_code);
516 5         22 _write_file($pm_file, $pm_code);
517              
518             # Mark as compiled to prevent auto-compile in DESTROY
519 5         25 $self->{_compiled} = 1;
520              
521             # Return info about what was created
522             return {
523 5         130 c_file => $c_file,
524             xs_file => $xs_file,
525             pm_file => $pm_file,
526             package => $package,
527             };
528             }
529              
530             # Generate just the C code portion (for embedding or custom builds)
531             sub write_c_file {
532 3     3 1 9203 my ($self, $filename) = @_;
533              
534 3 100       23 die "write_c_file requires a filename" unless defined $filename;
535 2 100       3 die "No functions attached" unless @{$self->{_functions}};
  2         18  
536              
537 1         3 my $code = $self->_generate_module_c_code();
538 1         11 _write_file($filename, $code);
539              
540             # Mark as compiled to prevent auto-compile in DESTROY
541 1         5 $self->{_compiled} = 1;
542              
543 1         5 return $self;
544             }
545              
546             # Generate the C function wrappers
547             sub _generate_module_c_code {
548 6     6   19 my ($self) = @_;
549              
550 6         25 my $code = "/* Generated by XS::JIT::Header */\n";
551 6         37 $code .= "/* This file can be compiled as part of a standard XS module */\n\n";
552              
553             # Add standard includes
554 6         15 $code .= "#include \n";
555 6         10 $code .= "#include \n";
556              
557             # Add the user's header
558 6 50       21 if ($self->{_header_path}) {
559 6         17 $code .= qq{#include "$self->{_header_path}"\n};
560             }
561              
562 6         11 $code .= "\n";
563              
564             # Generate wrapper code for each function
565 6         10 for my $func (@{$self->{_functions}}) {
  6         36  
566 14         42 my $wrapper = $self->_generate_wrapper($func);
567 14         66 $code .= $wrapper->{code} . "\n";
568             }
569              
570 6         37 return $code;
571             }
572              
573             # Generate the XS boot code
574             sub _generate_module_xs_code {
575 5     5   17 my ($self, $package, $module_name) = @_;
576              
577 5         20 my $xs = <<"END_XS";
578             /* Generated by XS::JIT::Header */
579              
580             #define PERL_NO_GET_CONTEXT
581             #include "EXTERN.h"
582             #include "perl.h"
583             #include "XSUB.h"
584              
585             /* Include the generated function wrappers */
586             #include "${module_name}_funcs.c"
587              
588             MODULE = $package PACKAGE = $package
589              
590             PROTOTYPES: DISABLE
591              
592             BOOT:
593             {
594             END_XS
595              
596             # Add newXS for each function
597 5         8 for my $func (@{$self->{_functions}}) {
  5         17  
598 12         20 my $safe_name = $func->{c_name};
599 12         25 $safe_name =~ s/\W/_/g;
600 12         19 my $perl_name = $func->{perl_name};
601              
602 12         35 $xs .= qq{ newXS("$perl_name", xs_$safe_name, __FILE__);\n};
603             }
604              
605 5         8 $xs .= "}\n";
606              
607 5         13 return $xs;
608             }
609              
610             # Generate the .pm file
611             sub _generate_module_pm_code {
612 5     5   13 my ($self, $package) = @_;
613              
614 5         17 my $pm = <<"END_PM";
615             package $package;
616              
617             use strict;
618             use warnings;
619              
620             our \$VERSION = '0.01';
621              
622             require XSLoader;
623             XSLoader::load('$package', \$VERSION);
624              
625             1;
626              
627             __END__