Zastąpić funkcję zdefiniowaną w module, ale przed użyciem w fazie wykonawczej?

20

Weźmy coś bardzo prostego,

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

Czy w każdym razie mogę test.pluruchomić kod, który zmienia $bazustawienia i powoduje Foo.pmwydrukowanie czegoś innego na ekranie?

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

Czy jest możliwe, aby fazy kompilatora wymusiły wydrukowanie powyższego 7?

Evan Carroll
źródło
1
To nie jest funkcja wewnętrzna - jest dostępna globalnie jako Foo::bar, ale use Foouruchomi zarówno fazę kompilacji (przedefiniowanie paska, jeśli coś tam wcześniej zdefiniowano), jak i fazę działania Foo. Jedyne, co mogę wymyślić, to głęboko zhakowany @INChak do modyfikacji sposobu ładowania Foo.
Grinnz
1
Chcesz całkowicie przedefiniować funkcję, tak? (Nie tylko zmieniasz część jego działania, jak ten wydruk?) Czy są jakieś konkretne powody do przedefiniowania przed uruchomieniem? Tytuł tego wymaga, ale treść pytania nie mówi / nie rozwija. Pewnie, że możesz to zrobić, ale nie jestem pewien celu, więc czy pasuje.
zdim
1
@zdim tak, istnieją powody. Chcę móc przedefiniować funkcję używaną w innym module przed fazą środowiska wykonawczego tego modułu. Dokładnie to, co sugerował Grinnz.
Evan Carroll
@Grinnz Czy ten tytuł jest lepszy?
Evan Carroll
1
Wymagany jest hack. require(a zatem use) zarówno kompiluje, jak i wykonuje moduł przed zwróceniem. To samo dotyczy eval. evalnie można go użyć do kompilacji kodu bez jego wykonania.
ikegami

Odpowiedzi:

8

Wymagany jest hack, ponieważ require(a zatem use) zarówno kompiluje, jak i wykonuje moduł przed zwróceniem.

To samo dotyczy eval. evalnie można go użyć do kompilacji kodu bez jego wykonania.

Najtrudniejszym rozwiązaniem, jakie znalazłem, byłoby zastąpienie DB::postponed. Jest to wywoływane przed oceną skompilowanego wymaganego pliku. Niestety jest wywoływany tylko podczas debugowania ( perl -d).

Innym rozwiązaniem byłoby odczytanie pliku, zmodyfikowanie go i ocena zmodyfikowanego pliku, podobnie jak to robi:

use File::Slurper qw( read_binary );

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

Powyższe nie ustawia się poprawnie %INC, miesza nazwę pliku używaną przez ostrzeżenia i takie, nie wywołuje DB::postponeditp. Poniżej przedstawiono bardziej niezawodne rozwiązanie:

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;

Użyłem UNITCHECK(który jest wywoływany po kompilacji, ale przed wykonaniem), ponieważ wstawiłem zastąpienie (używanie unread) zamiast wczytywania całego pliku i dołączania nowej definicji. Jeśli chcesz zastosować to podejście, możesz uzyskać uchwyt pliku, z którego będziesz mógł powrócić

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

Uznanie dla @Grinnz za wzmianki o @INChakach.

ikegami
źródło
7

Ponieważ jedyne opcje tutaj będą głęboko zhackowane, tak naprawdę chcemy tutaj uruchomić kod po dodaniu podprogramu do %Foo::skrytki:

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
źródło
6

Spowoduje to wygenerowanie niektórych ostrzeżeń, ale wyświetli 7:

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

Najpierw definiujemy Foo::bar. Jego wartość zostanie przedefiniowana przez deklarację w Foo.pm, ale zostanie uruchomione ostrzeżenie „Subroutine Foo :: bar redefined”, które wywoła procedurę obsługi sygnału, która ponownie zdefiniuje podprogram, aby zwrócić 7.

