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

3

5

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.

Sev answered 7/4, 2010 at 6:13 Comment(0)
Y
5

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.

Yore answered 7/4, 2010 at 8:41 Comment(1)
Intriguing ... I'm going to bite the Module::Build bullet and try this out. Should the files in contrib/ be C or XS? XS would be another bullet to bite.Sev
H
2

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

Hedden answered 7/4, 2010 at 8:32 Comment(0)
C
1

I've used techniques like this:

sub slow_function {
    # slow fallback perl code if possible
}
BEGIN {
    eval {
        require Inline;

        if ($condition) {
            Inline->import(C => q {
                int slow_function (...) {
                    // c function to conditionally compile
                }
            })
        } else {
            Inline->import(C => q {
                int slow_function (...) {
                    // c function with something different
                }
            })
        }   
        1;
    } or print STDERR "Inline::C error: $@ perl fallback used instead\n";
}
Cirilla answered 7/4, 2010 at 14:33 Comment(2)
This is similar to the approach I had in mind, but compiling at run-time (taking the hit the first time slow_function was used rather than the first time the script was called).Sev
That certainly works too. In my case I knew I was going to need the function ahead of time. During development, one of the nice things about using Inline::C to do this is that simply changing the C code will trigger a recompile the next time its run, so there is no need to remember to rerun the Build script.Cirilla

© 2022 - 2024 — McMap. All rights reserved.