Sign In/My Account | View Cart  
advertisement


Listen Print

Proxy Objects
by Matt Sergeant | Pages: 1, 2, 3

Doing the Right Thing

What we can now do (in true Blue Peter fashion) is create a module that has totally transparent weak-references support on Perl 5.6, while still allowing garbage collection on lower Perl versions. This is a little more complex than the scenario above, but here's how it works. First, we create a base class that our complex data-structure classes can subclass:

# base class for all circular reffing classes 
# rename this before use
package BaseClass;
use strict;

In that class we do some compile-time checking for Scalar::Util:

BEGIN {
  eval "use Scalar::Util qw(weaken);";
  if ($@) {
    $BaseClass::WeakRefs = 0;
  }
  else {
    $BaseClass::WeakRefs = 1;
  }
}

Next, we create a clever constructor. This constructor turns our class name (e.g. "XML::MyDOM::Node") into an implementation class name ("XML::MyDOM::NodeImpl"), and constructs an object of that type. Then if weak refs are available, then it returns that object, otherwise it constructs a proxy object that proxies to that object, and returns the proxy object instead:

sub new {
  my $class = shift;
  no strict 'refs';
  my $impl = $class . "Impl";
  my $this = $impl->new(@_);
  if ($BaseClass::WeakRefs) {
    return $this;
  }
  my $self = \$this;
  return bless $self, $class;
}

Finally, we have a more advanced version of our proxy object's AUTOLOAD method. This does some error checking to ensure that unknown methods are detected correctly, and that methods on this class are executed in the correct way. But otherwise it's doing exactly the same job as our original:

sub AUTOLOAD {
  my $method = $AUTOLOAD;
  $method =~ s/.*:://; # strip the package name
  no strict 'refs';
  *{$AUTOLOAD} = sub {
    my $self = shift;
    my $olderror = $@; # store previous exceptions
    my $obj = eval { $$self };
    if ($@) {
      if ($@ =~ /Not a SCALAR reference/) {
        croak("No such method $method in " . ref($self));
      }
      croak $@;
    }
    if ($obj) {
      # make sure $@ propogates if this method call was the result
      # of losing scope because of a die().
      if ($method =~ /^(DESTROY|del_parent_link)$/) {
        $obj->$method(@_);
        $@ = $olderror if $olderror;
        return;
      }
      return $obj->$method(@_);
    }
  };
  goto &$AUTOLOAD;
}

package BaseClassImpl; # Implementation class

sub new { die "Virtual base class" }

# All base class methods go below here

1;

Now, all classes derived from that need to look like this:

package CircRefClass;
@ISA = ('BaseClass');

package CircRefClassImpl;
@ISA = ('BaseClassImpl', 'CircRefClass');

sub new {
  my $class = shift;
  # ordinary constructor here
  my $self = bless {}, $class;
  # ... yada yada yada
  return $self;
}

sub DESTROY {
  my $self = shift;
  # Break child's link to me here
  $self->child->del_parent_link();
}

sub set_parent {
  my $self = shift;
  if ($BaseClass::WeakRefs) {
    Scalar::Util::weaken($self->{parent} = shift);
  }
  else {
    $self->{parent} = shift;
  }
}

sub del_parent_link {
  my $self = shift;
  $self->{parent} = undef;
}

# All class methods here

1;

And that's about all you need to change. The rest of your methods can remain the same, as long as they are moved to the PackageImpl class, rather than the Package class. And this will auto-detect the presence of Scalar::Util's weakref() method and use it accordingly if it's available.

The complicated work is in the AUTOLOAD class in the BaseClass package. The reason it looks so complex is because it implements a method cache for autoloaded methods, ensuring that this implementation won't slow down your program.

Also note that you can rename the del_parent_link and set_parent methods if those names aren't appropriate for your application. They just happen to work for a tree structure.

Conclusions

Circular references are a useful tool, and sometimes a neccessary evil. By using methods available to Perl 5.6 and up, combined with a custom proxying technique, we can create classes that implement circular references while still managing to be garbage collected on all versions of Perl. This is a powerful design to add to your Perl programmer's toolbox.