Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is my localized redefinition of a package sub not taking effect?

Tags:

perl

Given the following Perl program:

package Foo;
use strict;
use warnings;

sub new {
    my ($class) = @_;
    return bless {}, $class;
}

sub c {
    print "IN ORIG C\n";
}

sub DESTROY {
    print "IN DESTROY\n";
    c();
}

1;

package main;
use strict;
use warnings;
no warnings qw/redefine once/;

local *Foo::c = sub { print "IN MY C\n" };
my $f = Foo->new();
undef $f;

I expect output as:

IN DESTROY
IN MY C

But I actually get output as:

IN DESTROY
IN ORIG C

Q: Why is my localized redefinition of Foo::c not taking effect?

like image 577
Dylan Cali Avatar asked Mar 20 '15 00:03

Dylan Cali


3 Answers

When perl code is compiled, globs for package variables/symbols are looked up (and created as necessary) and referenced directly from the compiled code.

So when you (temporarily) replace the symbol table entry for *Foo::c at runtime, all the already compiled code that used *Foo::c still uses the original glob. But do/require'd code or eval STRING or symbolic references won't.

(Very similar to Access package variable after its name is removed from symbol table in Perl?, see the examples there.)

like image 57
ysth Avatar answered Nov 04 '22 02:11

ysth


This is a bug in perl which will be fixed in 5.22 (see Leon's comment below).

This happens because undef $f; doesn't actually free up and destroy $f, it just marks it as ready to be freed by a nextstate op.

nextstate ops exist roughly between each statement, and they are there to clean up the stack, among other things.

In your example, since undef $f is the last thing in the file, there is no nextstate after it, so your local destructor goes out of scope before $f's destructor is called (or, the global destruction that happens just isn't aware of your local change.)

When you add a print statement after undef $f, the nextstate op before the print calls your local destructor.

You can see the additional nextstate in your example at https://gist.github.com/calid/aeb939147fdd171cffe3#file-04-diff-concise-out.

You can also see this behaviour by checking caller() in your DESTROY method:

sub DESTROY {
  my ($pkg, $file, $line) = caller;
  print "Destroyed at $pkg, $file, $line\n";
  c();
}

mhorsfall@tworivers:~$ perl foo.pl
Destroyed at main, foo.pl, 0
IN DESTROY
IN ORIG C

mhorsfall@tworivers:~$ echo 'print "hi\n"' >> foo.pl
mhorsfall@tworivers:~$ perl foo.pl
Destroyed at main, foo.pl, 30
IN DESTROY
IN MY C
hi

(Line 30 being the print "hi\n")

Hope that sheds some light on this.

Cheers.

like image 2
Matthew Horsfall Avatar answered Nov 04 '22 04:11

Matthew Horsfall


The problem here doesn't have to do with compile time vs runtime but rather with scoping.

The use of local limits the scope of your modified Foo::c to the remainder of the current scope (which in your example is the remainder of your script). But DESTROY doesn't run in that scope, even when you explicitly undef $f (See http://perldoc.perl.org/perlobj.html#Destructors for more discussion of the behavior of DESTROY). It runs at an undetermined time later, specifically AFTER $f has "gone out of scope". Therefore, any localized changes you have made in the scope of $f will not apply whenever DESTROY finally runs.

You can see this yourself by simply removing the local in your example:

With local

IN DESTROY
IN ORIG C

Without local

IN DESTROY
IN MY C

Or by adding a few additional subroutines and calling them in package::main scope:

package Foo;
...
sub d {
    c();
}

sub DESTROY {
    print "IN DESTROY\n";
    c();
}

1;

package main;
...
sub e {
    Foo::c();
}

local *Foo::c = sub { print "IN MY C\n" };
my $f = Foo->new();
Foo::c();
Foo::d();
e();
undef $f;

Which prints

IN MY C
IN MY C
IN MY C
IN DESTROY
IN ORIG C

So only in DESTROY is the original c used, further demonstrating that this is a scoping issue.

Also see https://stackoverflow.com/a/19100461/232706 for a great explanation of Perl scoping rules.

like image 2
Rob Van Dam Avatar answered Nov 04 '22 02:11

Rob Van Dam