Перезаписать функцию, определенную в модуле, но перед тем, как использовать ее на этапе выполнения? - PullRequest
17 голосов
/ 30 октября 2019

Давайте возьмем что-то очень простое,

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

В любом случае я могу из test.pl запустить код, который изменяет значение, установленное для $baz, и заставляет Foo.pm печатать что-то еще наscreen?

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

Возможно ли на этапах компилятора принудительно заставить вышеприведенное напечатать 7?

Ответы [ 7 ]

8 голосов
/ 30 октября 2019

Требуется взлом, потому что require (и, следовательно, use) компилирует и выполняет модуль перед возвратом.

То же самое относится и к eval. eval нельзя использовать для компиляции кода без его выполнения.

Наименее навязчивым решением, которое я нашел, было бы переопределить 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;

Kudos @Grinnz за упоминание @INC перехватчиков.

7 голосов
/ 30 октября 2019

Так как единственные опции здесь будут очень хакерскими, мы действительно хотим здесь запустить код после добавления подпрограммы в %Foo:: stash:

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 голосов
/ 30 октября 2019

Это выдаст некоторые предупреждения, но напечатает 7:

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

Сначала мы определим Foo::bar. Это значение будет переопределено объявлением в Foo.pm, но сработает предупреждение «Подпрограмма Foo :: bar redefined», которое вызовет обработчик сигнала, который переопределяет подпрограмму снова, чтобы вернуть 7.

5 голосов
/ 31 октября 2019

Вот решение, которое объединяет перехват процесса загрузки модуля с возможностями чтения только для модуля Readonly:

$ 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
3 голосов
/ 01 ноября 2019

Я пересмотрел свое решение здесь, чтобы оно больше не зависело от 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;

test-run.pl

#!/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 голос
/ 01 ноября 2019

Давайте соревноваться в гольф!

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 не вытягивали оригинал.

1 голос
/ 31 октября 2019

Если 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 не будет переопределять подпрограмму «константа» (с прототипом ()), так что это только жизнеспособное решение, если ваша фиктивная функция постоянна.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...