XS 튜토리얼
XS 튜토리얼 (perlxstut)
C로 뭔가를 뚝딱 만들고 그걸 Perl에서 바로 부르고 싶을 때, XS는 그 다리 역할을 해요. 이 문서는 XS를 처음 시작하는 사람을 위한 단계별 튜토리얼이에요. 순서대로 따라 하면 XSUB 작성, h2xs로 모듈 스켈레톤 만들기, 메모리 관리, 참조·배열·해시 다루기까지 차근차근 익힐 수 있어요.
본문
튜토리얼 소개
이 튜토리얼은 Perl 확장 모듈을 만드는 데 필요한 과정을 차근차근 알려 주는 문서예요. 독자는 perlguts, perlclib, perlapi, perlxs를 참고할 수 있다고 가정해요.
튜토리얼은 아주 간단한 예제에서 시작해서 점점 복잡해져요. 새로운 예제가 추가될 때마다 새 기능이 덧붙여지는 방식이죠. 어떤 개념은 나중에 가서야 완전히 설명되는 경우도 있어요. 천천히 확장 모듈 만들기에 익숙해지도록 의도적으로 그렇게 구성한 것이니, 앞부분에서 막혀도 너무 걱정하지 마세요.
이 튜토리얼은 유닉스 관점에서 작성되었어요. 다른 플랫폼(예: Win32)에서 다르게 동작하는 부분이 알려져 있으면 그때그때 언급해 둘게요.
make
이 튜토리얼은 Perl이 사용하도록 설정된 make 프로그램이 make라고 가정해요. 뒤에서 나오는 예제에서 "make"를 그대로 쓰기보다는, Perl이 설정된 make 프로그램 이름으로 바꿔서 실행해야 할 수도 있어요. perl -V:make를 실행하면 그 이름이 무엇인지 알 수 있어요.
버전 주의사항
일반적인 배포를 목적으로 Perl 확장을 만들 때는, 여러분의 머신에 있는 Perl 버전과는 다른 버전의 Perl로 확장이 사용될 것임을 예상해야 해요. 이 문서를 읽고 있다면 여러분 머신의 Perl 버전은 아마 5.005 이상일 거예요. 하지만 여러분 확장의 사용자는 더 오래된 버전을 갖고 있을 수도 있어요.
어떤 종류의 호환성 문제가 생길 수 있는지, 그리고 드물게 여러분 머신의 Perl이 이 문서보다 오래된 버전일 때는, 아래의 "이 예제들의 문제 해결(Troubleshooting these Examples)" 섹션을 참고해요.
확장이 이전 Perl 릴리스에서는 없는 기능을 사용한다면, 사용자들은 조기에 의미 있는 경고를 받으면 고마워해요. 아마 README 파일에 그 정보를 넣게 될 거예요. 하지만 요즘 확장 설치가 CPAN.pm 모듈이나 다른 도구에 의해 자동으로 수행될 수도 있어요.
MakeMaker 기반 설치에서는 Makefile.PL이 버전 검사를 수행할 수 있는 가장 이른 기회예요. 이를 위해 Makefile.PL에 이런 것을 넣을 수 있어요:
eval { require 5.007 }
or die <<EOD;
############
### This module uses frobnication framework which is not available
### before version 5.007 of Perl. Upgrade your Perl before
### installing Kara::Mba.
############
EOD
동적 로딩 대 정적 로딩
흔히 시스템이 동적 로딩 기능이 없으면 XSUB를 만들 수 없다고 생각해요. 그건 틀렸어요. 만들 수는 있어요. 다만 XSUB 서브루틴을 Perl의 나머지 부분과 함께 링크해서 새 실행 파일을 만들어야 해요. 이 상황은 Perl 4와 비슷해요.
이 튜토리얼은 그런 시스템에서도 그대로 사용할 수 있어요. XSUB 빌드 메커니즘이 시스템을 검사해서, 가능하면 동적으로 로딩 가능한 라이브러리를 만들고, 그렇지 않으면 정적 라이브러리를 만든 다음 경우에 따라 그 정적 라이브러리를 링크한 새 정적 링크 실행 파일을 만들어요.
동적 로딩이 가능한 시스템에서도 정적 링크 실행 파일을 만들고 싶다면, 아래 모든 예제에서 인수 없는 "make" 명령 대신 "make perl"을 실행하면 돼요.
정적 링크 실행 파일을 선택해서 만들었다면, "make test" 대신 "make test_static"을 실행해야 해요. 동적 로딩 가능한 라이브러리를 전혀 만들 수 없는 시스템에서는 그냥 "make test"만 실행해도 충분해요.
스레드와 PERL_NO_GET_CONTEXT
스레드 빌드에서 perl은 현재 스레드의 컨텍스트 포인터를 요구해요. PERL_NO_GET_CONTEXT 없이는 perl이 컨텍스트를 가져오기 위해 함수를 호출해요.
성능 향상을 위해 아래처럼 include 해 주세요:
#define PERL_NO_GET_CONTEXT
자세한 내용은 perlguts를 참고해요.
튜토리얼 1: 첫 번째 확장 모듈 (EXAMPLE 1)
첫 확장은 아주 단순해요. 확장의 루틴을 호출하면 잘 알려진 메시지를 출력하고 돌아와요.
h2xs -A -n Mytest를 실행해요. 이 명령은 Mytest라는 디렉토리를 만들어요. 현재 작업 디렉토리에 ext/ 디렉토리가 있다면 그 아래에 만들어질 수도 있어요. Mytest 디렉토리 아래에는 MANIFEST, Makefile.PL, lib/Mytest.pm, Mytest.xs, t/Mytest.t, Changes 등 여러 파일이 만들어져요.
MANIFEST 파일은 Mytest 디렉토리에 막 만들어진 모든 파일의 이름을 담고 있어요.
Makefile.PL 파일은 대략 이렇게 생겼어요:
use ExtUtils::MakeMaker;
# See lib/ExtUtils/MakeMaker.pm for details of how to influence
# the contents of the Makefile that is written.
WriteMakefile(
NAME => 'Mytest',
VERSION_FROM => 'lib/Mytest.pm', # finds $VERSION
LIBS => [''], # e.g., '-lm'
DEFINE => '', # e.g., '-DHAVE_SOMETHING'
INC => '-I', # e.g., '-I. -I/usr/include/other'
);
Mytest.pm 파일은 대략 이렇게 시작해요:
package Mytest;
use 5.008008;
use strict;
use warnings;
require Exporter;
our @ISA = qw(Exporter);
our %EXPORT_TAGS = ( 'all' => [ qw(
) ] );
our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
our @EXPORT = qw(
);
our $VERSION = '0.01';
require XSLoader;
XSLoader::load('Mytest', $VERSION);
# Preloaded methods go here.
1;
__END__
# Below is the stub of documentation for your module. You better
# edit it!
.pm 파일의 나머지 부분은 확장에 대한 문서를 제공하기 위한 샘플 코드를 담고 있어요.
마지막으로 Mytest.xs 파일은 대략 이렇게 생겼어요:
#define PERL_NO_GET_CONTEXT
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#include "ppport.h"
MODULE = Mytest PACKAGE = Mytest
.xs 파일의 끝에 이 내용을 추가해 편집해요:
void
hello()
CODE:
printf("Hello, world!\n");
"CODE:" 줄에서 시작하는 줄들이 들여쓰기가 되지 않아도 괜찮아요. 다만 가독성을 위해 CODE:는 한 단계, 그 뒤 줄들은 한 단계 더 들여쓰기를 권장해요.
이제 perl Makefile.PL을 실행해요. 이 명령은 make가 필요로 하는 실제 Makefile을 만들어요. 출력은 대략 이렇게 생겼어요:
% perl Makefile.PL
Checking if your kit is complete...
Looks good
Writing Makefile for Mytest
%
이제 make를 실행하면 이런 출력이 나와요 (긴 줄은 가독성을 위해 줄였고, 관련 없는 줄은 삭제했어요):
% make
cp lib/Mytest.pm blib/lib/Mytest.pm
perl xsubpp -typemap typemap Mytest.xs > Mytest.xsc && \
mv Mytest.xsc Mytest.c
Please specify prototyping behavior for Mytest.xs (see perlxs manual)
cc -c Mytest.c
Running Mkbootstrap for Mytest ()
chmod 644 Mytest.bs
rm -f blib/arch/auto/Mytest/Mytest.so
cc -shared -L/usr/local/lib Mytest.o -o blib/arch/auto/Mytest/Mytest.so
chmod 755 blib/arch/auto/Mytest/Mytest.so
cp Mytest.bs blib/arch/auto/Mytest/Mytest.bs
chmod 644 blib/arch/auto/Mytest/Mytest.bs
Manifying blib/man3/Mytest.3pm
%
"prototyping behavior"에 대한 줄은 안심하고 무시해도 돼요. perlxs의 "PROTOTYPES: 키워드"에서 설명하고 있어요.
Perl은 테스트 스크립트를 쉽게 작성하는 특별한 방식을 가지고 있어요. 하지만 이 예제에서는 우리가 직접 테스트 스크립트를 만들어 볼게요. hello라는 파일을 만들고 이렇게 채워요:
use ExtUtils::testlib;
use Mytest;
Mytest::hello();
이제 스크립트를 실행하면 다음 출력을 볼 수 있어요:
% perl hello
Hello, world!
%
튜토리얼 2: 숫자 인수 받는 부수틴 (EXAMPLE 2)
이제 확장에 숫자 인수 하나를 받아서, 그 숫자가 짝수면 1을, 홀수면 0을 돌려주는 서브루틴을 추가해 볼게요.
Mytest.xs 끝에 다음을 추가해요:
int
is_even(input)
int input
CODE:
RETVAL = (input % 2 == 0);
OUTPUT:
RETVAL
"int input" 줄의 시작에 공백이 없어도 되지만, 가독성을 높이려면 두는 게 좋아요. 그 줄 끝에 세미콜론을 붙이는 것도 선택 사항이에요. "int"와 "input" 사이에는 어떤 종류와 양의 공백이든 넣을 수 있어요.
이제 make를 다시 실행해서 새 공유 라이브러리를 다시 빌드해요.
확장이 잘 동작하는지 확인하려면 Mytest.t 파일을 봐야 해요. 이 파일은 Perl 자체가 쓰는 것과 같은 종류의 테스트 구조를 흉내 내도록 설정되어 있어요. 테스트 스크립트 안에서는 확장의 동작을 확인하는 여러 테스트를 수행하고, 테스트가 맞으면 "ok"를, 틀리면 "not ok"를 출력해요.
use Test::More tests => 4;
BEGIN { use_ok('Mytest') };
#########################
# Insert your test code below, the Test::More module is use()ed here
# so read its man page ( perldoc Test::More ) for help writing this
# test script.
is( Mytest::is_even(0), 1 );
is( Mytest::is_even(1), 0 );
is( Mytest::is_even(2), 1 );
테스트 스크립트는 "make test" 명령을 통해 호출할 거예요. 출력은 대략 이렇게 보일 거예요:
%make test
PERL_DL_NONLAZY=1 /usr/bin/perl "-MExtUtils::Command::MM" "-e"
"test_harness(0, 'blib/lib', 'blib/arch')" t/*.t
t/Mytest....ok
All tests successful.
Files=1, Tests=4, 0 wallclock secs ( 0.03 cusr + 0.00 csys = 0.03 CPU)
%
지금까지 무슨 일이 일어난 걸까요?
h2xs 프로그램은 확장을 만들 때의 출발점이에요. 뒤의 예제에서는 h2xs를 사용해 헤더 파일을 읽고 C 루틴에 연결하는 템플릿을 생성하는 방법을 볼 거예요.
h2xs는 확장 디렉토리에 여러 파일을 만들어요. Makefile.PL 파일은 확장을 빌드할 실제 Makefile을 생성하는 perl 스크립트예요. 나중에 좀 더 자세히 볼게요.
.pm 파일과 .xs 파일은 확장의 핵심을 담고 있어요. .xs 파일은 확장을 구성하는 C 루틴을 담고 있고, .pm 파일은 Perl이 확장을 어떻게 로드할지 알려 주는 루틴을 담고 있어요.
Makefile을 생성하고 make를 실행하면 현재 작업 디렉토리에 blib("build library"의 줄임말)이라는 디렉토리가 생겨요. 이 디렉토리는 우리가 빌드할 공유 라이브러리를 담게 돼요. 테스트가 끝나면 최종 위치에 설치할 수 있어요.
"make test"를 통해 테스트 스크립트를 호출한 것은 매우 중요한 일을 했어요. 확장의 일부인 여러 파일을 찾을 수 있도록 perl을 모든 -I 인수와 함께 호출한 거예요. 확장을 아직 테스트하는 동안에는 반드시 "make test"를 사용하는 것이 매우 중요해요. 테스트 스크립트를 혼자서 실행하면 치명적인 오류가 날 거예요. "make test"를 사용해야 하는 또 다른 이유는, 이미 존재하는 버전의 업그레이드를 테스트할 때 "make test"를 사용해야 기존 버전이 아니라 새 확장을 테스트하게 되기 때문이에요.
Perl이 use extension;을 보면, use된 확장과 같은 이름에 .pm 접미사가 붙은 파일을 찾아요. 그 파일을 찾지 못하면 Perl은 치명적인 오류로 죽어요. 기본 검색 경로는 @INC 배열에 들어 있어요.
우리 경우 Mytest.pm은 perl에게 Exporter와 Dynamic Loader 확장이 필요하다고 알려 줘요. 그런 다음 @ISA와 @EXPORT 배열, $VERSION 스칼라를 설정하고, 마지막으로 perl에게 모듈을 부트스트랩하라고 알려 줘요. 그러면 Perl은 동적 로더 루틴(있다면)을 호출해서 공유 라이브러리를 로드해요.
@ISA와 @EXPORT 두 배열은 매우 중요해요. @ISA 배열은 현재 패키지에 존재하지 않는 메서드(또는 서브루틴)를 찾을 다른 패키지의 목록을 담고 있어요. 이것은 보통 객체 지향 확장에서만 중요하기 때문에, 대개 수정할 필요는 없어요. (객체 지향은 훨씬 나중에 이야기할 거예요.)
@EXPORT 배열은 확장의 변수와 서브루틴 중 어떤 것이 호출 패키지의 네임스페이스에 놓여야 하는지 Perl에 알려 줘요. 사용자가 이미 여러분의 변수·서브루틴 이름을 사용했을지 모르기 때문에, 무엇을 export할지 신중하게 고르는 것은 아주 중요해요. 좋은 이유 없이 메서드나 변수 이름을 기본적으로 export하지 마세요.
일반적인 규칙으로, 모듈이 객체 지향을 지향한다면 아무것도 export하지 마세요. 단순한 함수·변수 모음이라면 @EXPORT_OK라는 다른 배열을 통해 export할 수 있어요. 이 배열은 사용자가 명시적으로 요청하지 않는 한 서브루틴·변수 이름을 자동으로 네임스페이스에 넣지 않아요.
더 자세한 내용은 perlmod를 참고해요.
$VERSION 변수는 .pm 파일과 공유 라이브러리가 서로 "동기화"되어 있는지 확인하는 데 쓰여요. .pm이나 .xs 파일을 바꿀 때마다 이 변수의 값을 올려 주세요.
테스트 스크립트 잘 쓰기
좋은 테스트 스크립트를 쓰는 것의 중요성은 아무리 강조해도 지나치지 않아요. Perl 자체가 쓰는 "ok/not ok" 스타일을 그대로 따라야 해요. 그래야 각 테스트의 결과를 쉽고 모호함 없이 판별할 수 있어요. 버그를 찾아 고쳤다면 반드시 그에 대한 테스트 케이스를 하나 추가하세요.
make test를 실행하면 Mytest.t 스크립트가 실행되고 확장의 올바른 버전을 사용하게 돼요. 테스트 케이스가 많으면 테스트 파일을 "t" 디렉토리에 ".t" 접미사로 저장해요. make test를 실행하면 이 테스트 파일들이 모두 실행돼요.
튜토리얼 3: 반올림하기 (EXAMPLE 3)
세 번째 확장은 인수 하나를 받아서 그 값을 반올림하고, 인수를 반올림한 값으로 바꿔요.
Mytest.xs 끝에 다음을 추가해요:
void
round(arg)
double arg
CODE:
if (arg > 0.0) {
arg = floor(arg + 0.5);
} else if (arg < 0.0) {
arg = ceil(arg - 0.5);
} else {
arg = 0.0;
}
OUTPUT:
arg
Makefile.PL 파일을 편집해서 해당 줄이 이렇게 보이게 해요:
LIBS => ['-lm'], # e.g., '-lm'
Makefile을 생성하고 make를 실행해요. Mytest.t의 테스트 숫자를 "9"로 바꾸고 다음 테스트를 추가해요:
my $i;
$i = -1.5;
Mytest::round($i);
is( $i, -2.0, 'Rounding -1.5 to -2.0' );
$i = -1.1;
Mytest::round($i);
is( $i, -1.0, 'Rounding -1.1 to -1.0' );
$i = 0.0;
Mytest::round($i);
is( $i, 0.0, 'Rounding 0.0 to 0.0' );
$i = 0.5;
Mytest::round($i);
is( $i, 1.0, 'Rounding 0.5 to 1.0' );
$i = 1.2;
Mytest::round($i);
is( $i, 1.0, 'Rounding 1.2 to 1.0' );
make test를 실행하면 아홉 개 테스트가 모두 okay라고 출력돼요.
이 새 테스트 케이스에서 round에 넘긴 인수가 스칼라 변수라는 점에 주목해요. 상수나 리터럴도 반올림할 수 있는지 궁금할 거예요. 무슨 일이 일어나는지 보려면 Mytest.t에 다음 줄을 임시로 추가해요:
Mytest::round(3);
make test를 실행하면 Perl이 치명적 오류로 죽는 걸 볼 수 있어요. Perl은 상수의 값을 바꾸는 것을 허용하지 않아요!
여기서 새로 나온 점은?
- Makefile.PL을 조금 바꿨어요. 이 경우 확장의 공유 라이브러리에 링크할 추가 라이브러리, 여기서는 수학 라이브러리 libm을 지정했어요. 라이브러리의 모든 루틴을 호출할 수 있는 XSUB를 작성하는 방법은 나중에 이야기할게요.
- 함수의 값이 함수의 반환값으로 전달되는 게 아니라, 함수에 전달된 변수의 값을 바꾸는 방식으로 전달돼요. round의 반환 타입이 "void"인 걸 보고 이미 짐작했을 수도 있어요.
입력 및 출력 인자
XSUB에 전달될 인자는 함수의 반환 타입과 이름을 선언한 뒤의 줄에서 지정해요. 각 입력 인자 줄은 선택적 공백으로 시작하고, 선택적 세미콜론으로 끝날 수 있어요.
출력 인자 목록은 함수 맨 끝, OUTPUT: 지시자 바로 뒤에 와요. RETVAL을 사용하면 그 값을 XSUB 함수의 반환값으로 보내고 싶다는 뜻이에요. EXAMPLE 3에서는 전달한 원래 변수에 "반환값"을 넣고 싶었기 때문에, OUTPUT: 섹션에 RETVAL이 아니라 그 변수를 적었어요.
XSUBPP 프로그램
xsubpp 프로그램은 .xs 파일의 XS 코드를 받아 C 코드로 번역해서 .c 접미사가 붙은 파일에 넣어요. 만들어진 C 코드는 Perl 내부의 C 함수를 많이 사용해요.
TYPEMAP 파일
xsubpp 프로그램은 규칙을 사용해 Perl의 데이터 타입(스칼라, 배열 등)을 C의 데이터 타입(int, char 등)으로 변환해요. 이 규칙은 typemap 파일($PERLLIB/ExtUtils/typemap)에 저장돼요. 아래에 간단히 설명할게요. 자세한 내용은 perlxstypemap에 있어요. 충분히 새로운 버전의 perl(5.16 이상)이나 업그레이드된 XS 컴파일러(ExtUtils::ParseXS 3.13_01 이상)를 쓰면 별도 파일 대신 XS 안에 typemap을 인라인할 수 있어요. 어느 쪽이든 중요한 건 이 typemap이 세 부분으로 나뉜다는 것이에요.
첫 번째 섹션은 여러 C 데이터 타입을 이름에 매핑하는데, 이 이름은 대략 Perl의 여러 타입에 대응해요. 두 번째 섹션은 xsubpp이 입력 인자를 처리할 때 쓰는 C 코드를 담아요. 세 번째 섹션은 xsubpp이 출력 인자를 처리할 때 쓰는 C 코드를 담아요.
우리 확장을 위해 만들어진 .c 파일(Mytest.c)의 일부를 살펴볼게요:
XS(XS_Mytest_round)
{
dXSARGS;
if (items != 1)
Perl_croak(aTHX_ "Usage: Mytest::round(arg)");
PERL_UNUSED_VAR(cv); /* -W */
{
double arg = (double)SvNV(ST(0)); /* XXXXX */
if (arg > 0.0) {
arg = floor(arg + 0.5);
} else if (arg < 0.0) {
arg = ceil(arg - 0.5);
} else {
arg = 0.0;
}
sv_setnv(ST(0), (double)arg); /* XXXXX */
SvSETMAGIC(ST(0));
}
XSRETURN_EMPTY;
}
"XXXXX"로 주석 처리된 두 줄에 주목해요. typemap 파일(또는 섹션)의 첫 부분을 보면 double은 T_DOUBLE 타입이라는 걸 알 수 있어요. typemap의 INPUT 부분에서 T_DOUBLE인 인수는 SvNV 루틴을 호출한 뒤 double로 캐스팅해서 변수 arg에 할당돼요. 마찬가지로 OUTPUT 섹션에서는 arg가 최종 값을 갖게 되면 sv_setnv 함수에 전달되어 호출 서브루틴으로 다시 보내져요. 이 두 함수는 perlguts에 설명되어 있어요. "ST(0)"이 무엇인지는 나중에 인수 스택 섹션에서 이야기할게요.
출력 인자에 대한 경고
일반적으로 EXAMPLE 3처럼 입력 인자를 수정하는 확장을 작성하는 것은 좋지 않아요. 대신 배열로 여러 값을 반환해서 호출자가 알아서 다루게 하는 게 낫죠 (나중에 예제에서 그렇게 할 거예요). 다만 기존 C 루틴은 종종 입력 인자를 수정하므로, 그런 루틴 호출을 더 잘 수용하기 위해 이런 동작은 허용돼요.
튜토리얼 4: 미리 정의된 C 라이브러리와 상호작용 (EXAMPLE 4)
이 예제부터는 미리 정의된 C 라이브러리와 상호작용하는 XSUB를 작성하기 시작해요. 우선 아주 작은 라이브러리를 우리가 직접 만든 다음, h2xs가 .pm과 .xs 파일을 대신 작성하게 해요.
Mytest 디렉토리와 같은 수준에 Mytest2라는 새 디렉토리를 만들어요. Mytest2 안에 mylib이라는 또 다른 디렉토리를 만들고 그 안으로 들어가요.
여기서 테스트 라이브러리를 생성할 파일들을 만들 거예요. C 소스 파일과 헤더 파일이 포함돼요. 이 디렉토리에 Makefile.PL도 만들 거예요. 그리고 Mytest2 수준에서 make를 실행하면 이 Makefile.PL과 그 결과인 Makefile이 자동으로 실행되게 할 거예요.
mylib 디렉토리에 mylib.h라는 파일을 이렇게 만들어요:
#define TESTVAL 4
extern double foo(int, long, const char*);
mylib.c라는 파일도 이렇게 만들어요:
#include <stdlib.h>
#include "mylib.h"
double
foo(int a, long b, const char *c)
{
return (a + b + atof(c) + TESTVAL);
}
마지막으로 Makefile.PL 파일을 이렇게 만들어요:
use ExtUtils::MakeMaker;
$Verbose = 1;
WriteMakefile(
NAME => 'Mytest2::mylib',
SKIP => [qw(all static static_lib dynamic dynamic_lib)],
clean => {'FILES' => 'libmylib$(LIB_EXT)'},
);
sub MY::top_targets {
'
all :: static
pure_all :: static
static :: libmylib$(LIB_EXT)
libmylib$(LIB_EXT): $(O_FILES)
$(AR) cr libmylib$(LIB_EXT) $(O_FILES)
$(RANLIB) libmylib$(LIB_EXT)
';
}
"$(AR)"과 "$(RANLIB)"로 시작하는 줄에는 탭을 사용하고 공백을 쓰지 마세요. 공백을 쓰면 make가 제대로 동작하지 않아요. 또한 Win32 시스템에서는 $(AR)의 "cr" 인자가 불필요하다는 보고가 있어요.
이제 최상위 Mytest2 파일을 만들게요. Mytest2 위의 디렉토리로 이동해 다음 명령을 실행해요:
% h2xs -O -n Mytest2 Mytest2/mylib/mylib.h
이 명령은 Mytest2를 덮어쓴다는 경고를 출력하지만 괜찮아요. 우리 파일은 Mytest2/mylib에 저장되어 있어서 건드려지지 않아요.
h2xs가 생성하는 일반 Makefile.PL은 mylib 디렉토리를 모르기만 해요. 서브디렉토리가 있고 그 안에 라이브러리를 생성할 것임을 알려 줘야 해요. WriteMakefile 호출에 MYEXTLIB 인자를 추가해서 이렇게 만들어요:
WriteMakefile(
NAME => 'Mytest2',
VERSION_FROM => 'lib/Mytest2.pm', # finds $VERSION
LIBS => [''], # e.g., '-lm'
DEFINE => '', # e.g., '-DHAVE_SOMETHING'
INC => '', # e.g., '-I/usr/include/other'
MYEXTLIB => 'mylib/libmylib$(LIB_EXT)',
);
그리고 끝에 서브루틴을 하나 추가해요 (기존 서브루틴을 덮어쓰게 돼요). "cd"로 시작하는 줄 들여쓰기에 탭 문자를 쓰는 걸 잊지 마세요!
sub MY::postamble {
'
$(MYEXTLIB): mylib/Makefile
cd mylib && $(MAKE) $(PASSTHRU)
';
}
MANIFEST 파일도 고쳐서 다음 세 줄을 추가해요:
mylib/Makefile.PL
mylib/mylib.c
mylib/mylib.h
네임스페이스를 깨끗하게 유지하기 위해 .pm 파일을 편집해서 @EXPORT 변수를 @EXPORT_OK로 바꿔요. 마지막으로 .xs 파일에서 #include 줄을 이렇게 바꿔요:
#include "mylib/mylib.h"
그리고 .xs 파일 끝에 다음 함수 정의도 추가해요:
double
foo(a,b,c)
int a
long b
const char * c
OUTPUT:
RETVAL
기본 Perl은 현재 const char * 타입을 지원하지 않으므로 typemap도 만들어야 해요. 위 함수 앞의 XS 코드에 새 TYPEMAP 섹션을 포함해요:
TYPEMAP: <<END
const char * T_PV
END
이제 최상위 Makefile.PL에서 perl을 실행해요. mylib 디렉토리에도 Makefile이 생긴 걸 볼 수 있어요. make를 실행하면 mylib 디렉토리로 cd해서 그 안에서도 make를 실행하는 걸 볼 수 있어요.
이제 Mytest2.t 스크립트를 편집해서 테스트 숫자를 "5"로 바꾸고, 스크립트 끝에 다음 줄을 추가해요:
is( Mytest2::foo( 1, 2, "Hello, world!" ), 7 );
is( Mytest2::foo( 1, 2, "0.0" ), 7 );
ok( abs( Mytest2::foo( 0, 0, "-3.4" ) - 0.6 ) <= 0.01 );
(부동소수점 비교를 다룰 때는 같음을 검사하기보다, 기대값과 실제 결과의 차이가 일정 값(엡실론이라고 불러요, 여기서는 0.01) 아래인지 검사하는 게 좋아요.)
make test를 실행하면 모두 잘 될 거예요. Mytest2::mylib 확장의 테스트 누락에 대한 경고가 있지만 무시해도 돼요.
여기서 무슨 일이 일어난 걸까요?
이전 예제와 달리 이제 실제 include 파일에 대해 h2xs를 실행했어요. 그 결과 .pm과 .xs 파일 양쪽에 추가 요소가 생겼어요.
- .xs 파일에 이제 mylib.h 헤더 파일의 절대 경로가 있는 #include 지시자가 있어요. 우리는 확장 디렉토리를 옮길 수 있도록 상대 경로로 바꿨어요.
- .xs 파일에 새 C 코드가 추가됐어요.
constant루틴의 목적은 헤더 파일에 #define된 값들을 Perl 스크립트에서 접근할 수 있게 하는 거예요 (TESTVAL또는Mytest2::TESTVAL을 호출해서).constant루틴 호출을 허용하는 XS 코드도 있어요. - include 파일에 #include 지시자가 있었다면 h2xs가 처리하지 않았을 거예요. 이에 대한 좋은 해결책은 아직 없어요.
- 또한 mylib 서브디렉토리에 만든 라이브러리에 대해 Perl에 알려 줬어요. 그건 WriteMakefile 호출에
MYEXTLIB변수를 추가하고, postamble 서브루틴을 서브디렉토리로 cd해서 make를 실행하도록 교체한 것뿐이면 됐어요. 라이브러리의 Makefile.PL은 조금 더 복잡하지만 과하지는 않아요. 역시 postamble 서브루틴을 교체해 우리 코드를 넣었어요. 그 코드는 여기서 만들 라이브러리가 (동적으로 로딩 가능한 라이브러리가 아니라) 정적 아카이브 라이브러리임을 지정하고, 그것을 빌드할 명령을 제공했어요.
.xs 파일 해부
"EXAMPLE 4"의 .xs 파일에는 몇 가지 새 요소가 들어 있었어요. 이 요소들의 의미를 이해하려면 다음 줄에 주목해요:
MODULE = Mytest2 PACKAGE = Mytest2
이 줄 앞부분은 순수한 C 코드로, 어떤 헤더를 포함할지 기술하고 일부 편의 함수를 정의해요. 이 부분은 번역되지 않아요. 포함된 POD 문서를 건너뛰는 것(perlpod 참고) 외에는 그대로 생성된 출력 C 파일로 들어가요.
이 줄 이후는 XSUB 함수의 설명이에요. 이 설명들은 xsubpp에 의해 Perl 호출 규약을 사용해 이 함수들을 구현하고, Perl 인터프리터에서 이 함수들을 보이게 하는 C 코드로 번역돼요.
constant 함수에 특히 주목해요. 이 이름은 생성된 .xs 파일에서 두 번 나타나요. 한 번은 첫 부분의 정적 C 함수로, 또 한 번은 두 번째 부분에서 이 정적 C 함수에 대한 XSUB 인터페이스가 정의될 때 나타나요.
이것은 .xs 파일에서 아주 전형적인 모습이에요. 보통 .xs 파일은 기존 C 함수에 대한 인터페이스를 제공해요. 그러면 그 C 함수는 어딘가(외부 라이브러리나 .xs 파일의 첫 부분) 정의되어 있고, 그 함수에 대한 Perl 인터페이스("Perl glue")가 .xs 파일의 두 번째 부분에 기술돼요. "EXAMPLE 1", "EXAMPLE 2", "EXAMPLE 3"처럼 모든 작업이 "Perl glue" 안에서 일어나는 상황은 규칙이라기보다 예외에 가까워요.
XSUB에서 군더더기 빼기
"EXAMPLE 4"의 .xs 파일 두 번째 부분에는 이 XSUB 설명이 있었어요:
double
foo(a,b,c)
int a
long b
const char * c
OUTPUT:
RETVAL
"EXAMPLE 1", "EXAMPLE 2", "EXAMPLE 3"과 달리 이 설명에는 Perl 함수 foo()를 호출할 때 무엇을 하는 코드가 없어요. 무슨 일이 일어나는지 이해하려면 이 XSUB에 CODE 섹션을 추가해 볼 수 있어요:
double
foo(a,b,c)
int a
long b
const char * c
CODE:
RETVAL = foo(a,b,c);
OUTPUT:
RETVAL
하지만 이 두 XSUB는 거의 동일한 C 코드를 생성해요. xsubpp 컴파일러는 XSUB 설명의 처음 두 줄에서 CODE: 섹션을 알아낼 만큼 똑똑해요. 그럼 OUTPUT: 섹션은요? 사실 완전히 똑같아요! OUTPUT: 섹션도, CODE: 섹션이나 PPCODE: 섹션이 지정되지 않는 한, 제거할 수 있어요. xsubpp은 함수 호출 섹션을 생성해야 한다는 걸 보고 OUTPUT 섹션도 자동 생성해요. 따라서 XSUB를 이렇게 줄일 수 있어요:
double
foo(a,b,c)
int a
long b
const char * c
이런 XSUB에도 똑같이 할 수 있을까요?
int
is_even(input)
int input
CODE:
RETVAL = (input % 2 == 0);
OUTPUT:
RETVAL
"EXAMPLE 2"의 이 XSUB 말이에요. 이렇게 하려면 C 함수 int is_even(int input)을 정의해야 해요. ".xs 파일 해부"에서 봤듯이, 이 정의의 적절한 위치는 .xs 파일의 첫 부분이에요. 실제로 C 함수
int
is_even(int arg)
{
return (arg % 2 == 0);
}
는 이것에는 과하다고 볼 수도 있어요. #define처럼 단순한 것으로도 충분해요:
#define is_even(arg) ((arg) % 2 == 0)
이걸 .xs 파일의 첫 부분에 넣고 나면 "Perl glue" 부분은 이렇게 단순해져요:
int
is_even(input)
int input
glue 부분과 본체 부분을 이렇게 분리하는 기법은 분명한 트레이드오프가 있어요. Perl 인터페이스를 바꾸려면 코드의 두 곳을 바꿔야 해요. 하지만 코드의 군더더기가 많이 줄어들고, 본체 부분이 Perl 호출 규약의 특이성으로부터 독립적이 돼요. (실제로 위 설명에는 Perl 전용인 것이 아무것도 없어요. 다른 버전의 xsubpp라면 이것을 TCL glue나 Python glue로 번역했을 수도 있어요.)
XSUB 인자에 대해 더
EXAMPLE 4를 마치면, 인터페이스가 그렇게 깔끔하지 않은 실제 라이브러리를 흉내 내는 쉬운 방법을 갖게 돼요. 이제 xsubpp 컴파일러에 전달되는 인자에 대해 계속 이야기할게요.
.xs 파일에서 루틴에 인자를 지정할 때, 실제로 각 인자마다 세 가지 정보를 전달하는 거예요. 첫째는 다른 인자들에 대한 그 인자의 순서(첫 번째, 두 번째 등)예요. 둘째는 인자의 타입으로, 인자의 타입 선언(int, char* 등)으로 구성돼요. 셋째는 라이브러리 함수 호출에서 인자의 호출 규약이에요.
Perl은 함수에 인자를 참조로 전달하는 반면, C는 값을 전달해요. "인자" 중 하나의 데이터를 수정하는 C 함수를 구현하려면, 그 C 함수의 실제 인자는 데이터에 대한 포인터가 되어야 해요. 따라서 선언이
int string_length(char *s);
int upper_case_char(char *cp);
인 두 C 함수는 완전히 다른 의미를 가질 수 있어요. 첫 번째는 s가 가리키는 char 배열을 검사하고, 두 번째는 cp를 즉시 역참조해서 *cp만 조작할 수 있어요 (반환값을 성공 지시자로 쓸 수 있어요). Perl에서는 이 함수들을 완전히 다른 방식으로 사용할 거예요.
이 정보를 xsubpp에 전달하려면 인자 앞의 *를 &로 바꾸면 돼요. &는 그 인자를 주소로 라이브러리 함수에 전달해야 한다는 뜻이에요. 위 두 함수는 이렇게 XSUB화할 수 있어요:
int
string_length(s)
char * s
int
upper_case_char(cp)
char &cp
예를 들어 이렇게 생각해 봐요:
int
foo(a,b)
char &a
char * b
이 함수에 대한 첫 번째 Perl 인자는 char로 취급되어 변수 a에 할당되고, 그 주소가 foo 함수에 전달돼요. 두 번째 Perl 인자는 문자열 포인터로 취급되어 변수 b에 할당돼요. b의 값이 foo 함수에 전달돼요. xsubpp이 생성하는 foo 함수에 대한 실제 호출은 이렇게 보일 거예요:
foo(&a, b);
xsubpp은 다음 인자 목록을 동일하게 파싱해요:
char &a
char&a
char & a
다만 이해를 돕기 위해 "&"는 변수 이름 옆에(변수 타입에서 떨어져) 두고, "*"는 변수 타입 근처에(변수 이름에서 떨어져) 둘 것을 권장해요 (위 foo 호출처럼). 그렇게 하면 C 함수에 정확히 무엇이 전달될지 이해하기 쉬워지는데, "마지막 열"에 있는 것이 전달되기 때문이에요.
가능하다면 함수가 원하는 타입의 변수를 전달하려고 애쓰세요. 장기적으로 큰 도움이 될 거예요.
인자 스택 (Argument Stack)
EXAMPLE 1을 제외한 어떤 예제의 생성 C 코드를 봐도 ST(n)에 대한 참조가 여러 개 보이는데, n은 보통 0이에요. "ST"는 실제로 인자 스택에서 n번째 인자를 가리키는 매크로예요. 따라서 ST(0)는 스택의 첫 번째 인자, 즉 XSUB에 전달된 첫 번째 인자이고, ST(1)은 두 번째 인자, 이런 식이에요.
.xs 파일에서 XSUB의 인자를 나열하면, 그게 xsubpp에게 어느 인자가 인자 스택의 어느 것에 해당하는지 알려 줘요 (처음 나열한 것이 첫 인자, 이런 식). 함수가 기대하는 순서와 다르게 나열하면 재앙을 불러요.
인자 스택의 실제 값들은 전달된 값에 대한 포인터예요. 인자가 OUTPUT 값으로 나열되면, 스택에서 그에 대응하는 값(첫 인자였다면 ST(0))이 바뀌어요. EXAMPLE 3을 위해 생성된 C 코드를 보면 확인할 수 있어요. round() XSUB 루틴의 코드에는 이런 줄이 있어요:
double arg = (double)SvNV(ST(0));
/* Round the contents of the variable arg */
sv_setnv(ST(0), (double)arg);
arg 변수는 처음에 ST(0)의 값을 가져와 설정되고, 루틴 끝에 ST(0)에 다시 저장돼요.
XSUB는 스칼라뿐 아니라 리스트도 반환할 수 있어요. 이는 스택 값 ST(0), ST(1) 등을 살짝 다르게 조작해서 해야 해요. 자세한 내용은 perlxs를 참고해요.
XSUB는 Perl 함수 인자를 C 함수 인자로 자동 변환하는 것을 피할 수도 있어요. 자세한 내용은 perlxs를 참고해요. 어떤 사람은 자동 변환이 되는 경우에도 ST(i)를 검사해서 수동 변환을 선호하는데, XSUB 호출의 논리가 더 명확해진다고 주장해요. XSUB의 "Perl glue"와 "본체" 부분을 완전히 분리하는 것의 비슷한 트레이드오프는 "XSUB에서 군더더기 빼기"와 비교해 보세요.
전문가들은 이런 숙어에 대해 논쟁할 수 있지만, Perl 내부에 초보인 사람은 Perl 내부에 최대한 덜 의존하는 방식, 즉 자동 변환과 자동 호출 생성을 선호할 거예요 ("XSUB에서 군더더기 빼기"처럼). 이 방식은 XSUB 작성자를 Perl API의 미래 변화로부터 보호해 준다는 추가 이점도 있어요.
확장 기능 확장 (Extending your Extension)
때로 Perl과 확장 사이의 인터페이스를 더 단순하거나 이해하기 쉽게 만드는 추가 메서드나 서브루틴을 제공하고 싶을 수 있어요. 이런 루틴은 .pm 파일에 있어야 해요. 확장 자체가 로드될 때 자동으로 로드될지, 호출될 때만 로드될지는 서브루틴 정의가 .pm 파일의 어느 위치에 있느냐에 달려 있어요. 추가 서브루틴을 저장·로드하는 다른 방법에 대해서는 AutoLoader도 참고할 수 있어요.
확장 문서화 (Documenting your Extension)
확장을 문서화하지 않는 것은 변명의 여지가 전혀 없어요. 문서는 .pm 파일에 있어야 해요. 이 파일은 pod2man에 전달되어, 포함된 문서가 manpage 형식으로 변환된 뒤 blib 디렉토리에 놓여요. 확장이 설치될 때 Perl의 manpage 디렉토리로 복사돼요.
.pm 파일 안에서 문서와 Perl 코드를 섞을 수 있어요. 사실 메서드 자동 로드를 사용하려면 그렇게 해야 해요 (.pm 파일 안의 주석이 설명하는 것처럼).
pod 형식에 대한 더 많은 정보는 perlpod를 참고해요.
확장 설치 (Installing your Extension)
확장이 완성되고 모든 테스트를 통과하면 설치는 아주 간단해요. "make install"만 실행하면 돼요. Perl이 설치된 디렉토리에 쓰기 권한이 있거나, 시스템 관리자에게 대신 make를 실행해 달라고 요청해야 해요.
대신 확장 파일을 놓을 정확한 디렉토리를 지정할 수도 있어요. make install 뒤에 "PREFIX=/destination/directory"를 붙이면 돼요 (또는 멍청한 버전의 make를 쓴다면 make와 install 사이에). 이는 여러 시스템에 배포할 확장을 만들 때 아주 유용해요. 그러면 목적 디렉토리의 파일들을 아카이브해서 목적 시스템들에 배포하면 돼요.
튜토리얼 5: 배열 반환하기 (EXAMPLE 5)
이 예제에서는 인자 스택을 좀 더 다뤄 볼게요. 이전 예제들은 모두 단일 값만 반환했어요. 이제 배열을 반환하는 확장을 만들 거예요.
이 확장은 유닉스 지향이 강해요 (struct statfs와 statfs 시스템 콜). 유닉스 시스템이 아니라면, statfs 대신 여러 값을 반환하는 다른 함수로 바꾸거나, 호출자에게 반환할 값을 하드코딩할 수도 있어요 (그러면 오류 케이스를 테스트하기는 좀 더 어려워질 거예요). 그냥 이 예제를 건너뛰어도 돼요. XSUB를 바꾸면 테스트 케이스도 그에 맞게 고치는 걸 잊지 마세요.
Mytest 디렉토리로 돌아가 Mytest.xs 끝에 다음 코드를 추가해요:
void
statfs(path)
char * path
INIT:
int i;
struct statfs buf;
PPCODE:
i = statfs(path, &buf);
if (i == 0) {
XPUSHs(sv_2mortal(newSVnv(buf.f_bavail)));
XPUSHs(sv_2mortal(newSVnv(buf.f_bfree)));
XPUSHs(sv_2mortal(newSVnv(buf.f_blocks)));
XPUSHs(sv_2mortal(newSVnv(buf.f_bsize)));
XPUSHs(sv_2mortal(newSVnv(buf.f_ffree)));
XPUSHs(sv_2mortal(newSVnv(buf.f_files)));
XPUSHs(sv_2mortal(newSVnv(buf.f_type)));
} else {
XPUSHs(sv_2mortal(newSVnv(errno)));
}
"XSUB.h"의 include 바로 다음, .xs 파일 맨 위에 다음 코드도 추가해야 해요:
#include <sys/vfs.h>
또한 Mytest.t에 다음 코드 조각을 추가하면서 "9"개 테스트를 "11"로 늘려요:
my @a;
@a = Mytest::statfs("/blech");
ok( scalar(@a) == 1 && $a[0] == 2 );
@a = Mytest::statfs("/");
is( scalar(@a), 7 );
이 예제의 새 요소들
이 예제는 꽤 많은 새 개념을 추가했어요. 하나씩 살펴볼게요.
- INIT: 지시자는 인자 스택이 해독된 직후에 놓일 코드를 담아요. C는 함수 안 임의의 위치에서 변수 선언을 허용하지 않으므로, XSUB가 필요로 하는 지역 변수를 선언하는 가장 좋은 방법이 보통 이것이에요. (대안으로
PPCODE:섹션 전체를 중괄호로 감싸고 선언을 위쪽에 놓을 수도 있어요.) - 이 루틴은 statfs 호출의 성공 또는 실패에 따라 서로 다른 개수의 인자를 반환해요. 오류가 있으면 오류 번호가 단일 요소 배열로 반환돼요. 호출이 성공하면 7요소 배열이 반환돼요. 이 함수에는 인자가 하나만 전달되므로, 반환될 수 있는 7개 값을 담을 스택 공간이 필요해요.
것을 CODE: 지시자가 아니라 PPCODE: 지시자를 사용해서 해결해요. 이 지시자는 인자 스택에 놓일 반환 값을 우리가 직접 관리하겠다고 xsubpp에 알려 줘요.
- 호출자에게 반환할 값을 스택에 놓으려면 "XPUSH"로 시작하는 매크로 시리즈를 사용해요. 정수, 부호 없는 정수, double, 문자열, Perl 스칼라를 스택에 놓는 다섯 가지 버전이 있어요. 우리 예제에서는 Perl 스칼라를 스택에 놓았어요. (사실 이것이 여러 값을 반환하는 데 쓸 수 있는 유일한 매크로예요.)
XPUSH* 매크로는 반환 스택을 자동으로 확장해서 넘치는 것을 막아요. 호출 프로그램이 보기를 원하는 순서대로 값을 스택에 밀어 넣어요.
- XSUB의 반환 스택에 밀어 넣은 값은 실제로 mortal SV예요. 호출 프로그램이 값을 복사한 뒤에는 반환 값을 담았던 SV를 해제할 수 있도록 mortal로 만들어요. mortal이 아니라면 XSUB 루틴이 반환한 뒤에도 계속 존재하지만 접근할 수 없게 돼요. 이것은 메모리 누수예요.
- 성능이 아니라 코드 간결성에 관심이 있다면, 성공 분기에서
XPUSHs매크로 대신PUSHs매크로를 쓰고, 반환 값을 밀기 전에 스택을 미리 확장할 거예요:
EXTEND(SP, 7);
트레이드오프는 반환 값의 개수를 미리 계산해야 한다는 것이에요 (스택을 과도하게 확장해도 메모리 소비 외에는 보통 해를 끼치지 않아요).
마찬가지로 실패 분기에서는 스택을 확장하지 않고 PUSHs를 쓸 수 있어요. Perl 함수 참조가 스택에서 XSUB로 오기 때문에, 스택은 반환 값 하나를 담기에 항상 충분히 크거든요.
튜토리얼 6: 복잡한 자료형 다루기 (EXAMPLE 6)
이 예제에서는 배열에 대한 참조를 입력 인자로 받고, 해시들의 배열에 대한 참조를 반환할 거예요. 이것은 XSUB에서 복잡한 Perl 데이터 타입을 다루는 법을 보여 줘요.
이 확장은 다소 억지스러워요. 이전 예제의 코드를 기반으로 해요. statfs 함수를 여러 번 호출하는데, 파일 이름 배열에 대한 참조를 입력으로 받아, 각 파일 시스템의 데이터를 담은 해시 배열에 대한 참조를 반환해요.
Mytest 디렉토리로 돌아가 Mytest.xs 끝에 다음 코드를 추가해요:
SV *
multi_statfs(paths)
SV * paths
INIT:
AV * results;
SSize_t numpaths = 0, n;
int i;
struct statfs buf;
SvGETMAGIC(paths);
if ((!SvROK(paths))
|| (SvTYPE(SvRV(paths)) != SVt_PVAV)
|| ((numpaths = av_top_index((AV *)SvRV(paths))) < 0))
{
XSRETURN_UNDEF;
}
results = (AV *)sv_2mortal((SV *)newAV());
CODE:
for (n = 0; n <= numpaths; n++) {
HV * rh;
STRLEN l;
SV * path = *av_fetch((AV *)SvRV(paths), n, 0);
char * fn = SvPVbyte(path, l);
i = statfs(fn, &buf);
if (i != 0) {
av_push(results, newSVnv(errno));
continue;
}
rh = (HV *)sv_2mortal((SV *)newHV());
hv_store(rh, "f_bavail", 8, newSVnv(buf.f_bavail), 0);
hv_store(rh, "f_bfree", 7, newSVnv(buf.f_bfree), 0);
hv_store(rh, "f_blocks", 8, newSVnv(buf.f_blocks), 0);
hv_store(rh, "f_bsize", 7, newSVnv(buf.f_bsize), 0);
hv_store(rh, "f_ffree", 7, newSVnv(buf.f_ffree), 0);
hv_store(rh, "f_files", 7, newSVnv(buf.f_files), 0);
hv_store(rh, "f_type", 6, newSVnv(buf.f_type), 0);
av_push(results, newRV_inc((SV *)rh));
}
RETVAL = newRV_inc((SV *)results);
OUTPUT:
RETVAL
그리고 Mytest.t에 다음 코드를 추가하면서 "11"개 테스트를 "13"으로 늘려요:
my $results = Mytest::multi_statfs([ '/', '/blech' ]);
ok( ref $results->[0] );
ok( ! ref $results->[1] );
이 예제의 새 요소들
여기서 도입되는 새 개념이 꽤 있어요. 아래에 설명할게요.
- 이 함수는 typemap을 사용하지 않아요. 대신 SV*(스칼라) 인자 하나를 받고 SV* 값을 반환하는 것으로 선언하고, 코드 안에서 이 스칼라들을 채우는 일을 우리가 직접 해요. 값 하나만 반환하므로
PPCODE:지시자는 필요 없어요. 대신CODE:와OUTPUT:지시자를 사용해요. - 참조를 다룰 때는 주의가 중요해요.
INIT:블록은 paths가 tied 변수일 경우에 대비해 먼저 SvGETMAGIC(paths)를 호출해요. 그런 다음SvROK가 참을 반환하는지 확인하는데, 이는 paths가 유효한 참조임을 나타내요. (단순히SvROK만 확인하는 것은 tied 변수에서 FETCH를 트리거하지 않아요.) 그다음SvRV로 paths를 역참조하고SvTYPE으로 타입을 알아내서, paths가 가리키는 객체가 배열인지 확인해요. 추가 테스트로av_top_index함수를 사용해서 paths가 가리키는 배열이 비어 있지 않은지 확인해요 (배열이 비어 있으면 -1을 반환해요). 이 세 조건이 모두 충족되지 않으면 XSRETURN_UNDEF 매크로를 사용해 XSUB를 중단하고 undef 값을 반환해요. - 이 XSUB에서 배열을 여러 개 다뤄요. 배열은 내부적으로 AV* 포인터로 표현된다는 점에 주목해요. 배열을 다루는 함수와 매크로는 Perl의 함수와 비슷해요.
av_top_index는 $#array처럼 AV*에서 가장 높은 인덱스를 반환하고,av_fetch는 인덱스가 주어지면 배열에서 스칼라 값 하나를 가져오고,av_push는 스칼라 값을 배열 끝에 밀어 넣으며 필요에 따라 배열을 자동으로 확장해요.
구체적으로, 입력 배열에서 경로 이름을 한 번에 하나씩 읽어서 결과를 순서대로 출력 배열(results)에 저장해요. statfs가 실패하면 반환 배열에 밀어 넣은 요소는 실패 후의 errno 값이에요. statfs가 성공하면 반환 배열에 밀어 넣은 값은 statfs 구조체의 일부 정보를 담은 해시에 대한 참조예요.
반환 스택과 마찬가지로, 반환할 요소 개수를 알고 있으므로 데이터를 밀기 전에 반환 배열을 미리 확장하는 것도 가능해요 (그리고 약간의 성능 이득도 있어요):
av_extend(results, numpaths);
- 이 함수에서는 해시 연산을 하나만 수행하는데,
hv_store를 사용해 키 아래에 새 스칼라를 저장하는 거예요. 해시는 HV* 포인터로 표현돼요. 배열과 마찬가지로 XSUB에서 해시를 다루는 함수는 Perl에서 사용 가능한 기능을 그대로 반영해요. 자세한 내용은 perlguts와 perlapi를 참고해요. - 참조를 만들려면
newRV_inc함수를 사용해요. 이 경우(그리고 다른 많은 경우) AV나 HV를 SV* 타입으로 캐스팅할 수 있다는 점에 주목해요. 그래서 배열, 해시, 스칼라에 대한 참조를 같은 함수로 만들 수 있어요. 반대로SvRV함수는 항상 SV*를 반환하는데, 스칼라가 아닌 다른 것이면 적절한 타입으로 캐스팅해야 해요 (SvTYPE으로 확인). - 이 시점에서 xsubpp이 하는 일은 거의 없어요. Mytest.xs와 Mytest.c의 차이는 아주 작아요.
튜토리얼 7 (예정)
XPUSH 인자를 넣고 RETVAL을 설정하고 반환 값을 배열에 할당하기
튜토리얼 8 (예정)
$! 설정하기
튜토리얼 9: 열린 파일을 XS에 전달하기 (EXAMPLE 9)
파일을 XS에 전달하는 건 타입글로브 같은 것들이 많아서 어려울 거라고 생각할 거예요. 음, 그렇지 않아요.
어떤 이상한 이유로 표준 C 라이브러리 함수 fputs()에 대한 래퍼가 필요하다고 가정해 봐요. 이게 전부예요:
#define PERLIO_NOT_STDIO 0 /* For co-existence with stdio only */
#define PERL_NO_GET_CONTEXT /* This is more efficient */
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#include <stdio.h>
int
fputs(s, stream)
char * s
FILE * stream
실제 작업은 표준 typemap에서 처리돼요.
더 자세한 내용은 perlapio의 "Co-existence with stdio"를 참고해요.
하지만 perlio 레이어가 하는 모든 멋진 일을 잃게 돼요. 이건 그것들에 대해 아무것도 모르는 stdio 함수 fputs()를 호출하는 거예요.
표준 typemap은 PerlIO*의 세 가지 변형을 제공해요. InputStream(T_IN), InOutStream(T_INOUT), OutputStream(T_OUT)이에요. 맨땅의 PerlIO *는 T_INOUT으로 간주돼요. 코드에서 그것이 중요하다면(아래에서 왜일 수 있는지 볼게요) 구체적인 이름 중 하나를 #define하거나 typedef해서 XS 파일의 인자 또는 결과 타입으로 사용해요.
표준 typemap에는 perl 5.7 이전에는 PerlIO *가 없지만, 세 가지 스트림 변형은 있어요. 직접 PerlIO *를 사용하는 것은 자신만의 typemap을 제공하지 않는 한 역호환이 되지 않아요.
perl에서 오는 스트림의 경우 주요 차이점은 OutputStream이 출력 PerlIO *를 가져온다는 것인데, 이것은 소켓에서 차이가 날 수 있어요. 우리 예제처럼...
perl에 건네지는 스트림의 경우 새 파일 핸들(새 glob에 대한 참조)이 만들어지고 제공된 PerlIO *와 연결돼요. PerlIO *의 읽기/쓰기 상태가 올바르지 않으면 파일 핸들이 사용될 때 오류나 경고가 나올 수 있어요. 따라서 PerlIO *를 "w"로 열었다면 정말 OutputStream이어야 하고, "r"로 열었다면 InputStream이어야 해요.
이제 XS에서 perlio 레이어를 사용하고 싶다고 가정해 봐요. 예로 perlio의 PerlIO_puts() 함수를 사용할게요.
XS 파일의 C 부분(첫 MODULE 줄 위)에 이게 있어요:
#define OutputStream PerlIO *
or
typedef PerlIO * OutputStream;
그리고 이것이 XS 코드예요:
int
perlioputs(s, stream)
char * s
OutputStream stream
CODE:
RETVAL = PerlIO_puts(stream, s);
OUTPUT:
RETVAL
PerlIO_puts()는 fputs()와 비교해서 인자가 반대순이기 때문에 CODE 섹션을 사용해야 해요. 인자를 같게 유지하고 싶고요.
이것을 철저히 탐구하고 싶어서, PerlIO *에 stdio fputs()를 사용하고 싶다고 가정해 봐요. 즉 perlio 시스템에 stdio FILE *를 요청해야 해요:
int
perliofputs(s, stream)
char * s
OutputStream stream
PREINIT:
FILE *fp = PerlIO_findFILE(stream);
CODE:
if (fp != (FILE*) 0) {
RETVAL = fputs(s, fp);
} else {
RETVAL = -1;
}
OUTPUT:
RETVAL
참고: PerlIO_findFILE()는 레이어에서 stdio 레이어를 찾아요. 찾지 못하면 PerlIO_exportFILE()를 호출해서 새 stdio FILE을 생성해요. 새 FILE을 원할 때만 PerlIO_exportFILE()를 호출하세요. 호출할 때마다 하나를 생성하고 새 stdio 레이어를 밀어 넣을 거예요. 같은 파일에 반복해서 호출하지 마세요. PerlIO_findFILE()는 PerlIO_exportFILE()이 만든 stdio 레이어를 찾을 수 있어요.
이것은 perlio 시스템에만 적용돼요. 5.7 이전 버전에서는 PerlIO_exportFILE()이 PerlIO_findFILE()과 동일해요.
이 예제들의 문제 해결
이 문서 맨 위에서 언급했듯이, 이 예제 확장들에 문제가 있다면 아래 중 하나가 도움이 되는지 확인해 보세요.
- 이 문서는 "perl"이라는 실행 파일이 Perl 버전 5라고 가정해요. 어떤 시스템은 Perl 버전 5를 "perl5"로 설치했을 수도 있어요.
함께 보기
더 많은 정보는 perlguts, perlapi, perlclib, perlxs, perlmod, perlapio, perlpod를 참고해요.
저자
Jeff Okamoto <[email protected]>
Dean Roehrich, Ilya Zakharevich, Andreas Koenig, Tim Bunce가 검토하고 도왔어요.
PerlIO 자료는 Lupe Christoph가 기여했고 Nick Ing-Simmons가 설명을 도왔어요.
Perl 5.8.x 기준 h2xs 변경은 Renee Baecker가 했어요.
이 문서는 이제 Perl 자체의 일부로 유지 관리되고 있어요.