C 프로그램에 Perl을 심기

C 프로그램에 Perl을 심기 (perlembed)

이 문서는 C 프로그램 안에 Perl 인터프리터를 어떻게 심는(embed)지 알려드려요. C와 Perl을 조합해서 쓰고 싶을 때 필요한 지식이에요. 강사가 옆에서 하나씩 짚어 주듯 설명할게요.

무엇을 하고 싶은지 먼저 정해 볼게요:

Perl에서 C를 쓰고 싶다?

perlxstut, perlxs, perlclib, h2xs, perlguts, perlapi를 읽어 보세요.

Perl에서 Unix 프로그램을 쓰고 싶다?

perlfunc에서 역따옴표(back-quote)와 system, exec에 대해 읽어 보세요.

Perl에서 Perl을 쓰고 싶다?

perlfunc"do", "eval", "require", "use"를 읽어 보세요.

C에서 C를 쓰고 싶다?

설계를 다시 생각하세요.

C에서 Perl을 쓰고 싶다?

계속 읽어 주세요...

출처: perlembed - how to embed perl in your C program

본문

DESCRIPTION

PREAMBLE

ROADMAP

C 프로그램 컴파일하기

C 프로그램에 Perl 인터프리터 추가하기

C 프로그램에서 Perl 서브루틴 호출하기

C 프로그램에서 Perl 문장 평가하기

C 프로그램에서 Perl 패턴 매치와 치환 수행하기

C 프로그램에서 Perl 스택 다루기

영속적인 인터프리터 유지하기

여러 인터프리터 인스턴스 유지하기

C 프로그램에서, C 라이브러리를 쓰는 Perl 모듈 사용하기

Win32에서 Perl 임베딩하기

C 프로그램 컴파일하기 (Compiling your C program)

이 문서의 스크립트를 컴파일하는 데 문제가 있다면, 당신만 그런 게 아니에요. 핵심 규칙: 당신의 PERL이 컴파일된 것과 정확히 똑같은 방식으로 프로그램을 컴파일하세요. (소리 질러서 미안해요.)

또한 Perl을 쓰는 모든 C 프로그램은 perl 라이브러리를 링크해야 해요. 그게 뭐냐고요? Perl 그 자체가 C로 쓰여 있어요. perl 라이브러리는 여러분의 perl 실행 파일(/usr/bin/perl이나 그에 해당하는 것)을 만드는 데 쓰인 컴파일된 C 프로그램들의 모음이에요. (따름정리: Perl이 여러분의 머신에 컴파일됐거나 제대로 설치되지 않는 한 C 프로그램에서 Perl을 쓸 수 없어요. lib 디렉터리를 함께 복사하지 않고 perl 실행 파일을 머신에서 머신으로 무심코 복사하면 안 되는 이유예요.)

C에서 Perl을 쓸 때, C 프로그램은—보통—perl 라이브러리가 정의하는 PerlInterpreter 객체를 할당하고, "실행"하고, 해제해요.

여러분의 Perl 사본이 이 문서(버전 5.002 이상)를 담을 만큼 충분히 최신이라면, perl 라이브러리(그리고 함께 필요한 EXTERN.hperl.h)는 이런 디렉터리에 있을 거예요:

/usr/local/lib/perl5/your_architecture_here/CORE

아니면 아마 그냥

/usr/local/lib/perl5/CORE

또는 아마 이런:

/usr/opt/perl5/CORE

CORE를 어디서 찾을지 힌트를 보려면 이 문장을 실행하세요:

perl -MConfig -e 'print $Config{archlib}'

다음 절 "C 프로그램에 Perl 인터프리터 추가하기"의 예시를 내 Linux 머신에서 컴파일하는 방법이에요:

% gcc -O2 -Dbool=char -DHAS_BOOL -I/usr/local/include
-I/usr/local/lib/perl5/i586-linux/5.003/CORE
-L/usr/local/lib/perl5/i586-linux/5.003/CORE
-o interp interp.c -lperl -lm

(전부 한 줄이에요.) 오래된 5.003_05를 돌리는 내 DEC Alpha에서는 주문이 좀 달라요:

% cc -O2 -Olimit 2900 -I/usr/local/include
-I/usr/local/lib/perl5/alpha-dec_osf/5.00305/CORE
-L/usr/local/lib/perl5/alpha-dec_osf/5.00305/CORE -L/usr/local/lib
-D__LANGUAGE_C__ -D_NO_PROTO -o interp interp.c -lperl -lm

뭘 추가해야 하는지 어떻게 알 수 있을까요? Perl이 5.001 이후라고 가정하면 perl -V 명령을 실행하고 "cc"와 "ccflags" 정보에 특히 주의하세요.

머신에 맞는 컴파일러(cc, gcc 등)를 골라야 해요: perl -MConfig -e 'print $Config{cc}'가 무엇을 쓸지 알려줘요.

또한 머신에 맞는 라이브러리 디렉터리(/usr/local/lib/...)를 골라야 해요. 컴파일러가 어떤 함수가 정의되지 않았다고 하거나 -lperl을 찾을 수 없다고 하면 -L 뒤의 경로를 바꿔야 해요. EXTERN.hperl.h를 찾을 수 없다고 하면 -I 뒤의 경로를 바꿔야 해요.

추가 라이브러리를 더 넣어야 할 수도 있어요. 어떤 것들일까요? 아마 이 명령이 출력하는 것들이요:

perl -MConfig -e 'print $Config{libs}'

여러분의 perl 바이너리가 제대로 설정·설치됐다면 ExtUtils::Embed 모듈이 이 모든 정보를 알아서 결정해 줘요:

% cc -o interp interp.c `perl -MExtUtils::Embed -e ccopts -e ldopts`

