Menimpa fungsi yang didefinisikan dalam modul tetapi sebelum digunakan dalam fase runtime?

20

Mari kita ambil sesuatu yang sangat sederhana,

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

Apakah ada yang saya dapat dari test.plmenjalankan kode yang mengubah apa $bazyang diatur dan menyebabkan Foo.pmuntuk mencetak sesuatu yang lain ke layar?

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

Apakah mungkin dengan fase kompiler untuk memaksa yang di atas untuk mencetak 7?

Evan Carroll
sumber
1
Ini bukan fungsi internal - ini dapat diakses secara global Foo::bar, tetapi use Fooakan menjalankan fase kompilasi (mendefinisikan ulang bar jika ada yang sebelumnya didefinisikan di sana) dan fase runtime dari Foo. Satu-satunya hal yang dapat saya pikirkan adalah @INCkait yang sangat dalam untuk memodifikasi bagaimana Foo dimuat.
Grinnz
1
Anda ingin mendefinisikan ulang fungsinya sama sekali, ya? (Tidak hanya mengubah bagian dari operasinya, seperti cetakan itu?) Apakah ada alasan khusus untuk mendefinisikan ulang sebelum runtime? Judul meminta hal itu tetapi badan pertanyaan tidak mengatakan / menguraikan. Tentu Anda bisa melakukan itu tetapi saya tidak yakin tujuannya jadi apakah itu cocok.
zdim
1
@zdim ya ada alasannya. Saya ingin dapat mendefinisikan kembali fungsi yang digunakan dalam modul lain sebelum fase runtime dari modul itu. Persis seperti yang disarankan Grinnz.
Evan Carroll
@ Grinnz Apakah judul itu lebih baik?
Evan Carroll
1
Diperlukan hack. require(dan karenanya use) mengkompilasi dan mengeksekusi modul sebelum kembali. Sama berlaku untuk eval. evaltidak dapat digunakan untuk mengkompilasi kode tanpa juga menjalankannya.
ikegami

Jawaban:

8

Peretasan diperlukan karena require(dan karenanya use) mengkompilasi dan mengeksekusi modul sebelum kembali.

Sama berlaku untuk eval. evaltidak dapat digunakan untuk mengkompilasi kode tanpa juga menjalankannya.

Solusi paling tidak mengganggu yang saya temukan adalah menimpanya DB::postponed. Ini disebut sebelum mengevaluasi file yang diperlukan dikompilasi. Sayangnya, ini hanya dipanggil saat debugging ( perl -d).

Solusi lain adalah dengan membaca file, memodifikasinya, dan mengevaluasi file yang dimodifikasi, seperti yang berikut ini:

use File::Slurper qw( read_binary );

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

Di atas tidak diatur dengan benar %INC, itu mengacaukan nama file yang digunakan oleh peringatan dan semacamnya, itu tidak memanggil DB::postponed, dll. Berikut ini adalah solusi yang lebih kuat:

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;

Saya menggunakan UNITCHECK(yang dipanggil setelah kompilasi tetapi sebelum eksekusi) karena saya mendahulukan override (menggunakan unread) daripada membaca di seluruh file dan menambahkan definisi baru. Jika Anda ingin menggunakan pendekatan itu, Anda bisa mendapatkan pegangan file untuk kembali menggunakan

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

Kudos to @ Grinnz karena menyebutkan @INCkait.

ikegami
sumber
7

Karena satu-satunya opsi di sini akan sangat terjal, apa yang sebenarnya kita inginkan di sini adalah menjalankan kode setelah subrutin ditambahkan ke %Foo::simpanan:

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
sumber
6

Ini akan memancarkan beberapa peringatan, tetapi mencetak 7:

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

Pertama, kami mendefinisikan Foo::bar. Nilainya akan didefinisikan ulang oleh deklarasi di Foo.pm, tetapi peringatan "Subroutine Foo :: bar redefined" akan dipicu, yang akan memanggil pengendali sinyal yang mendefinisikan kembali subrutin untuk mengembalikan 7.

