Перезаписать функцию, определенную в модуле, но перед тем, как использовать ее на этапе выполнения?

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будет запускать как фазу компиляции (панель переопределения, если там что-то было ранее определено), так и фазу времени выполнения Foo. Единственное, о чем я могу думать, - это хакерский хак, @INCчтобы изменить способ загрузки Foo.
Гринз
1
Вы хотите полностью переопределить функцию, да? (Не просто изменить часть своей работы, например, эту печать?) Есть ли конкретные причины для переопределения перед выполнением? Название просит об этом, но тело вопроса не говорит / уточняет. Конечно, вы можете сделать это, но я не уверен в цели, поэтому подойдет ли она.
Здим
1
@zdim да, есть причины. Я хочу иметь возможность переопределить функцию, используемую в другом модуле, до фазы выполнения этого модуля. Именно то, что предложил Гриннц.
Эван Кэрролл
@Grinnz Это название лучше?
Эван Кэрролл
1
Взлом требуется. require(и, следовательно use) как компилирует, так и выполняет модуль перед возвратом. То же самое и для eval. evalне может быть использован для компиляции кода без его выполнения.
Ikegami

Ответы:

8

Требуется взлом, потому что 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;

Престижность @Grinnz для упоминания @INCкрючков.

Ikegami
источник
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;
Grinnz
источник
6

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

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

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

choroba
источник
3
Wellll это взлом, если я когда-либо видел.
Эван Кэрролл
2
Это невозможно без взлома. Если бы подпрограмма была вызвана в другой подпрограмме, это было бы намного проще.
choroba
Это будет работать только в том случае, если загружаемый модуль имеет предупреждения; Foo.pm не включает предупреждения и, следовательно, никогда не будет вызываться.
19
@szr: так называй это с perl -w.
choroba
@choroba: Да, это сработает, так как -w будет включать предупреждения везде, iirc. Но я хочу сказать, что вы не можете быть уверены, как пользователь выполнит это. Например, однострочники обычно запускают без ограничений или предупреждений.
СЗР
5

Вот решение, которое объединяет перехват процесса загрузки модуля с возможностями чтения только для модуля 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
gordonfish
источник
1
@ikegami Спасибо, я внес изменения, которые вы рекомендовали. Хороший улов.
Гордон Фиш
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;

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

чернь
источник
BEGIN { *Foo::bar = sub () { 7 } }лучше записать в видеsub Foo::bar() { 7 }
Ikegami
1
« Perl не будет переопределять« постоянную »подпрограмму », это тоже не так. Подпрограмма переопределяется до 42, даже если она постоянная. Причина, по которой он здесь работает, заключается в том, что вызов переопределяется перед переопределением. Если бы Эван использовал sub bar { 42 } my $baz = bar();вместо общего my $baz = bar(); sub bar { 42 }, это не сработало бы.
икегами
Даже в очень узкой ситуации это работает, это очень шумно, когда используются предупреждения. ( Prototype mismatch: sub Foo::bar () vs none at Foo.pm line 5.и 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, которую вы хорошо соединили. Мне удалось включить это в модульную версию моего ответа, которую я выложил ранее.
Гордонфиш
Ваш модуль - явный победитель в дизайне, но мне нравится, когда stackoverflow также дает минималистичный ответ.
без данных