ExtUtils::Embed 모듈이 Perl 배포판의 일부가 아니라면 https://metacpan.org/pod/ExtUtils::Embed에서 가져올 수 있어요. (이 문서가 Perl 배포판에서 왔다면 5.004 이상을 돌리고 있고 이미 갖고 있는 거예요.)

CPAN의 ExtUtils::Embed 키트는 이 문서에 있는 예시의 전체 소스 코드, 테스트, 추가 예시, 그리고 유용한 다른 정보도 담고 있어요.

C 프로그램에 Perl 인터프리터 추가하기 (Adding a Perl interpreter to your C program)

어떤 의미에서 perl(C 프로그램)은 Perl(언어)을 임베딩한 좋은 예시예요. 그래서 소스 배포판에 포함된 miniperlmain.c로 임베딩을 보여드릴게요. 임베딩의 핵심을 담은 miniperlmain.c의 변형된 비이식 버전이에요:

 #include <EXTERN.h>               /* from the Perl distribution     */
 #include <perl.h>                 /* from the Perl distribution     */

 static PerlInterpreter *my_perl;  /***    The Perl interpreter    ***/

 int main(int argc, char **argv, char **env)
 {
	PERL_SYS_INIT3(&argc,&argv,&env);
        my_perl = perl_alloc();
        perl_construct(my_perl);
	PL_exit_flags |= PERL_EXIT_DESTRUCT_END;
        perl_parse(my_perl, NULL, argc, argv, (char **)NULL);
        perl_run(my_perl);
        perl_destruct(my_perl);
        perl_free(my_perl);
	PERL_SYS_TERM();
	exit(EXIT_SUCCESS);
 }

우리가 env 포인터를 쓰지 않는다는 것에 주목하세요. 보통 perl_parse에 마지막 인자로 넘겨지는데, 여기서는 NULL로 대체돼서 현재 환경이 사용될 거예요.

PERL_SYS_INIT3()와 PERL_SYS_TERM() 매크로는 Perl 인터프리터를 실행하는 데 필요한 C 런타임 환경의 시스템 특유 조정을 제공해요. 인터프리터를 몇 개 만들든 파괴하든 관계없이 한 번만 호출해야 해요. 첫 인터프리터를 만들기 전에 PERL_SYS_INIT3()을, 마지막 인터프리터를 해제한 후에 PERL_SYS_TERM()을 호출하세요.

PERL_SYS_INIT3()이 env를 바꿀 수 있으므로, env를 perl_parse()의 인자로 제공하는 게 더 적절할 수 있어요.

또한 perl_parse()에 어떤 인자를 넘기든 PERL_SYS_INIT3()이 C main()의 argc, argv, env로 (한 번만) 호출돼야 한다는 점에 주목하세요.

argv[argc]는 C의 main 함수에 전달되는 것과 마찬가지로 NULL이어야 함을 명심하세요.

이제 이 프로그램(이름은 interp.c라고 할게요)을 실행 파일로 컴파일해요:

% cc -o interp interp.c `perl -MExtUtils::Embed -e ccopts -e ldopts`

성공적으로 컴파일한 뒤엔 interp를 perl 자체처럼 쓸 수 있어요:

% interp
print "Pretty Good Perl \n";
print "10890 - 9801 is ", 10890 - 9801;
<CTRL-D>
Pretty Good Perl
10890 - 9801 is 1089

또는

% interp -e 'printf("%x", 3735928559)'
deadbeef

C 프로그램의 한가운데서 파일에서 Perl 문장을 읽어 실행할 수도 있어요. perl_run을 호출하기 전에 파일 이름을 *argv[1]*에 두면 돼요.

C 프로그램에서 Perl 서브루틴 호출하기 (Calling a Perl subroutine from your C program)

개별 Perl 서브루틴을 호출하려면 perlcall에 문서화된 call_* 함수들 중 아무거나 쓸 수 있어요. 이 예시에서는 call_argv를 쓸 거예요.

그건 아래 showtime.c라는 프로그램에 있어요.

    #include <EXTERN.h>
    #include <perl.h>

    static PerlInterpreter *my_perl;

    int main(int argc, char **argv, char **env)
    {
        char *args[] = { NULL };
	PERL_SYS_INIT3(&argc,&argv,&env);
        my_perl = perl_alloc();
        perl_construct(my_perl);

        perl_parse(my_perl, NULL, argc, argv, NULL);
	PL_exit_flags |= PERL_EXIT_DESTRUCT_END;

        /*** skipping perl_run() ***/

        call_argv("showtime", G_DISCARD | G_NOARGS, args);

        perl_destruct(my_perl);
        perl_free(my_perl);
	PERL_SYS_TERM();
	exit(EXIT_SUCCESS);
    }

여기서 showtime은 인자를 받지 않는(G_NOARGS) Perl 서브루틴이고, 반환 값은 무시할 거예요(G_DISCARD). 이 플래그와 다른 것들은 perlcall에서 다뤄요.

showtime 서브루틴을 showtime.pl라는 파일에 정의할게요:

print "I shan't be printed.";

sub showtime {
    print time;
}

충분히 간단해요. 이제 컴파일하고 실행해요:

% cc -o showtime showtime.c \
    `perl -MExtUtils::Embed -e ccopts -e ldopts`
% showtime showtime.pl
818284590

1970년 1월 1일(Unix epoch의 시작)부터 내가 이 문장을 쓰기 시작한 순간까지 경과한 초 수를 내놓아요.

이 특정 경우에는 perl_run을 호출할 필요가 없어요. perl_destruct에서 END 블록을 실행하는 PL_exit_flag PERL_EXIT_DESTRUCT_END를 설정했으니까요.

Perl 서브루틴에 인자를 넘기고 싶다면 call_argv에 전달되는 NULL로 끝나는 args 리스트에 문자열을 추가할 수 있어요. 다른 데이터 타입이나 반환 값을 조사하려면 Perl 스택을 조작해야 해요. 그건 "C 프로그램에서 Perl 스택 다루기"에서 보여드릴게요.

