#!./miniperl =head1 NAME xsubpp - compiler to convert Perl XS code into C code =head1 SYNOPSIS B [B<-v>] [B<-C++>] [B<-except>] [B<-s pattern>] [B<-prototypes>] [B<-noversioncheck>] [B<-typemap typemap>]... file.xs =head1 DESCRIPTION I will compile XS code into C code by embedding the constructs necessary to let C functions manipulate Perl values and creates the glue necessary to let Perl access those functions. The compiler uses typemaps to determine how to map C function parameters and variables to Perl values. The compiler will search for typemap files called I. It will use the following search path to find default typemaps, with the rightmost typemap taking precedence. ../../../typemap:../../typemap:../typemap:typemap =head1 OPTIONS =over 5 =item B<-C++> Adds ``extern "C"'' to the C code. =item B<-except> Adds exception handling stubs to the C code. =item B<-typemap typemap> Indicates that a user-supplied typemap should take precedence over the default typemaps. This option may be used multiple times, with the last typemap having the highest precedence. =item B<-v> Prints the I version number to standard output, then exits. =item B<-prototypes> By default I will not automatically generate prototype code for all xsubs. This flag will enable prototypes. =item B<-noversioncheck> Disables the run time test that determines if the object file (derived from the C<.xs> file) and the C<.pm> files have the same version number. =back =head1 ENVIRONMENT No environment variables are used. =head1 AUTHOR Larry Wall =head1 MODIFICATION HISTORY See the file F. =head1 SEE ALSO perl(1), perlxs(1), perlxstut(1), perlapi(1) =cut # Global Constants $XSUBPP_version = "1.933"; require 5.002; sub Q ; $FH = 'File0000' ; $usage = "Usage: xsubpp [-v] [-C++] [-except] [-prototypes] [-noversioncheck] [-s pattern] [-typemap typemap]... file.xs\n"; $proto_re = "[" . quotemeta('\$%&*@;') . "]" ; $except = ""; $WantPrototypes = -1 ; $WantVersionChk = 1 ; $ProtoUsed = 0 ; SWITCH: while (@ARGV and $ARGV[0] =~ /^-./) { $flag = shift @ARGV; $flag =~ s/^-// ; $spat = shift, next SWITCH if $flag eq 's'; $cplusplus = 1, next SWITCH if $flag eq 'C++'; $WantPrototypes = 0, next SWITCH if $flag eq 'noprototypes'; $WantPrototypes = 1, next SWITCH if $flag eq 'prototypes'; $WantVersionChk = 0, next SWITCH if $flag eq 'noversioncheck'; $WantVersionChk = 1, next SWITCH if $flag eq 'versioncheck'; $except = " TRY", next SWITCH if $flag eq 'except'; push(@tm,shift), next SWITCH if $flag eq 'typemap'; (print "xsubpp version $XSUBPP_version\n"), exit if $flag eq 'v'; die $usage; } if ($WantPrototypes == -1) { $WantPrototypes = 0} else { $ProtoUsed = 1 } @ARGV == 1 or die $usage; ($dir, $filename) = $ARGV[0] =~ m#(.*)/(.*)# or ($dir, $filename) = $ARGV[0] =~ m#(.*[>\]])(.*)# or ($dir, $filename) = ('.', $ARGV[0]); chdir($dir); # Check for VMS; Config.pm may not be installed yet, but this routine # is built into VMS perl if (defined(&VMS::Filespec::vmsify)) { $Is_VMS = 1; $pwd = $ENV{DEFAULT}; } else { $Is_VMS = 0; chomp($pwd = `pwd`); } ++ $IncludedFiles{$ARGV[0]} ; sub TrimWhitespace { $_[0] =~ s/^\s+|\s+$//go ; } sub TidyType { local ($_) = @_ ; # rationalise any '*' by joining them into bunches and removing whitespace s#\s*(\*+)\s*#$1#g; s#(\*+)# $1 #g ; # change multiple whitespace into a single space s/\s+/ /g ; # trim leading & trailing whitespace TrimWhitespace($_) ; $_ ; } $typemap = shift @ARGV; foreach $typemap (@tm) { die "Can't find $typemap in $pwd\n" unless -r $typemap; } unshift @tm, qw(../../../../lib/ExtUtils/typemap ../../../lib/ExtUtils/typemap ../../lib/ExtUtils/typemap ../../../typemap ../../typemap ../typemap typemap); foreach $typemap (@tm) { next unless -e $typemap ; # skip directories, binary files etc. warn("Warning: ignoring non-text typemap file '$typemap'\n"), next unless -T $typemap ; open(TYPEMAP, $typemap) or warn ("Warning: could not open typemap file '$typemap': $!\n"), next; $mode = 'Typemap'; $junk = "" ; $current = \$junk; while () { next if /^\s*#/; if (/^INPUT\s*$/) { $mode = 'Input'; $current = \$junk; next; } if (/^OUTPUT\s*$/) { $mode = 'Output'; $current = \$junk; next; } if (/^TYPEMAP\s*$/) { $mode = 'Typemap'; $current = \$junk; next; } if ($mode eq 'Typemap') { chomp; my $line = $_ ; TrimWhitespace($_) ; # skip blank lines and comment lines next if /^$/ or /^#/ ; my($type,$kind, $proto) = /^\s*(.*?\S)\s+(\S+)\s*($proto_re*)\s*$/ or warn("Warning: File '$typemap' Line $. '$line' TYPEMAP entry needs 2 or 3 columns\n"), next; $type = TidyType($type) ; $type_kind{$type} = $kind ; # prototype defaults to '$' $proto = '$' unless $proto ; warn("Warning: File '$typemap' Line $. '$line' Invalid prototype '$proto'\n") unless ValidProtoString($proto) ; $proto_letter{$type} = C_string($proto) ; } elsif (/^\s/) { $$current .= $_; } elsif ($mode eq 'Input') { s/\s+$//; $input_expr{$_} = ''; $current = \$input_expr{$_}; } else { s/\s+$//; $output_expr{$_} = ''; $current = \$output_expr{$_}; } } close(TYPEMAP); } foreach $key (keys %input_expr) { $input_expr{$key} =~ s/\n+$//; } $END = "!End!\n\n"; # "impossible" keyword (multiple newline) # Match an XS keyword $BLOCK_re= '\s*(' . join('|', qw( REQUIRE BOOT CASE PREINIT INPUT INIT CODE PPCODE OUTPUT CLEANUP ALIAS PROTOTYPES PROTOTYPE VERSIONCHECK INCLUDE )) . "|$END)\\s*:"; # Input: ($_, @line) == unparsed input. # Output: ($_, @line) == (rest of line, following lines). # Return: the matched keyword if found, otherwise 0 sub check_keyword { $_ = shift(@line) while !/\S/ && @line; s/^(\s*)($_[0])\s*:\s*(?:#.*)?/$1/s && $2; } sub print_section { $_ = shift(@line) while !/\S/ && @line; for (; defined($_) && !/^$BLOCK_re/o; $_ = shift(@line)) { print "$_\n"; } } sub process_keyword($) { my($pattern) = @_ ; my $kwd ; &{"${kwd}_handler"}() while $kwd = check_keyword($pattern) ; } sub CASE_handler { blurt ("Error: `CASE:' after unconditional `CASE:'") if $condnum && $cond eq ''; $cond = $_; TrimWhitespace($cond); print " ", ($condnum++ ? " else" : ""), ($cond ? " if ($cond)\n" : "\n"); $_ = '' ; } sub INPUT_handler { for (; !/^$BLOCK_re/o; $_ = shift(@line)) { last if /^\s*NOT_IMPLEMENTED_YET/; next unless /\S/; # skip blank lines TrimWhitespace($_) ; my $line = $_ ; # remove trailing semicolon if no initialisation s/\s*;$//g unless /=/ ; # check for optional initialisation code my $var_init = '' ; $var_init = $1 if s/\s*(=.*)$//s ; $var_init =~ s/"/\\"/g; s/\s+/ /g; my ($var_type, $var_addr, $var_name) = /^(.*?[^& ]) *(\&?) *\b(\w+)$/s or blurt("Error: invalid argument declaration '$line'"), next; # Check for duplicate definitions blurt ("Error: duplicate definition of argument '$var_name' ignored"), next if $arg_list{$var_name} ++ ; $thisdone |= $var_name eq "THIS"; $retvaldone |= $var_name eq "RETVAL"; $var_types{$var_name} = $var_type; print "\t" . &map_type($var_type); $var_num = $args_match{$var_name}; $proto_arg[$var_num] = ProtoString($var_type) if $var_num ; if ($var_addr) { $var_addr{$var_name} = 1; $func_args =~ s/\b($var_name)\b/&$1/; } if ($var_init =~ /^=\s*NO_INIT\s*;?\s*$/) { print "\t$var_name;\n"; } elsif ($var_init =~ /\S/) { &output_init($var_type, $var_num, "$var_name $var_init"); } elsif ($var_num) { # generate initialization code &generate_init($var_type, $var_num, $var_name); } else { print ";\n"; } } } sub OUTPUT_handler { for (; !/^$BLOCK_re/o; $_ = shift(@line)) { next unless /\S/; my ($outarg, $outcode) = /^\s*(\S+)\s*(.*?)\s*$/s ; blurt ("Error: duplicate OUTPUT argument '$outarg' ignored"), next if $outargs{$outarg} ++ ; if (!$gotRETVAL and $outarg eq 'RETVAL') { # deal with RETVAL last $RETVAL_code = $outcode ; $gotRETVAL = 1 ; next ; } blurt ("Error: OUTPUT $outarg not an argument"), next unless defined($args_match{$outarg}); blurt("Error: No input definition for OUTPUT argument '$outarg' - ignored"), next unless defined $var_types{$outarg} ; if ($outcode) { print "\t$outcode\n"; } else { $var_num = $args_match{$outarg}; &generate_output($var_types{$outarg}, $var_num, $outarg); } } } sub CLEANUP_handler() { print_section() } sub PREINIT_handler() { print_section() } sub INIT_handler() { print_section() } sub GetAliases { my ($line) = @_ ; my ($orig) = $line ; my ($alias) ; my ($value) ; # Parse alias definitions # format is # alias = value alias = value ... while ($line =~ s/^\s*([\w:]+)\s*=\s*(\w+)\s*//) { $alias = $1 ; $orig_alias = $alias ; $value = $2 ; # check for optional package definition in the alias $alias = $Packprefix . $alias if $alias !~ /::/ ; # check for duplicate alias name & duplicate value Warn("Warning: Ignoring duplicate alias '$orig_alias'") if defined $XsubAliases{$pname}{$alias} ; Warn("Warning: Aliases '$orig_alias' and '$XsubAliasValues{$pname}{$value}' have identical values") if $XsubAliasValues{$pname}{$value} ; $XsubAliases{$pname}{$alias} = $value ; $XsubAliasValues{$pname}{$value} = $orig_alias ; } blurt("Error: Cannot parse ALIAS definitions from '$orig'") if $line ; } sub ALIAS_handler () { for (; !/^$BLOCK_re/o; $_ = shift(@line)) { next unless /\S/; TrimWhitespace($_) ; GetAliases($_) if $_ ; } } sub REQUIRE_handler () { # the rest of the current line should contain a version number my ($Ver) = $_ ; TrimWhitespace($Ver) ; death ("Error: REQUIRE expects a version number") unless $Ver ; # check that the version number is of the form n.n death ("Error: REQUIRE: expected a number, got '$Ver'") unless $Ver =~ /^\d+(\.\d*)?/ ; death ("Error: xsubpp $Ver (or better) required--this is only $XSUBPP_version.") unless $XSUBPP_version >= $Ver ; } sub VERSIONCHECK_handler () { # the rest of the current line should contain either ENABLE or # DISABLE TrimWhitespace($_) ; # check for ENABLE/DISABLE death ("Error: VERSIONCHECK: ENABLE/DISABLE") unless /^(ENABLE|DISABLE)/i ; $WantVersionChk = 1 if $1 eq 'ENABLE' ; $WantVersionChk = 0 if $1 eq 'DISABLE' ; } sub PROTOTYPE_handler () { my $specified ; death("Error: Only 1 PROTOTYPE definition allowed per xsub") if $proto_in_this_xsub ++ ; for (; !/^$BLOCK_re/o; $_ = shift(@line)) { next unless /\S/; $specified = 1 ; TrimWhitespace($_) ; if ($_ eq 'DISABLE') { $ProtoThisXSUB = 0 } elsif ($_ eq 'ENABLE') { $ProtoThisXSUB = 1 } else { # remove any whitespace s/\s+//g ; death("Error: Invalid prototype '$_'") unless ValidProtoString($_) ; $ProtoThisXSUB = C_string($_) ; } } # If no prototype specified, then assume empty prototype "" $ProtoThisXSUB = 2 unless $specified ; $ProtoUsed = 1 ; } sub PROTOTYPES_handler () { # the rest of the current line should contain either ENABLE or # DISABLE TrimWhitespace($_) ; # check for ENABLE/DISABLE death ("Error: PROTOTYPES: ENABLE/DISABLE") unless /^(ENABLE|DISABLE)/i ; $WantPrototypes = 1 if $1 eq 'ENABLE' ; $WantPrototypes = 0 if $1 eq 'DISABLE' ; $ProtoUsed = 1 ; } sub INCLUDE_handler () { # the rest of the current line should contain a valid filename TrimWhitespace($_) ; death("INCLUDE: filename missing") unless $_ ; death("INCLUDE: output pipe is illegal") if /^\s*\|/ ; # simple minded recursion detector death("INCLUDE loop detected") if $IncludedFiles{$_} ; ++ $IncludedFiles{$_} unless /\|\s*$/ ; # Save the current file context. push(@FileStack, { LastLine => $lastline, LastLineNo => $lastline_no, Line => \@line, LineNo => \@line_no, Filename => $filename, Handle => $FH, }) ; ++ $FH ; # open the new file open ($FH, "$_") or death("Cannot open '$_': $!") ; print Q<<"EOF" ; # #/* INCLUDE: Including '$_' from '$filename' */ # EOF $filename = $_ ; # Prime the pump by reading the first # non-blank line # skip leading blank lines while (<$FH>) { last unless /^\s*$/ ; } $lastline = $_ ; $lastline_no = $. ; } sub PopFile() { return 0 unless @FileStack ; my $data = pop @FileStack ; my $ThisFile = $filename ; my $isPipe = ($filename =~ /\|\s*$/) ; -- $IncludedFiles{$filename} unless $isPipe ; close $FH ; $FH = $data->{Handle} ; $filename = $data->{Filename} ; $lastline = $data->{LastLine} ; $lastline_no = $data->{LastLineNo} ; @line = @{ $data->{Line} } ; @line_no = @{ $data->{LineNo} } ; if ($isPipe and $? ) { -- $lastline_no ; print STDERR "Error reading from pipe '$ThisFile': $! in $filename, line $lastline_no\n" ; exit 1 ; } print Q<<"EOF" ; # #/* INCLUDE: Returning to '$filename' from '$ThisFile' */ # EOF return 1 ; } sub ValidProtoString ($) { my($string) = @_ ; if ( $string =~ /^$proto_re+$/ ) { return $string ; } return 0 ; } sub C_string ($) { my($string) = @_ ; $string =~ s[\\][\\\\]g ; $string ; } sub ProtoString ($) { my ($type) = @_ ; $proto_letter{$type} or '$' ; } sub check_cpp { my @cpp = grep(/^\#\s*(?:if|e\w+)/, @line); if (@cpp) { my ($cpp, $cpplevel); for $cpp (@cpp) { if ($cpp =~ /^\#\s*if/) { $cpplevel++; } elsif (!$cpplevel) { Warn("Warning: #else/elif/endif without #if in this function"); return; } elsif ($cpp =~ /^\#\s*endif/) { $cpplevel--; } } Warn("Warning: #if without #endif in this function") if $cpplevel; } } sub Q { my($text) = @_; $text =~ s/^#//gm; $text =~ s/\[\[/{/g; $text =~ s/\]\]/}/g; $text; } open($FH, $filename) or die "cannot open $filename: $!\n"; # Identify the version of xsubpp used print <) { last if ($Module, $Package, $Prefix) = /^MODULE\s*=\s*([\w:]+)(?:\s+PACKAGE\s*=\s*([\w:]+))?(?:\s+PREFIX\s*=\s*(\S+))?\s*$/; print $_; } &Exit unless defined $_; $lastline = $_; $lastline_no = $.; # Read next xsub into @line from ($lastline, <$FH>). sub fetch_para { # parse paragraph @line = (); @line_no = () ; if (! defined $lastline) { return 1 if PopFile() ; return 0 ; } if ($lastline =~ /^MODULE\s*=\s*([\w:]+)(?:\s+PACKAGE\s*=\s*([\w:]+))?(?:\s+PREFIX\s*=\s*(\S+))?\s*$/) { $Module = $1; $Package = defined($2) ? $2 : ''; # keep -w happy $Prefix = defined($3) ? $3 : ''; # keep -w happy ($Module_cname = $Module) =~ s/\W/_/g; ($Packid = $Package) =~ tr/:/_/; $Packprefix = $Package; $Packprefix .= "::" if $Packprefix ne ""; $lastline = ""; } for(;;) { if ($lastline !~ /^\s*#/ || $lastline =~ /^#[ \t]*(?:(?:if|ifn?def|else|elif|endif|define|undef|pragma)\b|include\s*["<].*[>"])/) { last if $lastline =~ /^\S/ && @line && $line[-1] eq ""; push(@line, $lastline); push(@line_no, $lastline_no) ; } # Read next line and continuation lines last unless defined($lastline = <$FH>); $lastline_no = $.; my $tmp_line; $lastline .= $tmp_line while ($lastline =~ /\\$/ && defined($tmp_line = <$FH>)); chomp $lastline; $lastline =~ s/^\s+$//; } pop(@line), pop(@line_no) while @line && $line[-1] eq ""; 1; } PARAGRAPH: while (fetch_para()) { # Print initial preprocessor statements and blank lines print shift(@line), "\n" while @line && $line[0] !~ /^[^\#]/; next PARAGRAPH unless @line; death ("Code is not inside a function") if $line[0] =~ /^\s/; # initialize info arrays undef(%args_match); undef(%var_types); undef(%var_addr); undef(%defaults); undef($class); undef($static); undef($elipsis); undef($wantRETVAL) ; undef(%arg_list) ; undef(@proto_arg) ; undef($proto_in_this_xsub) ; $ProtoThisXSUB = $WantPrototypes ; $_ = shift(@line); while ($kwd = check_keyword("REQUIRE|PROTOTYPES|VERSIONCHECK|INCLUDE")) { &{"${kwd}_handler"}() ; next PARAGRAPH unless @line ; $_ = shift(@line); } if (check_keyword("BOOT")) { &check_cpp; push (@BootCode, $_, @line, "") ; next PARAGRAPH ; } # extract return type, function name and arguments my($ret_type) = TidyType($_); # a function definition needs at least 2 lines blurt ("Error: Function definition too short '$ret_type'"), next PARAGRAPH unless @line ; $static = 1 if $ret_type =~ s/^static\s+//; $func_header = shift(@line); blurt ("Error: Cannot parse function definition from '$func_header'"), next PARAGRAPH unless $func_header =~ /^(?:([\w:]*)::)?(\w+)\s*\(\s*(.*?)\s*\)\s*$/s; ($class, $func_name, $orig_args) = ($1, $2, $3) ; ($pname = $func_name) =~ s/^($Prefix)?/$Packprefix/; # Check for duplicate function definition if (defined $Func_name{"${Packid}_$func_name"} ) { Warn("Warning: duplicate function definition '$func_name' detected") } else { push(@Func_name, "${Packid}_$func_name"); push(@Func_pname, $pname); } $Func_name{"${Packid}_$func_name"} ++ ; @args = split(/\s*,\s*/, $orig_args); if (defined($class)) { my $arg0 = ((defined($static) or $func_name =~ /^new/) ? "CLASS" : "THIS"); unshift(@args, $arg0); ($orig_args = "$arg0, $orig_args") =~ s/^$arg0, $/$arg0/; } $orig_args =~ s/"/\\"/g; $min_args = $num_args = @args; foreach $i (0..$num_args-1) { if ($args[$i] =~ s/\.\.\.//) { $elipsis = 1; $min_args--; if ($args[$i] eq '' && $i == $num_args - 1) { pop(@args); last; } } if ($args[$i] =~ /^([^=]*[^\s=])\s*=\s*(.*)/s) { $min_args--; $args[$i] = $1; $defaults{$args[$i]} = $2; $defaults{$args[$i]} =~ s/"/\\"/g; } $proto_arg[$i+1] = '$' ; } if (defined($class)) { $func_args = join(", ", @args[1..$#args]); } else { $func_args = join(", ", @args); } @args_match{@args} = 1..@args; $PPCODE = grep(/^\s*PPCODE\s*:/, @line); $ALIAS = grep(/^\s*ALIAS\s*:/, @line); # print function header print Q<<"EOF"; #XS(XS_${Packid}_$func_name) #[[ # dXSARGS; EOF print Q<<"EOF" if $ALIAS ; # dXSI32; EOF if ($elipsis) { $cond = ($min_args ? qq(items < $min_args) : 0); } elsif ($min_args == $num_args) { $cond = qq(items != $min_args); } else { $cond = qq(items < $min_args || items > $num_args); } print Q<<"EOF" if $except; # char errbuf[1024]; # *errbuf = '\0'; EOF if ($ALIAS) { print Q<<"EOF" if $cond } # if ($cond) # croak("Usage: %s($orig_args)", GvNAME(CvGV(cv))); EOF else { print Q<<"EOF" if $cond } # if ($cond) # croak("Usage: $pname($orig_args)"); EOF print Q<<"EOF" if $PPCODE; # SP -= items; EOF # Now do a block of some sort. $condnum = 0; $cond = ''; # last CASE: condidional push(@line, "$END:"); push(@line_no, $line_no[-1]); $_ = ''; &check_cpp; while (@line) { &CASE_handler if check_keyword("CASE"); print Q<<"EOF"; # $except [[ EOF # do initialization of input variables $thisdone = 0; $retvaldone = 0; $deferred = ""; %arg_list = () ; $gotRETVAL = 0; INPUT_handler() ; process_keyword("INPUT|PREINIT|ALIAS|PROTOTYPE") ; if (!$thisdone && defined($class)) { if (defined($static) or $func_name =~ /^new/) { print "\tchar *"; $var_types{"CLASS"} = "char *"; &generate_init("char *", 1, "CLASS"); } else { print "\t$class *"; $var_types{"THIS"} = "$class *"; &generate_init("$class *", 1, "THIS"); } } # do code if (/^\s*NOT_IMPLEMENTED_YET/) { print "\n\tcroak(\"$pname: not implemented yet\");\n"; $_ = '' ; } else { if ($ret_type ne "void") { print "\t" . &map_type($ret_type) . "\tRETVAL;\n" if !$retvaldone; $args_match{"RETVAL"} = 0; $var_types{"RETVAL"} = $ret_type; } print $deferred; process_keyword("INIT|ALIAS|PROTOTYPE") ; if (check_keyword("PPCODE")) { print_section(); death ("PPCODE must be last thing") if @line; print "\tPUTBACK;\n\treturn;\n"; } elsif (check_keyword("CODE")) { print_section() ; } elsif (defined($class) and $func_name eq "DESTROY") { print "\n\t"; print "delete THIS;\n"; } else { print "\n\t"; if ($ret_type ne "void") { print "RETVAL = "; $wantRETVAL = 1; } if (defined($static)) { if ($func_name =~ /^new/) { $func_name = "$class"; } else { print "${class}::"; } } elsif (defined($class)) { if ($func_name =~ /^new/) { $func_name .= " $class"; } else { print "THIS->"; } } $func_name =~ s/^($spat)// if defined($spat); print "$func_name($func_args);\n"; } } # do output variables $gotRETVAL = 0; undef $RETVAL_code ; undef %outargs ; process_keyword("OUTPUT|ALIAS|PROTOTYPE"); # all OUTPUT done, so now push the return value on the stack if ($gotRETVAL && $RETVAL_code) { print "\t$RETVAL_code\n"; } elsif ($gotRETVAL || $wantRETVAL) { &generate_output($ret_type, 0, 'RETVAL'); } # do cleanup process_keyword("CLEANUP|ALIAS|PROTOTYPE") ; # print function trailer print Q<