ajwood
ajwood

Reputation: 19027

Accessing subs from a require'd perl script

I'm going to import some perl code with the require statement. The code I'd like to import is in mylibA.pl:

#!/usr/bin/perl
package FOO::BAR;

sub routine {
    print "A message!\n";
}

and mylibB.pl:

#!/usr/bin/perl
package FOO::BAZ;

sub routine {
    print "Another message!\n";
}

Then I'm going to use it like this:

#!/usr/bin/perl
foreach my $lib (qw/ mylibA.pl mylibB.pl /){
     require $lib;
     print "Make a call to ${lib}'s &routine!\n";
}

Is there a way for my script to figure out the namespace that was pulled in with the require statement?

Upvotes: 2

Views: 346

Answers (4)

Theo Ohnsorge
Theo Ohnsorge

Reputation: 87

As mentioned before it is not possible to look up the namespace of a 'required' package without extra I/O, guessing or assuming.

Like Rick said before, one have to intrude the namespace of the caller or better 'main'. I prefer to inject specific hooks within a BEGIN block of the 'required' package.

#VENDOR/App/SocketServer/Protocol/NTP.pm
package VENDOR::App::SocketServer::Protocol::NTP;

BEGIN {
  no warnings;
  *main::HANDLE_REQUEST = \&HANDLE_REQUEST;
}

sub HANDLE_REQUEST {
}

#VENDOR/App/SocketServer.pm
my $userPackage= $ARGV[0];
require $userPackage;
main::HANDLE_REQUEST();

Instead of *main:: you can get more specific with *main::HOOKS::HANDLE_REQUESTS i.e. This enables you to resolve all injected hooks easily within the caller by iterating over the HOOK's namespace portion.

foreach my $hooks( keys %main::HOOKS ) {

}

Upvotes: 0

Slaven Rezic
Slaven Rezic

Reputation: 4581

Regarding the mostly academical problem of finding the package(s) in a perl source file:

You can try the CPAN module Module::Extract::Namespaces to get all packages within a perl file. It is using PPI and is thus not 100% perfect, but most of the time good enough:

perl -MModule::Extract::Namespaces -e 'warn join ",", Module::Extract::Namespaces->from_file(shift)' /path/to/foo.pm

But PPI can be slow for large files.

You can try to compare the active packages before and after the require. This is also not perfect, because if your perl library file loads additional modules then you cannot tell which is the package of the prinicipal file and what's loaded later. To get the list of packages you can use for example Devel::Symdump. Here's a sample script:

use Devel::Symdump;

my %before = map { ($_,1) } Devel::Symdump->rnew->packages;
require "/path/to/foo.pm";
my %after  = map { ($_,1) } Devel::Symdump->rnew->packages;

delete $after{$_} for keys %before;
print join(",", keys %after), "\n";

You can also just parse the perl file for "package" declarations. Actually, that's what the PAUSE upload daemon is doing, so it's probably "good enough" for most cases. Look at the subroutine packages_per_pmfile in https://github.com/andk/pause/blob/master/lib/PAUSE/pmfile.pm

Upvotes: 2

amon
amon

Reputation: 57590

There are two problems here:

  1. How do I change the behaviour of a script when executed as a standalone and when used as a module?
  2. How do I discover the package name of a piece of code I just compiled?

The general answer to question 2 is: You don't, as any compilation unit may contain an arbitrary number of packages.

Anyway, here are three possible solutions:

  1. Name your modules so that you already know the name when you load it.
  2. Have each module register itself at a central rendezvous point.
  3. Like #1, but adds autodiscovery of your plugins.

The simplest solution is to put all of the API in an ordinary module, and put the standalone logic in a seperate script:

/the/location/
  Module/
    A.pm
    B.pm
  a-standalone.pl
  b-standalone.pl

Where each standalone basically looks like

use Module::A;
Module::A->run();

If another script wants to reuse that code, it does

use lib "/the/location";
use Module::A;
...

If the loading happens on runtime, then Module::Runtime helps here:

use Module::Runtime 'use_module';
use lib "/the/location";
my $mod_a = use_module('Module::A');
$mod_a->run();

It isn't strictly necessary to place the contents of a-standalone.pl and Module/A.pm into separate files, although that is clearer. If you want to conditionally run code in a module only if it is used as a script, you can utilize the unless(caller) trick.