C 프로그램에서 Perl 문장 평가하기 (Evaluating a Perl statement from your C program)

Perl은 Perl 코드 조각을 평가하는 API 함수 두 개를 제공해요. "eval_sv" in perlapi"eval_pv" in perlapi예요.

주장하건대, 이 런타임들이 C 프로그램 안에서 Perl 코드 조각을 실행하는 데 필요할 유일한 루틴이에요. 코드는 원하는 만큼 길 수 있고, 여러 문장을 담을 수 있으며, 외부 Perl 파일을 포함하는 데 "use" in perlfunc, "require" in perlfunc, "do" in perlfunc를 쓸 수 있어요.

eval_pv는 개별 Perl 문자열을 평가한 뒤, C 타입으로 강제 변환할 변수를 뽑아낼 수 있게 해줘요. 다음 프로그램 string.c는 Perl 문자열 세 개를 실행해서 첫 번째에서 int를, 두 번째에서 float를, 세 번째에서 char *를 뽑아내요.

#include <EXTERN.h>
#include <perl.h>

static PerlInterpreter *my_perl;

main (int argc, char **argv, char **env)
{
    char *embedding[] = { "", "-e", "0", NULL };

    PERL_SYS_INIT3(&argc,&argv,&env);
    my_perl = perl_alloc();
    perl_construct( my_perl );

    perl_parse(my_perl, NULL, 3, embedding, NULL);
    PL_exit_flags |= PERL_EXIT_DESTRUCT_END;
    perl_run(my_perl);

    /** Treat $x as an integer **/
    eval_pv("$x = 3; $x **= 2", TRUE);
    printf("x = %d\n", SvIV(get_sv("x", 0)));

    /** Treat $x as a float **/
    eval_pv("$x = 3.14; $x **= 2", TRUE);
    printf("x = %f\n", SvNV(get_sv("x", 0)));

    /** Treat $x as a string **/
    eval_pv(
      "$x = 'rekcaH lreP rehtonA tsuJ'; $x = reverse($x);", TRUE);
    printf("x = %s\n", SvPV_nolen(get_sv("x", 0)));

    perl_destruct(my_perl);
    perl_free(my_perl);
    PERL_SYS_TERM();
}

이름에 sv가 들어가는 그 이상한 함수들은 모두 Perl 스칼라를 C 타입으로 변환하는 데 도와줘요. perlgutsperlapi에 설명돼 있어요.

string.c를 컴파일하고 실행하면 int를 만드는 SvIV(), float를 만드는 SvNV(), 문자열을 만드는 SvPV() 사용 결과를 볼 수 있어요:

a = 9
a = 9.859600
a = Just Another Perl Hacker

위 예시에서 우리는 eval된 표현식의 계산 값을 임시 저장할 전역 변수를 만들었어요. 대신 *eval_pv()*에서 반환 값을 가져오는 것도 가능하고 대부분 더 나은 전략이에요. 예시:

...
SV *val = eval_pv("reverse 'rekcaH lreP rehtonA tsuJ'", TRUE);
printf("%s\n", SvPV_nolen(val));
...

이렇게 하면 전역 변수를 만들지 않아 네임스페이스 오염을 피하고 코드도 단순화돼요.

C 프로그램에서 Perl 패턴 매치와 치환 수행하기 (Performing Perl pattern matches and substitutions from your C program)

eval_sv() 함수는 Perl 코드 문자열을 평가하게 해주므로, 그것을 써서 매치와 치환에 "특화"하는 함수 몇 개를 정의할 수 있어요: match(), substitute(), matches().

I32 match(SV *string, char *pattern);

문자열과 패턴(예: m/clasp/ 또는 /\b\w*\b/ — C 프로그램에서는 "/\\b\\w*\\b/"로 나타날 수 있어요)이 주어지면, match()는 문자열이 패턴과 일치하면 1을, 아니면 0을 반환해요.

int substitute(SV **string, char *pattern);

SV에 대한 포인터와 =~ 연산(예: s/bob/robert/g 또는 tr[A-Z][a-z])이 주어지면, substitute()는 연산에 따라 SV 안의 문자열을 수정하고 수행한 치환 수를 반환해요.

SSize_t matches(SV *string, char *pattern, AV **matches);

SV, 패턴, 빈 AV에 대한 포인터가 주어지면, matches()는 리스트 컨텍스트에서 $string =~ $pattern을 평가하고 matches를 배열 요소로 채우며, 찾은 매치 수를 반환해요.

