覆盖模块中定义但在运行时阶段使用的函数?


20

让我们做一个非常简单的事情,

# Foo.pm
package Foo {
  my $baz = bar();
  sub bar { 42 };  ## Overwrite this
  print $baz;      ## Before this is executed
}

无论如何,我可以从test.pl运行代码中更改$baz设置并导致Foo.pm在屏幕上打印其他内容吗?

# maybe something here.
use Foo;
# maybe something here

在编译器阶段是否可以强制上述内容打印7


1
它不是内部函数-可以通过全局访问Foo::bar,但是use Foo它将在编译阶段(如果先前在此定义过,则重新定义bar)和Foo的运行时阶段一起运行。我唯一能想到的就是@INC修改Foo的加载方式的钩子。
Grinnz '19

1
您想完全重新定义功能,对吗?(不仅要更改其操作的一部分,例如打印该内容?)在运行时之前是否有重新定义的特定原因?标题要求这样做,但问题正文没有说/详细说明。当然可以,但是我不确定目标是否合适。
zdim

1
@zdim是的,有原因。我希望能够在该模块的运行时阶段之前重新定义该模块中使用的功能。格林尼斯(Grinnz)的建议正是如此。
埃文·卡罗尔

@Grinnz这个头衔更好吗?
埃文·卡罗尔

1
需要骇客。require(因此use)在返回之前都会编译并执行模块。同样适用evaleval在不执行代码的情况下不能用于编译代码。
ikegami,

Answers:


8

之所以需要骇客,是因为require(因此use)在返回之前都编译并执行了模块。

同样适用evaleval在不执行代码的情况下不能用于编译代码。

我发现的最不干扰的解决方案是重写DB::postponed。在评估已编译的所需文件之前将调用此方法。不幸的是,它仅在调试(perl -d)时调用。

另一个解决方案是读取文件,对其进行修改并评估修改后的文件,类似于以下内容:

use File::Slurper qw( read_binary );

eval(read_binary("Foo.pm") . <<'__EOS__')  or die $@;
package Foo {
   no warnings qw( redefine );
   sub bar { 7 }
}
__EOS__

上面的设置不正确%INC,它弄乱了警告使用的文件名,因此不调用DB::postponed,等等。下面是一个更可靠的解决方案:

use IO::Unread  qw( unread );
use Path::Class qw( dir );

BEGIN {     
   my $preamble = '
      UNITCHECK {
         no warnings qw( redefine );
         *Foo::bar = sub { 7 };
      }
   ';    

   my @libs = @INC;
   unshift @INC, sub {
      my (undef, $fn) = @_;
      return undef if $_[1] ne 'Foo.pm';

      for my $qfn (map dir($_)->file($fn), @libs) {
         open(my $fh, '<', $qfn)
            or do {
               next if $!{ENOENT};
               die $!;
            };

         unread $fh, "$preamble\n#line 1 $qfn\n";
         return $fh;
      }

      return undef;
   };
}

use Foo;

UNITCHECK之所以使用它(是在编译后但在执行之前调用的),是因为我在重写之前(使用unread)而不是在其中读取整个文件并追加新定义。如果要使用该方法,则可以使用返回文件句柄

open(my $fh_for_perl, '<', \$modified_code);
return $fh_for_perl;

@Grinnz赞扬@INC钩子。


7

由于这里唯一的选择将非常棘手,因此我们真正想要的是在将子例程添加到%Foo::存储中之后运行代码:

use strict;
use warnings;

# bless a coderef and run it on destruction
package RunOnDestruct {
  sub new { my $class = shift; bless shift, $class }
  sub DESTROY { my $self = shift; $self->() }
}

use Variable::Magic 0.58 qw(wizard cast dispell);
use Scalar::Util 'weaken';
BEGIN {
  my $wiz;
  $wiz = wizard(store => sub {
    return undef unless $_[2] eq 'bar';
    dispell %Foo::, $wiz; # avoid infinite recursion
    # Variable::Magic will destroy returned object *after* the store
    return RunOnDestruct->new(sub { no warnings 'redefine'; *Foo::bar = sub { 7 } }); 
  });
  cast %Foo::, $wiz;
  weaken $wiz; # avoid memory leak from self-reference
}

use lib::relative '.';
use Foo;

6

这将发出一些警告,但显示7:

sub Foo::bar {}
BEGIN {
    $SIG{__WARN__} = sub {
        *Foo::bar = sub { 7 };
    };
}

首先,我们定义Foo::bar。它的值将由Foo.pm中的声明重新定义,但是将触发“ Subroutine Foo :: bar redefined”警告,该警告将调用再次重新定义该子例程的信号处理程序以返回7。


3
韦尔(Wellll)如果我曾经看过的话,那可真是个骇客。
埃文·卡罗尔

2
没有黑客,这是不可能的。如果在另一个子例程中调用该子例程,则将容易得多。
choroba

只有在正在加载的模块启用了警告的情况下,这才起作用。Foo.pm不启用警告,因此永远不会调用它。
szr

@szr:用来称呼它perl -w
choroba

@choroba:是的,这会起作用,因为-w会在任何地方启用警告,iirc。但是我的意思是,您不确定用户将如何运行它。例如,单层衬里通常会出现无狭窄或警告的情况。
szr

5

这是一个结合了挂钩模块加载过程和Readonly模块的readonly-make功能的解决方案:

