7

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?

Dylan Cali
  • 1,463
  • 1
  • 13
  • 17

3 Answers3

6

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.)

Community
  • 1
  • 1
ysth
  • 96,171
  • 6
  • 121
  • 214
  • ysth after playing with the code Rob's answer seems to be correct in this case, would you agree? – Dylan Cali Mar 21 '15 at 21:14
  • neither of us is right. `local *glob` doesn't actually replace the glob itself, it just replaces the GV in the glob, so my reasoning is wrong and the localization should work. but the DESTROY is supposed to run in the scope, at the time of the undef; it appears to be a bug that it doesn't *only when the undef is the last thing in the block* – ysth Mar 22 '15 at 04:43
  • temporarily undeleting so calid can see this – ysth Mar 22 '15 at 04:50
2

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.

Community
  • 1
  • 1
Rob Van Dam
  • 7,812
  • 3
  • 31
  • 34
  • Rob I didn't find the linked perldoc/SO all that useful for this particular question (neither explicitly discusses the scope of DESTROY being different from the instantiation scope). Do you think it makes sense to either remove them or replace them with more relevant ones? – Dylan Cali Mar 21 '15 at 22:40
  • DESTROY most certainly does run at the time of the undef $f (try `perl -wE'package Foo { sub new { bless {} } sub DESTROY { say "DESTROY" } } my $x=Foo->new; undef $x; say "done"'`) so is in the dynamic scope of the local – ysth Mar 22 '15 at 04:29
  • but there appears to be a bug when undef is the last thing in the block that is producing the undesired result – ysth Mar 22 '15 at 04:44
  • @ysth yes, adding a print after DESTROY seems to change the semantics and results in different output: https://gist.github.com/calid/aeb939147fdd171cffe3. With the print after undef it uses the localized `c`, without it it uses the orig. Is it a bug or just different scoping rules depending on when the undef occurs? – Dylan Cali Mar 22 '15 at 04:49
  • @ysth I'm not so sure. [perlobj](http://perldoc.perl.org/perlobj.html#Destructors) documents this as "If you want to do something when the object is destroyed, you can define a DESTROY method in your class. This method will always be called by Perl **at the appropriate time**" And further states "Because DESTROY methods can be called **at any time**..." That to me indicates there are no hard and fast rules about exactly when destruction occurs, and perl will do it when perl deems it to be appropriate, regardless of whether you explicitly called undef. – Dylan Cali Mar 22 '15 at 07:42
  • The answer to this SO seems to concur: http://stackoverflow.com/questions/10772220/how-do-you-explicitly-destroy-an-object-in-perl – Dylan Cali Mar 22 '15 at 07:42
2

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.

Dylan Cali
  • 1,463
  • 1
  • 13
  • 17
  • 1
    Better yet, this was fixed in blead last August, and the fix will be in 5.22 http://perl5.git.perl.org/perl.git/commitdiff/4dda930be – Leon Timmermans Mar 24 '15 at 16:25