Перезапис функції, визначеної в модулі, але перед використанням у фазі її виконання?


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гачки.


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, але буде запущено попередження "Підпрограма Foo :: bar redefined", яке викликає обробник сигналу, який знову визначає підпрограму, щоб повернути 7.


3
Ну, це хак, якщо я коли-небудь бачив його.
Еван Керролл

2
Це неможливо без злому. Якби підпрограму викликали в іншій підпрограмі, це було б набагато простіше.
choroba

Це буде працювати лише в тому випадку, якщо модуль, що завантажується, увімкнено попередження; Foo.pm не вмикає попередження, тому це ніколи не буде викликано.
szr

@szr: Так називаємо це perl -w.
choroba

@choroba: Так, це спрацювало б, оскільки -w дозволить попереджати всюди, iirc. Але моя думка полягає в тому, що ви не можете бути впевнені, як користувач буде це виконувати. Наприклад, однолінійки зазвичай виконують стриктури або попередження.
szr

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

1
@ikegami Дякую, я вніс зміни, які ви рекомендували. Хороший улов.
gordonfish

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

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 не переосмислить" постійну "підпрограму ", це теж неправда. Sub дій переосмислюється на 42, навіть коли він є постійним sub. Причина, по якій він працює тут, полягає в тому, що виклик стає вкладеним перед переглядом. Якби Еван використовував звичайніші 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.і 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не потягнули за оригінал.


Дуже приємне рішення. Я спочатку спробував щось подібне, коли я вперше почав, але мені не вистачало ін'єкційної частини + НАЧАЛИ аспект, який ви добре з'єднали. Я міг чудово включити це в модульну версію своєї відповіді, яку я розміщував раніше.
gordonfish

Ваш модуль є явним переможцем дизайну, але мені подобається, коли stackoverflow також дає мінімалістичну відповідь.
даних
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.