Substituindo uma função definida em um módulo, mas antes usada em sua fase de tempo de execução?

20

Vamos pegar algo muito simples,

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

Existe alguma maneira que eu possa test.plexecutar código que altera o que $bazestá definido e faz Foo.pmcom que imprima outra coisa na tela?

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

É possível, com as fases do compilador, forçar a impressão acima 7?

Evan Carroll
fonte
11
Não é uma função interna - é acessível globalmente Foo::bar, mas use Foofuncionará tanto a fase de compilação (redefinindo a barra, se alguma coisa foi definida anteriormente lá) quanto a fase de execução do Foo. A única coisa em que consigo pensar seria em um @INCgancho profundamente hacky para modificar como o Foo é carregado.
Grinnz 30/10/19
11
Você quer redefinir a função completamente, sim? (Não basta alterar parte de sua operação, como a impressão?) Existem razões específicas para redefinir antes do tempo de execução? O título pede isso, mas o corpo da pergunta não diz / elabora. Claro que você pode fazer isso, mas não tenho certeza do objetivo, para saber se ele se encaixaria.
Zdim 30/10/19
11
@ zdim sim, existem razões. Desejo redefinir uma função usada em outro módulo antes da fase de tempo de execução desse módulo. Exatamente o que Grinnz sugeriu.
Evan Carroll
@Grinnz Esse título é melhor?
Evan Carroll
11
É necessário um hack. require(e, portanto use), compila e executa o módulo antes de retornar. O mesmo vale para eval. evalnão pode ser usado para compilar código sem também executá-lo.
Ikegami # 30/19

Respostas:

8

Um hack é necessário porque require(e, portanto,use ), compila e executa o módulo antes de retornar.

O mesmo vale para eval.evalnão pode ser usado para compilar código sem também executá-lo.

A solução menos intrusiva que encontrei seria substituir DB::postponed. Isso é chamado antes de avaliar um arquivo necessário compilado. Infelizmente, ele é chamado apenas durante a depuração (perl -d ).

Outra solução seria ler o arquivo, modificá-lo e avaliar o arquivo modificado, como o seguinte:

use File::Slurper qw( read_binary );

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

O item acima não está configurado corretamente %INC, altera o nome do arquivo usado pelos avisos e, portanto, não chama DB::postponedetc. A seguir, é uma solução mais robusta:

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;

Eu usei UNITCHECK(que é chamado após a compilação, mas antes da execução) porque eu antecede a substituição (usando unread) em vez de ler o arquivo inteiro e anexar a nova definição. Se você quiser usar essa abordagem, poderá obter um identificador de arquivo para retornar usando

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

Parabéns ao @Grinnz por mencionar @INCganchos.

ikegami
fonte
7

Como as únicas opções aqui serão profundamente hacky, o que realmente queremos aqui é executar o código após a adição da sub-rotina ao %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;
Grinnz
fonte
6

Isso emitirá alguns avisos, mas imprime 7:

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

Primeiro, nós definimos Foo::bar. Seu valor será redefinido pela declaração em Foo.pm, mas o aviso "Subrotine Foo :: bar redefined" será acionado, o que chamará o manipulador de sinal que redefine a sub-rotina novamente para retornar 7.

choroba
fonte
3
Bem, isso é um truque, se eu já vi um.
Evan Carroll
2
Isso não é possível sem um hack. Se a sub-rotina fosse chamada em outra sub-rotina, seria muito mais fácil.
choroba
Isso funcionará apenas se o módulo que está sendo carregado tiver avisos ativados; Foo.pm não habilita avisos e, portanto, isso nunca será chamado.
Szr 30/10/19
@szr: Então chame-o com perl -w.
Choror 30/10/19
@choroba: Sim, isso funcionaria, pois -w ativará avisos em todos os lugares, iirc. Mas o que quero dizer é que você não pode ter certeza de como um usuário executará isso. Por exemplo, one-liners normalmente executam sem restrições ou avisos.
Szr 01/11/19
5

Aqui está uma solução que combina a conexão do processo de carregamento do módulo com os recursos de criação somente leitura do módulo 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
fonte
11
@ikegami Obrigado, eu fiz as alterações que você recomendou. Boa pegada.
gordonfish
3

Revisei minha solução aqui, para que ela não se baseie mais Readonly.pm, depois de saber que havia perdido uma alternativa muito simples, com base na resposta do m-conrad , que reformulei na abordagem modular que comecei aqui.

Foo.pm (O mesmo que no post de abertura )

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 Atualizado

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;

Execução e saída:

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

Se o sub barinterior Foo.pmtiver um protótipo diferente de uma Foo::barfunção existente , o Perl não o substituirá? Esse parece ser o caso e torna a solução bastante simples:

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

ou tipo a mesma coisa

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

Atualização: não, a razão pela qual isso funciona é que o Perl não redefinirá uma sub-rotina "constante" (com protótipo ()); portanto, essa é apenas uma solução viável se a sua função simulada for constante.

multidão
fonte
BEGIN { *Foo::bar = sub () { 7 } }é melhor escrito comosub Foo::bar() { 7 }
ikegami 30/10/19
11
Re " Perl não redefinirá uma sub-rotina" constante " ". Isso também não é verdade. O sub é redefinido para 42, mesmo quando é um sub constante. A razão pela qual funciona aqui é porque a chamada é incorporada antes da redefinição. Se Evan tivesse usado o mais comum em sub bar { 42 } my $baz = bar();vez de my $baz = bar(); sub bar { 42 }, não funcionaria.
Ikegami 31/10/19
Mesmo na situação muito estreita que funciona, isso é muito barulhento quando avisos são usados. ( Prototype mismatch: sub Foo::bar () vs none at Foo.pm line 5.e Constant subroutine bar redefined at Foo.pm line 5.)
ikegami
1

Vamos ter um concurso de golfe!

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;

Isso apenas prefixa o código do módulo com a substituição do método, que será a primeira linha de código executada após a fase de compilação e antes da fase de execução.

Em seguida, preencha a %INCentrada para que cargas futuras use Foonão puxem a original.

M Conrad
fonte
Solução muito boa. Inicialmente, eu tentei algo assim quando comecei, mas estava faltando a parte da injeção + BEGIN que você havia conectado. Consegui incorporar isso perfeitamente na versão modular da minha resposta que havia publicado anteriormente.
gordonfish
Seu módulo é o vencedor claro do design, mas eu gosto quando o stackoverflow também fornece uma resposta minimalista.
dataless