여기 세 가지를 모두 쓰는 샘플 프로그램 match.c가 있어요(긴 줄은 여기서 줄바꿈했어요):

 #include <EXTERN.h>
 #include <perl.h>

 static PerlInterpreter *my_perl;

 /** my_eval_sv(code, error_check)
 ** kinda like eval_sv(),
 ** but we pop the return value off the stack
 **/
 SV* my_eval_sv(SV *sv, I32 croak_on_error)
 {
     dSP;
     SV* retval;

     PUSHMARK(SP);
     eval_sv(sv, G_SCALAR);

     SPAGAIN;
     retval = POPs;
     PUTBACK;

     if (croak_on_error && SvTRUE(ERRSV))
        croak_sv(ERRSV);

     return retval;
 }

 /** match(string, pattern)
 **
 ** Used for matches in a scalar context.
 **
 ** Returns 1 if the match was successful; 0 otherwise.
 **/

 I32 match(SV *string, char *pattern)
 {
     SV *command = newSV(0), *retval;

     sv_setpvf(command, "my $string = '%s'; $string =~ %s",
 	      SvPV_nolen(string), pattern);

     retval = my_eval_sv(command, TRUE);
     SvREFCNT_dec(command);

     return SvIV(retval);
 }

 /** substitute(string, pattern)
 **
 ** Used for =~ operations that
 ** modify their left-hand side (s/// and tr///)
 **
 ** Returns the number of successful matches, and
 ** modifies the input string if there were any.
 **/

 I32 substitute(SV **string, char *pattern)
 {
     SV *command = newSV(0), *retval;

     sv_setpvf(command, "$string = '%s'; ($string =~ %s)",
 	      SvPV_nolen(*string), pattern);

     retval = my_eval_sv(command, TRUE);
     SvREFCNT_dec(command);

     *string = get_sv("string", 0);
     return SvIV(retval);
 }

 /** matches(string, pattern, matches)
 **
 ** Used for matches in a list context.
 **
 ** Returns the number of matches,
 ** and fills in **matches with the matching substrings
 **/

 SSize_t matches(SV *string, char *pattern, AV **match_list)
 {
     SV *command = newSV(0);
     SSize_t num_matches;

     sv_setpvf(command, "my $string = '%s'; @array = ($string =~ %s)",
 	      SvPV_nolen(string), pattern);

     my_eval_sv(command, TRUE);
     SvREFCNT_dec(command);

     *match_list = get_av("array", 0);
     num_matches = av_top_index(*match_list) + 1;

     return num_matches;
 }

 main (int argc, char **argv, char **env)
 {
     char *embedding[] = { "", "-e", "0", NULL };
     AV *match_list;
     I32 num_matches, i;
     SV *text;

     PERL_SYS_INIT3(&argc,&argv,&env);
     my_perl = perl_alloc();
     perl_construct(my_perl);
     perl_parse(my_perl, NULL, 3, embedding, NULL);
     PL_exit_flags |= PERL_EXIT_DESTRUCT_END;

     text = newSV(0);
     sv_setpv(text, "When he is at a convenience store and the "
	"bill comes to some amount like 76 cents, Maynard is "
	"aware that there is something he *should* do, something "
	"that will enable him to get back a quarter, but he has "
	"no idea *what*.  He fumbles through his red squeezey "
	"changepurse and gives the boy three extra pennies with "
	"his dollar, hoping that he might luck into the correct "
	"amount.  The boy gives him back two of his own pennies "
	"and then the big shiny quarter that is his prize. "
	"-RICHH");

     if (match(text, "m/quarter/")) /** Does text contain 'quarter'? **/
 	printf("match: Text contains the word 'quarter'.\n\n");
     else
 	printf("match: Text doesn't contain the word 'quarter'.\n\n");

     if (match(text, "m/eighth/")) /** Does text contain 'eighth'? **/
 	printf("match: Text contains the word 'eighth'.\n\n");
     else
 	printf("match: Text doesn't contain the word 'eighth'.\n\n");

     /** Match all occurrences of /wi../ **/
     num_matches = matches(text, "m/(wi..)/g", &match_list);
     printf("matches: m/(wi..)/g found %d matches...\n", num_matches);

     for (i = 0; i < num_matches; i++)
         printf("match: %s\n",
                  SvPV_nolen(*av_fetch(match_list, i, FALSE)));
     printf("\n");

     /** Remove all vowels from text **/
     num_matches = substitute(&text, "s/[aeiou]//gi");
     if (num_matches) {
 	printf("substitute: s/[aeiou]//gi...%lu substitutions made.\n",
	       (unsigned long)num_matches);
 	printf("Now text is: %s\n\n", SvPV_nolen(text));
     }

     /** Attempt a substitution **/
     if (!substitute(&text, "s/Perl/C/")) {
 	printf("substitute: s/Perl/C...No substitution made.\n\n");
     }

     SvREFCNT_dec(text);
     PL_perl_destruct_level = 1;
     perl_destruct(my_perl);
     perl_free(my_perl);
     PERL_SYS_TERM();
 }

이것은 이 출력을 만들어내요(역시 긴 줄은 여기서 줄바꿈했어요):

match: Text contains the word 'quarter'.

match: Text doesn't contain the word 'eighth'.

matches: m/(wi..)/g found 2 matches...
match: will
match: with

substitute: s/[aeiou]//gi...139 substitutions made.
Now text is: Whn h s t  cnvnnc str nd th bll cms t sm mnt lk 76 cnts,
Mynrd s wr tht thr s smthng h *shld* d, smthng tht wll nbl hm t gt
bck qrtr, bt h hs n d *wht*.  H fmbls thrgh hs rd sqzy chngprs nd
gvs th by thr xtr pnns wth hs dllr, hpng tht h mght lck nt th crrct
mnt.  Th by gvs hm bck tw f hs wn pnns nd thn th bg shny qrtr tht s
hs prz. -RCHH

substitute: s/Perl/C...No substitution made.

C 프로그램에서 Perl 스택 다루기 (Fiddling with the Perl stack from your C program)

스택을 설명하려고 할 때 대부분의 컴퓨터 과학 교재는 용수철로 받치는 카페테리아 접시 기둥에 대해 중얼거려요: 스택에 마지막으로 밀어 넣은 것이 가장 먼저 빼내는 것이죠. 우리 목적에도 그걸로 충분해요: C 프로그램이 몇몇 인자를 "Perl 스택"에 밀어 넣고, 어떤 마법이 일어나는 동안 눈을 감고 있다가, 결과—Perl 서브루틴의 반환 값—를 스택에서 빼내면 돼요.

먼저 newSViv(), sv_setnv(), newAV()와 그 친구들로 C 타입과 Perl 타입 사이를 변환하는 방법을 알아야 해요. perlgutsperlapi에 설명돼 있어요.

그다음 Perl 스택을 조작하는 방법을 알아야 해요. perlcall에 설명돼 있어요.

그것들을 이해하면 C에 Perl을 임베딩하는 건 쉬워요.

C에는 정수 거듭제곱을 위한 내장 함수가 없으니, 우리의 ** 연산자를 C에 쓰이게 만들어 볼게요(생각보다 덜 유용해요. Perl이 C의 pow() 함수로 **를 구현하니까요). 먼저 power.pl에 스텁 거듭제곱 함수를 만들게요:

