Sign In/My Account | View Cart  
advertisement


Listen Print

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

Fixing Circular References - with Perl 5.6+

Perl 5.6.0 introduced a new feature to "fix" all of the problems with circular references. This feature is called weakrefs. The basic idea is to flag a reference as weakened, so as to not include it in the reference counting. In order to use weakrefs, you need to install the Scalar::Util module (which is included with Perl 5.8.0). It is simple to use. Let's see what happens with our example above:

package CircObj;
  use Scalar::Util qw(weaken);
  sub new { bless {}, shift }
  sub parent { my $self = shift; @_ ? weaken($self->{parent} = 
       shift) : $self->{parent} }
  sub child { my $self = shift; @_ ? $self->{child} = 
       shift : $self->{child} 
  }
  sub DESTROY { warn("CircObj::DESTROY\n") }
  
  for (1..1) {
    my $parent = CircObj->new;
     my $child = CircObj->new;
     $parent->child($child);
     $child->parent($parent);
     warn("Leaving scope\n");
  }
warn("Scope left\n");

It's important to note that we only need to weaken our parent reference -- since one reference is OK. But this time, it outputs the expected:

Leaving scope
CircObj::DESTROY
CircObj::DESTROY
Scope left

The weaken method is well-proven, and is a stable way to ensure that circular references don't mess up the operation of Perl's garbage collector. You should definitely use them if you can -- for example in your in-house code.

However, that still leaves CPAN module authors and those stuck with Perl 5.005 with a problem: what do we do with older versions of Perl?

Fixing Circular References - with Perl 5.00503 and Lower

Anyone who works for a large company, or who puts out open-source Perl modules, will know that there are still an awful lot of people using Perl 5.00503 -- upgrading takes time, and many people are running older OS's that only come with Perl 5.00503.

So we have to get inventive -- we need some proxy objects.

Proxy objects are a way to access objects indirectly via another object. The term proxy object is from the Design Patterns book (http://hillside.net/patterns/DPBook/DPBook.html). A proxy object works by being an intermediary between an object and its methods -- passing all communication on to the real object, perhaps after doing something based on the method called. We can implement a basic proxy object in Perl using AUTOLOAD:

package ProxyObject;

sub new {
  my $class = shift;
  my $real_obj = shift;
  my $self = { real_obj => $real_obj };
  return bless $self, $class;
}

sub AUTOLOAD {
  my $self = shift;
  my $method = $AUTOLOAD;
  $method =~ s/.*:://;
  warn("Proxying: $method\n");
  $self->{real_obj}->$method(@_);
}

1;

And we can use that as follows:

use ProxyObject;
use Time::localtime;
my $time = localtime();  
### Create a localtime object
my $proxy = ProxyObject->new($time);  
### Create a proxy to that object
print $time->hour, " is the same as ", $proxy->hour, "\n";

Which gives us the output:

Proxying: hour
14 is the same as 14
Proxying: DESTROY

So, this all looks very interesting, but you're probably wondering how that helps with circular references. Well, let's look at a circular-reference example that uses ProxyObject:

package CircObj;
sub new { bless {}, shift }
sub parent { my $self = shift; @_ ? $self->{parent} = 
     shift : $self->{parent} }
sub child { my $self = shift; @_ ? $self->{child} = 
     shift : $self->{child} }
sub DESTROY { warn("CircObj::DESTROY\n") }

use ProxyObject;
for (1..1) {
  my $parent = CircObj->new;
  my $child = CircObj->new;
  my $proxy = ProxyObject->new($parent);
  $parent->child($child);
  $child->parent($parent);
  warn("Leaving scope\n");
}
warn("Scope left\n");

Now what we see from our output is:

Leaving scope
Proxying: DESTROY
CircObj::DESTROY
Scope left
CircObj::DESTROY
CircObj::DESTROY

So we have made some progress; the DESTROY method on our $parent variable is now called, albeit twice. But what we can do now is use that call to DESTROY to break our reference loop. To do this, we implement a small DESTROY method in our class that clears out the circular reference:

package CircObj;
...
sub DESTROY {
  my $self = shift;
  warn("CircObj::DESTROY\n");
  if ($self->{child}) {
    $self->{child}->parent(undef); 
	# set child's parent to undef, breaking the loop
  }
}

Which finally leads to the desired output:

Leaving scope
Proxying: DESTROY
CircObj::DESTROY
CircObj::DESTROY
CircObj::DESTROY
Scope left

Of course, this being Perl, it is possible to wrap all of this up into a fairly simple class, which I'll demonstrate in the next section.

Problems With This Technique

There is one major problem with this technique: It's possible to confuse the garbage collector with it. The easiest way to see this happen is if you build a tree using the above technique, and then hold onto a sub-tree while letting the root of the tree go out of scope. All of a sudden you'll find your data structure has become untangled, like so much wool from a snagged jumper. Unfortunately, there's little you can do about this, except perhaps for allowing the use of a proxy object as an option, and providing a method for freeing the tree that manually breaks the links. This is what XML::XPath does: By specifying $XML::XPath::SafeMode = 1 at runtime, you can switch this behavior on and off.

Another problem is that every property access now needs to go through method calls. If you have the proxy object (which is just a simple hash ref in this example, but could be a scalar ref), then you can't try and access the hash entries directly and expect to get those in the object we are proxying to. You may be able to get this working via the TIE mechanism, but it doesn't come for free. So we potentially lose some speed this way, though you should be using methods anyway for the sake of encapsulation.

Pages: 1, 2, 3

Next Pagearrow