2002-11-16

Import Subversion in Perl

Perl has a way to subvert its import mechanism, which would be useful for making it retreive modules from databases or other non-standard sources. The examples that follow don't do anything complicated, but the technique could be adapted to use the DBI to talk to a database, or on Windows (say with ActiveState Perl), you could reach oout with COM and do other interesting things.

The subversion mechanism in Perl is very easy. There is an array called @INC that normally contains one element for each entry in the path search sequence. However, you have the ability to put references to subroutines or even objects in this array, which allows you to intercept the request for a module.

I wrote a simple one to test things. The class is called PX. All it does is map Foo.pm to Foo.px, and look for the file in the current directory.

PX.pm

#!/usr/bin/perl -w
use FileHandle;
package PX;

sub import { unshift @INC, PX->new; }
sub new { return bless { }, shift; }

sub PX::INC
{
  my ($self, $name) = @_;
  $name =~ s/\.pm$/.px/;
  return FileHandle->new($name);
}

1;

The import() routine does the work of adding an instance of this class to the front end of @INC whenever another module does a 'use PX;'. The implementation of new() creates a trival instance. The PX::INC routine is the magic. It uses a regexp to do the translation and then returns a filehandle. The filehandle will be undefined if the file is not found, which will cause the importing mechanism to go on to the next entry in @INC, just as you would want.

So, here's a trivial FOO.px file that we want to include:

FOO.px

#!/usr/bin/perl -w
package FOO;

BEGIN {
  use Exporter;
  use vars qw(@ISA @EXPORT);
  @ISA = qw(Exporter);
  @EXPORT = qw(&foo);
};

sub foo { print "FOO::foo()\n"; }

1;

This module uses the standard Perl idiom for exporting a subroutine to the use-ing package.

Here is a simple test script, that invokes the PX module (which hooks into the 'use' mechanism), then imports a module, which is found via the hook:

tryme.pl

#!/usr/bin/perl -w
use PX;
use FOO;

FOO::foo();
foo();

exit 0;

The first call to FOO::foo() shows calling the imported subroutine via its explicit package name. The second call to foo() shows that indeed the FOO module's use of Exporter has successfully exported the foo() subroutine into the main package so it can be called without qualification.

The good news in all this is that any code that can take a file name like Foo/Bar/Splee.pm and either return undef if the module is not found or return an instance of the FileHandle class if it is, can be used to subvert the built-in Perl import mechanism, without requiring people to be aware of the situation.

In fact, you can be quite devious if you want to. If you set the PERL5OPT environment variable to "-MPX" (in the example just shown), then the PX module will be transparently use-d for all scripts that are run on the system, installing the @INC subversion hook. Scripts don't even know it, so the tryme.pl example from above will have the exact same behavior if the PERL5OPT variable is set as described and the script is modified thusly:

tryme.pl

#!/usr/bin/perl -w
use FOO;

FOO::foo();
foo();

exit 0;

So, even with no mention of the PX module in the script, the FOO.px file is loaded when Perl tries to 'use' FOO.pm.

No comments: