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   432907 use strict;
  3         4  
  3         86  
4 3     3   10 use warnings;
  3         4  
  3         120  
5 3     3   50 use 5.010;
  3         32  
6              
7             our $VERSION = '0.18';
8              
9 3     3   1086 use XS::JIT;
  3         6  
  3         101  
10 3     3   1563 use XS::JIT::Builder;
  3         10  
  3         146  
11 3     3   1319 use XS::JIT::Header::Parser;
  3         6  
  3         95  
12 3     3   1300 use XS::JIT::Header::TypeMap;
  3         26  
  3         190  
13 3     3   17 use File::Spec;
  3         3  
  3         65  
14 3     3   9 use Digest::MD5 qw(md5_hex);
  3         9  
  3         162  
15 3     3   25 use Config;
  3         11  
  3         9736  
16              
17             sub new {
18 14     14 1 182611 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     445 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       68 if ($self->{header}) {
39 14         47 $self->_parse_header();
40             }
41              
42 13         243 return $self;
43             }
44              
45             # Parse the header file
46             sub _parse_header {
47 14     14   29 my ($self) = @_;
48              
49             $self->{_parser} = XS::JIT::Header::Parser->new(
50             include => $self->{include},
51             define => $self->{define},
52 14         160 );
53              
54 14         25 my $header = $self->{header};
55              
56             # Handle system headers (e.g., 'math.h')
57 14 100       84 if ($header !~ m{^[/.]}) {
58 5         16 $header = $self->_find_header($header);
59             }
60              
61 14 100       443 if (-f $header) {
62 13         55 $self->{_parser}->parse_file($header);
63 13         63 $self->{_header_path} = $header;
64             }
65             else {
66 1         22 die "Cannot find header file: $self->{header}";
67             }
68              
69             # Generate unique module ID based on header content and options
70 13         125 $self->{_module_id} = $self->_generate_module_id();
71              
72 13         29 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 9 my ($class_or_self) = @_;
78              
79 5         7 my @paths;
80              
81             # Primary: Perl's configured system include (most reliable)
82 5 50       323 push @paths, $Config{usrinc} if $Config{usrinc};
83              
84             # From Perl's library paths (may contain include dirs)
85 5 50       73 if ($Config{locincpth}) {
86 5         41 push @paths, split(/\s+/, $Config{locincpth});
87             }
88              
89             # Standard fallbacks
90 5         15 push @paths, '/usr/include', '/usr/local/include';
91              
92             # macOS: use xcrun to find SDK (cached)
93 5 50       62 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         8 my %seen;
105 5 100 66     11 return grep { defined $_ && -d $_ && !$seen{$_}++ } @paths;
  45         876  
106             }
107              
108             # Find a header file in system include paths
109             sub _find_header {
110 5     5   12 my ($self, $header) = @_;
111              
112             # User-specified include paths first
113             my @search_paths = (
114 5         8 @{$self->{include}},
  5         21  
115             $self->system_include_paths,
116             );
117              
118 5         14 for my $dir (@search_paths) {
119 5         98 my $path = File::Spec->catfile($dir, $header);
120 5 50       87 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   29 my ($self) = @_;
130              
131             my $sig = join("\0",
132             $self->{header} || '',
133             $self->{lib} || '',
134             $self->{package},
135             $self->{prefix},
136 13   50     131 map { "$_=$self->{define}{$_}" } sort keys %{$self->{define}},
  0   50     0  
  13         109  
137             );
138              
139 13         116 my $hash = substr(md5_hex($sig), 0, 8);
140 13         61 my $pkg = $self->{package};
141 13         68 $pkg =~ s/::/_/g;
142              
143 13         55 return "${pkg}_Header_${hash}";
144             }
145              
146             # Attach a single function
147             sub attach {
148 15     15 1 712 my ($self, $c_name, @args) = @_;
149              
150 15 50       37 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         22 my ($perl_name, $arg_types, $return_type);
155              
156 15 50 0     31 if (@args == 0) {
    0 0        
    0          
    0          
157             # attach('func') - use same name, strip prefix
158 15         21 $perl_name = $c_name;
159 15 50       38 $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         59 my $func_info = $self->{_parser}->function($c_name);
181              
182 15 0 33     32 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     90 $arg_types //= $func_info->{param_types} if $func_info;
188 15 50 33     117 $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         150 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     28 is_variadic => $func_info ? $func_info->{is_variadic} : 0,
      50        
199             };
200              
201 15         42 return $self;
202             }
203              
204             # Attach multiple functions
205             sub attach_all {
206 1     1 1 25 my ($self, $filter) = @_;
207              
208 1         23 my @names = $self->{_parser}->function_names;
209              
210 1         10 for my $name (@names) {
211             # Apply filter if provided
212 4 50       17 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         15 $self->attach($name);
222             }
223              
224 1         4 return $self;
225             }
226              
227             # Get list of function names from parsed header
228             sub functions {
229 2     2 1 1237 my ($self) = @_;
230 2         7 return $self->{_parser}->function_names;
231             }
232              
233             # Get function info
234             sub function {
235 4     4 1 53 my ($self, $name) = @_;
236 4         17 return $self->{_parser}->function($name);
237             }
238              
239             # Get list of constant names
240             sub constants {
241 1     1 1 11 my ($self) = @_;
242 1         4 return $self->{_parser}->constant_names;
243             }
244              
245             # Get constant value
246             sub constant {
247 2     2 1 1485 my ($self, $name) = @_;
248 2         8 my $info = $self->{_parser}->constant($name);
249 2 50       6 return $info ? $info->{value} : undef;
250             }
251              
252             # Generate XS wrapper code for a function
253             sub _generate_wrapper {
254 14     14   22 my ($self, $func) = @_;
255              
256 14         243 my $b = XS::JIT::Builder->new;
257 14         26 my $safe_name = $func->{c_name};
258 14         32 $safe_name =~ s/\W/_/g;
259              
260 14         15 my @arg_types = @{$func->{arg_types}};
  14         29  
261 14         25 my $return_type = $func->{return_type};
262              
263 14         121 $b->xs_function("xs_$safe_name")
264             ->xs_preamble;
265              
266             # Check argument count
267 14         38 my $num_args = scalar @arg_types;
268 14 100       31 if ($num_args > 0) {
269 10         28 my $usage = join(', ', map { "\$arg$_" } 0 .. $#arg_types);
  20         69  
270 10         117 $b->check_items($num_args, $num_args, $usage);
271             }
272              
273             # Convert arguments from Perl to C
274 14         20 my @c_args;
275 14         49 for my $i (0 .. $#arg_types) {
276 20         28 my $type = $arg_types[$i];
277 20         74 my $type_info = XS::JIT::Header::TypeMap::resolve($type);
278 20         32 my $arg_name = "arg$i";
279              
280 20 100       88 if ($type_info->{perl} eq 'IV') {
    50          
    50          
    0          
    0          
    0          
    0          
    0          
281 12         61 $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         35 $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         49 push @c_args, $arg_name;
325             }
326              
327             # Call the C function
328 14         25 my $ret_info = XS::JIT::Header::TypeMap::resolve($return_type);
329 14         35 my $call = "$func->{c_name}(" . join(', ', @c_args) . ")";
330              
331 14 100       25 if ($return_type eq 'void') {
332 2         17 $b->line("$call;");
333 2         8 $b->xs_return_undef;
334             }
335             else {
336 12         49 $b->line("$ret_info->{c} retval = $call;");
337              
338 12 100       42 if ($ret_info->{perl} eq 'IV') {
    50          
    100          
    50          
    0          
    0          
    0          
    0          
339 6         17 $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         12 $b->line("ST(0) = sv_2mortal(newSVnv((NV)retval));");
351             }
352             elsif ($ret_info->{perl} eq 'PV') {
353 2         5 $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         40 $b->xs_return(1);
376             }
377              
378 14         34 $b->xs_end;
379              
380             return {
381 14         176 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 197 my ($self, %opts) = @_;
485              
486 6 100       7 die "No functions attached" unless @{$self->{_functions}};
  6         48  
487              
488 5   50     22 my $dir = $opts{dir} || '.';
489 5   66     36 my $package = $opts{package} || $self->{package};
490              
491             # Create directory structure based on package name
492 5         21 my @parts = split /::/, $package;
493 5         16 my $module_name = pop @parts; # Last part is the module name
494              
495             # Build directory path
496 5         33 my $lib_dir = File::Spec->catdir($dir, 'lib', @parts);
497 5         21 _mkpath($lib_dir);
498              
499             # File paths
500 5         93 my $pm_file = File::Spec->catfile($lib_dir, "$module_name.pm");
501 5         32 my $xs_file = File::Spec->catfile($lib_dir, "$module_name.xs");
502 5         45 my $c_file = File::Spec->catfile($lib_dir, "${module_name}_funcs.c");
503              
504             # Generate the C code (function wrappers)
505 5         29 my $c_code = $self->_generate_module_c_code();
506              
507             # Generate the XS file (boot section and function registration)
508 5         44 my $xs_code = $self->_generate_module_xs_code($package, $module_name);
509              
510             # Generate the .pm file
511 5         17 my $pm_code = $self->_generate_module_pm_code($package);
512              
513             # Write files
514 5         17 _write_file($c_file, $c_code);
515 5         21 _write_file($xs_file, $xs_code);
516 5         15 _write_file($pm_file, $pm_code);
517              
518             # Mark as compiled to prevent auto-compile in DESTROY
519 5         21 $self->{_compiled} = 1;
520              
521             # Return info about what was created
522             return {
523 5         87 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 1438 my ($self, $filename) = @_;
533              
534 3 100       15 die "write_c_file requires a filename" unless defined $filename;
535 2 100       3 die "No functions attached" unless @{$self->{_functions}};
  2         13  
536              
537 1         3 my $code = $self->_generate_module_c_code();
538 1         3 _write_file($filename, $code);
539              
540             # Mark as compiled to prevent auto-compile in DESTROY
541 1         4 $self->{_compiled} = 1;
542              
543 1         3 return $self;
544             }
545              
546             # Generate the C function wrappers
547             sub _generate_module_c_code {
548 6     6   17 my ($self) = @_;
549              
550 6         25 my $code = "/* Generated by XS::JIT::Header */\n";
551 6         18 $code .= "/* This file can be compiled as part of a standard XS module */\n\n";
552              
553             # Add standard includes
554 6         12 $code .= "#include \n";
555 6         8 $code .= "#include \n";
556              
557             # Add the user's header
558 6 50       19 if ($self->{_header_path}) {
559 6         12 $code .= qq{#include "$self->{_header_path}"\n};
560             }
561              
562 6         7 $code .= "\n";
563              
564             # Generate wrapper code for each function
565 6         14 for my $func (@{$self->{_functions}}) {
  6         36  
566 14         46 my $wrapper = $self->_generate_wrapper($func);
567 14         57 $code .= $wrapper->{code} . "\n";
568             }
569              
570 6         15 return $code;
571             }
572              
573             # Generate the XS boot code
574             sub _generate_module_xs_code {
575 5     5   24 my ($self, $package, $module_name) = @_;
576              
577 5         17 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         7 for my $func (@{$self->{_functions}}) {
  5         23  
598 12         17 my $safe_name = $func->{c_name};
599 12         17 $safe_name =~ s/\W/_/g;
600 12         14 my $perl_name = $func->{perl_name};
601              
602 12         31 $xs .= qq{ newXS("$perl_name", xs_$safe_name, __FILE__);\n};
603             }
604              
605 5         10 $xs .= "}\n";
606              
607 5         8 return $xs;
608             }
609              
610             # Generate the .pm file
611             sub _generate_module_pm_code {
612 5     5   9 my ($self, $package) = @_;
613              
614 5         13 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__