모듈에서 정의되었지만 런타임 단계에서 사용되기 전에 함수를 덮어 쓰시겠습니까?


20

아주 간단한 것을 봅시다.

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

어쨌든 설정된 test.pl것을 변경 하고 화면에 다른 것을 인쇄 하는 실행 코드를 사용할 수 있습니까?$bazFoo.pm

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

컴파일러 단계에서 위의 내용을 강제로 인쇄 할 수 7있습니까?


1
내부 함수는 아니지만-로 전역 적으로 액세스 할 수 Foo::bar있지만 use Foo컴파일 단계 (이전에 정의 된 경우 재정의 막대)와 Foo의 런타임 단계를 모두 실행합니다. 내가 생각할 수있는 유일한 것은 @INCFoo 가로 드되는 방식을 수정하기 위한 매우 해킹 된 후크 일 것입니다 .
그린 즈

1
함수를 완전히 재정의하고 싶습니까? (인쇄와 같이 조작의 일부만 변경하지 않습니까?) 런타임 전에 재정의해야하는 특정한 이유가 있습니까? 제목은 그것을 요구하지만 질문 본문은 말하거나 정교하지 않습니다. 물론 당신은 그렇게 할 수 있지만 그것이 목적에 맞는지 확실하지 않습니다.
zdim

1
@zdim 예가 있습니다. 해당 모듈의 런타임 단계 전에 다른 모듈에서 사용되는 함수를 재정의 할 수 있기를 원합니다. 그린 즈가 제안한 것
Evan Carroll

@ 그린 츠 제목이 더 나은가요?
Evan Carroll

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;

@INC후크 를 언급하기 위해 @Grinnz에게 찬사를 보냅니다 .


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 재정의 됨"경고가 트리거되며, 서브 루틴을 다시 재정의하는 신호 처리기를 호출하여 7을 반환합니다.


3
내가 본 적이 있다면 Wellll은 해킹입니다.
Evan Carroll

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

m-conrad의 답변Readonly.pm 에 따라 매우 간단한 대안을 놓쳤다는 것을 알게 된 후에 더 이상 의지하지 않기 위해 여기에서 솔루션 을 수정했습니다.

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기능을 펄을 덮어 쓰지 않습니다? 그것은 사실 인 것처럼 보이고 솔루션을 매우 간단하게 만듭니다.

# 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 }
이케 가미

1
" Perl은"일정한 "서브 루틴을 재정의 하지 않을 것입니다. 서브가 상수 서브 인 경우에도 서브는 42로 재정의됩니다. 여기서 작동하는 이유는 재정의 전에 통화가 인라인되기 때문입니다. 에반 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.)
이케 가미

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 측면이 없어서 잘 연결되었습니다. 나는 이것을 이전에 게시 한 모듈 식 버전 의 답변 에 멋지게 통합 할 수있었습니다 .
gordonfish

모듈은 디자인의 확실한 승자이지만, stackoverflow가 미니멀리스트 답변을 제공 할 때도 좋습니다.
데이터
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.