Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I conditionally compile C code snippets to my Perl module?

Tags:

perl

swig

xs

I have a module that will target several different operating systems and configurations. Sometimes, some C code can make this module's task a little easier, so I have some C functions that I would like to bind the code. I don't have to bind the C functions -- I can't guarantee that the end-user even has a C compiler, for instance, and it's generally not a problem to failover gracefully to a pure Perl way of accomplishing the same thing -- but it would be nice if I could call the C functions from the Perl script.

Still with me? Here's another tricky part. Just about all of the C code is system specific -- a function written for Windows won't compile on Linux and vice-versa, and the function that does a similar thing on Solaris will look totally different.

#include <some/Windows/headerfile.h>
int foo_for_Windows_c(int a,double b)
{
  do_windows_stuff();
  return 42;
}

#include <path/to/linux/headerfile.h>
int foo_for_linux_c(int a,double b)
{
  do_linux_stuff(7);
  return 42;
}

Furthermore, even for native code that targets the same system, it's possible that only some of them can be compiled on any particular configuration.

#include <some/headerfile/that/might/not/even/exist.h>
int bar_for_solaris_c(int a,double b)
{
  call_solaris_library_that_might_be_installed_here(11);
  return 19;
}

But ideally we could still use the C functions that would compile with that configuration. So my questions are:

  • how can I compile C functions conditionally (compile only the code that is appropriate for the current value of $^O)?

  • how can I compile C functions individually (some functions might not compile, but we still want to use the ones that can)?

  • can I do this at build-time (while the end-user is installing the module) or at run-time (with Inline::C, for example)? Which way is better?

  • how would I tell which functions were successfully compiled and are available for use from Perl?

All thoughts appreciated!


Update: Thanks to all who responded. So here's what I did:

I considered a scheme of run-time binding with Inline::C inside of eval statements, but ultimately settled on subclassing Module::Build and customizing the ACTION_build method:

my $builderclass = Module::Build->subclass(
 class => 'My::Custom::Builder',
 code => <<'__CUSTOM_BUILD_CODE__,',
 sub ACTION_build {
   use File::Copy;
   my $self = shift;

   ### STEP 1: Compile all .xs files, remove the ones that fail ###    
   if (! -f "./lib/xs/step1") {
     unlink <lib/xs/*>;
     foreach my $contrib_file (glob("contrib/*.xs")) {
       File::Copy::copy($contrib_file, "lib/xs/");
     }
     open my $failed_units_fh, '>', 'lib/xs/step1';
     local $@ = undef;
     do {
       my $r = eval { $self->ACTION_code() };
       if ($@ =~ /error building (\S+\.o) from/i
          || $@ =~ /error building dll file from '(\S+\.c)'/i) {
        my $bad_file = $1;
        $bad_file =~ s!\\!/!g;
        my $bad_xs = $bad_file;
        $bad_xs =~ s/.[oc]$/.xs/;

        print STDERR "ERROR COMPILING UNIT $bad_xs ... removing\n\n";
        unlink $bad_xs;
        print $failed_units_fh "$bad_xs\n";
      } elsif ($@) {
         print STDERR "Compile error not handled in $^O:   $@\n";
       }
     } while $@;
     print "Removed all uncompilable units from lib/xs/\n";
     close $failed_units_fh;
   }

   ### STEP 2: Combine valid .xs files into a single .xs file ###
   if (! -f "./lib/xs/step2") {
     open my $valid_units_fh, '>', "lib/xs/step2";
     my (@INCLUDE,%INCLUDE,$MODULE,@PREMOD,@POSTMOD);
     foreach my $xs (glob("lib/xs/*.xs")) {
       open my $xs_fh, '<', $xs;
       while (<$xs_fh>) {
         if (m/#include/) {
           next if $INCLUDE{$_}++;
           push @INCLUDE, $_;
         } elsif (/^MODULE/) {
           $MODULE = $_;
           push @POSTMOD, <$xs_fh>;
         } else {
           push @PREMOD, $_;
         }
       }
       close $xs_fh;
       print $valid_units_fh "$xs\n";
     }
     close $valid_units_fh;
     unlink <lib/xs/*>, <blib/arch/auto/xs/*/*>;
     unlink 'lib/My/Module.xs';
     open my $xs_fh, '>', 'lib/My/Module.xs' or croak $!;
     print $xs_fh @INCLUDE, @PREMOD, $MODULE, @POSTMOD;
     close $xs_fh;
     print "Assembled remaining XS files into lib/My/Module.xs\n";
   }

   ### STEP 3: Clean all .xs stuff and compile My/Module.xs ###
   unlink <lib/xs/*>;
   $self->ACTION_code();
   return $self->SUPER::ACTION_build(@_);
  }
}

The check on $@ is probably pretty fragile. It works on the systems I've tried (all using gcc), but it probably won't work as it's written everywhere.

like image 375
mob Avatar asked Apr 07 '10 06:04

mob


2 Answers

Ideally, use Module::Build. At configure time (perl Build.PL), detect the platform and header location (but also let the user specify command-line options to override detection), set the relevant extra_compiler_flags and extra_linker_flags in the constructor and then copy the relevant files from e.g. contrib to lib (where they will be automatically picked up by ExtUtils::CBuilder). Now the distribution is customised to the platform - the next steps (./Build ; …) will work as normal.

like image 163
daxim Avatar answered Nov 15 '22 03:11

daxim


In one of my modules I have the following piece of code:

my $C_support = Module::Build::ConfigData->feature("C_support")

my $builder = Module::Build->new(
    ...
    config_data => {
        C_support => $C_support
    }
);
$builder->xs_files({}) if not $C_support;

Then in the code I detect it by loading Module_name::ConfigData and calling the config method.

if (Module_name::ConfigData->config("C_support")) {
    XSLoader::load(__PACKAGE__, $VERSION);
}
if (not defined &somefunction) {
    #define it
}

For details, look at my Build.PL and Module.pm

like image 33
Leon Timmermans Avatar answered Nov 15 '22 04:11

Leon Timmermans