Of course all of this is tricksing: Here we determine the file name from the module name, not the other way round – which as I already mentioned we cannot do.

What we can do is have each module register itself at a certain predefined location, e.g. by

Rendezvous::Point->register(__FILE__ => __PACKAGE__);

Of course the standalone version has to shield against the possibility that there is no Rendezvous::Point, therefore:

if (my $register = Rendezvous::Point->can("register")) {
  $register->(__FILE__ => __PACKAGE__);
}

Eh, this is silly and violates DRY. So let's create a Rendezvous::Point module that takes care of this:

In /the/location/Rendezvous/Point.pm:

package Rendezvous::Point;
use strict; use warnings;

my %modules_by_filename;

sub get {
  my ($class, $name) = @_;
  $modules_by_filename{$name};
}

sub register {
  my ($file, $package) = @_;
  $modules_by_filename{$file} = $package;
}

sub import {
  my ($class) = @_;
  $class->register(caller());
}

Now, use Rendezvous::Point; registers the calling package, and the module name can be retrived by the absolute path.

The script that wants to use the various modules now does:

use "/the/location";
use Rendezvous::Point ();  # avoid registering ourself

my $prefix = "/the/location";
for my $filename (map "$prefix/$_", qw(Module/A.pm Module/B.pm)) {
  require $filename;
  my $module  = Rendezvous::Point->get($filename)
             // die "$filename didn't register itself at the Rendezvous::Point";
  $module->run();
}

Then there are fully featured plugin systems like Module::Pluggable. This system works by looking at all paths were Perl modules may reside, and loads them if they have a certain prefix. A solution with that would look like:

/the/location/
  MyClass.pm
  MyClass/
    Plugin/
      A.pm
      B.pm
  a-standalone.pl
  b-standalone.pl

Everything is just like with the first solution: Standalone scripts look like

use lib "/the/location/";
use MyClass::Plugin::A;
MyClass::Plugin::A->run;

But MyClass.pm looks like:

package MyClass;
use Module::Pluggable require => 1;  # we can now query plugins like MyClass->plugins

sub run {
  # Woo, magic! Works with inner packages as well!
  for my $plugin (MyClass->plugins) {
    $plugin->run();
  }
}

Of course, this still requires a specific naming scheme, but it auto-discovers possible plugins.

Upvotes: 1

Rick Sarvas
Rick Sarvas

Reputation: 769

Wow. I have to say this is the one of the most interesting Perl questions I've seen in a while. On the surface this seems like a very simple request - get an included module's namespace, but there really is no way to do this. You can get it while in the package, but not from outside the package. I tried using EXPORT to send the local package name back to the caller script but that ended up going nowhere given the difference in how "use" and "require" work. A more module type of approach probably would have worked with a "use" statement, but the requirement that the required script be able to run by themselves prevented that approach. The only thing left to do was to directly pollute the caller's namespace and hope for the best (assume that the caller had no package namespace) - something that modules are designed to prevent.

BTW - I can't believe this actually works - in strict mode, no less.

caller.pl

#!/usr/bin/perl
use strict;

#package SomePackageName; #if you enable this then this will fail to work

our $ExportedPackageName;

print "Current package=".__PACKAGE__."\n";

foreach my $lib (qw/ mylibA.pl mylibB.pl /){
    require $lib;
    print "Make a call to ${lib}'s &routine!\n";
    print "Package name exported=".$ExportedPackageName."\n";
    $ExportedPackageName->routine;
} #end foreach

print "Normal Exit";
exit;

__END__

mylibA.pl

#!/usr/bin/perl
package FOO::BAR;
use strict;

#better hope the caller does not have a package namespace
$main::ExportedPackageName=__PACKAGE__;

sub routine {
    print "A message from ".__PACKAGE__."!\n";
}

1;

mylibB.pl

#!/usr/bin/perl
package FOO::BAZ;
use strict;

#better hope the caller does not have a package namespace
$main::ExportedPackageName=__PACKAGE__;

sub routine {
    print "Another message, this time from ".__PACKAGE__."!\n";
}

1;

Result:

c:\Perl>
c:\Perl>perl caller.pl
Current package=main
Make a call to mylibA.pl's &routine!
Package name exported=FOO::BAR
A message from FOO::BAR!
Make a call to mylibB.pl's &routine!
Package name exported=FOO::BAZ
Another message, this time from FOO::BAZ!
Normal Exit

Upvotes: 5

Related Questions