sub expo {
    my ($x, $y) = @_;
    return $x ** $y;
}

이제 두 인자를 *expo()*에 밀어 넣고 반환 값을 빼내는 데 필요한 모든 perlguts를 담은 함수 *PerlPower()*를 가진 C 프로그램 power.c를 만들게요. 심호흡하세요...

#include <EXTERN.h>
#include <perl.h>

static PerlInterpreter *my_perl;

static void
PerlPower(int a, int b)
{
  dSP;                            /* initialize stack pointer      */
  ENTER;                          /* everything created after here */
  SAVETMPS;                       /* ...is a temporary variable.   */
  PUSHMARK(SP);                   /* remember the stack pointer    */
  XPUSHs(sv_2mortal(newSViv(a))); /* push the base onto the stack  */
  XPUSHs(sv_2mortal(newSViv(b))); /* push the exponent onto stack  */
  PUTBACK;                      /* make local stack pointer global */
  call_pv("expo", G_SCALAR);      /* call the function             */
  SPAGAIN;                        /* refresh stack pointer         */
                                /* pop the return value from stack */
  printf ("%d to the %dth power is %d.\n", a, b, POPi);
  PUTBACK;
  FREETMPS;                       /* free that return value        */
  LEAVE;                       /* ...and the XPUSHed "mortal" args.*/
}

int main (int argc, char **argv, char **env)
{
  char *my_argv[] = { "", "power.pl", NULL };

  PERL_SYS_INIT3(&argc,&argv,&env);
  my_perl = perl_alloc();
  perl_construct( my_perl );

  perl_parse(my_perl, NULL, 2, my_argv, (char **)NULL);
  PL_exit_flags |= PERL_EXIT_DESTRUCT_END;
  perl_run(my_perl);

  PerlPower(3, 4);                      /*** Compute 3 ** 4 ***/

  perl_destruct(my_perl);
  perl_free(my_perl);
  PERL_SYS_TERM();
  exit(EXIT_SUCCESS);
}

컴파일하고 실행해요:

% cc -o power power.c `perl -MExtUtils::Embed -e ccopts -e ldopts`

% power
3 to the 4th power is 81.

영속적인 인터프리터 유지하기 (Maintaining a persistent interpreter)

대화형이거나 잠재적으로 오래 실행되는 애플리케이션을 개발할 때, 새 인터프리터를 여러 번 할당·구성하는 대신 영속적인 인터프리터를 유지하는 게 좋아요. 주요 이유는 속도예요: Perl이 메모리에 한 번만 로드되니까요.

하지만 영속적인 인터프리터를 쓸 때는 네임스페이스와 변수 스코프에 더 주의해야 해요. 이전 예시들에서 우리는 기본 패키지 main의 전역 변수를 써 왔어요. 어떤 코드가 실행될지 정확히 알았고, 변수 충돌과 터무니없는 심볼 테이블 성장을 피할 수 있다고 가정했죠.

애플리케이션이 가끔 임의 파일에서 Perl 코드를 실행하는 서버라고 해 봐요. 서버는 어떤 코드를 실행할지 알 방법이 없어요. 아주 위험해요.

파일이 perl_parse()로 당겨지고 새로 구성된 인터프리터로 컴파일된 뒤, 나중에 perl_destruct()로 정리되면, 대부분의 네임스페이스 문제로부터 보호돼요.

이 시나리오에서 네임스페이스 충돌을 피하는 한 방법은 파일 이름을 보장된 고유 패키지 이름으로 변환한 뒤, "eval" in perlfunc로 그 패키지 안에 코드를 컴파일하는 거예요. 아래 예시에서 각 파일은 한 번만 컴파일돼요. 또는 애플리케이션이 더 이상 필요 없어진 뒤 그 파일과 연관된 심볼 테이블을 정리하는 선택을 할 수 있어요. "call_argv" in perlapi를 써서 persistent.pl 파일에 사는 Embed::Persistent::eval_file 서브루틴을 호출하고 파일 이름과 불리언 정리/캐시 플래그를 인자로 넘길 거예요.

사용하는 각 파일마다 프로세스가 계속 커진다는 점을 주목하세요. 게다가 AUTOLOAD된 서브루틴이나 Perl의 심볼 테이블을 키우는 다른 조건이 있을 수 있어요. 메모리 소비가 최소화되도록 프로세스 크기를 추적하는 로직이나 특정 요청 수 후에 스스로 재시작하는 로직을 추가하고 싶을 거예요. 가능하면 변수를 "my" in perlfunc로 스코프 지정하는 것도 원할 거예요.

 package Embed::Persistent;
 #persistent.pl

 use strict;
 our %Cache;
 use Symbol qw(delete_package);

 sub valid_package_name {
     my($string) = @_;
     $string =~ s/([^A-Za-z0-9\/])/sprintf("_%2x",unpack("C",$1))/eg;
     # second pass only for words starting with a digit
     $string =~ s|/(\d)|sprintf("/_%2x",unpack("C",$1))|eg;

     # Dress it up as a real package name
     $string =~ s|/|::|g;
     return "Embed" . $string;
 }

 sub eval_file {
     my($filename, $delete) = @_;
     my $package = valid_package_name($filename);
     my $mtime = -M $filename;
     if(defined $Cache{$package}{mtime}
        &&
        $Cache{$package}{mtime} <= $mtime)
     {
        # we have compiled this subroutine already,
        # it has not been updated on disk, nothing left to do
        print STDERR "already compiled $package->handler\n";
     }
     else {
        local *FH;
        open FH, $filename or die "open '$filename' $!";
        local($/) = undef;
        my $sub = <FH>;
        close FH;

        #wrap the code into a subroutine inside our unique package
        my $eval = qq{package $package; sub handler { $sub; }};
        {
            # hide our variables within this block
            my($filename,$mtime,$package,$sub);
            eval $eval;
        }
        die $@ if $@;

        #cache it unless we're cleaning out each time
        $Cache{$package}{mtime} = $mtime unless $delete;
     }

     eval {$package->handler;};
     die $@ if $@;

     delete_package($package) if $delete;

     #take a look if you want
     #print Devel::Symdump->rnew($package)->as_string, $/;
 }

 1;

 __END__
