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.
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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With