choroba
źródło
3
Cóż, to hack, jeśli kiedykolwiek go widziałem.
Evan Carroll
2
Nie jest to możliwe bez włamania. Gdyby podprogram został wywołany w innym podprogramie, byłoby to znacznie łatwiejsze.
choroba
Działa to tylko wtedy, gdy ładowany moduł ma włączone ostrzeżenia; Foo.pm nie włącza ostrzeżeń i dlatego nigdy nie zostanie wywołane.
szr
@szr: Więc zadzwoń perl -w.
choroba
@choroba: Tak, to zadziałałoby, ponieważ -w włączy ostrzeżenia wszędzie, iirc. Ale chodzi mi o to, że nie możesz być pewien, jak użytkownik to uruchomi. Na przykład, jednowierszowe zwykle uruchamiają bez ograniczeń lub ostrzeżeń.
szr
5

Oto rozwiązanie, które łączy przechwytywanie procesu ładowania modułu z możliwościami tylko do odczytu modułu 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
źródło
1
@ikegami Dzięki, wprowadziłem zmiany, które poleciłeś. Dobry chwyt
gordonfish
3

Zmieniłem tutaj moje rozwiązanie, aby nie Readonly.pmopierało się już na tym , po tym, jak dowiedziałem się, że przegapiłem bardzo prostą alternatywę, opartą na odpowiedzi m-conrada , którą przerobiłem na podejście modułowe, które zacząłem tutaj.

Foo.pm ( Taki sam jak w poście otwierającym )

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 Zaktualizowano

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;

Uruchom i wyjdź:

$ ./test-run.pl 
5
Gordonfish
źródło
1

Jeśli sub barwnętrze Foo.pmma inny prototyp niż istniejąca Foo::barfunkcja, Perl go nie zastąpi? Wydaje się, że tak jest i sprawia, że ​​rozwiązanie jest dość proste:

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

lub coś w tym rodzaju

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

Aktualizacja: nie, powodem tego jest to, że Perl nie przedefiniuje podprogramu „stałego” (z prototypem ()), więc jest to realne rozwiązanie tylko wtedy, gdy twoja próbna funkcja jest stała.

tłum
źródło
BEGIN { *Foo::bar = sub () { 7 } }lepiej napisać jakosub Foo::bar() { 7 }
ikegami,
1
Re „ Perl nie zdefiniuje na nowo„ stałego ”podprogramu , to też nie jest prawda. Subwoofer zostaje ponownie zdefiniowany na 42, nawet jeśli jest stałym subwooferem. Powodem, dla którego tutaj działa, jest to, że wezwanie zostaje wprowadzone przed redefinicją. Gdyby Evan użył bardziej powszechnego sub bar { 42 } my $baz = bar();zamiast my $baz = bar(); sub bar { 42 }, nie zadziałałoby.
ikegami
Nawet w bardzo wąskiej sytuacji to działa, jest to bardzo głośne, gdy używane są ostrzeżenia. ( Prototype mismatch: sub Foo::bar () vs none at Foo.pm line 5.i Constant subroutine bar redefined at Foo.pm line 5.)
ikegami
1

Zróbmy zawody golfowe!

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;

To tylko poprzedza kod modułu zastąpieniem metody, która będzie pierwszą linią kodu, która działa po fazie kompilacji i przed fazą wykonania.

Następnie wypełnij %INCwpis, aby przyszłe ładunki use Foonie pobierały oryginału.

M. Conrad
źródło
Bardzo fajne rozwiązanie. Na początku próbowałem czegoś takiego, kiedy zaczynałem, ale brakowało mi części iniekcji + POCZĄTKU, którą ładnie połączyłeś. Byłem w stanie ładnie włączyć to do modułowej wersji mojej odpowiedzi, którą zamieściłem wcześniej.
gordonfish,
Twój moduł jest wyraźnym zwycięzcą w projektowaniu, ale podoba mi się, gdy przepływ stosu zapewnia również minimalistyczną odpowiedź.
danych,