/* persistent.c */
#include <EXTERN.h>
#include <perl.h>

/* 1 = clean out filename's symbol table after each request,
   0 = don't
*/
#ifndef DO_CLEAN
#define DO_CLEAN 0
#endif

#define BUFFER_SIZE 1024

static PerlInterpreter *my_perl = NULL;

int
main(int argc, char **argv, char **env)
{
    char *embedding[] = { "", "persistent.pl", NULL };
    char *args[] = { "", DO_CLEAN, NULL };
    char filename[BUFFER_SIZE];
    int failing, exitstatus;

    PERL_SYS_INIT3(&argc,&argv,&env);
    if((my_perl = perl_alloc()) == NULL) {
       fprintf(stderr, "no memory!");
       exit(EXIT_FAILURE);
    }
    perl_construct(my_perl);

    PL_origalen = 1; /* don't let $0 assignment update the
                        proctitle or embedding[0] */
    failing = perl_parse(my_perl, NULL, 2, embedding, NULL);
    PL_exit_flags |= PERL_EXIT_DESTRUCT_END;
    if(!failing)
	failing = perl_run(my_perl);
    if(!failing) {
       while(printf("Enter file name: ") &&
             fgets(filename, BUFFER_SIZE, stdin)) {

           filename[strlen(filename)-1] = '\0'; /* strip \n */
           /* call the subroutine,
                    passing it the filename as an argument */
           args[0] = filename;
           call_argv("Embed::Persistent::eval_file",
                          G_DISCARD | G_EVAL, args);

           /* check $@ */
           if(SvTRUE(ERRSV))
               fprintf(stderr, "eval error: %s\n", SvPV_nolen(ERRSV));
       }
    }

    PL_perl_destruct_level = 0;
    exitstatus = perl_destruct(my_perl);
    perl_free(my_perl);
    PERL_SYS_TERM();
    exit(exitstatus);
}

이제 컴파일해요:

% cc -o persistent persistent.c \
      `perl -MExtUtils::Embed -e ccopts -e ldopts`

예시 스크립트 파일이에요:

#test.pl
my $string = "hello";
foo($string);

sub foo {
    print "foo says: @_\n";
}

이제 실행해요:

% persistent
Enter file name: test.pl
foo says: hello
Enter file name: test.pl
already compiled Embed::test_2epl->handler
foo says: hello
Enter file name: ^C

END 블록 실행 (Execution of END blocks)

전통적으로 END 블록은 perl_run의 끝에서 실행됐어요. 이는 perl_run을 결코 호출하지 않는 애플리케이션에 문제를 일으켜요. perl 5.7.2부터 PL_exit_flags |= PERL_EXIT_DESTRUCT_END를 지정해 새 동작을 얻을 수 있어요. 이는 또한 perl_parse가 실패해도 END 블록 실행을 가능하게 하고, perl_destruct가 종료 값을 반환하게 해요.

$0 할당

perl 스크립트가 $0에 값을 할당하면 perl 런타임이 perl_parse()에 전달된 argv가 가리키는 메모리를 갱신하고, 가능한 곳에서는 setproctitle() 같은 API 함수도 호출해서, 그 값이 "ps"가 보고하는 프로그램 이름으로 나타나게 하려 해요. 이 동작은 perl을 임베딩할 때 적절하지 않을 수 있고, perl_parse()가 호출되기 전에 변수 PL_origalen에 값 1을 할당해 비활성화할 수 있어요.

위의 persistent.c 예시는 예를 들어 PL_origalen = 1; 할당이 제거되면 $0에 할당할 때 segfault할 가능성이 높아요. perl이 embedding[] 문자열의 읽기 전용 메모리에 쓰려고 하기 때문이에요.

여러 인터프리터 인스턴스 유지하기 (Maintaining multiple interpreter instances)

일부 드문 애플리케이션은 세션 동안 인터프리터를 두 개 이상 만들어야 해요. 그런 애플리케이션은 가끔 인터프리터와 연관된 자원을 해제하기로 결정할 수 있어요.

프로그램은 이것이 다음 인터프리터가 구성되기 전에 일어나도록 주의해야 해요. 기본적으로 perl이 특별한 옵션 없이 빌드되면, 전역 변수 PL_perl_destruct_level0으로 설정돼요. 프로그램이 수명 내내 단일 인터프리터만 만든다면 보통 추가 정리가 필요 없기 때문이에요.

PL_perl_destruct_level1로 설정하면 모든 게 삐걱거리지 않게 깨끗해져요:

while(1) {
    ...
    /* reset global variables here with PL_perl_destruct_level = 1 */
    PL_perl_destruct_level = 1;
    perl_construct(my_perl);
    ...
    /* clean and reset _everything_ during perl_destruct */
    PL_perl_destruct_level = 1;
    perl_destruct(my_perl);
    perl_free(my_perl);
    ...
    /* let's go do it again! */
}

*perl_destruct()*가 호출되면 인터프리터의 구문 파싱 트리와 심볼 테이블이 정리되고 전역 변수가 리셋돼요. PL_perl_destruct_level에 대한 두 번째 할당은 perl_construct가 그것을 0으로 리셋하기 때문에 필요해요.