choroba
sumber
3
Yah itu hack jika saya pernah melihatnya.
Evan Carroll
2
Ini tidak mungkin dilakukan tanpa peretasan. Jika subrutin dipanggil dalam subrutin lain, itu akan jauh lebih mudah.
choroba
Itu hanya akan berfungsi jika modul yang dimuat memiliki peringatan yang diaktifkan; Foo.pm tidak mengaktifkan peringatan dan karenanya ini tidak akan pernah dipanggil.
szr
@ szr: Sebut saja dengan perl -w.
choroba
@ choroba: Ya, itu akan berhasil, karena -w akan mengaktifkan peringatan di mana saja, iirc. Tetapi poin saya adalah bahwa Anda tidak dapat memastikan bagaimana pengguna akan menjalankannya. Sebagai contoh, one-liners biasanya menjalankan penyempitan atau peringatan.
szr
5

Berikut ini adalah solusi yang menggabungkan mengaitkan proses pemuatan modul dengan kemampuan membuat-baca dari modul 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
sumber
1
@ikegami Terima kasih, saya telah membuat perubahan yang Anda rekomendasikan. Tangkapan yang bagus.
gordonfish
3

Saya telah merevisi solusi saya di sini, sehingga tidak lagi bergantung pada Readonly.pm, setelah mengetahui bahwa saya telah melewatkan alternatif yang sangat sederhana, berdasarkan jawaban m-conrad , yang telah saya ulang ke pendekatan modular yang saya mulai di sini.

Foo.pm ( Sama seperti pada posting pembuka )

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 Diperbarui

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;

Jalankan dan hasilkan:

$ ./test-run.pl 
5
gordonfish
sumber
1

Jika bagian sub bardalam Foo.pmmemiliki prototipe yang berbeda dari Foo::barfungsi yang ada , Perl tidak akan menimpanya? Sepertinya itulah masalahnya, dan membuat solusinya cukup sederhana:

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

atau sejenisnya

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

Pembaruan: tidak, alasan ini berhasil adalah Perl tidak akan mendefinisikan ulang subrutin "konstan" (dengan prototipe ()), jadi ini hanya solusi yang layak jika fungsi tiruan Anda konstan.

massa
sumber
BEGIN { *Foo::bar = sub () { 7 } }lebih baik ditulis sebagaisub Foo::bar() { 7 }
ikegami
1
Re " Perl tidak akan mendefinisikan ulang" konstanta "subrutin ", Itu juga tidak benar. Sub tidak didefinisikan ulang menjadi 42 bahkan ketika itu adalah sub konstan. Alasannya bekerja di sini adalah karena panggilan akan diuraikan sebelum redefinisi. Jika Evan menggunakan yang lebih umum, sub bar { 42 } my $baz = bar();bukan my $baz = bar(); sub bar { 42 }, itu tidak akan berhasil.
ikegami
Bahkan dalam situasi yang sangat sempit itu berhasil, ini sangat bising ketika peringatan digunakan. ( Prototype mismatch: sub Foo::bar () vs none at Foo.pm line 5.dan Constant subroutine bar redefined at Foo.pm line 5.)
ikegami
1

Ayo ikuti kontes Golf!

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;

Ini hanya mengawali kode modul dengan penggantian metode, yang akan menjadi baris kode pertama yang berjalan setelah fase kompilasi dan sebelum fase eksekusi.

Kemudian, isi %INCentri tersebut agar nantinya use Footidak ada yang menarik yang asli.

M Conrad
sumber
Solusi yang sangat bagus Saya awalnya mencoba sesuatu seperti ini ketika saya mulai, tetapi tidak ada bagian injeksi + BEGIN yang terhubung dengan baik. Saya dapat dengan baik memasukkan ini ke dalam versi modular dari jawaban saya yang telah saya posting sebelumnya.
gordonfish
Modul Anda adalah pemenang yang jelas untuk desain, tetapi saya suka ketika stackoverflow juga memberikan jawaban minimalis juga.
dataless