$ cat Foo.pm 
package Foo {
  my $baz = bar();
  sub bar { 42 };  ## Overwrite this
  print $baz;      ## Before this is executed
}


$ cat test.pl 
#!/usr/bin/perl

use strict;
use warnings;

use lib qw(.);

use Path::Tiny;
use Readonly;

BEGIN {
    my @remap = (
        '$Foo::{bar} => \&mybar'
    );

    my $pre = join ' ', map "Readonly::Scalar $_;", @remap;

    my @inc = @INC;

    unshift @INC, sub {
        return undef if $_[1] ne 'Foo.pm';

        my ($pm) = grep { $_->is_file && -r } map { path $_, $_[1] } @inc
           or return undef;

        open my $fh, '<', \($pre. "#line 1 $pm\n". $pm->slurp_raw);
        return $fh;
    };
}


sub mybar { 5 }

use Foo;


$ ./test.pl   
5

1
@ikegami谢谢,我已经进行了您建议的更改。接得好。
gordonfish '19

3

我在这里修改了我的解决方案,因此Readonly.pm在得知我错过了基于m-conrad的答案的非常简单的选择之后,不再依赖于该解决方案,我将其重新设计为从此处开始的模块化方法。

Foo.pm与开头的帖子相同

package Foo {
  my $baz = bar();
  sub bar { 42 };  ## Overwrite this
  print $baz;      ## Before this is executed
}
# Note, even though print normally returns true, a final line of 1; is recommended.

OverrideSubs.pm 更新

package OverrideSubs;

use strict;
use warnings;

use Path::Tiny;
use List::Util qw(first);

sub import {
    my (undef, %overrides) = @_;
    my $default_pkg = caller; # Default namespace when unspecified.

    my %remap;

    for my $what (keys %overrides) {
        ( my $with = $overrides{$what} ) =~ s/^([^:]+)$/${default_pkg}::$1/;

        my $what_pkg  = $what =~ /^(.*)\:\:/ ? $1 : $default_pkg;
        my $what_file = ( join '/', split /\:\:/, $what_pkg ). '.pm';

        push @{ $remap{$what_file} }, "*$what = *$with";
    }

    my @inc = grep !ref, @INC; # Filter out any existing hooks; strings only.

    unshift @INC, sub {
        my $remap = $remap{ $_[1] } or return undef;
        my $pre = join ';', @$remap;

        my $pm = first { $_->is_file && -r } map { path $_, $_[1] } @inc
            or return undef;

        # Prepend code to override subroutine(s) and reset line numbering.
        open my $fh, '<', \( $pre. ";\n#line 1 $pm\n". $pm->slurp_raw );
        return $fh;
   };
}

1;

测试运行

#!/usr/bin/env perl

use strict;
use warnings;

use lib qw(.); # Needed for newer Perls that typically exclude . from @INC by default.

use OverrideSubs
    'Foo::bar' => 'mybar';

sub mybar { 5 } # This can appear before or after 'use OverrideSubs', 
                # but must appear before 'use Foo'.

use Foo;

运行并输出:

$ ./test-run.pl 
5

1

如果sub bar内部Foo.pm具有与现有Foo::bar函数不同的原型,Perl不会覆盖它吗?似乎是这样,并使解决方案非常简单:

# test.pl
BEGIN { *Foo::bar = sub () { 7 } }
use Foo;

或同一件事

# test.pl
package Foo { use constant bar => 7 };
use Foo;

更新:不,之所以()可行,是因为Perl不会重新定义“常量”子例程(带有prototype ),因此,仅当您的模拟函数为常量时,这才是可行的解决方案。


BEGIN { *Foo::bar = sub () { 7 } }最好写成sub Foo::bar() { 7 }
ikegami

1
关于“ Perl不会重新定义“常量”子例程 ”,那也不是。即使它是一个常数子,该子的确会重新定义为42。之所以在这里起作用,是因为该调用在重新定义之前已内联。如果Evan使用了更通用的sub bar { 42 } my $baz = bar();而不是my $baz = bar(); sub bar { 42 },它将无法正常工作。
ikegami,

即使在非常狭窄的情况下,它也可以工作,但是在使用警告时,这却非常嘈杂。(Prototype mismatch: sub Foo::bar () vs none at Foo.pm line 5.and Constant subroutine bar redefined at Foo.pm line 5.
ikegami

1

让我们来一场高尔夫比赛!

sub _override { 7 }
BEGIN {
  my ($pm)= grep -f, map "$_/Foo.pm", @INC or die "Foo.pm not found";
  open my $fh, "<", $pm or die;
  local $/= undef;
  eval "*Foo::bar= *main::_override;\n#line 1 $pm\n".<$fh> or die $@;
  $INC{'Foo.pm'}= $pm;
}
use Foo;

这只是在模块代码之前加上方法的替换,该方法将是在编译阶段之后和执行阶段之前运行的第一行代码。

然后,填写%INC条目,以免将来的装箱use Foo拉入原件。


非常好的解决方案。刚开始时,我最初尝试过类似的方法,但是缺少您很好连接的注入部分+ BEGIN方面。我能够很好地将其合并到我先前发布的答案的模块化版本中。
gordonfish

您的模块是设计的明显赢家,但是当stackoverflow还提供极简答案时,我也喜欢它。
无数据,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.