이제 같은 시간에 인터프리터 인스턴스가 두 개 이상 실행된다고 가정해 봐요. 이것은 가능하지만, perl을 빌드할 때 Configure 옵션 -Dusemultiplicity-Dusethreads -Duseithreads 옵션 중 하나를 썼을 때만 가능해요. 기본적으로 이 Configure 옵션 중 하나를 켜면 인터프리터별 전역 변수 PL_perl_destruct_level1로 설정돼서 철저한 정리가 자동이고 인터프리터 변수가 올바르게 초기화돼요. 두 개 이상의 인터프리터를 동시에 실행할 생각이 없고 위 예시처럼 순차적으로 실행하려 해도, -Dusemultiplicity 옵션으로 perl을 빌드하는 것을 권장해요. 그렇지 않으면 연속 실행 사이에 일부 인터프리터 변수가 올바르게 초기화되지 않아 애플리케이션이 크래시할 수 있어요.

(예전에는 스레드의 두 형태가 있었고 ithreads가 나중에 나타났어요. 하지만 ithreads가 더 우수함이 증명됐고, 이전 모델에 대한 지원은 Perl v5.10에서 제거됐어요. 하지만 문서와 코드는 이전 용어에서 정화된 적이 없고, 어쩌면 ithreads를 대체할 새 모델이 올 수도 있어서 둘 다 유지하는 게 가장 좋아 보여요. 사용된 스레드 모델을 신경 쓰지 않는다면, 미래 발전에 따라 코드가 바뀌지 않도록 usethreads를 쓰세요. 코드가 ithreads 모델에 의존한다면 useithreads를 써서 항상 그것을 얻도록 하세요. 이것은 C 언어 심볼 USE_THREADSUSE_ITHREADS에도 동일하게 적용돼요.)

또한 "Dealing with embedded perls and threads" in perlclib을 보세요.

서로 다른 스레드에서 여러 인터프리터를 동시에 실행하려면 -Dusemultiplicity 대신 -Dusethreads -Duseithreads를 쓰는 게 더 적절해요. 시스템의 스레드 라이브러리를 인터프리터와 링크하는 지원을 활성화하기 때문이에요.

한번 해 봐요:

#include <EXTERN.h>
#include <perl.h>

/* we're going to embed two interpreters */

#define SAY_HELLO "-e", "print qq(Hi, I'm $^X\n)"

int main(int argc, char **argv, char **env)
{
    PerlInterpreter *one_perl, *two_perl;
    char *one_args[] = { "one_perl", SAY_HELLO, NULL };
    char *two_args[] = { "two_perl", SAY_HELLO, NULL };

    PERL_SYS_INIT3(&argc,&argv,&env);
    one_perl = perl_alloc();
    two_perl = perl_alloc();

    PERL_SET_CONTEXT(one_perl);
    perl_construct(one_perl);
    PERL_SET_CONTEXT(two_perl);
    perl_construct(two_perl);

    PERL_SET_CONTEXT(one_perl);
    perl_parse(one_perl, NULL, 3, one_args, (char **)NULL);
    PERL_SET_CONTEXT(two_perl);
    perl_parse(two_perl, NULL, 3, two_args, (char **)NULL);

    PERL_SET_CONTEXT(one_perl);
    perl_run(one_perl);
    PERL_SET_CONTEXT(two_perl);
    perl_run(two_perl);

    PERL_SET_CONTEXT(one_perl);
    perl_destruct(one_perl);
    PERL_SET_CONTEXT(two_perl);
    perl_destruct(two_perl);

    PERL_SET_CONTEXT(one_perl);
    perl_free(one_perl);
    PERL_SET_CONTEXT(two_perl);
    perl_free(two_perl);
    PERL_SYS_TERM();
    exit(EXIT_SUCCESS);
}

PERL_SET_CONTEXT() 호출에 주목하세요. 이것들은 특정 프로세스나 스레드에서 실행될 수 있는 "현재" 인터프리터가 어느 것인지 추적하는 전역 상태를 초기화하는 데 필요해요. 인터프리터가 두 개 이상 있고 두 인터프리터 모두에 인터리브 방식으로 perl API 호출을 하고 있다면 항상 써야 해요.

interp가 그것을 만들지 않은(perl_alloc()이나 더 난해한 perl_clone()을 써서) 스레드에서 사용될 때마다 PERL_SET_CONTEXT(interp)도 호출해야 해요.

평소처럼 컴파일해요:

% cc -o multiplicity multiplicity.c \
 `perl -MExtUtils::Embed -e ccopts -e ldopts`

실행하고, 실행해요:

% multiplicity
Hi, I'm one_perl
Hi, I'm two_perl

C 프로그램에서, C 라이브러리를 쓰는 Perl 모듈 사용하기 (Using Perl modules, which themselves use C libraries, from your C program)

위 예시들로 놀면서 Socket 같은, 그 자체로 C나 C++ 라이브러리를 쓰는 Perl 모듈을 *use()*하는 스크립트를 심으려 했다면 아마 이런 일이 생겼을 거예요:

Can't load module Socket, dynamic loading not available in this perl.
 (You may need to build a new perl executable which either supports
 dynamic loading or has the Socket module statically linked into it.)

뭐가 문제일까요?

여러분의 인터프리터는 스스로 이런 확장과 통신하는 법을 몰라요. 약간의 접착제(glue)가 도움이 될 거예요. 지금까지 *perl_parse()*를 호출하면서 두 번째 인자에 NULL을 넘겨 왔어요:

perl_parse(my_perl, NULL, argc, my_argv, NULL);

그게 Perl과 링크된 C/C++ 루틴 사이의 초기 접촉을 만들 접착제 코드를 넣을 자리예요. Perl이 어떻게 하는지 perlmain.c의 몇몇 조각을 보겠어요:

static void xs_init (pTHX);

EXTERN_C void boot_DynaLoader (pTHX_ CV* cv);
EXTERN_C void boot_Socket (pTHX_ CV* cv);

EXTERN_C void
xs_init(pTHX)
{
       char *file = __FILE__;
       /* DynaLoader is a special case */
       newXS("DynaLoader::boot_DynaLoader", boot_DynaLoader, file);
       newXS("Socket::bootstrap", boot_Socket, file);
}

간단히 말하면: Perl 실행 파일과 링크된 각 확장(컴퓨터에서의 초기 구성이나 새 확장을 추가할 때 결정)에 대해, 확장의 루틴을 통합할 Perl 서브루틴이 만들어져요. 보통 그 서브루틴은 *Module::bootstrap()*이라 불리고 use Module이라고 말할 때 호출돼요. 차례로 이것은 XSUB boot_Module에 연결되는데, 이는 확장의 각 XSUB에 대한 Perl 대응물을 만들어요. 이 부분은 걱정하지 마세요. xsubpp와 확장 작성자에게 맡기세요. 확장이 동적으로 로드되면 DynaLoader가 그 자리에서 *Module::bootstrap()*을 만들어요. 사실 동작하는 DynaLoader가 있다면 다른 확장을 정적으로 링크할 필요가 거의 없어요.

이 코드를 가지면 *perl_parse()*의 두 번째 인자에 쑤셔 넣어요:

perl_parse(my_perl, xs_init, argc, my_argv, NULL);

그리고 컴파일해요:

% cc -o interp interp.c `perl -MExtUtils::Embed -e ccopts -e ldopts`

% interp
  use Socket;
  use SomeDynamicallyLoadedModule;

  print "Now I can use extensions!\n"'

ExtUtils::Embedxs_init 접착제 코드 작성을 자동화할 수도 있어요.

% perl -MExtUtils::Embed -e xsinit -- -o perlxsi.c
% cc -c perlxsi.c `perl -MExtUtils::Embed -e ccopts`
% cc -c interp.c  `perl -MExtUtils::Embed -e ccopts`
% cc -o interp perlxsi.o interp.o `perl -MExtUtils::Embed -e ldopts`

자세한 내용은 perlxs, perlclib, perlguts, perlapi를 참고하세요.

POSIX 로케일과 임베디드 Perl 사용하기 (Using embedded Perl with POSIX locales)

(이것들에 대한 정보는 perllocale을 보세요.) Perl 인터프리터가 보통 시작할 때 시스템에 시스템 기본 로케일을 쓰겠다고 알려요. 이것은 자주—항상은 아니지만—"C"나 "POSIX" 로케일이에요. perl 코드 안에 "use locale"이 없으면 대부분 효과가 없지만("Not within the scope of "use locale"" in perllocale 참고), 또한 임베디드 perl에서 쓰고 싶은 로케일이 시스템 기본과 같으면 문제가 없어요. 하지만 시스템 기본이 아닌 로케일을 설정하고 쓰고 싶다면 이건 작동하지 않아요. Perl v5.20부터 임베디드 Perl 인터프리터에게 로케일이 이미 제대로 설정돼 있고 자체 정상 초기화를 건너뛰라고 말할 수 있어요. 환경 변수 PERL_SKIP_LOCALE_INIT이 설정돼 있으면(0이나 ""으로 설정돼 있어도) 건너뜁니다. 이 능력이 있는 perl은 C 전처리기 심볼 HAS_SKIP_LOCALE_INIT을 정의해요. 이는 여러 Perl 버전과 작동해야 하는 코드가 오래된 Perl을 만났을 때 어떤 식으로든 work-around를 할 수 있게 해줘요.

프로그램이 POSIX 2008 다중 스레드 로케일 기능을 쓰고 있다면, Perl 인터프리터를 시작하기 전에 전역 로케일로 전환해 제대로 설정해야 해요. 그러면 Perl이 스레드 안전 함수를 쓰도록 제대로 돌아갈 거예요.

Hiding Perl_

Perl 공개 API의 짧은 형태를 완전히 숨기려면 컴파일 플래그에 -DPERL_NO_SHORT_NAMES를 추가하세요. 이는 예를 들어 이렇게 쓰는 대신:

warn("%d bottles of beer on the wall", bottlecount);

명시적 전체 형태를 써야 한다는 뜻이에요:

Perl_warn(aTHX_ "%d bottles of beer on the wall", bottlecount);

(aTHX_에 대한 설명은 "Background and MULTIPLICITY" in perlguts를 보세요.) 짧은 형태를 숨기는 것은 다른 소프트웨어 패키지와의 온갖 성가신(C 전처리기든 아니든) 충돌을 피하는 데 아주 유용해요. (Perl은 이런 짧은 이름으로 약 2400개의 API를 정의하는데, 몇 백 개쯤 더 빼거나 더할 수 있어서 충돌 여지가 충분해요.)

MORAL

가끔 C에서 더 빠른 코드를 쓸 수 있지만, Perl에서는 항상 더 빨리 코드를 쓸 수 있어요. 서로에게서 쓸 수 있으니, 원하는 대로 결합하세요.

AUTHOR

Jon Orwant [email protected]와 Doug MacEachern [email protected], 그리고 Tim Bunce, Tom Christiansen, Guy Decoux, Hallvard Furuseth, Dov Grobgeld, Ilya Zakharevich의 작은 기여가 함께했어요.

Doug MacEachern은 The Perl Journal(http://www.tpj.com/) 제1권 4호에 임베딩에 관한 기사를 썼어요. Doug는 또한 가장 널리 쓰이는 Perl 임베딩인 mod_perl 시스템(perl.apache.org)의 개발자이기도 해요. 이것은 Apache 웹 서버에 Perl을 심어요. Oracle, Binary Evolution, ActiveState, Ben Sugars의 nsapi_perl은 Oracle·Netscape·Internet Information Server Perl 플러그인에 이 모델을 사용했어요.

Copyright (C) 1995, 1996, 1997, 1998 Doug MacEachern and Jon Orwant. All Rights Reserved.

이 문서는 Perl 자체와 같은 조건으로 배포될 수 있어요.

더 알아보기 (Learn more)