XS 언어 참조 매뉴얼
XS 언어 참조 매뉴얼 (perlxs)
C로 작성한 코드를 Perl에서 그냥 함수처럼 부르고 싶을 때 쓰는 게 XS예요. 이 문서는 XS라는 템플릿 언어의 공식 참조 매뉴얼이에요. XS로 XSUB(외부·확장 서브루틴)을 선언하면, 대부분의 보일러플레이트 코드는 컴파일러가 대신 만들어 줘요. 다만 입문자라면 이 문서보다 먼저 perlxstut 튜토리얼부터 읽는 걸 권해요.
본문
개요 (SYNOPSIS)
/* This is a simple example of an XS file. The first half of an XS
* file is uninterpreted C code; all lines are passed through
* unprocessed. */
=pod
Except that any POD is stripped.
=cut
/* Standard boilerplate: */
/* For efficiency, always define PERL_NO_GET_CONTEXT: not enabled by
* default for backwards compatibility. For details, see "How multiple
* interpreters and concurrency are supported" in perlguts. */
#define PERL_NO_GET_CONTEXT
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#include "ppport.h"
/* Any general C code here; for example: */
#define FOO 1
static int
my_helper_function(int i) { /* do stuff */ }
/* The first MODULE line starts the XS half of the file: */
MODULE = Foo::Bar PACKAGE = Foo::Bar
# Indented '#' are XS code comments.
# C preprocessor directives are still allowed and are passed
# through:
#define BAR 2
# File-scoped XS directives
PROTOTYPES: DISABLE
# A simple XSUB: generate a wrapper for the strlen() C library
# function.
int
strlen(char *s)
=pod
A more complex example:
C<multi16(i,j)>: do a 16-bit multiply
=cut
unsigned int
multi16(unsigned int i, \
unsigned int j)
CODE:
i = i & 0xFFFF;
j = j & 0xFFFF;
RETVAL = (i * j) & 0xFFFF;
OUTPUT:
RETVAL
설명 (DESCRIPTION)
이 문서는 XS 언어의 참조 매뉴얼이에요. XS는 C 코드 소스 파일을 생성하는 템플릿 언어의 일종인데, 그 소스 파일은 C로 작성된 함수들을 담고 있어요. 이 함수들은 Perl에서 부를 수 있고, Perl 서브루틴처럼 행동해요. 이 함수들을 external(외부) 또는 extension(확장) 서브, 줄여서 XSUB이라고 불러요.
이 POD 파일은 2025년에 대대적으로 다시 쓰이고 현대화됐어요. "K&R" 스타일 XSUB 함수 시그니처 선언 같은 옛 관행은 더 이상 권장되지 않아요. 하지만 오래된 코드 상당수는 여전히 그 방식을 쓰고 있으니, 새 코드를 작성할 때 구식 코드를 예시로 삼을 때는 주의해야 해요.
버전 숫자 (Version numbers)
별도로 명시하지 않는 한, 이 문서에 나온 문법은 최소한 Perl 5.8.0에 딸려 온 XS 파서 유틸리티 xsubpp 1.9508 버전까지는 유효해요.
xsubpp 2.09_01 버전에서 이 유틸리티의 코드 대부분이 ExtUtils::ParseXS라는 별도 모듈로 분리됐어요. 이 모듈은 xsubpp의 버전 번호 체계를 물려받았고, 그 후로 xsubpp는 로드된 ParseXS.pm의 버전 번호를 그대로 쓰고 있어요. 이 분리 덕분에 더 새로운 버전의 ExtUtils::ParseXS를 CPAN으로 설치하면, 더 오래된 Perl 설치에 더 새로운 XS 문법을 쓸 수 있어요.
(1.98_01과 2.09_01 사이에서 ExtUtils::ParseXS는 xsubpp와 분리된 포크로 존재했으며, 일부 변경 사항이 perl 배포판의 xsubpp와 혼란스럽게 오가며 이식됐어요.)
이 문서는 XS 문법의 변화를 xsubpp 버전 번호를 기준으로 언급해요. 이는 보통 같은 버전 번호의 ExtUtils::ParseXS에 직접 대응한다고 이해하면 돼요. 이 모듈이 어떤 Perl 배포에 딸려 왔는지 알아보려면, Perl 설치에 보통 포함되는 corelist 유틸리티를 쓸 수 있어요. 예를 들면:
corelist -a ExtUtils::ParseXS
XS 파일의 공식 문법 (THE FORMAL SYNTAX OF AN XS FILE)
다음은 XS 파일 문법에 대한 BNF 비슷한 설명이에요. 사람이 읽기 좋게 만든 것이지 기계가 읽을 수 있게 만든 게 아니고, 줄바꿈이 정확히 어디서 일어나는지까지 정밀하게 규정하려고 하지는 않아요.
Key:
foo BNF token.
"bar" Literal terminal symbol.
/.../ Terminal symbol defined by a pattern.
[Foo::Bar] Terminal symbol defined by way of an example.
* + ? | ( ) These have their usual regex-style meanings.
// ... BNF Comments.
XS_file = C_file_part ( module_decl XS_file_part )+
C_file_part = (
// Lines of C code (including /* ... */),
// which are all passed through uninterpreted.
|
pod // These are stripped.
)*
pod = /^=/ .. /^=cut\s*$/
module_decl = blank_line
// NB: all on one line:
"MODULE =" [Foo::Bar] "PACKAGE =" [Foo::Bar]
( "PREFIX =" [foo_] )?
blank_line = /^\s*$/
XS_file_part = ( file_scoped_decls* xsub )*
file_scoped_decls =
blank_line
// Any valid CPP directive: these are passed through:
| "#if" | "# if" | "#define" | // etc
| #comment // anything not recognised as CPP directive
| pod
| "SCOPE:" enable
| "EXPORT_XSUB_SYMBOLS:" enable
| "PROTOTYPES:" enable
| "VERSIONCHECK:" enable
| "FALLBACK:" ("TRUE" | "FALSE" | "UNDEF")
| "INCLUDE:" [foo.xs]
| "INCLUDE_COMMAND:" [... some command line ...]
| "REQUIRE:" [1.23] // min xsubpp version
| "BOOT:"
code_block
| "TYPEMAP: <<"[EOF]
// Heredoc with typemap declarations.
[EOF]
enable = ( "ENABLE" | "DISABLE" )
code_block = // Lines of C and/or blank lines terminated by the
// next keyword or XSUB start. POD is stripped.
xsub = blank_line // not *always* necessary
xsub_decl
( cases | xbody )
xsub_decl = return_type
xsub_name "(" parameters ")" "const" ?
return_type = "NO_OUTPUT" ? "extern \"C\"" ? "static" ? C_type
C_type = [const char *] // etc: any valid C type
C_expression = [foo(ix) + 1] // etc: any valid C expression
xsub_name = [foo] | [X::Y::foo] // simple name or C++ name
parameters = empty
| parameter ( "," parameter )*
empty = /\s*/
parameter = (
in_out_decl ?
C_type ?
/\w+/ // variable name
// Default or optional value:
( "=" ( C_expression | "NO_INIT" ) )?
// Pseudo-param: foo must match another param name:
| C_type "length(" [foo] ")"
| "..."
)
in_out_decl = "IN" | "OUT" | "IN_OUT" | "OUTLIST" | "IN_OUTLIST"
cases = (
"CASE:" ( C_expression | empty )
xbody
)+
xbody = implicit_input ?
xbody_input_part *
xbody_init_part *
xbody_code_part
xbody_output_part * // Not after PPCODE.
xbody_cleanup_part * // Not after PPCODE.
implicit_input = ( blank_line | input_line )+
xbody_input_part =
"INPUT:" ( blank_line | input_line )*
| "PREINIT:"
code_block
| xbody_generic_key
| c_args
| interface_macro
| "SCOPE:" enable // Only in xsubpp 3.58 onwards.
input_line = C_type
"&" ?
/\w+/ // variable name
// Optional initialiser:
(
( "=" | ";" ) "NO_INIT"
|
// Override or add to the default typemap.
// The expression is eval()ed as a
// double-quotish string.
"=" [ a_typemap_override($arg) ]
|
("+" | ";") [ a_deferred_initialiser($arg) ]
)?
";" ?
xbody_init_part = "INIT:"
code_block
| xbody_generic_key
| c_args
| interface
| interface_macro
xbody_code_part =
autocall
| "CODE:"
code_block
| "PPCODE:"
code_block
| // Only recognised if immediately following
// an INPUT section:
"NOT_IMPLEMENTED_YET:"
// Implicit call to wrapped library function.
autocall = empty
xbody_output_part =
xbody_postcall *
xbody_output *
xbody_postcall = "POSTCALL:"
code_block
| xbody_generic_key
xbody_output = "OUTPUT:"
( blank_line
| output_line
| "SETMAGIC:" enable
)*
| xbody_generic_key
// Variable name with optional expression which
// overrides the typemap
output_line = /\w+/ ( [ sv_setfoo(ST[0], RETVAL) ] )?
xbody_cleanup_part = "CLEANUP:"
code_block
| xbody_generic_key
// Text to use as the arguments for an autocall;
// may be spread over multiple lines:
c_args = "C_ARGS:" [foo, bar, baz]
// Comma-separated list of Perl subroutine names
// which use the XSUB, over one or more lines:
interface = "INTERFACE:" [foo, bar, Bar::baz]
interface_macro =
"INTERFACE_MACRO:"
[GET_MACRO_NAME]
[SET_MACRO_NAME] ?
// These can appear anywhere in an XSUB.
xbody_generic_key = pod
| alias
| "PROTOTYPE:" ( enable | [$$@] )
// Whitespace-separated list of overload types,
// over one or more lines:
| "OVERLOAD:" [ cmp eq <=> etc ]
// Whitespace-separated list of attribute names,
// over one or more lines:
| "ATTRS:" [foo bar baz]
alias = "ALIAS:"
// One or more lines; each with zero or more
// {alias_name, op, index} triplets:
(
[bar] "=" [5]
| [Foo::baz] "=" [A_CPP_DEFINE]
| [Bar::boz] "=>" [Foo::baz]
)*
XS와 XSUB의 개요 (OVERVIEW OF XS AND XSUBS)
첫 읽기와 추가 읽기 (Initial and Further Reading)
이 문서는 여러분이 XS와 XSUB의 아주 기초적인 부분을 이미 알고 있다고 가정하고 구성됐어요. 특히 코드 예시에서 파일 뒷부분에서만 설명하는 흔한 키워드를 미리 쓰는 경우가 있어요. 그러니 기본 친숙함을 얻은 뒤에 이 문서를 순서대로 읽으면 돼요.
이 문서는 크게 두 부분으로 나뉘어요. 먼저 긴 개요 부분이 있는데, XS와 XSUB가 무엇인지, perl 인터프리터가 XSUB를 어떻게 부르는지, 데이터가 XSUB로/에서 어떻게 오가는지를 아주 상세히 설명해요. 단순한 XSUB를 작성하는 데 꼭 필요한 것보다 훨씬 많은 내용이지만, 이 문서는 포괄적이기로 의도했어요. 그다음이 본격적인 참조 매뉴얼로, 각 키워드와 XSUB 선언·정의의 다른 부분마다 섹션이 있고, typemap 사용이나 정적 데이터 저장 같은 일반 주제도 있어요.
필요하면 perlxstut부터 읽어 보세요. 더 부드러운 튜토리얼 입문이에요. 추가로 다음 Perl 문서들이 도움이 될 수 있어요.
- perlxstypemap: typemap 파일과 새 typemap을 만드는 법을 설명해요. typemap은 XS 컴파일러가 Perl과 C 데이터 타입 사이를 변환하는 코드를 자동 생성할 때 쓰는 코드 템플릿이에요. C 라이브러리에 대한 인터페이스를 만드는 일은 때때로 그 라이브러리가 쓰는 새 데이터 타입을 처리할 새 typemap 항목을 추가하는 것일 뿐이기도 해요.
- perlguts: Perl 인터프리터 내부의 선택된 부분에 대한 세부 내용을 담고 있어요. 이를 더 잘 이해하면 더 복잡한 XSUB를 작성하거나 디버깅할 때 도움이 돼요.
이 문서의 데이터 전달 개요 부분 상당수는 XS 코드 작성에 가장 관련 있는 perlguts 부분을 요약한 것에 불과해요. 그 문서까지 실제로 읽지 않아도 되게 도와줄 수도 있어요.
- perlclib: XS에서 기본 C 및 OS 라이브러리 함수를 언제 어떻게 써야 하는지 설명해요. 종종 Perl API에는 표준 C 라이브러리 함수 대신 써야 하는 함수가 있어요. 예를 들어
fread()대신PerlIO_read()를 쓰는 식이에요. - perlcall: C에서 Perl 함수를 호출하고
eval {}에 해당하는 일을 하는 방법을 설명해요. - perlembed: 다른 애플리케이션 안에 완전한 Perl 인터프리터를 내장하고 C에서
eval ""에 해당하는 일을 하는 방법을 설명해요.
XS와 XSUB 소개 (An Introduction to XS and XSUBs)
공식적으로 XSUB는 보통 C나 C++로 작성된 컴파일된 함수로, Perl 함수인 것처럼 Perl에서 부를 수 있어요. 여러 개가 .so나 .dll 라이브러리 파일로 컴파일되고, 보통 use Foo::Bar 시점에 동적으로 로드돼요(원칙적으로는 perl 인터프리터에 정적으로 링크할 수도 있어요).
Perl 관점에서 XSUB는 다른 어떤 서브와도 똑같이 보이고 같은 방식으로 호출돼요. 가장 일반적인 경우, XSUB는 임의의 값 리스트를 받고 돌려줄 수 있어요. 더 흔히는 XSUB를 기존 C 라이브러리 함수를 부르는 얇은 래퍼로 쓸 때처럼, 고정된 인수 리스트를 받고 단일 결과를 돌려주거나(void 함수라면 0개 항목) 할 수 있어요.
XS 파일은 C 코드와 XSUB 선언이 섞여 있는 템플릿 파일 형식이에요. 대부분의 보일러플레이트 코드(예: 인수 값을 C와 Perl 사이에서 변환)가 자동으로 처리되는 XSUB를 생성하는 데 쓰여요.
이 문서는 XS 파일에 있는 것과 그로부터 생성된 C 함수를 둘 다 XSUB라고 부른다는 점을 유의하세요. 어느 쪽을 말하는지는 문맥에서 분명해요.
XSUB를 특정 C 라이브러리의 함수와 Perl 사이의 얇은 래퍼로 쓸 때, XS 파일의 XSUB 정의는 이름·매개변수·반환 타입의 선언 몇 줄뿐인 경우가 많아요. XS 파서가 거의 모든 무거운 일을 대신 해 줘요.
XS는 선택 사항이에요. 원칙적으로 직접 C 코드를 쓰거나, Inline::C나 SWIG 같은 다른 시스템을 쓸 수도 있어요. 기존 컴파일된 라이브러리에 대한 간단한 바인딩을 만들려면 FFI::Platypus나 FFI::Raw 같은 CPAN 모듈의 libffi 인터페이스도 있어요. XS를 만드는 게 처음엔 그보다 노력이 더 들 수 있지만, 의존성 면에서는 가벼워요.
XSUB에는 세 가지 주요 역할이 있어요. C 라이브러리 함수의 얇은 래퍼로 쓰일 수 있고(예: Digest::SHA), 순수 Perl보다 빠르거나 C로 하기 더 쉬운 함수를 작성하는 데 쓰일 수 있고(예: List::Util), Perl 인터프리터 자체를 확장하는 데 쓰일 수 있어요(예: threads).
XS는 첫 번째 역할을 광범위하게 지원하고, 두 번째를 쓸 때 보일러플레이트 코드를 덜 필요하게 만들어요. 이 문서는 세 번째 역할을 다루지 않는데, 보통 Perl 인터프리터 내부에 대한 광범위한 지식이 필요하기 때문이에요.
Perl에 딸려 오는 h2xs 유틸리티는 원칙적으로 C 헤더 파일에서 초기 XS 파일을 생성하는 데 쓸 수 있어요. (아마 사소한 편집만 거치면) 전체 C API를 감싸는 데 쓸 수 있어요. 다만 이 유틸리티는 꽤 오래됐고 더 현대적인 C 헤더 코드는 다루지 못할 수 있다는 점을 유의하세요.
h2xs(및 다른 도구)는 헤더 파일에서 유래하지 않았을 때도 초기 "빈" 골격 배포판을 생성하는 데 쓸 수 있어요(자세한 내용은 perlxstut 참고).
Typemap은 int 같은 C 타입을 T_IV 같은 논리적 XS 타입으로, 그리고 거기서 $var = ($type)SvIV($arg)와 sv_setiv($arg, (IV)$var) 같은 INPUT·OUTPUT 템플릿으로 매핑하는 규칙 집합이에요. 이 템플릿은 변수 확장(variable expansion)을 거쳐 Perl 인수와 C 자동 변수 사이를 앞뒤로 변환하는 C 코드를 생성해요.
흔한 C·Perl 타입에 대한 규칙을 담은 표준 시스템 typemap 파일이 있어요. 거기에 더해 여러분만의 typemap 파일을 추가할 수 있고, xsubpp 3.01부터는 XS 파일 안에 typemap 선언을 인라인으로 추가할 수도 있어요. 새 C 타입을 기존 XS 타입에 매핑해서 기존 템플릿을 활용할 수도 있고, 새 템플릿을 추가할 수도 있어요. 전자의 예로, 이런 C 헤더 파일을 쓴다면:
typedef int my_int;
이 typemap 항목을 추가하는 것만으로:
my_int T_IV
my_int 매개변수 타입을 가진 XSUB를 처리할 때 XS 파서가 기존 T_IV 템플릿을 쓰도록 충분해요. 자세한 내용은 "Using Typemaps"와 perlxstypemap을 보세요.
XS 파일은 ExtUtils::ParseXS나 xsubpp 유틸리티(모듈의 얇은 래퍼)가 파싱해서 .c 파일을 생성해요. xsubpp는 보통 배포판의 Makefile(ExtUtils::MakeMaker가 생성)에서 빌드 시점에 호출되거나, ExtUtils::ParseXS를 Module::Build처럼 직접 쓸 수도 있어요. 그 C 파일은 다시 모듈 빌드·설치 시점에 .so나 .dll로 컴파일돼요.
XS 파일의 구조 (The Structure of an XS File)
XS 파일은 두 부분이 있고, 완전히 다르게 파싱·취급돼요. 바로 C 절반과 XS 절반이에요.
첫 MODULE 지시어 줄 앞의 모든 것은 순수 C로 취급돼요(POD 섹션은 버려지고요). C 전처리기 지시어와 C 코드 주석을 포함한 그 줄들은 모두 처리되지 않은 채 대상 C 파일로 전달돼요. XS 주석(아래 설명)은 XS 파서가 인식하지 못하고 그냥 처리되지 않은 채 전달돼요.
이 섹션에 기계가 생성한 C 코드가 1열에 등호 문자를 포함할 가능성이 있는데, 그러면 POD로 오인될 수 있어요. 그게 우려된다면 그런 가상의 코드 생성기가 앞에 공백 문자를 포함하도록 해야 해요.
이 절반은 그 아래 XSUB 코드에 유용할 것들(#include, #define, typedef, static C 함수)을 두는 곳이에요. 일반적으로 XS 파일에서 static 데이터를 선언하는 건 피해야 해요. 자세한 내용과 우회책은 "Safely Storing Static Data in XS"를 보세요.
첫 MODULE 줄 이후의 파일 나머지는 XS 문법으로 해석돼요. 필요하면 추가 MODULE 키워드가 나와 현재 패키지를 바꿀 수 있어요(단일 Perl Foo.pm 파일이 여러 package 문을 갖는 것과 비슷해요).
이 두 번째 절반은 대부분 일련의 XSUB 정의로 구성돼요. 이 XSUB들 사이에는 몇몇 파일 스코프 키워드(추가 MODULE 줄 포함), POD, C 전처리기 지시어, XS(#) 주석, 빈 줄이 올 수 있어요. 자세한 내용은 "File-scoped XS Keywords and Directives"를 보세요.
XS 파일의 절반은 두 단계로 파싱된다고 생각할 수 있어요. 초기 처리 단계에서 XS 파서는 다음과 같은 기본 텍스트 처리를 해요.
-
XS 부분 안의 후행 백슬래시(즉
/\\\n/)는 줄 연속(line continuation)으로 취급돼요. 그런 줄들의 연속은 결합되고, 한 줄로 취급돼요. 이는 다음 줄이 특별한 의미를 잃는다는 뜻이에요. 예를 들어 키워드나 XSUB 끝 빈 줄로 인식되지 않을 수 있어요.단, 결합된 줄에는 그 두 문자
"\\\n"이 유지돼서, 나중에 XS 파서의 본부분이 해석해요. 대부분 이 두 추가 문자는 파서를 혼란시킬 뿐이지만, 그런 줄이 그대로 출력 C 파일로 전달되는 곳(CODE블록이나 C 전처리기 지시어 같은)에서는 백슬래시와 줄바꿈이 C 코드에 나타나는 결과가 돼요.연속 문자를 줄에 남겨 두는 주요 예외는 XSUB의 시그니처인데, 거기서는 후행 백슬래시가 파싱 전에 제거돼요.
-
파서는 POD 줄을 버려요.
/^=/ .. /^=cut\s*$/에 맞는 줄 시퀀스는 모두 POD로 간주돼요. -
XS 주석 줄을 버려요.
/^\s*#/로 시작하면서 XS 파서가 유효한 C 전처리기 지시어로 인식하지 못하는 줄은 XS 주석 줄로 취급돼요.
C 전처리기 지시어와의 혼동을 피하려면 XS # 주석 앞에 공백을 하나 이상 넣는 걸 권장해요.
XS 주석 줄이 백슬래시로 끝나면, 그다음 줄도 그 주석 줄의 일부로 취급되어 함께 버려져요.
그 기본 텍스트 전처리가 끝나면 본격적인 XS 파싱이 일어나요. XS 문법은 매우 줄 지향적이에요. XS 줄과 섹션은 대부분 이런 형태의 키워드로 시작해요:
/^\s*[A-Z_]+:/
파일 스코프 키워드는 1열에 두는 게 좋고, XSUB 스코프 키워드는 들여 쓰는 게 좋아요. 이렇게 하면 XS 파서의 경계 케이스에서 놀라움을 피할 수 있어요.
키워드는 한 줄짜리일 수도 있고(예: PROTOTYPES: ENABLE), 여러 줄짜리일 수도 있어요. 후자는 다음 키워드, 또는 새 XSUB의 가능한 시작(/\n\n\S/), 또는 EOF까지 줄을 소비해요. 여러 줄 키워드는 키워드 뒤 줄의 나머지 텍스트를 데이터의 첫 줄로 취급해요. 예외는 CODE:나 BOOT:처럼 코드 블록을 도입하는 키워드인데, 첫 줄의 나머지를 조용히 무시해요. (그렇다, 이건 구현 결함이에요.)
각 파일 스코프 항목 사이와 각 XSUB 시작 앞에 빈 줄을 두는 게 좋아요. 어떤 항목들은 XSUB 시작 바로 앞 줄에 있어도 올바르게 처리되지만, 파서는 그 처리에서 일관성이 없어요.
XSUB는 /\\n\\n\\S/를 만나면 끝나요. 즉 빈 줄 다음 1열에 뭔가 오면 끝나요. (그래서 XSUB 스코프 키워드를 들여 쓰는 걸 권장하는 거예요.) 1열의 것이 XSUB 사이에 나타날 수 있는 항목(파일 스코프 키워드 같은) 중 하나와 맞으면 그 항목과 이후 줄이 그렇게 처리돼요. 1열에서 시작하면서 달리 인식되지 않는 것은 다음 XSUB 정의의 첫 줄로 해석돼요. 특히 XSUB의 반환 타입으로 해석되는데, /* */처럼 무언가가 예상치 못하게 새 XSUB의 시작으로 해석될 때 이상한 오류가 날 수 있어요. (/* */는 코드 블록 안이 아니면 XS 절반에서 유효하지 않아요.)
C_ARGS 같은 일부 여러 줄 키워드는 단일 해석되지 않은 여러 줄 문자열로 취급돼요. OUTPUT 같은 다른 키워드는 섹션 안의 각 줄이 파싱되는 줄별 문법을 가져요. 마지막으로 CODE 같은 코드 블록은 그대로 출력 C 파일에 복사돼요(컴파일러 오류 메시지가 올바른 위치를 보고하도록 #line 지시어로 감싸질 수도 있어요).
XS 파서는 C 주석을 인식하지 못해요. 그러니 C 코드 안을 제외하고는 쓰지 마세요(예: XSUB 시그니처에). 더 일반적으로, XS 파서는 C 문법이나 의미를 이해하지 못하고, 조잡한 정규식을 써서 XS 파일을 파싱해요. 예를 들어 파서는 이런 XSUB 선언을 처리할 수 있어요:
int
foo(int a, char *b = ",")
여기서 파서는 (...) 사이의 모든 것을 꺼내 쉼표로 쪼개는데, 짝을 이루는 큰따옴표 안의 쉼표를 무시할 정도의 지능만 갖추고 있어요. 파서는 C 타입 선언 문법을 이해하지 못해요. 예를 들어 보통 매개변수 이름으로 보이는 것 앞의 모든 것을 추출해서 그게 타입이라고 가정해요. 그 "타입"은 나중에 typemap에서 찾아보고, 항목이 없으면 그때 가서야 오류를 냅니다. 그래서 다음 매개변수들을 올바르게 파싱하지 못해요:
int
foo(int a /* not-a-comment */, this is seen as a type!! b)
추가로 XS 파서는 역사적으로 매우 관대했어요. 심지어 말도 안 되는 입력도 받아들였죠. xsubpp 3.54-3.61 릴리스쯤부터는 XS 파싱 중에 더 많은 것들이 경고하거나 오류를 낼 가능성이 커졌고, 조용히 컴파일 불가능한 C 코드 파일을 만들기보다는 그렇게 돼요.
앞서 말했듯 XSUB 정의는 보통 /\\n\\n\\S/로 시작해서 다음 /\\n\\n\\S/까지 계속돼요. XSUB 정의는 선언(보통 두 줄)과 그 뒤 선택적 본문으로 구성돼요. 선언은 함수의 이름·매개변수·반환 타입을 주며, C 함수 선언을 흉내 내도록 의도됐어요. 보통 두 줄이에요.
XSUB의 본문은 일련의 키워드로 구성돼요. XSUB의 주요 C 코드는 CODE나 PPCODE 섹션으로 지정돼요. 이것이 없으면 XSUB와 같은 이름·인수를 가진 C 함수 호출로 구성된 짧은 본문이 자동 생성돼요. 이렇게 해서 XSUB는 Perl과 C 라이브러리 함수 사이의 짧은 래퍼 함수가 되고, 래퍼가 Perl과 C 인수 사이를 변환해요. 이 문서에서는 이것을 autocall이라고 불러요.
다른 키워드로 XSUB용으로 생성된 코드를 수정하거나, 인터프리터에 등록되는 방식을 바꿀 수 있어요(예: 속성 추가).
이게 XSUB의 기본 구조예요. 실제 XSUB가 어떻게 생겼는지는 나중에 "The Anatomy of an XSUB"에서 다룰 거예요. 그 전에 잠시 다른 이야기로 빠져볼게요.
XSUB로/부터 데이터가 오가는 방식 개요 (Overview of how data is passed to and from an XSUB)
이 섹션은 XSUB가 어떻게 호출되는지, 인수가 무엇으로 구성되는지, XSUB 인수가 Perl로/부터 어떻게 전달되는지에 대한 기본 배경을 담고 있어요. 본질적으로 perlguts의 관련 섹션을 요약한 거예요. 더 자세한 탐구는 그 문서를 보세요.
이 섹션 정보의 대부분은 기본 XSUB를 만들 때 필요하지 않아요. 하지만 더 복잡한 요구나 디버깅을 위해서는 뒤에서 무슨 일이 일어나는지 이해하는 게 도움이 돼요.
Perl OPs
(다음 문단은 확실히 디버깅 배경용이에요.)
OP는 perl 인터프리터 안의 데이터 구조예요. perl 소스가 컴파일될 때 만들어진 트리 구조 안의 노드를 담는 데 쓰여요. 보통 덧셈이나 함수 호출 같은 perl 소스 안의 단일 연산을 나타내요. 이 구조는 다양한 플래그와 데이터, 그리고 그 OP의 동작을 구현하는 데 쓰이는 C 함수(PP 함수라고 함) 포인터를 가져요. perl 인터프리터의 메인 루프는 현재 OP(PL_op)와 연결된 PP 함수를 호출하고, 보통 PL_op->op_next로 업데이트하는 것으로 구성돼요.
특히 OP_ENTERSUB OP는 pp_entersub() 호출을 통해 함수 호출(XS 함수 호출 포함)을 수행하거나 최소한 시작해요.
SV와 Perl 인터프리터의 인수 스택 (SVs and the Perl interpreter's argument stack)
Perl 인터프리터의 거의 모든 런타임 데이터(모든 Perl 변수 포함)는 SV 구조에 저장돼요. 이 SV는 정수(IV — integer value), 문자열(PV — pointer value), 참조(RV), 배열(AV), 배열의 요소, 서브루틴(CV — code value) 등 다양한 타입의 데이터를 담을 수 있어요. 이들은 아래에서 더 자세히 논의할 거예요.
Perl에는 인수 스택이 있어요. SV 포인터들의 C 배열이죠. PP 함수의 런타임 동작 대부분은 SV 포인터를 스택에 밀어 넣거나 꺼내 처리하는 것으로 구성돼요. 거기에는 mark 스택도 있는데, 인수 스택 오프셋인 정수 배열이에요. 이 mark들은 스택을 프레임으로 구분해 줘요.
이 서브루틴 호출을 생각해 봐요:
@a = foo(1, $x);
함수가 호출되기까지 Perl 인터프리터가 실행하는 여러 OP는: 새 인수 스택 프레임의 시작을 나타내는 mark를 푸시하고, 정수 값 1을 담은 SV를 푸시하고, 현재 변수 $x와 연결된 SV를 푸시하고, *foo 타입글로브를 푸시해요. 그러면 OP_ENTERSUB와 연결된 PP 함수 pp_entersub()가 그 타입글로브를 꺼내, 그로부터 &foo CV를 추출하고, 그것이 일반 CV인지 XSUB CV인지 확인해요.
일반 Perl 서브루틴 호출의 경우 pp_entersub()는: mark 스택에서 맨 위 mark를 꺼내고, 그 mark와 스택 꼭대기 사이의 SV 포인터를 꺼내 @_에 저장하고, &foo CV가 가리키는 첫 OP로 PL_op를 설정해요. 그 OP들은 함수의 마지막 문과 연결된 OP(또는 명시적 return)가 스택에 반환 값을 SV 포인터로 남길 때까지 메인 루프가 실행해요.
XSUB 서브의 경우 pp_entersub()는 대신 맨 위 mark의 값(꺼내지 않고)을 기록하고, CV가 가리키는 C 함수(XS 파서가 생성한 XSUB)를 호출해요. XSUB 자체가 mark 스택을 꺼내고, 스택 위의 인수를 처리하고, 반환 값을 푸시할 책임이 있어요. 다만 단순한 XSUB의 경우 이것은 보통 XS 파서가 생성한 보일러플레이트 코드가 다 해 줘요. 정확히 무엇이 자동으로 되고 무엇이 재정의·수동으로 처리될 수 있는지는 이 문서의 주제 중 하나예요. 마지막으로 pp_entersub()는 반환된 값에 대한 후처리를 해요. 예를 들어 함수 호출이 스칼라 문맥이었다면 꼭대기 항목을 제외한 나머지를 버려요.
SV의 참조 카운트 (An SV's reference count)
Perl은 참조 카운팅을 가비지 컬렉션 방법으로 써요. SV에 항상 존재하는 필드 중 하나가 참조 카운트이고, SvREFCNT(sv)로 접근할 수 있어요.
보통 SV의 참조 카운트는 SV에 대한 포인터가 어딘가 저장될 때마다 증가하고, 그런 포인터가 제거될 때마다 감소해요. 참조 카운트가 0에 도달하면 그 SV에 연결된 소멸자가 호출되고, SV가 해제돼요. 참조 카운트를 잘못 관리하면 SV가 누출되거나 조기에 해제될 수 있어요.
XS가 모든 보일러플레이트 코드를 생성하게 맡기면 참조 카운트 정리는 보통 자동으로 처리돼요. 직접 다루기 시작하면 고려할 점이 몇 가지 있어요.
newSViv(i)처럼 새 SV를 만드는 함수는 초기 SvREFCNT()가 하나인 SV를 돌려줘요. 실제로는 하나가 너무 높은 값이에요. 아직 이 SV에 대한 포인터가 어디에도 저장되지 않았으니까요. 이 SV가 곧 어딘가에 내장될(예: 배열에 저장) 것으로 기대하는데, 그러면 그 "하나"의 카운트를 "소유"하게 돼요. 프로그램이 새 SV가 내장되기 전에 croak()나 그와 유사한 것을 호출하면 누출돼요. croak()은 eval()에 잡힐 수 있으니, croak()이 여러 번 호출되어 매번 누출될 수도 있어요. 또한 많은 것이 간접적으로 croak()을 유발할 수 있어요. 예를 들어 tied 변수와 연결된 SV의 값에 접근하면 그 FETCH() 메서드 호출이 촉발되고, 그것이 die를 부를 수 있어요. 그래서 새 SV는 빨리 내장돼야 해요.
그런 새 SV는 이미 참조 카운트가 하나이므로, 내장할 때 참조 카운트를 늘리지 않는 방식으로 해야 해요. 예를 들어 이것은 sv를 정수 값을 담은 새로 만든 SV에 대한 참조로 수정해요. 즉 Perl의 $sv = \\99에 해당하죠:
sv_setrv_noinc(sv, newSViv(99));
_noinc 변형을 쓰는 이유는 그것에 대한 참조를 만들 때 정수 값 SV의 참조 카운트를 늘리지 않기 때문이에요.
적절한 곳에서 참조 카운트는 SvREFCNT_inc()와 SvREFCNT_dec() 및 그 변형으로 조정할 수 있어요.
이 시스템의 예외는 인수 스택이에요. 스택 위의 SV에 대한 포인터는 그 SV의 참조 카운트에 기여하지 않아요. XS가 보통 생성하는 코드는 이를 활용해요. 예를 들어 단일 값을 반환할 준비가 되면 XSUB는 새 SV 포인터를 현재 스택 프레임의 기저에 저장하고(옛 값을 덮어쓰고) 인수 스택 포인터를 프레임 기저 + 1로 재설정한 다음 반환해요. 스택 위의 원래 값들은 모두 참조 카운트 조정 없이 버려져요.
XSUB가 새 SV를 반환할 때는 문제가 될 수 있어요. 이 SV는 (참조 카운트를 갖지 않는) 스택 위에만 내장되어 있으므로, 코드가 croak하면 스택 위의 SV가 누출돼요. 이를 피하기 위해 Perl 인터프리터에는 별도의 temps 스택이 있어요. 이 스택의 항목은 참조 카운트가 계산돼요. 보통 temps 스택은 각 문장 시작 시 어떤 특정 레벨로 재설정돼요. 이 레벨 위의 각 SV는 참조 카운트가 감소해요. SV를 temps 스택에 두는 것을 mortalise한다고 불러요. 새 SV를 만들면서 동시에 mortalise하는 게 흔해요. 예는 다음과 같아요:
SV *sv_99 = sv_2mortal(newSViv(99));
SV *sv_abc = newSVpvn_flags("abc", 3, SVs_TEMP);
많은 OP에는 PADTMP라는 SV가 붙어 있어요. 이 SV는 그 OP가 속한 서브와 같은 수명을 갖고(보통 이름 있는 서브가 컴파일될 때 만들어지고, 그 서브가 삭제될 때, 흔히 프로그램 실행 끝에 해제돼요) 보통 참조 카운트가 하나예요. 많은 OP가 값을 반환하기 위해 임시 SV를 (만들었다 나중에 해제하는 대신) 피할 수 있게 해 줘요. 예를 들어 $a + $b의 ADD op는 보통 두 인수의 정수 값을 꺼내 합을 계산하고, 그 PADTMP를 그 값으로 설정하고 스택에 푸시해요. 보통 XSUB를 호출하는 OP_ENTERSUB에는 PADTMP가 붙어 있고, 값을 반환할 때 XS가 생성한 XSUB 보일러플레이트 코드는 보통 매 호출마다 새 mortal SV를 만드는 대신 그 PADTMP를 써서 값을 반환하려 해요.
고도로 실험적인 perl 인터프리터 빌드 옵션 PERL_RC_STACK이 있어서, 그 아래에서는 인수 스택이 참조 카운트되지만, 그건 현재 이 문서의 범위 밖이에요.
IV, NV 등의 타입 (The IV, NV etc types)
IV(Integer Value)는 perl 인터프리터 헤더 파일의 typedef로 C 정수에 매핑돼요. 정확한 정수 타입과 크기는 인터프리터의 빌드 구성에 따라 달라져요. 포인터를 담을 만큼 충분히 큰 것이 보장돼요. UV는 같은 것이지만 부호가 없어요. NV(numeric value)는 부동소수점 값으로 보통 double이에요. 이 타입들은 perl 인터프리터 안에서 널리 쓰여요.
PV(pointer value) "타입"은 문서와 구조 필드 이름 등에서 문자열 포인터(char*)를 가리키는 비공식 용도로 자주 쓰이지만, 실제 선언된 타입은 아니에요. 마찬가지로 RV(reference value)는 다른 SV에 대한 포인터를 비공식적으로 말해요.
또한 SSize_t와 Size_t가 있어, C 배열의 항목 수를 나타내는 부호 있는·없는 정수 값을 담을 만큼 커요. STRLEN은 문자열의 문자 수를 저장하는 변수에 특별히 쓰여요(보통 Size_t의 별칭일 뿐이에요).
SV 스칼라 값 구조 (The SV scalar value structure)
(이 섹션에도 단순 XSUB를 만들 때 몰라도 되지만 디버깅 배경으로 유용한 세부 내용이 많아요.)
앞서 말했듯 perl 인터프리터의 거의 모든 런타임 데이터는 SV(스칼라 값) 구조에 저장돼요. SV 구조의 머리는 3~4개 필드로 구성돼요: 참조 카운트, 타입과 플래그, body에 대한 포인터, 그리고 perl 5.10.0부터 일반 페이로드 필드예요. 약 17개 타입이 있고, 타입은 SV 머리에서 body(있다면)가 가리키는 것을 나타내요. body 타입은 body가 담을 수 있는 데이터의 종류만 나타내고, SV의 실제 "타입"(IV, NV, PV, RV 등)은 대부분 어떤 플래그가 설정됐는지로 나타나요.
단순한 SV는 body가 없을 수 있어요. 정의되지 않은 값은 보통 body가 없어요. 또한 일부 IV, NV, RV 값은 페이로드 필드에 직접 저장돼요. 이 경우 body 포인터는 머리를 다시 가리키도록 위조되지만, 적절한 오프셋을 줘서 "body" 안의 IV 필드(예를 들어)에 접근하려는 시도가 실제로는 머리의 페이로드 필드에 있는 IV 값을 읽게 해요.
body가 있는 SV의 경우 머리의 페이로드 필드는 보통 body에 저장해야 할 하나의 흔한 값을 저장하는 데 쓰여요. 그렇지 않으면 접근하려면 추가 포인터 간접 참조가 필요하니까요. 예를 들어 Perl 문자열 SV의 char* 포인터는 머리에 저장되고, 길이는 body에 저장돼요.
SV의 필드(머리와 body 둘 다)는 보통 매크로로 접근해요. 덕분에 수년 동안 이전 버전 호환성을 유지하면서 머리와 body 필드를 재배치할 수 있었어요. 항상 매크로를 쓰세요. 예를 들어 SvIVX(sv)는 SV의 IV 필드를 직접 접근해요(SV 타입에 따라 머리나 body에 있을 수 있어요). SV에 유효한 정수 값이 있으면 SVf_IOK 플래그가 설정되고, SvIOK(sv) 매크로로 검사할 수 있어요.
SV의 body는 수명 동안 더 "큰" 것으로 업그레이드될 수 있지만, 보통 다운그레이드되지는 않아요. 예를 들어 이 perl 코드를 실행하는 동안:
my $x;
$x = "1";
$u = $x + 1;
undef $x;
처음에 SV에는 body가 없고 SVf_IOK, SVf_NOK, SVf_POK, SVf_ROK 중 어느 플래그도 설정돼 있지 않아요. IV, NV, PV, RV 값 중 어느 것도 없다는 뜻이죠. 그 플래그들이 전혀 없으면 정의되지 않은 값이에요. 문자열이 할당된 후에는 body 타입이 SVt_PV로 설정되고 대응 body가 주어져요. 문자열 포인터와 길이는 body에 저장되고(또는 포인터는 페이로드 워드에), SVf_POK 플래그가 설정되어 SV가 유효한 문자열 값임을 나타내요.
Perl이 그 SV를 정수로 쓰려고 하면 SvIV(sv) 같은 매크로를 써서 정수 값을 돌려받아요. 직접적인 SvIVX() 매크로와 달리, 이 매크로는 먼저 SvIOK(sv)를 확인하고 참이 아니면 현재 문자열 값에서 정수 값을 계산하는 함수를 호출해요. 이 호출의 효과는 SV의 타입과 body를 SVt_PVIV로 업데이트하는 것인데, 이는 문자열 과 정수 값을 모두 담을 수 있고, SVf_POK 플래그에 더해 SVf_IOK 플래그를 설정해요.
마지막으로 undef는 문자열을 해제하고 SVf_IOK와 SVf_POK 플래그를 끄지만 body 타입은 SVt_PVIV로 남겨 둬요. (그래서 SV의 현재 Perl 수준 타입은 body 타입이 아니라 플래그로 결정해야 해요.)
SvIVX() 같은 매크로로 필드에 직접 접근하는 건(X는 direct를 뜻해요), 대응 플래그(예: SvIOK())를 방금 검사했을 때가 아니면 절대 하면 안 돼요. 일반적으로 항상 SvIV() 같은 매크로를 쓰세요. 그런 매크로가 검사와 변환을 다 해 줘요.
SV에는 한 가지 더 복잡함이 있어요. 하나 이상의 매직 항목이 붙을 수 있어요. 이것은 get/set 등의 동작을 하는 함수 포인터 점프 테이블에 대한 포인터와 함께 있는 작은 페이로드예요. $1, $., tied 변수 같은 것을 구현하는 데 쓰여요. 아이디어는 SvIV() 같은 매크로가 먼저 SV에 get 매직이 있는지(SvGMAGICAL(sv)로) 확인하고, 있으면 그 get 메서드를 먼저 호출한다는 거예요. 예를 들어 tied 변수의 경우 이 C 수준 get 함수가 Perl 수준 FETCH() 메서드를 호출하고 그 반환 값을 SV에 할당해요. 그다음에야 SvIV()가 SvIOK() 검사를 해요.
알 수 없는 SV가 제시되면, SV의 플래그 값을 검사하기 전에 항상 그 매직을 먼저 확인해야 해요.
종합하면, SvIV(sv) 매크로는 대략 이와 같은 일을 해요:
if (SvGMAGICAL(sv))
mg_get(sv); /* do FETCH() etc; update the SV's value / flags */
if (!SvIOK(sv))
sv_2iv(sv); /* convert undef to 0, "1" to 1 etc */
return SvIVX(sv); /* use the raw value */
곧 보게 될 거예요: XS의 typemap 템플릿은 대부분 SvIV() 같은 고수준 매크로를 쓰므로, 보통은 이 모든 것이 자동으로 처리돼요. 직접 타입 변환을 시작할 때만 이 세부 사항을 신경 쓸 필요가 있어요.
get 매직을 검사·호출하는 걸 잊으면, 누군가 tied 변수 같은 것을 XSUB에 전달하기 전까지는 보통 잘 작동하는 것처럼 보여요. 그러면 FETCH()가 호출되지 않아요. SvPOK()를 먼저 검사하지 않고 SvPVX() 등으로 필드에 접근하면 존재하지 않는 body의 필드에 접근해 SEGV를 유발할 수 있어요.
매직은 "사용"당 한 번만 호출해야 해요. 예를 들어 tied 스칼라가 XSUB의 인수로 전달되면 FETCH()가 한 번만 호출될 거라 기대해요. 보통은 쉬워요. 여러분(또는 typemap 코드)이 SvIV()를 한 번만 호출하니까요. 가끔 플래그를 확인하려고 명시적으로 mg_get()을 먼저 호출했을 수 있는데, 그 경우 SvIV_nomg() 같은 변형으로 두 번째 매직 호출을 건너뛸 수 있어요. 예:
SvGETMAGIC(sv); /* this calls mg_get() if SvGMAGICAL() */
if (SvNOK(sv))
/* special-case: do something with a floating-point value */
else {
IV i = SvIV_nomg(sv);
/* fall-back to treating it as an integer value */
}
Perl 참조는 또 다른 스칼라 타입일 뿐이에요. SvROK()가 참인 것으로 표시되고, 피참조 SV에 대한 포인터는 SvRV()로 접근해요.
문자열에 대한 SvIV()의 동등물은 SvPV()(및 SvPVutf8 같은 변형)이에요:
STRLEN len;
char *pv = SvPV(sv, len);
이것은 문자열 포인터를 꺼내고 len을 그 길이로 설정해요. (SvPV는 매크로라서 명시적 &len 없이 len을 갱신할 수 있어요.) 이 호출 후 SvPOK(sv)가 참이거나 pv == SvPVX(sv)라는 보장은 없어요. 예를 들어 sv가 오버로드된 stringify("") 메서드를 가진 blessed 객체에 대한 참조일 수 있어요. 그 경우 뒤에서 임시 SV가 생겨 메서드 호출 결과를 담고, pv는 그 SV의 문자열 버퍼를 가리켜요. sv는 참조로 남아 있어요. 마찬가지로, 오버로드되지 않은 배열 참조는 "ARRAY(0x12345678)" 같은 임시 문자열을 돌려줄 수 있어요.
SV를 문자열로 강제 변환해야 한다면(예: 문자열 버퍼를 직접 수정하기 전에) SvPV_force()나 그 변형을 쓰세요. 예를 들어 배열 참조에 쓰면 SV가 참조에서 SvPVX() 값이 "ARRAY(0x12345678)"인 평범한 문자열 SV로 변환되고, 배열의 참조 카운트가 감소해요.
SV가 PV로 강제 변환되면(SvPOK(sv)가 참), SvLEN(sv)는 할당된 버퍼 크기를, SvCUR(sv)는 문자열의 현재 길이(바이트)를 나타내요. 유니코드에서는 SvCUR(sv)가 Perl 내장 length(sv)(문자 개수의 길이)가 돌려주는 값과 같지 않을 수 있다는 점을 유의하세요. 그 값은 sv_len_utf8(sv) 함수로 얻을 수 있어요. 자세한 내용은 아래 "Unicode and UTF-8"을 보세요.
SV 구조는 단순 스칼라 값이 아닌 것을 저장하는 데도 쓸 수 있어요. 특히 배열, 해시, 코드 값이 그렇죠. AV, HV, CV 구조에 대한 typedef가 있어요(그 외 몇 개 더). 이 구조들은 SV와 동일하고 적절한 캐스팅으로 일반적으로 상호 교환해 쓸 수 있어요. 예: SV *ret = (SV*)av. 이 비스칼라 SV의 주요 특징은 이런 경우의 타입 필드 값(SVt_PVAV, SVt_PVHV, SVt_PVCV 등)이 어떤 body를 갖는지 나타내는 대신 실제로 Perl 타입을 나타낸다는 거예요.
중요한 것 하나: AV와 HV는 서브루틴과 XSUB를 호출·반환할 때 스택에 직접 푸시되는 일이 절대 없어요. 대신 필요할 때 그것들에 대한 참조(RV)가 푸시돼요. 적절한 typemap으로 자동되거나, newRV() 등으로요. "Bizarre copy of ..." 오류 메시지가 나오기 시작하면 그런 오류를 처음 발견하게 될 거예요.
Unicode와 UTF-8 (Unicode and UTF-8)
단순한 Perl 문자열 SV는 때때로 바이트 인코딩이라고 불리는 것을 사용해요. 각 문자가 단일 바이트로 표현되죠. 하지만 Perl 문자열이 코드 포인트 >= 0x100을 포함하면, 문자열의 각 문자는 UTF-8 인코딩 체계를 사용해 가변 바이트 수로 저장되며, SvUTF8(sv) 플래그가 설정되어 이를 나타내요. 다른 문자열은 그 문자열의 이력에 따라 UTF-8을 쓸 수도 안 쓸 수도 있어요. 예를 들어:
my $s = "A\x80";
$s .= "\x{100}";
chop $s;
문자열은 바이트 인코딩으로 시작해요. SvCUR(sv) == 2, sv_len_utf8(sv) == 2이고 각 바이트가 한 문자를 나타내죠. 추가 문자가 더해지면 문자열은 UTF-8로 업그레이드돼요. SvCUR(sv) == 5, sv_len_utf8(sv) == 3이고 두 번째·세 번째 문자가 각각 2바이트 저장을 써요. 세 번째 문자가 제거되면 문자열은 UTF-8로 남아요. SvCUR(sv) == 3, sv_len_utf8(sv) == 2이고 두 번째 문자가 2바이트를 써요. 그래서 그런 문자열 SV를 XSUB에 전달하면 두 가지 가능한 표현이 있고, 어느 것이 쓰일지는 다소 예측 불가능해요.
안타깝게도 XS는 현재 UTF-8을 지원하지 않아요. char * 같은 모든 표준 typemap 항목은 문자열 SV의 버퍼가 XSUB가 조작하거나 해석 없이 C 함수에 전달할 바이트 배열이라고 가정해요. XSUB가 인수의 UTF-8 상태를 제어해야 한다면 매개변수 타입을 SV*로 선언하고 직접 조작하는 게 가장 좋아요. 문자열 값을 반환할 때도 마찬가지예요.
SV의 문자열 표현은 SvPVbyte() 및 변형으로 바이트로 강제할 수 있어요. 문자열이 단일 바이트로 표현할 수 없는 문자를 포함하면 그 호출은 Wide character 오류로 croak해요. 반대로 SvPVutf8()와 변형은 문자열을 UTF-8로 강제해요.
자세한 내용은 perlunicode를 보세요.
XSUB의 해부 (The Anatomy of an XSUB)
이전 섹션에서 인수가 스택에 어떻게 푸시되는지, 그 인수가 어떻게 생겼는지, XSUB가 어떻게 호출되는지 설명했어요. 이제 XSUB 함수가 호출된 안에서 무슨 일이 일어나는지 볼게요. 특히 스택 위의 인수에서 값을 꺼내고, 나중에 스택 위에 값 하나 또는 여러 개를 반환하는 방법, 그리고 XS와 typemap이 이 대부분을 어떻게 자동화하는지요.
이 섹션은 XSUB가 XS에서 어떻게 생겼는지 개요와, 그것을 위해 어떤 C 코드가 생성되는지 둘 다 제공할 거예요. 이 문서의 나머지 대부분은 여기 언급된 XSUB의 여러 부분을 더 자세히 설명할 거예요. XSUB 정의의 여러 키워드는 보통 그 XSUB를 위해 생성되는 C 코드와 밀접하게(그리고 같은 순서로) 대응된다는 점을 유의하세요. XSUB를 위해 생성되는 보일러플레이트 코드의 대부분은 시작에서 스택에서 인수 값을 꺼내고, 끝에서 스택에 결과 값 0개 또는 1개를 반환하는 것과 관련돼요.
전형적인 XSUB 정의는 이렇게 생겼어요:
MODULE = Foo::Bar PACKAGE = Foo::Bar
short
baz(int a, char *b = "")
PREINIT:
long z = ...;
CODE:
... do stuff ...;
RETVAL = some_function(a, b, z);
OUTPUT:
RETVAL
XSUB의 처음 두 줄은 그 선언이고, 반드시 빈 줄이 앞에 와야 해요. XSUB의 반환 타입, 이름, 매개변수(기본값 포함)를 줘요. C 문법을 모델로 했지만 실제로는 XS 문법이에요(그래서 /* ... */는 인식되지 않아요). 반환 타입과 이름은 둘 다 1열에서 시작해야 해요. 다만 XS 파서는 실제로 둘 다 같은 줄에 두는 것도 허용해요. 예:
short baz(...)
이 XSUB 정의는 시작이 이렇게 생긴 C 함수로 번역돼요(정확한 세부 내용은 XS 파서 릴리스에 따라 다를 수 있어요):
void
XS_Foo__Bar_baz(pTHX_ CV* cv)
{
dVAR; dXSARGS;
if (items < 1 || items > 2)
croak_xs_usage(cv, "a, b= \"\"");
함수의 첫 줄은 실제로 XS_EXTERNAL() 같은 매크로로 지정돼요. 하지만 설명 목적으로 위에 보인 것은 Perl 버전과 XS 구성에 따른 그 매크로의 가능한 확장 중 하나예요.
주목할 중요한 점은 XSUB의 인수가 C 함수의 인수로 전달되는 게 아니라는 거예요. 여전히 Perl 인수 스택에 있어요. XSUB의 반환 값도 C 함수가 반환하는 게 아니에요.
C 함수의 이름은 XSUB 이름과 현재 XS 패키지(s/:/_/g)를 바탕으로 해요. 디버깅 외에는 이 이름을 일반적으로 알 필요가 없어요.
함수의 매개변수는 이 XSUB와 연결된 CV(즉 &Foo::Bar::baz)와, MULTIPLICITY/스레드 빌드에서는 현재 Perl 인터프리터 문맥에 대한 포인터예요. 대부분 이것들을 직접 쓸 필요는 없어요.
C 함수의 처음 몇 줄은 모든 XSUB에 추가되는 표준 보일러플레이트예요. Perl 인터프리터 매크로의 명명 규칙으로, d로 시작하는 것은 선언이에요. 변수를 선언할 수 있는 곳에 들어가고, 보통 변수 하나 이상과 그 초기화를 선언해요.
dVAR는 대부분 no-op이에요. 예전에는 일부 난해한 Perl 인터프리터 구성에 필요했고 이전 버전 호환성을 위해 여전히 출력돼요.
dXSARGS는 mark 스택에서 인덱스 하나를 꺼내고 스택 위의 인수에 접근할 수 있게 하는 몇몇 자동 변수를 설정해요. 구체적으로 items 변수가 선언되는데, 이는 몇 개의 인수가 전달됐는지 나타내고, 스택에서 인수 n(0부터 셈)의 포인터를 꺼내는 ST(n) 매크로가 쓰는 숨은 변수도 선언돼요. 스택 포인터는 아직 실제로 감소하지 않아요.
일반적인 리스트 처리 XSUB의 경우 이 인수 접근 변수와 매크로를 직접 쓸 수 있어요. 하지만 더 흔히는 고정 시그니처를 가진 XSUB(위 예처럼)에서 파서가 각 매개변수에 대해 C 자동 변수를 선언하고, (시스템 또는 사용자 typemap을 써서) ST(0) 등에서 추출한 값을 할당해요. 또한 XSUB의 반환 타입(void가 아니면)을 가진 RETVAL이라는 변수를 선언하는데, 보통 코드 작성자가 값을 할당하고 그 값이 자동으로 반환돼요. 위 예를 계속하면, XSUB 입력 부분에 대해 생성된 코드는 이와 비슷해요:
{
long z = ...;
short RETVAL;
int a = (int)SvIV(ST(0));
char *b;
if (items < 2)
b = "";
else
b = (char *)SvPV_nolen(ST(1));
이것은 a, b, z, RETVAL 선언과 그것들을 초기화하는 코드로 구성돼요. (int)SvIV(ST(0))처럼 스택 위의 SV에서 값을 추출하는 코드 부분은 typemap 항목에서 파생돼요. a처럼 단순한 항목의 경우 코드가 변수 자체의 선언의 일부로 추가될 수 있고, 그렇지 않으면 b처럼 모든 변수 선언 뒤에 초기화가 별도 문장으로 이뤄질 수 있어요.
변수 선언은 INPUT과 PREINIT 블록에 나타난 순서대로 나오고, 그다음 RETVAL, 그리고 시그니처 안에서 완전히 정의된 매개변수들(즉 INPUT 섹션으로 타입을 지정하지 않은 것들)이 따라와요.
INPUT 섹션은 요즘 대부분 구식이고 PREINIT도 거의 필요 없어요. 5.36 이전 Perl은 C89 컴파일러 의미론을 썼는데, 이는 문장 뒤의 변수 선언을 허용하지 않았어요. CPAN 모듈은 컴파일러 플래그를 어떻게 설정하느냐에 따라 여전히 C89로 기본 설정될 수 있어요. 이를 우회하기 위해 PREINIT 키워드로 함수 일찍 추가 변수 선언 코드를 주입할 수 있어요.
입력 부분에 이어 함수의 본문이 출력돼요. 이는 CODE나 PPCODE 섹션이 있으면 그대로 정확히 복사돼요. 둘 다 없으면 파서는 이 XSUB가 XSUB와 같은 이름의 C 라이브러리 함수를 감싸는 것뿐이라고 가정하고, 다음과 같은 코드를 자동 생성해요:
RETVAL = baz(a, b);
INIT와 POSTCALL 키워드로 메인 코드 바로 앞과 뒤에 코드를 추가할 수 있어요. 보통 autocall에서만 유용해요.
PPCODE는 CODE와 같지만, 인수 처리 후 스택 포인터가 프레임 기저로 재설정되고, 코드 작성자가 반환 값을 스택에 푸시할 책임을 지는 점이 달라요. PPCODE 뒤에는 어떤 키워드도 올 수 없어요. 보통 단일 값 반환을 넘어 리스트를 반환하거나 다른 복잡한 요구가 있는 XSUB에 쓰여요.
CODE와 autocall의 경우, 반환 타입이 void가 아니면 파서가 RETVAL의 값을 반환하는 코드를 생성해요. autocall의 경우 자동이지만, CODE에서는 OUTPUT: RETVAL로 파서에게 요청해야 해요. 어느 경우든 생성된 코드는 이렇게 생길 수 있어요:
{
SV *RETVALSV = sv_newmortal();
sv_setiv(RETVALSV, (IV)RETVAL);
ST(0) = RETVALSV;
}
임시 SV가 생성되고(역시 typemap 템플릿을 써서) RETVAL의 값으로 설정된 다음 스택에 놓여져요. 실제로는 여러 최적화가 쓰일 수 있어요. 특히 앞서 설명한 것처럼 매 호출마다 SV를 할당·해제하는 대신 호출 OP_ENTERSUB에 붙은 PADTMP 대상 SV가 쓰일 수 있어요.
OUT이나 OUTLIST로 선언된 XSUB 매개변수는 추가 출력 코드를 생성해요. 전자는 전달된 인수 중 하나의 값을 갱신하고, 후자는 그 매개변수의 값을(RETVAL에 더해) 스택에 푸시해요.
마지막으로(PPCODE 제외) C 함수 끝에 이런 매크로가 추가돼요:
XSRETURN(1);
이것은 스택 포인터를 프레임 기저보다 하나 위로 재설정하고(그래서 스택 꼭대기 항목이 ST(0)이 돼요) return을 해요.
void XSUB에는 XSRETURN_EMPTY가 대신 쓰여요.
XSUB에서 값 반환하기 (Returning Values from an XSUB)
XSUB의 선언된 반환 타입은 보통 int나 char* 같은 C 타입이에요. XS는 단일 C-스타일 값을 반환하는 이 흔한 경우를 자동화하는 데 아주 능숙해요. 뒤에서 임시 SV를 만들고, 적절한 typemap 템플릿을 써서 그 SV를 RETVAL의 값으로 설정하고, 그 SV를 스택에 반환해요.
하지만 때로는 C-스타일 값이 아니라 Perl-스타일 값을 반환하고 싶을 수 있어요. 예를 들어 Perl의 undef 값이나 Perl 배열 참조요. 또는 여러 값을 반환하거나 전달된 인수 중 하나를 갱신하고 싶을 수도 있어요. 다음 하위 섹션들이 그러한 여러 경우를 설명해요.
XSUB는 Perl lvalue 서브와 다소 비슷하다는 점을 유의하세요. 실제 SV를 호출자에게 반환하는 반면, 일반 Perl 서브는 각 반환 값의 임시 복사본을 반환해요. int 같은 C 값을 반환할 때는 XSUB가 어차피 임시 SV를 반환하므로 문제가 되지 않아요. 하지만 여러분만의 SV를 반환할 때는 이론적으로 눈에 보이는 차이가 있을 수 있어요. 예:
sub foo { $_[0]++ }
foo(an_xsub_which_returns_element_0_of_an_array(\@a));
는 $a[0]을 증가시킬 거예요.
undef / TRUE / FALSE / 빈 리스트 반환 (Returning undef / TRUE / FALSE / empty list)
때로는 정의되지 않은 값을 반환해야 해요. 예를 들어 실패를 나타내려고요. 임시 SV 생성을 건너뛰고 undefined 값으로 CODE 블록에서 일찍 반환할 수 있어요. 예:
int
file_size(char *filename)
CODE:
RETVAL = file_size(filename);
if (RETVAL == -1)
XSRETURN_UNDEF;
OUTPUT:
RETVAL
XSRETURN_UNDEF 매크로는 특수 Perl SV PL_sv_undef의 주소가 ST(0)에 저장되게 하고(Perl 함수 undef가 반환하는 값과 같아요) XSUB가 즉시 반환하게 해요.
autocall을 쓰면 POSTCALL 섹션에서 일찍 반환할 수도 있어요:
int
file_size(char *filename)
POSTCALL:
if (RETVAL == -1)
XSRETURN_UNDEF;
비슷한 매크로들로 Perl의 참·거짓 값이나 빈 리스트를 반환할 수 있어요:
XSRETURN_YES
XSRETURN_NO
XSRETURN_EMPTY
XSUB가 항상 명시적으로 특수 SV를 반환하고 typemap 변환을 절대 필요로 하지 않는다면(예: 항상 XSRETURN_YES나 XSRETURN_NO로 반환한다면) 반환 타입을 그냥 SV*로 선언하면 돼요.
XSUB에서의 조기 반환은 항상 XSRETURN 매크로 중 하나로 해야 하고 return으로 직접 하면 안 된다는 점을 유의하세요. 전자는 인수 스택과 관련된 정리를 해 주기 때문이에요.
SV* 반환 (Returning an SV*)
더 일반적으로, XSUB 보일러플레이트 코드가 임시 SV를 만들고 C-스타일 값으로 설정하게 하는 대신, 직접 SV를 만들고 반환하고 싶을 수 있어요. 이 경우 반환 타입을 SV*로 선언해요. 예:
SV*
abc(bool uc)
CODE:
RETVAL = newSVpv(uc ? "ABC" : "abc", 3);
OUTPUT:
RETVAL
SV* 같은 반환 타입을 쓸 때 특별한 처리가 일어나요. 먼저 int 같은 C 반환 타입의 경우 임시 SV의 값을 설정하는 typemap 템플릿이 이렇게 생길 수 있어요:
sv_setiv($arg, (IV)$var);
확장 후에는 이렇게 생길 수 있어요:
sv_setiv(RETVALSV, (IV)RETVAL);
여기서 임시 SV는 이전에 RETVALSV에 할당됐어요.
이제 반환 타입 SV*로 XSUB를 선언하면, typemap 템플릿이 이렇게 생길 거라 기대할 수 있어요:
sv_setsv($arg, (SV*)$var);
이 Perl 라이브러리 함수는 한 SV의 값을 다른 SV로 복사해요(XS 사용자가 Perl $a = $b에 해당하는 것).
하지만 SV* 타입에 특히 typemap 템플릿을 이렇게 하기로 설계 결정이 내려졌어요:
$arg = $var;
여기서 특별한 처리가 들어와요. XS 컴파일러는 $arg = ...로 시작하는 출력 템플릿의 경우 임시 SV 생성은 건너뛰고 RETVAL의 SV를 직접 반환해요. 그래서 typemap 템플릿은 이렇게 확장돼요:
ST(0) = RETVAL;
이것은 복사보다 빠르죠.
하지만 추가로, 어떤 $arg = ... 템플릿(SV*용 템플릿뿐 아니라)에 대해 XS 컴파일러는 한 가지 더 가정을 해요: 할당 오른쪽의 표현식이 참조 카운트가 하나 너무 높은 SV로 평가된다고요. 그래서 XS 컴파일러는 추가로 이걸 출력해요:
sv_2mortal(RETVAL);
또는 비슷한 것. 이는 SV의 참조 카운트가 (보통) 다음 문장 시작 시점에 1 감소하게 해요. SV가 newSVfoo() 계열 함수 중 하나로 새로 만들어졌다면 말이 돼요. 이 논의는 "An SV's reference count"를 보세요.
하지만 SV가 다른 데서 온 것이라면, 예를 들어 Perl 배열 조회를 통해 왔다면 그 참조 카운트를 조정할 필요가 없으니, mortalise하면 조기에 해제될 거예요. 이 경우 SV의 참조 카운트를 인위적으로 늘려야 해요. 보통 SvREFCNT_inc()를 써요. 아래처럼요.
앞선 예는 newSVpv()로 새 SV를 만드는 것이었어요. 여기 SV가 배열에 미리 존재하는 예가 있어요:
SV*
lookup(int i)
CODE:
{
SV** svp = av_fetch(some_array_AV, i, 0);
if (!svp)
XSRETURN_UNDEF;
/* compensate for the implicit mortalisation */
RETVAL = SvREFCNT_inc(*svp);
}
OUTPUT:
RETVAL
마지막으로, 아주 오래된(1996년 이전) XS 문서 중에는 이런 코드로 여러분만의 SV를 반환하라고 제안한 것이 있어요:
void
foo(...)
CODE:
ST(0) = some_SV;
이것은 매우 잘못됐어요. void 선언이 XS 코드에 스택에서 0개의 항목을 반환하라고 알려주니까요. 와일드에는 아직 이런 코드가 있어서, 이를 우회하기 위해 XS 컴파일러는 CODE 블록 안에서 ST(0)이 할당되는 걸 보면 void XSUB에 대해 아주 특별하고 추한 해킹을 해요. XSUB가 실제로 SV*를 반환한다고 선언된 것처럼 가장해서 XSRETURN_EMPTY가 아니라 XSRETURN(1)을 출력해요. 하지만 이것에 의존하지 마세요. 언젠가 경고할 가능성이 높아요. XSUB가 직접 ST(0)을 설정한다면 항상 반환 타입을 SV*로 선언하세요.
mark 스택은 인수를 반환할 때 쓰이지 않아요. 대신 XSUB의 호출자(보통 OP_ENTERSUB)가 XSUB를 호출하기 전의 인수 스택 프레임 기저 오프셋과 반환 시점의 스택 포인터 오프셋을 기록하고, 거기서 반환된 인수 개수를 알아내요.
AV* 등 참조 반환 (Returning AV* etc refs)
때로는 AV, HV, CV 같은 비스칼라 SV를 반환하고 싶을 수 있어요. 하지만 이것들은 인수 스택에 직접 올 수 없어요. 대신 AV에 대한 참조를 반환해야 해요. Perl 서브가 \@foo를 반환하는 것과 비슷하죠.
표준 typemap이 이 참조를 자동으로 만들어줄 수 있어요. 그래서 반환 타입 AV*의 XSUB는 실제로 RETVAL의 AV를 참조하는 RV 스칼라를 만들고 반환해요. 그래서 Perl return [8,9]의 XS 동등물은 이렇게 될 수 있어요:
AV *
array89()
CODE:
RETVAL = newAV();
/* see text below for why this line is needed */
sv_2mortal((SV*)RETVAL);
av_store(RETVAL, 0, newSViv(8));
av_store(RETVAL, 1, newSViv(9));
OUTPUT:
RETVAL
RETVAL 변수는 AV* 타입으로 선언되지만, 호출자에게 실제로 반환되는 것은 RETVAL에 대한 참조인 임시 SV예요. AV* 타입의 표준 출력 typemap 템플릿은 이렇게 생겼어요:
$arg = newRV((SV*)$var);
이것은 AV를 참조하는 새 RV를 만들어요. $arg = ... typemap 규칙 때문에 RV는 반환 전에 올바르게 mortalise돼요. 하지만 newRV() 함수는 참조 대상(RETVAL AV)의 참조 카운트를 증가시켜요. AV는 newAV()로 방금 만들어졌고 참조 카운트가 하나 너무 높으므로 누출돼요. 그래서 sv_2mortal()이 필요한 거예요. 반대로 미리 존재하는 AV라면 mortalise가 필요 없어요.
xsubpp 3.06부터 AV 등에 쓸 수 있는 대안 XS 타입 세트가 있어요. 이 타입들은 새 RV가 가리킬 때 AV의 참조 카운트를 증가시키지 않아요. AV* 등 C 타입을 이 새 XS 타입들에 매핑해 켤 수 있어요:
TYPEMAP: <<EOF
AV* T_AVREF_REFCOUNT_FIXED
HV* T_HVREF_REFCOUNT_FIXED
CV* T_CVREF_REFCOUNT_FIXED
SVREF T_SVREF_REFCOUNT_FIXED
EOF
또는 반환 타입을 SV*로 선언하고 RV 생성을 직접 처리할 수도 있어요:
SV *
create_array_ref()
CODE:
RETVAL = newRV_noinc((SV*)newAV());
OUTPUT:
RETVAL
대신 평평해진 배열을 반환하고 싶다면(Perl return @a의 동등물) 배열 요소를 PPCODE 블록에서 스택에 하나씩 푸시해야 해요. 아래 "Returning a list"를 보세요.
마지막으로, 표준 typemap의 C SVREF 타입은 스칼라에 대한 참조를 만들고 반환하는 방법이에요. 이것은 스칼라만 반환하는 SV* 타입과 대조적이에요.
SV 등과 달리 SVREF는 표준 내장 Perl 타입이 아니라는 점을 유의하세요. 순전히 typemap의 항목으로만 존재해요. 그래서 이 경우 C 컴파일러에게 SVREF가 SV*의 또 다른 이름이라고 알려줘야 해요:
typedef SV *SVREF;
그런 다음 이렇게 XSUB에서요.
SVREF
foo()
CODE:
RETVAL = newSViv(9);
sv_2mortal(RETVAL);
OUTPUT:
RETVAL
RETVAL은 SVREF 타입으로 선언되고(그래서 위의 typedef가 필요해요) XSUB는 RETVAL SV에 대한 참조를 반환해요. 이 XSUB는 Perl my $x = 9; return \$x에 해당해요.
인수 갱신과 여러 값 반환 (Updating arguments and returning multiple values.)
IN_OUT 같은 매개변수 수정자를 써서, XS는 RETVAL에 더해(또는 대신에) 추가 값을 반환하는 것을 제한적으로 지원해요. 전달된 인수의 값을 갱신하거나(OUT), 매개변수(및 의사 매개변수) 일부를 추가 반환 값으로 돌려주거나(OUTLIST)요. 임의의 값 리스트를 반환하려면 다음 섹션을 보세요.
간단한 XS 예 두 개와 대략적인 Perl 동등물이 있어요:
# Update a passed argument
void sub inc9 {
inc9(IN_OUT int i) my $i = $_[0];
CODE: $i += 9;
i += 9 $_[0] = $i;
}
# Return (2*$i, 3*$i)
void sub mul23 {
mul23(int i, \ my $i = $_[0];
OUTLIST int x, \ my ($x, $y);
OUTLIST int y) $x = $i * 2;
CODE $y = $i * 3;:
x = i * 2; return $x, $y;
y = i * 3; }
전체 세부 내용은 "Updating and returning parameter values: the IN_OUT etc keywords"를 보세요.
리스트 반환 (Returning a list)
리스트, 즉 스택 위의 임의 개수 항목을 반환하고 싶다면, 단일 값 반환에 맞춰진 XS가 생성하는 보일러플레이트 코드 일부의 편리함을 포기해야 해요. 대신 SV를 직접 만들고 푸시해야 해요. PPCODE 키워드가 정확히 이 목적을 위해 있어요. Perl 수준 return 1..$n과 같은 일을 하는 간단한 예가 있어요:
void
one_to_n(int n)
PPCODE:
{
int i;
if (n < 1)
Perl_croak_nocontext(
"one_to_n(): argument %d must be >= 1", n);
EXTEND(SP, n);
for (i = 1; i <= n; i++)
mPUSHi(i);
}
PPCODE 키워드는 인수 스택 포인터를 처음에 프레임 기저로 재설정하고(전달된 인수를 버리고) 자동 반환 코드 생성은 억제해요. XSUB의 반환 타입은 무시되는데, void로 선언하면 RETVAL 변수의 선언만 억제해요.
EXTEND() 매크로는 스택에 최소 그만큼의 여유 슬롯이 있게 해요(첫 인수는 항상 SP여야 해요). mPUSHi() 매크로는 새 SV를 만들고 mortalise하고 그 값을 정수 i로 설정하고 스택에 푸시해요.
인수로 전달된 배열을 평평하게 하는 또 다른 예가 있어요. 이 Perl의 동등물이에요:
sub flatten { my $aref = $_[0]; @$aref: }
이 예에서 푸시되는 SV는 참조 카운트가 하나 너무 높은 채 새로 만들어진 것이 아니므로 mortalise할 필요가 없어요.
void
flatten(AV *av)
PPCODE:
{
int i;
int max_ix = AvFILL(av);
SV **svp;
EXTEND(SP, max_ix + 1);
for (i = 0; i <= max_ix; i++) {
svp = av_fetch(av, i, 0);
PUSHs(svp ? *svp : &PL_sv_undef);
}
}
이 함수는 실제로 배열에 대한 참조를 받을 것으로 기대해요. AV*의 입력 typemap 항목이 인수를 역참조하고 실제로 참조가 아니면 croak해요. PUSHs() 매크로는 mortalise나 복사 없이 SV를 스택에 푸시해요. 배열의 "구멍"은 undef로 채워져요.
XPUSHs() 매크로가 있어서 푸시와 EXTEND(1)을 결합한다는 점을 유의하세요. 하지만 시작 시점에 얼마나 많은 항목을 푸시할지 안다면 한 번에 큰 extend를 하는 게 더 효율적이에요.
또한 XSUB가 호출될 때 스택에 항상 할당된 슬롯 하나가 보장된다는 점도 유의하세요. 인수가 없어도요. 그래서 단일 값을 반환하는 특정 경우에는 extend가 필요 없어요.
부트스트래핑 (Bootstrapping)
각 XSUB 선언에 대해 생성되는 XS_Foo__Bar_baz() C 함수에 더해, 각 XS 파일마다 하나씩 boot_Foo__Bar() C 함수도 자동 생성돼요. 이 XSUB 함수는 모듈이 처음 로드될 때 한 번 호출돼요. 파일의 각 선언된 XSUB에 대해 부트 함수에 다음과 같은 줄이 추가돼요:
newXS("Foo::Bar::baz", XS_Foo__Bar_baz);
(코드의 정확한 세부 내용은 릴리스와 구성에 따라 달라져요.) 이 호출은 CV를 만들고, 그것을 XSUB로 표시하고, 그것에서 XS_Foo__Bar_baz()로의 포인터를 추가한 다음, CV를 Perl 인터프리터 심볼 테이블의 *FOO::Bar::baz 타입글로브에 추가해요. 이것은 Perl 수준의 XS 동등물이에요:
*FOO::Bar::baz = sub { ... }
일부 XSUB의 경우 별명이나 오버로딩 같은 것을 처리하기 위해 파서가 부트 XSUB에 추가 줄을 더할 수 있어요.
BOOT 키워드를 써서 부트 XSUB에 여러분만의 추가 줄을 더할 수 있어요.
전형적인 Perl 모듈 Foo/Bar.pm에는 이런 코드가 있어야 해요:
package Foo::Bar;
our $VERSION = '1.01';
require XSLoader;
XSLoader::load();
이것은 Bar.so나 Bar.dll 파일이 동적으로 링크되고 boot_Foo__Bar() 함수가 호출되게 해요. 이 보일러플레이트 코드는 새 배포판의 골격을 처음 만들 때 h2xs로 보통 자동 생성돼요. 자세한 내용은 perlxstut를 보세요.
참조 매뉴얼 (REFERENCE MANUAL)
이 문서의 이 부분은 각 XS 키워드가 무엇을 하는지 설명해요. XS 파일 안에서 나타날 수 있는 대략적인 순서로, 그다음 XSUB 선언 안에서 나타날 순서로 정렬했어요. 관련 키워드끼리 묶었어요.
MODULE 선언 (The MODULE Declaration)
MODULE = Foo::Bar PACKAGE = Foo::Bar
MODULE = Foo::Bar PACKAGE = Foo::Bar::Baz
MODULE = Foo::Bar PACKAGE = Foo::Bar PREFIX = foobar_
MODULE 키워드는 파일의 XS 절반을 시작하고, 정의되는 함수의 패키지를 지정하는 데 쓰여요. MODULE 키워드는 1열에서 시작해야 해요. 첫 MODULE 키워드 앞의 모든 텍스트는 C 코드로 간주되고 POD를 뺀 채 그대로 출력으로 전달돼요. 그 외에는 손대지 않아요.
보통 각 MODULE 선언 앞에 빈 줄을 넣어야 해요.
첫 선언의 경우 MODULE과 PACKAGE 값이 보통 같아요. 이후 항목에서는 MODULE 값을 그대로 두고 PACKAGE 값이 달라져요. 사실 마지막 선언의 MODULE 값만 사용되고, 모듈이 로드될 때(보통 use Foo::Bar로) 호출되는 부트 XSUB의 이름을 지정해요.
PACKAGE 키워드의 값은 Perl package 키워드와 유사하고, 이후 XSUB가 어떤 패키지에 생성될지 결정해요. 같은 PACKAGE 값이 두 번 이상 나타나는 것도 Perl과 유사하게 허용돼요.
이론상 PACKAGE 키워드는 선택 사항이고 기본값은 ''이에요. 이는 이후 XSUB가 main:: 패키지에 놓이게 된다는 뜻이에요. 실제로는 항상 패키지를 지정해야 해요.
선택적 PREFIX 값은 XSUB의 Perl 이름을 생성할 때 XSUB 이름에서 제거돼요. 보통 autocall XSUB를 만드는 것을 단순화하는 데 쓰여요. Perl에는 패키지 이름이 있지만 C에는 함수 이름 접두사만 있다는 문제를 다루죠. foobar라는 C 라이브러리가 있는데 foobar_read()와 foobar_write() 같은 함수가 있다고 해 볼게요. 이것들을 Foo::Bar라는 Perl 모듈에서 접근 가능하게 만들고 싶어요. PREFIX = foobar_가 있으면 각 XSUB 이름의 그런 접두사는 XSUB의 Perl 이름을 정할 때 제거돼요. 예:
MODULE = Foo::Bar PACKAGE = Foo::Bar PREFIX = foobar_
char* foobar_read(int n)
int foobar_write(char *text, int n)
이것은 Perl 네임스페이스에 Foo::Bar::read()와 Foo::Bar::write()라는 두 XSUB를 넣는데, 호출되면 스스로 C 함수 foobar_read()와 foobar_write()를 불러요.
파일 스코프 XS 키워드와 지시어 (File-scoped XS Keywords and Directives)
첫 MODULE 키워드 후에는 파일의 나머지가 XSUB 정의와 XSUB 사이에 오는 것들로 구성돼요. XSUB는 아래에서 더 설명할 거예요. 이 섹션은 그 사이의 것을 다뤄요. 여기에는 다음 중 어떤 것이든 올 수 있어요.
- 몇몇 파일 스코프 키워드(추가 MODULE 선언 포함). 그 효과는 보통 파일 나머지까지 지속돼요. 이 키워드들은 이 섹션에서 더 자세히 다룰 거예요.
- POD(제거됨).
=cut으로 끝나야 해요. - 빈 줄(버려짐).
- 알려진
/^#/C 전처리기 지시어(그대로 전달됨).#if와#else같은 조건부 지시어는 기본 분석이 수행되는데, 특히 "duplicate XSUB" 경고를 내지 않고 같은 XSUB의 두 변형을 선언할 수 있게 해줘요. 이 경고 억제는 else 분기가 있을 때만 작동해요. 예를 들어 이것은 작동해요:
#ifdef USE_2ARG
int foo(int a, int b)
#else
int foo(int a)
#endif
하지만 이 형식은 여전히 경고를 내요:
#ifdef USE_2ARG
...
#endif
#ifndef USE_2ARG
...
#endif
- XS 주석 줄(제거됨). C 전처리기 지시어로 인식되지 않는
/^#/또는/^\s+#/. - 그 외의 것은 오류이지만, 1열에서 시작하면 새 XSUB의 시작으로 취급돼요.
다음 파일 스코프 키워드가 지원돼요. SCOPE는 기술적으로 파일 스코프 키워드일 수도 있지만, 아래에서 XSUB 키워드로 설명돼요.
REQUIRE: 키워드
REQUIRE: 3.58
REQUIRE 키워드는 XS 모듈을 컴파일하는 데 필요한 ExtUtils::ParseXS XS 컴파일러(및 그 xsubpp 래퍼)의 최소 버전을 나타내는 데 쓰여요. /\\d+\\.\\d+/ 형식의 부동소수점 숫자여야 해요. Perl 프로그램에서 use v5.xx가 필요한 perl 인터프리터 최소 버전을 나타내는 것과 유사해요.
VERSIONCHECK: 키워드
VERSIONCHECK: DISABLE | ENABLE
버전 검사(기본적으로 활성화)는 .so나 .dll 파일에 컴파일된 버전이 .pm 파일의 $VERSION 값과 맞는지 확인하고, 맞지 않으면 이런 오류 메시지로 die해요:
Foo::Bar object version 1.03 does not match bootstrap parameter 1.04
보통 모듈이 처음 빌드될 때 .pm 파일의 $VERSION 변수 값이 생성된 Makefile에 XS_VERSION으로 복사되고, 거기서 -DXS_VERSION=... 컴파일러 옵션을 통해 부트 XSUB에 구워져요. 모듈이 로드되고 부트 코드가 호출되면 버전을 비교하고, 불일치하면 croak해요. 이는 보통 .so와 .pm 파일이 다른 설치에서 왔음을 뜻해요. 예를 들어 누군가 최신 .pm 파일을 복사했지만 .so를 복사하거나 재빌드하는 걸 잊은 경우예요.
PM 모듈의 버전이 부동소수점 숫자이면 비교 전에 문자열화되는데, 정밀도 손실이 있을 수 있어서(현재 소수점 9자리로 잘림) XS 모듈 버전과 더 이상 맞지 않을 수 있어요. 긴 버전 번호를 쓴다면 $VERSION 선언을 따옴표로 감싸서 문자열로 만드는 게 권장돼요.
이 검사를 비활성화할 좋은 이유는 거의 없어요.
이 모듈 버전 검사는 XS 컴파일러 버전에 대한 검사인 REQUIRE 키워드와 완전히 무관하다는 점을 유의하세요.
VERSIONCHECK 키워드는 xsubpp의 -versioncheck와 -noversioncheck 옵션에 대응해요. 이 키워드가 명령줄 옵션을 덮어써요.
PROTOTYPES: 키워드
PROTOTYPES: DISABLE | ENABLE
프로토타입이 활성화되면(기본적으로 비활성화) 이후 XSUB에 Perl 프로토타입이 주어져요. 프로토타입 문자열은 보통 XSUB의 매개변수 목록에서 생성돼요. 이 키워드는 XS 모듈에서 여러 번 써서 모듈의 다른 부분에 대해 프로토타입을 켜고 끌 수 있어요.
예를 들어 이 두 XSUB 선언이:
int add1(int a, int b)
PROTOTYPES: ENABLE
int add2(int a, int b)
Perl 수준에서 이렇게 행동해요:
sub add1 { ... }
sub add2($$) { ... }
또한 프로토타입은 XSUB 수준의 PROTOTYPE 키워드로 XSUB마다 덮어쓸 수 있다는 점도 유의하세요.
일반적으로 XSUB 프로토타입은(Perl sub 프로토타입과 유사하게) 사용 가치가 아주 제한적이고, 보통 Perl 내장 함수의 동작을 흉내 내는 데만 쓰여요. 예를 들어 Perl 인터프리터에게 @a를 평평하게 만들지 말라고 알릴 방법 없이는 push @a, ...; 스타일 함수를 구현할 방법이 없어요. 이런 좁은 용도 외에는 프로토타입을 쓰는 것이 일반적으로 실수예요.
XS 초기에는 프로토타입을 쓰는 것이 아마 좋은 일이라고 생각했고 프로토타입이 기본적으로 활성화됐어요. 곧 기본 비활성화로 바뀌었고, 선호를 명시하지 않으면 경고가 추가됐어요. 그래서 PROTOTYPES 키워드가 없으면 이런 귀찮은 경고를 받아요:
Please specify prototyping behavior for Foo.xs (see perlxs manual)
그래서 99%의 경우 .xs 파일의 XS 절반 시작에 이걸 추가하고 싶을 거예요:
PROTOTYPES: DISABLE
PROTOTYPES 키워드는 xsubpp의 -prototypes와 -noprototypes 옵션에 대응해요.
Perl 프로토타입에 대한 자세한 정보는 perlsub의 "Prototypes"를 보세요.
EXPORT_XSUB_SYMBOLS: 키워드
EXPORT_XSUB_SYMBOLS: ENABLE | DISABLE
이 키워드는 xsubpp 3.04부터 있고, 그 값은 기본적으로 비활성화돼요.
3.04 이전에는 XSUB를 구현한 C 함수가 export 됐어요. 3.04부터 기본적으로 static으로 선언돼요. 활성화하면 옛 동작을 복원해요. 이 키워드가 필요할 일은 거의 없을 거예요.
INCLUDE: 키워드
INCLUDE: const-xs.inc
INCLUDE: some_command |
이 키워드는 다른 파일의 내용을 XS 파일의 "XS" 부분으로 끌어오는 데 쓸 수 있어요. 최상위 XS 파일과 달리 포함된 파일에는 "C" 전반부가 없고, 파일 전체 내용이 그 줄에 모두 삽입된 것처럼 XS로 취급돼요.
INCLUDE의 흔한 용도 중 하나는 ExtUtils::Constant가 생성한 상수 정의를 포함하는 거예요.
INCLUDE 키워드의 매개변수 뒤에 파이프(|)가 오면 XS 파서가 그 매개변수를 명령으로 해석해요. 이 기능은 아래 문서화된 INCLUDE_COMMAND: 지시어를 위해 약하게(deprecated) 폐기됐어요. 후자를 쓰면 명령에 쓰인 perl(있다면)이 XS 파서를 실행하는 것과 같은 것임을 보장할 수 있어요.
INCLUDE_COMMAND: 키워드
INCLUDE_COMMAND: $^X -e '...'
xsubpp 2.2205부터.
INCLUDE: some_command|와 비슷하지만 |가 암시적이고, 있으면 특수 토큰 $^X를 XS 파서를 실행 중인 perl 인터프리터의 경로로 변환해요.
TYPEMAP: 키워드
TYPEMAP: <<EOF
myint T_MYIV
INPUT
T_MYIV
$var = ($type)my_SvIV($arg)
OUTPUT
T_MYIV
my_sv_setiv($arg, (IV)$var);
EOF
xsubpp 3.01부터.
Typemap은 Perl과 C 값을 변환하는 코드 조각을 XS 파서가 자동 생성하게 하는 매핑과 코드 템플릿이에요. TYPEMAP 키워드는 별도 파일의 typemap 대신(또는 그에 더해) typemap 선언을 XS 코드에 직접 내장하는 데 쓸 수 있어요. 이런 내장 typemap 여러 개는 XS 코드에 나타난 순서대로 처리돼요. typemap은 다음 순서로 처리돼요:
- 시스템 typemap 파일.
- 로컬 typemap 파일. 보통 Makefile에서
xsubpp -typemap typemap으로 지정돼요. TYPEMAP:항목들(순서대로).
가장 최근에 적용된 항목이 우선하므로, 예를 들어 TYPEMAP:로 시스템 typemap의 특정 TYPEMAP, INPUT, OUTPUT 항목을 개별적으로 덮어쓸 수 있어요. 일반적으로 typemap 변경은 파일 안의 이후 XSUB들에 영향을 주며, 더 갱신될 때까지 그렇죠.
다만 파싱의 특성상 xsubpp 3.61 이전에서는 XSUB 바로 다음에 오는 TYPEMAP: 블록이 그 XSUB가 쓰는 항목에 영향을 줄 수 있어요. 마치 그 블록이 XSUB 바로 앞에 나타난 것처럼요. 그런 typemap 블록을 모두 XS 파일 시작 근처에 두면 이 문제가 없어요. 실제로 typemap 의미가 XS 파일 진행 중에 바뀌길 원할 때만(드묾) 문제가 될 수 있어요.
TYPEMAP 키워드 문법은 Perl의 "heredoc" 문법을 흉내 내도록 의도됐고, 키워드 뒤에는 다음 세 형식 중 하나가 와야 해요:
<< FOO
<< 'FOO'
<< "FOO"
여기서 FOO는 거의 어떤 문자 시퀀스든 될 수 있고, 이후 줄의 시작에서 일치해야 해요.
typemap 작성에 대한 자세한 내용은 "Using Typemaps"와 perlxstypemap을 보세요.
BOOT: 키워드
BOOT:
# Print a message when the module is loaded
printf("Hello from the bootstrap!\n");
BOOT 키워드는 확장의 부트스트랩 함수에 코드를 추가하는 데 쓰여요. 이 함수는 XS 파서가 생성하고 보통 XSUB를 Perl에 등록하는 데 필요한 문장을 담아요. 보통 use Foo::Bar 시점에 한 번 호출돼요.
이 키워드는 혼자 한 줄에 나타나야 해요. 이후 줄들은 다음 키워드나 새 XSUB의 가능한 시작(/\\n\\n\\S/)까지 C 전처리기 지시어를 포함한 통과시킬 C 코드 줄로 해석되지만, POD와 # 주석은 제외돼요.
FALLBACK: 키워드
MODULE = Foo PACKAGE = Foo::Bar
FALLBACK: TRUE | FALSE | UNDEF
xsubpp 2.09_01부터.
각 패키지에 대해 기본값 UNDEF예요. 현재 패키지(위 예의 Foo:Bar)의 오버로드된 메서드에 대한 기본 fallback 처리 동작을 설정해요. Perl 수준과 유사해요:
package Foo::Bar;
use overload "fallback" => 1 | 0 | undef;
현재 패키지에 OVERLOAD 키워드가 있는 XSUB가 최소 하나 이상 있게 되는 경우에만 효과가 있어요. 자세한 내용은 overload의 "fallback"을 보세요.
XSUB의 구조 (The Structure of an XSUB)
파일 스코프 XS 키워드와 지시어 다음에 XSUB가 나타날 수 있어요. XSUB의 시작은 보통 XSUB 키워드나 파일 스코프 지시어로 인식되지 않는 어떤 것이 1열에서 시작하는 빈 줄로 표시돼요.
XSUB 정의는 선언(보통 두 줄)과 선택적 본문으로 구성돼요. 선언은 XSUB의 이름·매개변수·반환 타입을 지정해요. 본문은 키워드로 시작되는 섹션들로 구성되고, 매개변수와 반환 값을 어떻게 처리할지, XSUB의 주요 C 코드 본문이 무엇인지 지정할 수 있어요. 다른 키워드는 XSUB의 동작을 바꾸거나 Perl에 등록되는 방식을 바꿀 수 있어요(예: 추가 명명된 별명). CODE나 PPCODE 키워드로 지정된 명시적 주요 C 코드 본문이 없으면 파서가 본문을 자동 생성해요. 이 문서에서는 이를 autocall이라고 해요.
키워드 섹션 사이에는 POD, XS 주석, 후행 빈 줄 외에는 아무것도 올 수 없어요. 이들은 모두 본격 파싱 전에 제거돼요. 그 외의 것은 오류를 내거나 새 XSUB의 시작으로 해석돼요.
XSUB의 본문은 최대 다섯 부분으로 생각할 수 있어요. 나타나는 순서대로 Input, Init, Code, Output, Cleanup 부분이에요. 이 구조를 정의하는 공식 문법은 없어요. 특정 키워드가 특정 부분에만 나타날 수 있고 그래서 특정 키워드 뒤에서만 나타날 수 있다는 이해일 뿐이에요.
XSUB 선언 (An XSUB Declaration)
# A simple declaration:
int
foo1(int i, char *s)
# All on one line; plus a default parameter value:
int foo2(int i, char *s = "")
# Complex parameters; plus variable argument count:
int
foo3(OUT int i, IN_OUTLIST char *s, STRLEN length(s), ...)
# No automatic argument processing:
void
foo4(...)
PPCODE:
# C++ method; plus various return type qualifiers:
NO_OUTPUT extern "C" static int
X::Y::foo5(int i, char *s) const
XSUB 선언은 반환 타입, 이름, 매개변수, 선택적 NO_OUTPUT, extern "C", static, const 키워드로 구성돼요.
XSUB의 반환 타입과 NO_OUTPUT 키워드
반환 타입은 void를 포함한 어떤 유효한 C 타입이든 될 수 있어요. void가 아니면 두 가지 목적을 서빙해요. 첫째, 그 타입의 C 자동 변수인 RETVAL이 선언되게 해요. 둘째, (보통) XSUB가 반환 시점에 RETVAL의 값으로 설정된 단일 SV를 반환하게 해요. 또한 void가 아닌 autocall XSUB는 기본 C 라이브러리 함수를 호출하고 그 반환 값을 RETVAL에 할당해요.
추가로 반환 타입은 Perl 패키지 이름일 수도 있어요. 자세한 내용은 "Fully-qualified type names and Perl objects"를 보세요.
반환 타입에 NO_OUTPUT 키워드가 접두되면 RETVAL 변수는 여전히 선언되지만, 그 값을 반환하는 코드는 억제돼요. 보통 autocall 함수 인터페이스를 더 Perl스럽게 만들 때, 특히 C 반환 값이 그냥 오류 조건 표시일 때 유용해요. 예:
NO_OUTPUT int
delete_file(char *name)
# implicit autocall code here: RETVAL = delete_file(name);
POSTCALL:
if (RETVAL != 0)
croak("Error %d while deleting file '%s'", RETVAL, name);
여기서 생성된 XS 함수는 성공 시 아무것도 반환하지 않고, 오류 시 의미 있는 오류 메시지로 die()해요. XSUB의 int 반환 타입은 RETVAL 선언과 autocall을 위해서만 의미가 있어요.
반환 타입은 extern "C"와 static 수정자를 포함할 수도 있는데, 있으면 그 순서여야 하고 NO_OUTPUT 키워드와 반환 타입 사이에 와요. extern 선언은 정확히 보이는 대로, 즉 공백 하나와 C를 큰따옴표로 감싸서 써야 해요. 이 두 수정자는 주로 C++로 작성된 XSUB에 유용해요. C++ XSUB 선언은 C++ 문법을 흉내 낸 후행 const 키워드도 허용해요. 자세한 내용은 "Using XS With C++"을 보세요.
XSUB의 이름 (An XSUB's name)
XSUB의 이름은 보통 반환 타입 다음 줄에 두는데, 그 경우 1열에 있어야 해요. 타입과 이름을 같은 줄에 두는 것도 허용돼요.
이름은 어떤 유효한 Perl 서브루틴 이름이든 될 수 있어요. 가장 최근 MODULE 선언의 PACKAGE 값이 XSUB에 완전히 한정된 Perl 이름을 주는 데 쓰여요.
이름에 패키지 구분자 ::가 포함되면 C++ 메서드 선언으로 취급되고, 암시적 THIS 매개변수 선언 같은 추가 처리가 일어나요. XSUB의 Perl 패키지 이름은 여전히 현재 XS 패키지에 의해 결정되고 C++ 클래스 이름이 아니에요. 자세한 내용은 "Using XS With C++"을 보세요.
XSUB의 매개변수 목록 (An XSUB's parameter list)
XSUB 이름 다음에 괄호 안에 쉼표로 구분된 매개변수 목록이 있어요. 표면적으로 C 함수 선언과 똑같아 보이지만 다르다는 점을 주의하세요. 특히 XS 컴파일러(단순한 정규식 기반 텍스트 프로세서)가 파싱하고 C 타입 문법을 완전히 이해하지 못하며, C 스타일 주석도 인식하지 못해요.
실제로 하는 일은 (...) 사이의 텍스트를 추출하고 쉼표로 쪼개는 것인데, 따옴표 문자열 안의 쉼표와 닫는 괄호를 무시할 정도의 지능은 있어요. 각 매개변수 선언이 추출되면 처리돼요. 아래 "An XSUB Parameter"에서 설명할 거예요.
각 매개변수 선언은 보통 같은 이름의 C 자동 변수 선언과, 대응하는 전달 인수의 값을 그 변수에 할당하는 초기화 코드를 생성해요. 어떤 상황에서는 그 값을 반환하는 코드도 생성될 수 있어요.
원래 XS 문법은 각 매개변수의 타입을 pre-C89 "K&R" C 문법을 흉내 내 하나 이상의 INPUT 섹션에서 별도로 지정해야 했어요. 이를 지원하기 위해 선언 바로 뒤에 실제 키워드를 넣을 필요 없이 암시적 INPUT 섹션이 있어요. 이 패턴은 오래된 XS 코드에서 매우 자주 볼 수 있어요.
암시적 INPUT 키워드가 있는 옛 스타일(흔한 패턴):
int
foo(a, b)
long a
char *b
CODE:
...
명시적 INPUT 키워드가 있는 옛 스타일(드묾):
int
foo(a, b)
INPUT:
long a
char *b
CODE:
...
새 스타일(새 코드에 권장):
int
foo(long a, char *b)
CODE:
...
일반적으로 INPUT 줄에서 지정할 수 있지만 시그니처에는 없는 몇몇 난해한 기능을 제외하고는 더 이상 옛 스타일을 쓸 이유가 없어요.
XSUB 매개변수 (An XSUB Parameter)
유효한 XSUB 매개변수 선언의 몇 가지 예:
char *foo # parameter with type
Foo::Bar foo # parameter with Perl package type
char *foo = "abc" # default value
char *foo = NO_INIT # doesn't complain if arg missing
OUT char *foo # caller's arg gets updated
IN_OUTLIST char *foo # parameter value gets returned
int length(foo) # pseudo-parameter that gets the length of foo
foo # placeholder, or parameter without type
SV* # placeholder
... # ellipsis: zero or more further arguments
XSUB의 매개변수 목록에서 가장 간단한 타입의 선언은 char *foo처럼 C 타입 뒤에 매개변수 이름이 오는 것으로 구성돼요. 여기에는 두 가지 주요 효과가 있어요. 첫째, 그 이름의 C 자동 변수가 선언되게 해요. 둘째, 그 변수가 그 매개변수에 대응하는 전달 인수의 값으로 초기화돼요. 예:
void
foo(int i, char *s)
Perl로는 대략 이렇게 돼요:
sub foo {
my $i = int($_[0]);
my $s = "$_[1]";
...
}
생성된 C 코드는 이렇게 생길 수 있어요:
if (items != 2)
croak_xs_usage(cv, "i, s");
{
int i = (int)SvIV(ST(0));
char *s = (char *)SvPV_nolen(ST(1));
foo(i, s); /* autocall */
...
}
변수 선언과 초기화에 더해, 매개변수의 이름은 보통 usage 메시지와 autocall에 쓰여요(위처럼). 이 변수들은 CODE 블록의 사용자 코드에서 접근 가능해요. 그것들의 값은 보통 반환되지 않아요.
이 기본 패턴에는 몇 가지 변형이 있고, 다음 하위 섹션에서 설명할 거예요.
완전히 한정된 타입 이름과 Perl 객체 (Fully-qualified type names and Perl objects)
Foo::Bar
foo(Foo::Bar self, ...)
보통 XSUB의 매개변수·반환 값 타입은 "char *" 같은 유효한 C 타입이에요. 하지만 Perl 패키지 이름을 쓸 수도 있어요. 타입 이름에 콜론이 포함되면 추가 처리가 일어나요. 특히 C 파일로 출력되는 실제 타입은 (xsubpp가 -hiertype로 호출되지 않았다면) s/:/_/g로 변환되어 합법적인 C 타입이 있어요. Foo::Bar 타입의 전체 효과는 이래요.
Foo::Bar 타입 문자열은 그대로 typemap에서 조회되어 논리적 XS 타입을 찾고, 그다음 INPUT·OUTPUT typemap 템플릿이 $ntype 변수는 "Foo::Bar"로, $type 변수는 "Foo__Bar"로 설정되어 확장돼요. 대응 자동 변수의 선언은 수정된 타입 문자열을 쓰므로, 위 예는 C 코드에서 이런 선언을 만들 수 있어요:
Foo__Bar RETVAL;
Foo__Bar self = ...;
적절한 XS typemap 항목과 C typedef로, 이는 Perl 객체를 전달·반환하는 XSUB를 선언하는 데 도움이 될 수 있어요. 흔한 T_PTROBJ typemap 타입을 쓰는 예는 "T_PTROBJ and opaque handles"을 보세요.
전달된 Perl 객체가 올바른 클래스인지 검사하는 것은 특정 typemap의 구현에 달려 있다는 점을 유의하세요. 예를 들어 T_PTROBJ는 전달된 SV 인수가 Foo::Bar 또는 파생 클래스로 blessed 되지 않으면 croak해요.
XSUB 매개변수 플레이스홀더 (XSUB Parameter Placeholders)
때로는 인수를 건너뛰고 싶을 수 있어요. 플레이스홀더를 효율적으로 선언하는 두 가지 지원 방법이 있어요. 둘 다 C 자동 변수의 선언과 초기화를 완전히 건너뛰지만 인수는 여전히 소비해요.
이름이 있지만 타입이 지정되지 않은 벌거벗은 매개변수 이름은 시그니처에도, 뒤의 어떤 INPUT 섹션에도 타입이 없으면 플레이스홀더로 취급돼요. 예:
void
foo(int a, b, char *c)
CODE:
...
Perl로는 대략:
sub foo {
my $a = int($_[0]);
my $c = "$_[2]";
...
}
타입 SV*만 있고 이름이 없는 매개변수는 특별히 취급돼요. XS 파서의 버그 때문에 파싱할 수 없는 매개변수 선언을 건너뛰곤 했어요. 이로 인해 많은 것이 의도치 않게 사실상 플레이스홀더 선언이 됐죠. 흔한 사용은 SV*였고, 지금은 이전 버전 호환성을 위해 공식적으로 플레이스홀더로 취급돼요. xsubpp 3.57부터 매개변수 이름이 없는 다른 벌거벗은 타입은 오류예요. SV* 텍스트가 어떤 usage() 오류 메시지에 나타난다는 점을 유의하세요. 예:
void
foo(int a, SV*, char *c)
이렇게 croak할 수 있어요:
Usage: Foo::Bar::foo(a, SV*, c) at ...
플레이스홀더는 C_ARGS로 빠진 인수를 덮어쓰지 않는 한 autocall에 쓸 수 없어요. 예:
void
foo(int a, b, char *c)
C_ARGS: a, c
매개변수 값 갱신·반환: IN_OUT 등 키워드
IN int i
int i
IN_OUT int i
IN_OUTLIST int i
OUT int i
OUTLIST int i
보통 매개변수 선언은 같은 이름의 C 자동 변수를 선언하고 대응 전달 인수의 값으로 초기화해요. 이 수정자들은 매개변수가 값을 갱신하거나 반환하게 하고, 초기화를 건너뛰게 할 수도 있어요. 매개변수 선언의 시작에 온다.
이 수정자들은, 단순한 C 함수가 고정 개수의 읽기 전용 매개변수를 받고 단일 값을 반환하므로, 기본 XSUB 문법이 그 패턴을 반영하도록 설계됐다는 문제를 다뤄요.
더 복잡한 C 함수 API를 만드는 일반적인 방법은 변수에 대한 포인터를 전달하는 것이에요. C 함수가 그 포인터로 변수를 설정하거나 갱신하죠. 예를 들어 가상의 C 함수 몇 개가 이렇게 호출될 수 있어요:
int time = ....; // an integer in the range 0..86399
int hour, min, sec;
parse_time(time, &hour, &min, &sec); // set hour, min, sec
increment_time(&hour, &min, &sec); // update hour, min, sec
XS의 IN_OUT 등 수정자로 autocall로 그런 함수를 감싸는 XSUB를 쓸 수 있고, 일반적으로 전달 인수를 갱신하거나 여러 값을 반환할 수 있어요.
이 매개변수 수정자의 규칙은:
- 그러한 모든 매개변수는 수정자와 무관하게 같은 이름의 C 자동 변수를 선언하게 해요.
- 수정자가 없으면 기본값
IN이에요. - 수정자 텍스트는 밑줄로 구분된 한두 부분이에요. 입력 부분이 먼저 오고, 있으면 값이
IN이어야 해요. 출력 부분은 있으면OUT또는OUTLIST중 하나일 수 있어요. - 입력 부분
IN(기본값)은 그 변수를 대응 전달 인수의 값으로 초기화해요. 그렇지 않으면 초기화를 건너뛰어요. 특히 autocall 함수에 초기화되지 않은 값에 대한 포인터가 전달된다는 뜻이에요. 라이브러리 함수가 그 값을 설정하지만 사용하지 않을 거라는 가정이죠. - 출력 부분이 없으면(기본값) 자동 변수의 값을 반환하는 일을 하지 않아요.
- 출력 부분
OUT또는OUTLIST는 값을 어떤 방식으로든 반환하게 하고, 추가로 어떤 autocall 코드도 감싸진 C 라이브러리 함수를 호출할 때 그런 변수에&를 접두하도록 해요. - 출력 부분
OUT은 대응 전달 인수가 변수의 값으로 갱신되게 해요. - 출력 부분
OUTLIST는 변수의 값이(있으면)RETVAL값 뒤에 추가 SV로 반환되게 해요. XSUB 매개변수 목록에 나타난 순서대로 반환돼요. - 수정자가
OUTLIST인 특정 경우, 그것은 의사 매개변수이고 인수를 소비하지 않아요. XSUB의 시그니처의 일부를 형성하지 않지만 어떤 autocall에는 사용돼요. 그래서 예를 들어:
int
foo(int a, OUTLIST int b, int c)
대략 이 C 코드로 변환돼요:
if (items != 2)
croak_xs_usage(cv, "a, c");
{
int RETVAL;
int a = (int)SvIV(ST(0));
int b;
int c = (int)SvIV(ST(1));
RETVAL = foo(a, &b, c);
}
... push the values of RETVAL and b onto the stack and return ...
이 수정자들의 대략적인 Perl 동등물은 아래 예에 있어요. 여기서 Perl 코드 real_foo(\$i)가 C autocall foo(&i)를 대신해요.
IN int i sub foo {
int i my $i = $_[N];
real_foo($i);
}
IN_OUT int i sub foo {
my $i = $_[N];
real_foo(\$i);
$_[N] = $i;
}
IN_OUTLIST int i sub foo {
my $i = $_[N];
real_foo(\$i);
return ..., $i, ...;
}
OUT int i sub foo {
my $i;
real_foo(\$i);
$_[N] = $i;
}
OUTLIST int i sub foo {
my $i; # NB $_[N] is not consumed
real_foo(\$i);
return ..., $i, ...;
}
함께 쓰면 포인터로 추가 값을 반환하는 C 함수를 감쌀 수 있어요. OUT으로 perl에서 C-스타일 API를 유지하거나, OUTLIST로 더 Perl스러운 API를 제공하거나요. 예를 들어 위 예의 parse_time() 함수를 감싸는 것은 OUT으로 할 수 있어요:
void
parse_time(int time, \
OUT int hour, OUT int min, OUT int sec)
Perl에서 이렇게 호출할 수 있어요:
my ($hour, $min, $sec);
# set ($hour, $min, $sec) to (23,59,59):
parse_time(86399, $hour, $min, $sec);
또는 OUTLIST로:
void
parse_time(int time, \
OUTLIST int hour, OUTLIST int min, OUTLIST int sec)
Perl에서 이렇게 호출할 수 있어요:
# set ($hour, $min, $sec) to (23,59,59):
my ($hour, $min, $sec) = parse_time(86399);
기본 매개변수 값 (Default Parameter Values)
int
foo(int i, char *s = "abc")
int
bar(int i, int j = i + ')', char *s = "abc,)")
int
baz(int i, char *s = NO_INIT)
선택적 매개변수는 매개변수 선언에 = C_expression을 붙여 표시할 수 있어요. 인수가 충분하지 않으면 C 표현식이 평가돼요. 기본값이 있는 매개변수는 필수 매개변수 뒤에 와야 해요(현재 XS 컴파일러가 강제하진 않지만). 값은 그 왼쪽에 선언된 매개변수의 값을 포함한 어떤 유효한 컴파일 타임 또는 런타임 C 표현식(아래 참고)이든 될 수 있어요. 특수 값 NO_INIT는 대응 인수가 없으면 매개변수를 초기화하지 않은 채로 두는 것을 나타내요.
XS 파서의 기본 표현식 처리는 다소 단순해요. 그냥 쉼표로 구분된 목록에서 매개변수 선언(선택적 후행 기본값 포함)을 추출하려 하지만 C 문법을 이해하지 못해요. 따옴표 문자열 안의 쉼표와 닫는 괄호는 처리할 수 있지만, '\''나 "\"" 같은 이스케이프 따옴표는 현재 처리할 수 없어요. int j = (i+1) 같은 균형 잡힌 괄호도 처리할 수 없어요.
구현 결함 때문에 기본 값 표현식은 현재 파싱 중에 typemap 템플릿과 유사하게 큰따옴표 문맥에서 eval돼요. 그래서 예를 들어 char *s = "$arg"는 char *s = "ST(0)" 또는 유사한 것으로 확장돼요. 이 동작은 언젠가 고쳐질 수 있어요. 그동안은 기본 값 표현식에서 $와 @ 문자를 피하는 게 좋아요.
length(param_name) 의사 매개변수
int
foo(char *s, int length(s))
C 함수가 문자열 포인터와 길이를 두 인수로 받는 것은 흔하지만, Perl에서는 문자열 값 SV가 문자열과 길이를 단일 값으로 결합해요. 그런 상황에서 autocall 코드 생성을 단순화하기 위해 length(foo) 의사 매개변수가 매개변수 foo의 길이로 작용해요. 인수를 소비하지도 XSUB의 usage 메시지에 나타나지도 않지만, autocall된 C 함수에는 전달돼요. 예를 들어 이 XS는:
void
foo(char *s, short length(s), int t)
이런 C 코드로 번역돼요:
if (items != 2)
croak_xs_usage(cv, "s, t");
{
STRLEN STRLEN_length_of_s;
short XSauto_length_of_s;
char * s = (char *)SvPV(ST(0), STRLEN_length_of_s);
int t = (int)SvIV(ST(1));
XSauto_length_of_s = STRLEN_length_of_s;
foo(s, XSauto_length_of_s, t);
}
Perl에서 이렇게 호출될 수 있어요:
foo("abcd", 9999);
생성되는 정확한 C 코드는 릴리스에 따라 달라지지만, 주목할 중요한 점은:
XSauto_length_of_foo자동 변수는 지정된 타입으로 선언되고 어떤 autocall 함수에도 전달되지만 usage 메시지에는 나타나지 않아요. 이 변수는CODE블록 등에서 쓸 수 있어요.STRLEN_length_of_s자동 변수는SvPV()가 기대하는 타입과 길이 의사 매개변수에 선언된 타입 사이의 변환을 허용하기 위해 추가로 쓰여요.- 길이 매개변수는 시그니처 어디에나 나타날 수 있고, 심지어 같은 이름의 문자열 매개변수보다 앞에 올 수도 있어요. 하지만 어떤 autocall에서의 위치는 시그니처에서의 위치와 일치해요.
- 각 길이 매개변수는 같은 이름의 다른 매개변수와 일치해야 해요. 그 매개변수는 문자열 타입이어야 해요(
T_PVtypemap 타입에 매핑되는 무언가).
타원: 가변 길이 매개변수 목록 (Ellipsis: variable-length parameter lists)
int
foo(char *s, ...)
XSUB는 C 함수 선언과 유사하게 마지막 매개변수로 타원을 지정해 가변 길이 매개변수 목록을 가질 수 있어요. 주요 효과는 매개변수가 너무 많다는 오류 검사를 비활성화하는 것이에요. 선언된 매개변수는 여전히 평소처럼 처리되지만, 프로그래머는 스택의 n 번째 항목에 접근하는 ST(n) 매크로와 전달된 총 인수 개수(고정 인수 포함)를 나타내는 items 변수를 사용해 추가 인수를 수동으로 접근해야 해요. ST(0)이 첫 전달 인수인 반면, 첫 번째 타원 인수는 ST(i)인데 여기서 i는 타원 앞의 고정 인수 개수예요.
현재 XS는 가변 길이 C 함수를 autocall하는 메커니즘을 제공하지 않으므로, 타원은 본문이 있는 XSUB에서만 써야 한다는 점을 유의하세요.
예를 들어 지정된 범위 안의 모든 인수의 합을 반환하는 Perl 서브루틴을 생각해 보세요:
sub minmax_sum {
my $min = shift;
my $max = shift;
my $RETVAL = 0;
$RETVAL += $_ for grep { $min <= $_ && $_ <= $max } @_;
return $RETVAL;
}
이 XSUB는 동등한 기능을 제공해요:
int
minmax_sum(int min, int max, ...)
CODE:
{
int i = 2; /* skip the two fixed arguments */
RETVAL = 0;
for (; i < items; i++) {
int val = (int)SvIV(ST(i));
if (min <= val && val <= max)
RETVAL += val;
}
}
OUTPUT:
RETVAL
리스트를 받고 반환하는 XSUB를 작성하는 것도 가능해요. 예를 들어 이 XSUB는 Perl map { $_*3 } ...에 해당해요:
void
triple(...)
PPCODE:
SP += items;
{
int i;
for (i = 0; i < items; i++) {
int val = (int)SvIV(ST(i));
ST(i) = sv_2mortal(newSViv(val*3));
}
}
CODE와 비교해서 PPCODE 키워드는 인수 스택 포인터의 로컬 복사본을 재설정하고, 코드 작성자가 반환 값을 스택에 놓는 데 의존한다는 점을 유의하세요. 위 예는 SP를 스택 꼭대기로 되돌려 전달 인수를 회수한 다음 스택의 항목을 하나씩 교체해요.
XSUB 입력 부분 (The XSUB Input Part)
XSUB의 선언 부분 다음에 XSUB의 본문이 따라와요. 본문의 첫 부분은 입력 부분이고, 주로 자동 변수를 선언하고 전달 매개변수에서 추출한 값을 그것들에 할당하는 것과 관련돼요. 이 활동과 관련된 두 주요 키워드는 PREINIT와 INPUT이에요. 첫 번째는 추가 변수 선언 줄을 주입하게 하고, 후자는 예전에 각 매개변수의 타입을 지정하는 데 필요했지만 지금은 주로 역사적 관심 대상이에요. 여기에는 드물게 쓰이는 SCOPE 키워드 자리이기도 해요.
"XSUB Generic Keywords"와 "Sharing XSUB bodies"에서 설명한 키워드가 이 부분에 나타날 수도 있고, C_ARGS, INTERFACE_MACRO 키워드도 나타날 수 있어요.
PREINIT: 키워드
PREINIT:
int i;
char *prog_name = get_prog_name();
이 키워드는 매개변수 선언이나 INPUT 줄에서 생성된 자동 변수의 선언 바로 앞에 추가 변수를 선언하고 선택적으로 초기화하게 해요. PREINIT 뒤의 줄들은(POD와 XS 주석 제외) 다음 키워드까지 그대로 C 코드 파일로 복사돼요. 여러 PREINIT 키워드가 허용돼요.
전통적인 C에서는 모든 변수 선언이 어떤 문장보다 앞에 와야 해서 가끔 필요해요. Perl 5.36.0 이후 perl 인터프리터 소스에서는 더 이상 제약이 아니지만, XS 코드를 컴파일할 때 쓰는 C 컴파일러 플래그는 다를 수 있으므로 컴파일러에 따라 여전히 올바른 순서를 유지해야 할 수 있어요.
INPUT이 생성한 변수 선언과 PREINIT 줄은 XS 소스에 나타난 순서대로 출력되고, 그다음 XSUB의 매개변수 선언에서 생성된 변수 선언이 이어져요. 그다음 그 변수들을 초기화하는 문장이 올 수 있어요. 그래서 나중 INIT이나 CODE 블록의 변수 선언은 declaration-after-statement로 표시될 수 있어요.
PREINIT 코드는 이전에 선언된 변수가 이미 초기화됐다고 가정하면 안 돼요. 초기화 코드(보통 typemap에서 얻어짐)가 단순한 type var = init; 형태가 아니거나 기본값이 있으면 초기화가 지연되거든요.
예:
void
foo(int i = 0)
PREINIT:
int j = 1;
CODE:
bar(i, j);
이런 C 코드로 번역될 수 있어요:
{
int j = 1;
int i;
if (items < 1)
i = 0;
else {
i = (int)SvIV(ST(0));
}
bar(i, j);
}
보통 CODE 블록을 중괄호로 감싸면 PREINIT을 없앨 수 있어요. 하지만 변수 초기화의 순서에 민감하다면 필요할 수 있어요. 예를 들어 변화하는 전역 상태에 영향을 받는 경우요.
INPUT: 키워드
void
foo(a, b, c, d, e, int f)
# implicit INPUT section
int a
# explicit INPUT section
INPUT:
long &b
int c = ($type)MySvIV($arg)
int d = NO_INIT
int e + if (some_condition) { $var += 1 }
...
XSUB의 선언 바로 뒤에는 암시적 INPUT 섹션이 있어요. 즉 파서가 본문의 첫 줄 앞에 문자 그대로 "INPUT:\n" 줄이 주입된 것처럼 행동해요. 이 뒤에 명시적 INPUT 섹션이 0개 이상 올 수 있고, PREINIT 같은 다른 키워드·섹션과 섞일 수 있어요.
XS가 처음 만들어졌을 때는 매개변수의 타입을 별도로 지정해야 했던 pre-ANSI C의 문법을 모델로 했어요. 나중에 ANSI C처럼 매개변수 목록에서 타입을 지정할 수 있게 갱신됐어요. 그래서 지금 INPUT 섹션을 쓸 좋은 이유는 거의 없지만, 오래된 코드에서는 자주 마주칠 거예요.
각 INPUT 줄은 최소한 XSUB의 시그니처에 나열된 매개변수의 타입을 지정해요. 예:
char *s
추가로 변수 이름 앞에 &가 붙어(autoall 함수에 변수에 대한 포인터를 전달해야 한다는 뜻)올 수 있고, = + ; 세 문자 중 하나로 시작하는 후치 초기화 수정자가 있을 수 있어요.
변수 이름이 선언된 매개변수와 일치하지 않으면(Perl 버전과 초기화 오버라이드 여부에 따라) 자동 변수 선언으로 취급될 수 있다는 점을 유의하세요. 이 불량 기능은 언젠가 폐기될 수 있으니 의존하지 마세요. 필요하면 PREINIT 섹션을 쓰세요. 이 두 예는 대부분 동등하며, 첫 형식이 선호돼요:
void
foo(int a)
PREINIT:
short b = 1;
void
foo(a)
int a
short b = 1;
INPUT의 & 변수 수정자
& 변수 수정자는 단일 효과가 있어요. autocall 함수에 전달되는 대응 인수가 변수 이름에 &가 접두된 것이 된다는 거예요. OUTPUT: foo와 결합하면 변수의 주소가 감싸진 함수에 전달되어 그 변수의 값이 갱신되고, 반환 시 XSUB가 호출자의 인수를 그 값으로 갱신해요. 현대적 동등물은 매개변수를 IN_OUT으로 선언하는 거예요. 이 두 XSUB는 동등해요:
void
foo(IN_OUT int i)
void
foo(i)
int &i
OUTPUT:
i
둘 다 int * 인수 하나를 받는(아마 가리키는 정수를 갱신하는) foo() C 함수를 감싸요. 둘 다 이런 C 코드를 생성해요:
int i = (int)SvIV(ST(0));
foo(&i);
sv_setiv(ST(0), (IV)i);
INPUT에서 변수 초기화 변경 (Altering variable initialisation in INPUT)
보통 각 선언된 매개변수는 같은 이름의 C 자동 변수를 선언하게 하고, 그 변수를 대응 전달 인수의 값으로 초기화하는 코드를 심어요. 초기화 코드는 보통 매개변수 타입에 대응하는 typemap 템플릿을 확장해서 얻어져요. = + ; 세 문자 중 하나와 초기화 표현식을 INPUT 줄에 붙여 그 초기화 코드를 덮어쓰거나, 보강하거나, 건너뛸 수 있어요.
void
foo(a,b,c,d,e,f,g)
# Use the standard typemap entry:
int a
# and with optional trailing colon
int b;
# Override the typemap entry:
int c = ($type)MySvIV($arg)
# Skip the initialisation entirely:
int d = NO_INIT
int e ; NO_INIT
# Add deferred initialisation code
# *in addition* to the standard init:
int f + if (some_condition) { $var += 1 }
# Add deferred initialisation code
# *instead of* the standard init:
int g ; if (some_condition) { $var += 1 }
어떤 오버라이드 코드든 typemap 템플릿과 같은 방식으로 템플릿 확장을 거치며, $var, $arg, $type 등이 확장돼요. 지연 초기화 코드는 모든 변수 선언 뒤에 놓여져요.
INPUT이 자주 쓰이지 않는 현대 XS에서는 이런 초기화 효과 중 일부를 다른 방식으로 달성할 수 있어요:
- 오버라이드된 typemap 항목은
TYPEMAP으로 이 변수의 타입에 대한 템플릿을 추가해 지정할 수 있어요. - 초기화 건너뛰기는
OUT과OUTLIST매개변수 선언 수정자로 달성할 수 있어요. - 지연 초기화 코드 추가는
PREINIT이나INIT블록으로 달성할 수 있어요.
SCOPE: 키워드와 typemap 항목
# XSUB-scoped
void
foo(int i)
SCOPE: ENABLE
CODE:
...
# file-scoped
SCOPE: ENABLE
void
bar(int i)
CODE:
...
# typemap entry
TYPEMAP: <<EOF
INPUT
T_MYINT
$var = my_int($arg); /* SCOPE */
EOF
SCOPE 키워드는 특정 XSUB에 대해 스코핑을 활성화하는 데 쓸 수 있어요(기본적으로 비활성화). 그 효과는 XSUB의 주요 본문(대부분의 매개변수·반환 값 처리 포함)을 { ENTER;와 LEAVE; } 쌍 안에 감싸는 거예요. 이는 코드 본문 끝에 누적된 savestack 항목을 지우는 효과가 있어요. 비활성화하면 savestack은 어차피 호출자가 보통 지우므로, 이 키워드는 드물게 쓰여요.
SCOPE 키워드는 XSUB 스코프 또는 파일 스코프일 수 있어요(이것은 XS 파일 안에서의 키워드 스코프를 말하지, 키워드가 만드는 스코프를 말하지 않아요). 전자는 XSUB의 입력 부분 어디에나 나타날 수 있어요. 후자는 파일 스코프 어디에나 나타날 수 있지만, 오래된 파서 버그 때문에 키워드 상태가 각 XSUB의 시작에서 재설정돼요. 그래서 XSUB 선언 바로 앞에 그리고 같은 문단의 일부로(즉 빈 줄이 없이) 나타날 때만 효과가 있어요(위 예처럼). 단일 다음 XSUB에만 영향을 줘요.
XSUB 스코프 형식은 xsubpp 1.9506부터 가능했지만 2.21 릴리스에서 고장났고 3.58에서 고쳐졌어요. 파일 스코프 형식은 2.21부터 가능해요.
잠재적으로 복잡한 타입 매핑을 지원하기 위해, INPUT typemap 항목이 /* SCOPE */ 같은 코드 주석을 포함하면 그 typemap 항목을 쓰는 어떤 XSUB에 대해서도 스코핑이 자동으로 활성화돼요. 이것은 현재 타입이 옛 스타일 INPUT 줄로 지정된 매개변수에 대해서만 작동해요(ANSI 스타일 선언, 즉 foo(int i)는 아님). 실제로 XS 파서는 typemap에서 SCOPE 주석을 찾을 때 현재 매우 관대해요. 사실 "scope" 텍스트와 그 외 다른 것을 포함한 어떤 코드 주석과의 대소문자 무시 매칭이에요. 하지만 이것에 의존하면 안 돼요. 항상 여기 보인 형식을 쓰세요. 더 나은 건 아예 쓰지 않는 거예요.
XSUB Init 부분 (The XSUB Init Part)
XSUB의 입력 부분 다음에 선택적 init 부분이 따라와요. 아래 설명할 INIT 키워드와, "XSUB Generic Keywords"와 "Sharing XSUB bodies"에서 설명한 키워드, 그리고 C_ARGS로만 구성돼요.
INIT: 키워드
INIT 키워드는 모든 변수 선언(및 그 초기화) 뒤, 주요 코드 본문 앞에 임의의 초기화 코드를 삽입하게 해요. 주로 주요 본문이 C 함수에 대한 autocall일 때 쓰려고 의도됐어요. 예를 들어 이 두 XSUB는 동등해요:
int
foo(int i)
INIT:
if (i < 0)
XSRETURN_UNDEF;
int
foo(int i)
CODE:
if (i < 0)
XSRETURN_UNDEF;
RETVAL = foo(i);
OUTPUT:
RETVAL
INIT 뒤의 줄들은(POD와 XS 주석 제외) 다음 키워드까지 그대로 C 코드 파일로 복사돼요. 여러 INIT 키워드가 허용돼요.
XSUB 코드 부분 (The XSUB Code Part)
XSUB의 선택적 init 부분 다음에 선택적 코드 부분이 따라와요. 이는 주로 CODE나 PPCODE 키워드로 구성되고, XSUB 주요 본문의 코드 블록을 제공해요. 이 두 키워드는 비슷하지만 PPCODE는 더 낮은 수준에서 작동한다고 볼 수 있어요. 스택 포인터를 스택 프레임의 기저로 재설정하고 프로그램 작성자가 반환 값을 푸시하는 데 의존해요. 반면 CODE는(요청하면) RETVAL의 값을 반환하는 코드를 자동 생성해요.
드물게 쓰이는 NOT_IMPLEMENTED_YET 키워드도 있는데, croak하는 본문을 생성해요.
이 부분에는 이 키워드 중 하나만 나타날 수 있고 최대 한 번이에요. 이 부분에서는 다른 키워드가 인식되지 않아요(그런 키워드는 앞뒤 init·output 부분의 꼬리·머리에서 처리될 수 있지만요).
그 세 키워드가 없으면 XS 컴파일러는 autocall을 생성해요. XSUB와 같은 이름의 C 함수 호출이죠.
C 함수 auto-calling (Auto-calling a C function)
CODE, PPCODE, NOT_IMPLEMENTED_YET 중 어떤 명시적 주요 본문 코드도 없으면 XS 파서가 자동으로 본문을 생성해요(이 문서에서는 autocall이라고 함). 가장 기본적인 형태로, 파서는 XSUB가 같은 이름, 같은 매개변수, 같은 반환 타입을 가진 C 함수의 단순 래퍼라고 가정해요. 그래서 이 두 XSUB 정의는 동등하지만, 첫 번째는 보일러플레이트가 덜 필요한 autocall이에요:
int
foo(char *s, short flags)
int
foo(char *s, short flags)
CODE:
RETVAL = foo(s, flags);
OUTPUT:
RETVAL
XSUB C 함수와 감싸진 C 함수는 서로 다른 두 존재라는 점을 유의하세요. 첫 번째는 XS_Foo__Bar_foo 같은 이름을 가져요. Perl 코드가 'Perl' 함수 Foo::Bar::foo()를 호출하면, 뒤에서 Perl 인터프리터가 XS_Foo__Bar_foo()를 호출해요. 이 함수는 두 전달 인수 SV에서 문자열과 short int 값을 추출하고, foo()를 호출한 다음, 그 반환 값을 SV에 채워 Perl 호출자에게 반환해요.
생성되는 autocall 코드의 두 기본 타입은:
foo(a, b, c);
RETVAL = foo(a, b, c);
XSUB가 void로 선언됐는지에 따라 달라져요. 함수에 전달되는 변수는 보통 XSUB 매개변수의 이름을 같은 순서로 나열한 것뿐이에요. 기본값이 있는 매개변수는 포함되고, 타원은 무시돼요. 그래서 예를 들어
int
foo(int a, int b = 0, ...)
이 autocall 코드를 생성해요:
RETVAL = foo(a, b);
autocall의 기본 동작을 수정하는 데 쓸 수 있는 여러 키워드가 있어요.
- PREFIX 키워드: 이름에 공통 접두사를 공유하는 감싸진 C 함수를 그 접두사가 없는 perl 함수에 매핑할 수 있게 해줘요.
- OUT 등 매개변수 수정자: 해당 매개변수를
&접두사로 autocall 함수에 전달하게 해요. 감싸진 함수가 포인터를 기대하고 가리키는 위치를 갱신할 거라는 가정에 기반해요. - length(param_name) 의사 매개변수: Perl 함수의 매개변수가 아니더라도 다른 매개변수의 길이를 별도 인수로 감싸진 함수에 전달할 수 있게 해줘요.
- C_ARGS 키워드: 감싸진 함수에 전달되는 인수를 완전히 덮어쓰게 해줘요. perl 함수와 비교해 인수를 건너뛰거나 재정렬해야 할 때 편리해요.
- INIT 키워드: autocall 바로 앞에 코드를 추가하게 해줘요.
- POSTCALL 키워드: autocall 바로 뒤에 코드를 추가하게 해줘요.
- C++ XSUB 지원: (다른 것 중에서) autocall을 C++ 메서드 호출로 수정할 수 있어요. 예:
THIS->foo(s,flags).
C_ARGS: 키워드
void foo1(int a, int b, int c)
C_ARGS: b, a
void foo2(int a, int b)
C_ARGS: a < 0 ? 0 : a,
b,
0
보통 autocall의 인수는 XSUB의 매개변수 선언에 기반해 자동 생성돼요. C_ARGS 키워드로 이것을 덮어쓰고 autocall의 괄호 사이에 놓일 텍스트를 수동으로 지정할 수 있어요. Perl과 C 사이에서 매개변수의 순서·성격이 다를 때 CODE나 PPCODE 섹션을 쓸 필요 없이 유용해요.
C_ARGS 섹션은 다음 키워드까지 또는 XSUB 끝까지의 모든 텍스트 줄로 구성되고, 수정 없이 사용돼요(POD나 XS 주석은 제거됨).
CODE: 키워드
int
abs_double(int i)
CODE:
if (i < 0)
i = -i;
RETVAL = i * 2;
OUTPUT:
RETVAL
CODE 키워드는 XSUB의 주요 본문으로 여러분만의 코드를 제공하는 일반적인 메커니즘이에요. 보통 XSUB가 라이브러리 함수를 감싸는 대신, Perl보다 C에서 더 쉽거나 효율적으로 구현할 수 있는 일반 기능을 제공할 때 쓰여요. 또는 autocall이 처리하기엔 너무 복잡한 경우 라이브러리 함수를 감싸는 데도 쓰일 수 있어요.
CODE 코드 블록에 들어갈 때 전달된 인수의 값은 자동 변수에 할당됐지만, 원래 SV는 여전히 스택 위에 있고 필요하면 ST(i)로 접근할 수 있다는 점을 유의하세요.
autocall XSUB와 유사하게, XSUB의 반환 값이 void가 아니면 RETVAL 변수가 선언돼요. autocall과 달리, OUTPUT 키워드를 써서 RETVAL의 값을 반환하는 코드를 생성하라고 XS 컴파일러에 명시적으로 알려야 해요. (이것을 요구한 건 아마 나쁜 설계 결정이었지만, 지금 우리는 그걸 안고 가야 해요.) 더 새로운 XS 파서는 CODE 섹션에서 RETVAL이 보이는데 대응 OUTPUT 섹션이 없으면 경고할 거예요.
CODE XSUB는 보통 RETVAL 값만 반환해요(또는 OUTLIST 매개변수 수정자로 더 많은 항목도). 반환 값을 완전히 제어하려면 PPCODE 키워드를 대신 쓸 수 있어요. CODE 섹션도 스택을 직접 조작한 다음 XSRETURN(n)으로 직접 반환해 스택에 n개 항목이 있음을 나타내는 방식으로 그렇게 할 수 있다는 점을 유의하세요. 이는 XS 파서가 CODE 줄 뒤에 심어 놓을 일반적인 XSRETURN(1) 등을 우회해요. 하지만 보통 PPCODE를 쓰는 것이 더 깔끔해요.
CODE 뒤의 줄들은(POD와 XS 주석 제외) 다음 키워드까지 그대로 C 코드 파일로 복사돼요. 여러 CODE 키워드는 허용되지 않아요.
PPCODE: 키워드
# XS equivalent of: sub one_to_n { my $n = $_[0]; 1..$n }
void
one_to_n(int n)
PPCODE:
{
int i;
if (n < 1)
Perl_croak_nocontext(
"one_to_n(): argument %d must be >= 1", n);
EXTEND(SP, n);
for (i = 1; i <= n; i++)
mPUSHi(i);
}
PPCODE 키워드는 CODE 키워드와 비슷하지만, 들어갈 때 스택 포인터를 현재 스택 프레임의 기저로 재설정하고, RETVAL 등을 반환하는 코드를 생성하지 않아요. 반환 값을 스택에 푸시하는 것은 프로그래머에게 맡겨져요. 이런 방식으로 CODE의 더 낮은 수준 대안으로 볼 수 있어요. 인수 스택 조작을 완전히 제어하고 싶을 때요. 이름의 "PP"는 "PUSH/PULL"을 뜻해서 낮은 수준의 스택 조작을 반영해요. PPCODE는 보통 RETVAL 값만 반환하는 CODE와 비교해, 여러 값이나 심지어 임의의 리스트를 반환하고 싶을 때 쓰여요.
PPCODE 키워드는 XSUB의 마지막 키워드여야 해요. PPCODE 뒤의 줄들은(POD와 XS 주석 제외) XSUB 끝까지 그대로 C 코드 파일로 복사돼요. 여러 PPCODE 키워드는 허용되지 않아요.
보통 PPCODE XSUB를 void 반환 타입으로 선언해요. 다른 반환 타입은 그 타입의 RETVAL 자동 변수를 선언하게 하지만, 그 외에는 쓰이지 않아요.
PPCODE 코드 블록에 들어갈 때 선언된 매개변수 인수의 값은 이미 자동 변수에 할당됐지만, 원래 SV는 여전히 스택 위에 있고 처음에는 필요하면 ST(i)로 접근할 수 있어요. 하지만 PPCODE 블록의 기본 가정은 이미 공급된 인수 처리를 끝냈고, 많은 반환 값을 스택에 푸시하고 싶다는 거예요. 위에 보인 단순한 one_to_n() 예는 그 가정에 기반해요. 하지만 더 복잡한 전략도 가능해요.
PPCODE 블록에서 스택에 접근·조작하는 기본 방법은 두 가지가 있어요. 첫째, ST(i) 매크로로 현재 스택 프레임의 i 번째 항목을 얻거나, 수정하거나, 교체하는 것이고, 둘째, (보통 임시) 반환 값을 스택에 푸시하는 것이에요. 첫 번째는 XSUB에 들어갈 때 설정되고 현재 스택 프레임의 기저 인덱스인 숨은 ax 변수를 써요. 이것은 XSUB 실행 내내 변하지 않아요. 두 번째 방법은 로컬 스택 포인터 SP를 쓰는데(아래 더), PPCODE 블록에 들어갈 때 스택 프레임의 기저를 가리켜요. mPUSHi() 같은 매크로는 그 위치에 임시 SV를 저장한 다음 SP를 증가시켜요. PPCODE XSUB에서 반환할 때 SP의 현재 값이 호출자에게 몇 개의 값을 반환하는지 나타내는 데 쓰여요.
일반적으로 이 두 스택 접근 방식을 섞으면 혼란이 일어나기 쉽상이니 섞지 말아야 해요. PUSH 전략은 전달 인수를 더 이상 쓸 일이 없고 값 목록을 생성·반환하고 싶을 때 가장 유용해요(위 one_to_n() 예처럼). ST(i) 전략은 여전히 전달 인수에 접근해야 할 때 더 좋아요. 아래 예에서,
# XS equivalent of: sub triple { map { $_ * 3} @_ }
void
triple(...)
PPCODE:
SP += items;
{
int i;
for (i = 0; i < items; i++) {
int val = (int)SvIV(ST(i));
ST(i) = sv_2mortal(newSViv(val*3));
}
}
SP는 먼저 증가되어 아직 스택 위에 있는 전달 인수를 회수하고, 그다음 하나씩 각 전달 인수를 꺼내고, 각 스택 슬롯을 새 mortal 값으로 교체해요. 루프가 끝나면 현재 스택 프레임에는 mortal 목록이 있고, SP가 반환되는 항목 수를 나타내며 호출자에게 반환돼요. 이 예에서 SP += items는 끝에 해도 됐다는 점을 유의하세요. 하지만 코드가 전달 인수 갱신과 추가 반환 값 푸시를 섞는 일을 한다면 (첫 푸시 전에) 일찍 설정하는 것이 중요할 거예요.
스택에 반환 값을 푸시하거나(또는 전달 인수 개수보다 높은 ST(i) 위치에 값을 저장) 전에 스택에 충분한 공간이 있는지 확인하는 것이 필요해요. 위 one_to_n() 예의 EXTEND(SP, n) 매크로로 달성하거나, mXPUSHi() 같은 푸시 매크로의 'X' 변형을 써서 매번 스택을 검사·확장할 수 있어요. 미리 단일 EXTEND를 하는 것이 더 효율적이에요. EXTEND는 스택에 n개 더 항목을 푸시할 충분한 공간이 최소한 있게 보장해요.
PUSH 전략을 쓰면 푸시와 로컬 스택 포인터 SP가 어떻게 구현되는지 더 자세히 이해하는 게 유용해요. 생성된 C 파일은 (다른 것 중) 다음과 같은 매크로 정의에 접근할 수 있어요:
#define dSP SV **sp = PL_stack_sp
#define SP sp
#define PUSHs(s) *++sp = (s)
#define mPUSHi(i) sv_setiv(PUSHs(sv_newmortal()), (IV)(i))
#define PUTBACK PL_stack_sp = sp
#define SPAGAIN sp = PL_stack_sp
#define dXSARGS dSP; ....
전역(또는 인터프리터당) 변수 PL_stack_sp는 스택의 현재 맨 위 항목에 대한 포인터로, 처음에는 &ST(items-1)과 같아요. XSUB에 들어갈 때 그 꼭대기의 dXSARGS가 sp 변수를 선언·초기화하게 해요. 이것은 인수 스택 포인터의 로컬 복사본이 돼요. PUSHs 같은 표준 스택 조작 매크로는 모두 이 로컬 복사본을 써요.
XS 파서는 보통 PP 코드 블록 줄 주위에 이런 C 코드 두 줄을 출력해요:
SP -= items;
... PP lines ...
PUTBACK; return;
이것은 로컬 스택 포인터 복사본(스택 포인터 자체는 아님)을 현재 스택 프레임의 기저로 재설정해 전달 인수를 버리는 효과가 있어요. 원래 인수는 여전히 스택 위에 있어요. PUSHs() 등은 스택 프레임의 기저에서 시작해 원래 인수를 점차 덮어써요. 마지막으로 PUTBACK이 실제 스택 포인터를 그 복사본으로 설정해 변경을 영구화하고, 호출자가 몇 개의 인수가 반환됐는지 결정하게 해줘요.
XSUB에서 호출되는 어떤 함수든 PL_stack_sp의 값은 보지만 SP는 보지 못해요. 그래서 스택을 조작하는 함수를 호출할 때 둘을 재동기화해야 할 수 있어요. 예:
PUTBACK;
push_contents_of_array(av);
SPAGAIN;
EXTEND(SP,n)과 mXPUSHfoo() 매크로는 확장이 스택 재할당을 일으키면 PL_stack_sp와 SP 둘 다 갱신해요.
mPUSHfoo() 매크로가 여러 개 있고, 일반적으로 임시 SV를 만들고, 값을 인수로 설정하고, 스택에 푸시한다는 점을 유의하세요. 이들은:
mPUSHs(sv) mortalise and push an SV
mPUSHi(iv) create+push mortal and set to the integer val
mPUSHu(uv) create+push mortal and set to the unsigned val
mPUSHn(n) create+push mortal and set to the num (float) val
mPUSHp(str, len) create+push mortal and set to the string+length
mPUSHpvs("string") create+push mortal and set to the literal string
(perl 5.38.0 onwards)
NOT_IMPLEMENTED_YET: 키워드
void
foo(int a)
NOT_IMPLEMENTED_YET:
이 키워드는 CODE, PPCODE, autocall에 대한 네 번째 대안으로, 단지 다음 C 코드로만 구성된 XSUB 본문을 생성해요:
Perl_croak(aTHX_ "Foo::Bar::foo: not implemented yet");
현재 구현은 파싱과 XSUB 안에서 키워드가 나타날 수 있는 위치가 꽤 버그가 많아서, 보통 피하는 게 좋아요. 완전성을 위해 여기 문서화했어요.
XSUB 출력 부분 (The XSUB Output Part)
XSUB의 코드 부분 다음에 결과가 후처리되고 반환될 수 있어요. 두 키워드가 특히 이를 지원해요. POSTCALL은 어떤 autocall 뒤에 코드 블록을 추가해 호출의 반환 값을 후처리하게 하고, OUTPUT은 RETVAL의 값을 반환하거나 전달된 인수 하나 이상의 값을 갱신하는 코드를 생성하라고 파서에게 말해요.
이 두 선택적 키워드는 각각 최대 한 번, 그리고 그 순서로만 써야 해요. 하지만 (이전 버전 호환성을 위해 유지된) 파싱 버그 때문에 어느 순서로든 여러 번 나타날 수 있어요. 그러지 마세요.
"XSUB Generic Keywords"와 "Sharing XSUB bodies"에서 설명한 키워드도 이 부분에 나타날 수 있다는 점을 유의하세요.
POSTCALL: 키워드
POSTCALL 키워드는 어떤 autocall이나 CODE 코드 블록 바로 뒤에 코드 블록을 삽입하게 해요(정말 autocall에서만 유용하지만). 보통 autocall의 반환 값을 정리하는 데 쓰여요. 예를 들어 이 두 XSUB는 동등해요:
int
foo(int a)
POSTCALL:
if (RETVAL < 0)
RETVAL = 0
int
foo(int a)
CODE:
RETVAL = foo(a);
if (RETVAL < 0)
RETVAL = 0
OUTPUT:
RETVAL
OUTPUT: 키워드
# Common usage:
OUTPUT:
RETVAL
# Rare usage:
OUTPUT:
arg0
SETMAGIC: DISABLE
arg1
SETMAGIC: ENABLE
arg2 sv_setfoo(ST[2], arg2)
OUTPUT 키워드는 RETVAL의 값을 스택에서 호출자에게 반환하고/하거나 특정 전달된 Perl 인수의 값을 대응 매개변수 변수의 현재 값으로 갱신해야 한다는 것을 나타내는 데 쓸 수 있어요. OUTPUT 블록의 각 비어 있지 않은 줄은 변수 이름 하나와 선택적 설정 코드, 또는 ENABLE/DISABLE 값을 가진 SETMAGIC: 키워드를 포함해야 해요.
흔한 사용은 RETVAL 변수만 나열하는 거예요:
int
foo()
CODE:
RETVAL = ...;
OUTPUT:
RETVAL
CODE 블록을 포함한 XSUB가 RETVAL의 값을 호출자에게 반환하는 C 코드를 생성하라고 XS 컴파일러에 알려야 하므로 필요해요. autocall XSUB의 경우 OUTPUT 키워드 없이 자동으로 이뤄져요.
OUTPUT의 두 번째 사용법은 갱신할 매개변수를 지정하는 것이에요. 이 사용법은 OUT 매개변수 수정자로 거의 완전히 대체됐어요. 예를 들어 이 두 XSUB는 동일한 동작을 하지만 두 번째가 선호되는 형식이에요:
int
foo1(a)
INPUT:
int &a
OUTPUT:
a
int
foo2(IN_OUT int a)
둘 다 이런 출력 C 코드를 심어요(첫 부분은 typemap에서 파생):
sv_setiv(ST(0), (IV)a);
SvSETMAGIC(ST(0));
전달된 SV의 값을 현재 a 값으로 갱신한 다음 SV의 set 매직을 호출해요. 예를 들어 tied 변수의 STORE() 메서드가 호출되게 해요.
SETMAGIC: DISABLE로 SvSETMAGIC() 매직 호출 심기를 건너뛸 수 있어요. 이 섹션 시작의 예에서 arg0과 arg2는 set 매직을 갖지만 arg1은 갖지 않아요. SETMAGIC 설정은 다른 SETMAGIC까지, 또는 개념적으로 현재 OUTPUT 블록 끝까지 유효해요. 실제로 현재 설정은 같은 XSUB의 이후 OUTPUT 선언으로, 또는 xsubpp 3.58부터는 같은 CASE 분기 안의 선언으로만 이월돼요.
SETMAGIC의 현재 설정은 RETVAL에 대해 무시돼요. RETVAL은 보통 어차피 매직이 붙지 않을 새 임시 SV의 값을 설정하는 거니까요.
마지막으로, RETVAL이나 다른 변수에서 임시 SV 또는 전달 인수의 값을 설정하는 데 쓰이는 typemap 항목을 덮어쓰는 것이 가능해요. 보통 이런 XSUB에서:
int
foo(int abc)
OUTPUT:
abc
int 타입은(시스템 typemap의 2단계 조회로) 이 출력 typemap 항목을 산출해요:
sv_setiv($arg, (IV)$var);
변수 확장 후 이렇게 산출될 수 있어요:
sv_setiv(ST(0), (IV)abc);
또는 비슷하게요. 이것은 덮어쓸 수 있어요. 예:
int
foo(int abc)
OUTPUT:
abc my_setiv(ST(0), (IV)abc);
하지만 중요하게, INPUT 줄의 유사한 문법과 달리 오버라이드 텍스트는 변수 확장되지 않아요. 그래서 올바른 인수가 사용되도록 보장하는 것이 까다로워요(ST(0) 같은). 기본적으로 이 기능은 설계 결함이 있고 아마 피해야 해요. xsubpp 3.01부터 TYPEMAP 키워드로 로컬 정의 typemap을 갖는 것이 가능한데, 값이 반환되는 방식을 수정하는 더 나은 방법일 수 있어요. 예:
typedef int myint
...
TYPEMAP: <<EOF
myint T_MYINT
INPUT
T_MYINT
$var = ($type)my_getiv($arg)
OUTPUT
T_MYINT
my_setiv($arg, (IV)$var);
EOF
int
foo2(IN_OUT myint abc)
XSUB 정리 부분 (The XSUB Cleanup Part)
RETVAL과 OUT/OUTLIST 매개변수의 값을 반환하는 코드가 심어진 XSUB의 출력 부분 다음에, CLEANUP 키워드로 몇 가지 최종 정리 코드를 주입할 수 있어요.
"XSUB Generic Keywords"와 "Sharing XSUB bodies"에서 설명한 키워드도 이 부분에 나타날 수 있다는 점을 유의하세요.
CLEANUP: 키워드
char *
foo(int a)
CODE:
RETVAL = get_foo(a);
OUTPUT:
RETVAL
CLEANUP:
free(RETVAL); /* assuming get_foo() returns a malloced buffer */
CLEANUP 키워드는 자동으로 또는 OUTPUT 키워드를 통해 생성된 어떤 출력 코드 바로 뒤에 코드 블록을 삽입하게 해요. XSUB가 종료 전에 특별한 정리 절차를 요구할 때 쓸 수 있어요. 정리 블록에 지정된 코드는 최종 XSRETURN(1); 등 앞의 XSUB 마지막 문장으로 추가돼요.
XSUB 일반 키워드 (XSUB Generic Keywords)
XSUB의 본문 어디에나 나타날 수 있는 몇몇 XSUB별 키워드가 있어요. 이것은 XSUB가 Perl 인터프리터에 등록되는 방식을 바꾸는 것이지, XSUB 자체의 C 코드가 생성되는 방식을 바꾸는 게 아니기 때문이에요. 다음 하위 섹션에서 설명할 거예요. 추가로 "Sharing XSUB bodies" 아래에서 설명할 일반 키워드가 몇 개 더 있어요.
미학상 이유로 이 키워드들을 XSUB 시작 근처에 쓰는 게 좋아요.
PROTOTYPE: 키워드
int
foo1(int a, int b = 0)
# this XSUB gets an auto-generated '$;$' prototype
PROTOTYPE: ENABLE
int
foo2(int a, int b)
# this XSUB doesn't get a prototype
PROTOTYPE: DISABLE
int
foo3(SV* a, int b)
# this XSUB gets the specified prototype:
PROTOTYPE: \@$
int
foo4(int a, int b)
# this XSUB gets a blank () prototype
PROTOTYPE:
파일 스코프 PROTOTYPES 키워드가 이후 모든 XSUB에 대한 자동 프로토타입 생성을 켜거나 끄는 반면, XSUB별 PROTOTYPE 키워드는 현재 XSUB에 대해서만 설정을 덮어써요. 프로토타입이 무엇이고 왜 거의 필요하지 않은지에 대한 자세한 내용은 PROTOTYPES 섹션을 보세요.
이 키워드의 값은 자동 프로토타입 생성을 켜거나 끄는 ENABLE/DISABLE 중 하나이거나, 명시적 프로토타입 문자열(빈 프로토타입 포함)을 지정할 수 있어요.
OVERLOAD: 키워드
MODULE = Foo PACKAGE = Foo::Bar
SV*
subtract(SV* a, SV* b, bool swap)
OVERLOAD: - -=
CODE:
...
OVERLOAD 키워드는 이 XSUB가 현재 패키지의 지정된 연산자에 대한 오버로드 메서드로 작용한다고 선언하게 해요. 위 예는 대략 이 Perl 코드와 동등해요:
package Foo::Bar;
sub subtract { ... }
use overload
'-' => \&subtract,
'-=' => \&subtract;
키워드 뒤 줄의 나머지와 다음 키워드까지의 추가 줄은 공백으로 구분된 오버로드된 연산자 목록으로 해석돼요. 유효한 연산자 이름인지 검사는 없어요. 이름과 심볼은 결국 C 파일에서 큰따옴표 문자열 안에 들어가므로 큰따옴표는 이스케이프해야 해요. 특히:
OVERLOAD: \"\"
이것은 구현 버그로 볼 수 있지만, 지금 우리는 그걸 안고 가야 해요.
오버로드 메서드에 쓰이는 XSUB는 Perl 서브루틴과 같은 인수로 호출돼요. 예를 들어 오버로드된 이항 연산자는 이항 연산자의 두 피연산자 중 하나를 나타내는 오버로드된 객체를 첫 인수로, 다른 피연산자(객체일 수도 아닐 수도)를 둘째, swap 플래그를 셋째로 해서 XSUB 메서드 호출을 촉발해요. 이 함수들이 어떤 인수로 어떻게 호출되는지에 대한 전체 세부 내용은 overload를 보세요. swap은 거짓에 더해 undef일 수도 있어서 += 같은 할당 오버로드를 나타낼 수 있다는 점을 유의하세요. 이 차이가 코드에 중요하면 swap을 SV* 타입으로 선언해 SvOK()와 SvTRUE()를 쓸 수 있게 하세요.
비트 연산자 메서드는 때때로 추가 인수를 받아요. 특히 use feature 'bitwise' 아래에서요. 그래서 타원(어떤 것처럼 (lobj, robj, swap, ...))을 써서 그것들을 건너뛰고 싶을 수 있어요.
OVERLOAD 키워드의 순효과는 부트 XSUB에 약간의 추가 코드를 더해 이 XSUB를 지정된 오버로드 동작의 핸들러로 등록하는 것이에요. use overload가 Perl 메서드에 대해 하는 것과 같은 방식이죠.
현재 패키지의 fallback 동작 설정 방법은 파일 스코프 FALLBACK 키워드도 보세요.
OVERLOAD는 ALIAS 키워드와 섞으면 안 된다는 점을 유의하세요. 어떤 오버로드 메서드 호출에 대해서도 ix 값이 정의되지 않을 거예요.
"T_PTROBJ and opaque handles" 섹션에는 T_PTROBJ typemap을 써서 간단한 산술 라이브러리를 감싸는 완전한 예가 있어요. 그 래퍼의 결과로 이런 Perl 코드를 쓸 수 있어요:
my $i2 = My::Num->new(2);
my $i7 = My::Num->new(7);
my $i13 = My::Num->new(13);
my $x = $i13->add($i7)->divide($i2);
printf "val=%d\n", $x->val();
오버로딩을 쓰면 그 마지막 두 줄을 더 간단히 쓰고 싶을 거예요:
my $x = ($i13 + $i7)/$i2;
printf "val=%d\n", $x;
그 예시 XS 코드에 대한 다음 추가·수정이 오버로딩을 추가하는 방법을 보여줘요:
FALLBACK: UNDEF
int
mynum_val(My::Num x, ...)
OVERLOAD: 0+
My::Num
mynum_add(My::Num x, My::Num y, bool swap)
OVERLOAD: +
C_ARGS: x, y
INIT:
if (swap) {
mynum* tmp = x; x = y; y = tmp;
}
# ... and three similar XSUBs for
# mynum_subtract, mynum_multiply, mynum_divide ...
FALLBACK 줄은 어차피 기본값이라 실제로 필요 없지만, 그 키워드를 쓸 수 있음을 상기시키려고 포함했어요.
mynum_val() 메서드에 오버로딩을 추가해 숫자 문맥(위 printf처럼)에서 쓰일 때 객체의 값을 자동 반환하게 해요. 오버로드 메서드에 전달되는 추가 두 인수를 무시하려고 타원을 추가했어요.
T_PTROBJ 예의 mynum_add() 메서드는 별명을 통해 네 산술 연산을 모두 처리했지만, ALIAS와 OVERLOAD는 섞이지 않으므로 이제 네 개의 별도 XSUB로 나뉘어요.
각 산술 XSUB의 주요 변경은 OVERLOAD 키워드 추가 외에 추가 swap 매개변수가 있어요. 덧셈과 곱셈에는 실제로 쓸 필요가 없지만, 교환 법칙이 성립하지 않는 뺄셈과 나눗셈에는 중요해요.
그 예는 T_PTROBJ typemap으로 두 번째 인수를 처리해요. 가장 일반적인 사용에서 그 인수는 객체가 아닐 수 있어요. 예를 들어 이 두 번째·세 번째 줄은 Expected foo to be of type My::Num, got scalar 오류로 croak할 거예요:
$i13 + My::Num->new(7);
$i13 + 7;
$i13 + "7";
이것을 처리해야 한다면 여러분만의 typemap을 만들어야 할 수 있어요. 예를 들어 T_PTROBJ와 비슷하지만 이런 INPUT 템플릿이 있는 것:
T_MYNUM
SV *sv = $arg;
SvGETMAGIC(sv);
if (!SvROK(sv)) {
sv = sv_newmortal();
sv_setref_pv(sv, "$ntype", mynum_new(SvIV($arg));
}
....
마지막으로 XS와 직접 관련은 없지만, 정수 리터럴을 직접 쓸 수 있게 Num.pm에 이걸 추가할 수 있어요:
sub import {
overload::constant integer =>
sub {
my $str = shift;
return My::Num->new($str);
};
}
그러면 이 줄들이:
my $i2 = My::Num->new(2);
my $i7 = My::Num->new(7);
my $i13 = My::Num->new(13);
더 깔끔하게 이렇게 다시 쓸 수 있어요:
my $i2 = 2;
my $i7 = 7;
my $i13 = 13;
ATTRS: 키워드
MODULE = Foo::Bar PACKAGE = Foo::Bar
SV*
debug()
ATTRS: lvalue
PPCODE:
# return $Foo::Bar::DEBUG, creating it if not already present
# (NB: XPUSHs() not needed here as the stack always has one
# allocated slot available when an XSUB is called):
PUSHs(GvSV(gv_fetchpvs("Foo::Bar::DEBUG", GV_ADD, SVt_IV)));
ATTRS 키워드로 XSUB에 Perl 서브루틴과 유사한 방식으로 서브루틴 속성을 적용할 수 있어요. 위 예의 XSUB는 이 Perl과 동등해요:
sub debug :lvalue { return $Foo::Bar::DEBUG }
둘 다 이렇게 호출할 수 있어요:
use Foo::Bar;
Foo::Bar::debug() = 99;
print "$Foo::Bar::DEBUG\n"; # prints 99
이 키워드는 다음 키워드까지 모든 줄을 소비해요. 각 줄의 내용은 공백으로 구분된 속성으로 해석돼요. 속성은 XS 모듈이 로드될 때 적용돼요. 이:
void
foo(...)
ATTRS: aaa
bbb(x,y) ccc
대략 이와 동등해요:
use attributes Foo::Bar, \&foo, 'aaa';
use attributes Foo::Bar, \&foo, 'bbb(x,y)';
use attributes Foo::Bar, \&foo, 'ccc';
Perl 서브와 마찬가지로 사용자 정의 속성은 attributes에서 설명한 것처럼 MODIFY_CODE_ATTRIBUTES() 호출을 촉발해요.
내장 서브루틴 속성 전부가 XSUB에 적용하는 게 꼭 의미가 있는 것은 아니라는 점을 유의하세요.
현재 공백 파싱은 조잡해요. bbb(x, y)는 'bbb(x,'와 'y)' 두 개의 별도 속성으로 오해석돼요.
ATTRS 키워드는 현재 ALIAS나 INTERFACE와 함께 쓸 수 없어요. 그 경우 속성은 조용히 무시돼요.
XSUB 본문 공유 (Sharing XSUB bodies)
때로는 매우 비슷한 XSUB를 여러 개 쓰고 싶을 수 있어요. 모두 같은 시그니처를 갖고, Perl과 C 사이에서 인수·반환 값을 변환하는 같은 생성 코드를 갖고, 주요 본문 몇 줄이나 감싸는 C 라이브러리 함수에서만 다를 수 있어요. 사실 여러 Perl CV가 같은 XSUB 함수를 공유하는 것이 가능해요. 예를 들어 &Foo::Bar::add와 &Foo::Bar::subtract는 둘 다 같은 XSUB(XS_Foo__Bar__add()라고 합시다)를 가리키는 Perl 네임스페이스의 두 개별 CV일 수 있어요. 하지만 각 CV는 XSUB가 접근할 수 있는 어떤 고유 식별자를 보유해서 add로 행동할지 subtract로 행동할지 결정할 수 있어요.
ALIAS와 INTERFACE 키워드(둘 다 아래)는 여러 CV가 같은 XSUB를 공유하게 해요. 둘의 차이는 ALIAS는 여러분이 XSUB의 주요 본문을 직접 공급할 때(예: CODE 사용) 의도되고, (전달된 CV에서 파생된) 정수 변수 ix를 설정하는데, 이를 switch() 문 등에서 쓸 수 있어요. 반대로 INTERFACE는 autocall과 함께 쓰기 위한 것으로, CV에 저장된 정보가 어떤 C 라이브러리 함수를 autocall해야 하는지 나타내요.
마지막으로 CASE 키워드가 있는데, XSUB 본문 전체(CODE 부분뿐 아니라)에 대체 케이스를 허용해요. C 수준이 아니라 최상위 XS 수준에서 작동하는 switch() 유사체로 생각할 수 있어요. CASE가 작용하는 값은 예를 들어 items일 수도 있고, ALIAS 키워드와 함께 써서 ix 값에 대해 switch할 수도 있어요.
ALIAS: 키워드
int add(int x, int y)
ALIAS:
# implicit: add = 0
subtract = 1
multiply = 2 divide = 3
CODE:
switch (ix) { ... }
이 키워드는 XSUB 본문 어디에나 나타날 수 있다는 점을 유의하세요.
ALIAS 키워드로 단일 XSUB가 두 개 이상의 Perl 이름을 갖고, 호출됐을 때 어떤 이름이 쓰였는지 알 수 있어요. 각 별명에 정수 인덱스 값이 주어지고, XSUB의 주요 이름이 인덱스 0이에요. 이 인덱스는 호출된 CV(즉 호출된 Perl 서브루틴)에 기반해 초기화되는 ix 변수로 접근할 수 있어요.
XSUB는 여러 CV가 공유할 수 있고, 각 CV는 여러 이름을 가질 수 있다는 점을 유의하세요. 위 add XSUB 정의와 이 Perl 코드가 주어졌을 때:
use Foo::Bar;
BEGIN { *addition = *add }
그러면 Foo::Bar 네임스페이스에서 add와 addition 항목은(인덱스 0이 저장된) 같은 CV를 가리키고, subtract는 인덱스 1의 두 번째 CV를 가리키고 계속돼요. 네 CV 모두 같은 C 함수 XS_Foo__Bar__add()를 가리켜요.
별명 이름은 단순 함수 이름이거나 패키지 이름을 포함할 수 있어요. = 오른쪽의 별명 값은 리터럴 양의 정수이거나 단어(CPP define 또는 enum 상수로 예상됨)일 수 있어요.
ALIAS 키워드 다음 줄의 나머지와 다음 키워드까지의 추가 줄에는 별명 이름-값 쌍이 0개 이상 있다고 가정돼요.
같은 인덱스 값에 별명을 두 개 이상 만들면 경고가 생성돼요. 같은 값의 별명 여러 개를 원하면 똑같은 값의 별도 CPP define으로 달성하는 이전 버전 호환 방식이 있어요. 예:
#define DIVIDE 3
#define DIVISION 3
ALIAS:
divide = DIVIDE
division = DIVISION
xsubpp 3.51부터 별명 값은 = 심볼 대신 =>를 써서 다른 별명 이름(또는 주요 함수 이름)을 참조할 수 있어요:
ALIAS:
divide = 3
division => divide
별명 이름과 => 값 둘 다 완전히 한정될 수 있어요:
ALIAS:
red = 1
COLOR::red => red
COLOUR::red => COLOR::red
PREFIX가 XSUB의 주요 이름에 적용되지만 어떤 별명에는 적용되지 않는다는 점을 유의하세요.
별명을 쓰는 완전한 예는 "T_PTROBJ and opaque handles"을 보세요.
autocall에 더 적합한 ALIAS의 대안은 아래 INTERFACE를 보세요. ALIAS는 ATTRS, INTERFACE, OVERLOAD 중 무엇과도 함께 쓰면 안 된다는 점을 유의하세요.
INTERFACE: 키워드
MODULE = Foo::Bar PACKAGE = Foo::Bar PREFIX = foobar_
int
arith(int a, int b)
INTERFACE: foobar_add foobar_subtract
foobar_divide foobar_multiply
이 키워드는 XSUB의 초기화 부분 어디에나 나타날 수 있어요.
이 키워드는 ALIAS와 유사한 기능을 제공하지만, autocall을 쓰는 XSUB를 위해 의도됐어요. Perl 네임스페이스에서 단일 XSUB가 여러 이름을 갖게 하고, 호출되면 올바른 감싸진 C 라이브러리 함수를 호출하게 해줘요.
위 예에는 단일 C XSUB 함수(이름 XS_Foo__Bar_arith)가 생성되고, Perl 네임스페이스에는 Foo::Bar::add 등의 네 CV가 있어요. Perl에서 Foo::Bar::add()를 호출하면 어떤 C 함수를 호출할지에 대한 표시와 함께 XS_Foo__Bar_arith()가 호출되고, 그것이 autocall돼요. ALIAS는 각 CV에 인덱스 값을 저장하고 ix 변수로 접근 가능하게 해서 이를 달성하는 반면, INTERFACE는 현재 각 CV에 C 함수 포인터를 저장해서 이를 달성해요. 그래서 Foo::Bar::add() CV는 foobar_add() C 함수에 대한 포인터를 보유해요. XSUB의 동작은 전달 인수에서 매개변수 값을, CV에서 함수 포인터를 추출한 다음 기본 C 함수를 호출하는 것이에요.
CV에 함수 포인터를 저장하는 것은 미래에 바뀔 수 있는 구현 세부 사항이라는 점을 유의하세요. CV에서 이 값의 설정·조회를 사용자화하는 방법은 "The INTERFACE_MACRO: Keyword"를 보세요.
INTERFACE 키워드 다음 줄의 나머지와 다음 키워드까지의 추가 줄에는 공백(또는 쉼표)으로 구분된 인터페이스 이름이 0개 이상 있다고 가정돼요.
인터페이스 이름은 항상 감싸진 C 함수의 이름으로 그대로 사용돼요. 이름에 패키지 구분자가 포함되면 그대로 Perl 이름을 생성하는 데 쓰이고, 그렇지 않으면 접두사가 제거되고 현재 패키지 이름이 앞에 붙어요. 다음은 몇 가지 인터페이스 이름이 어떻게 처리되는지 보여줘요(현재 PACKAGE와 PREFIX가 Foo::bar와 foobar_라고 가정):
Interface name Perl function name C function name
-------------- ------------------ ----------------
abc Foo::Bar::abc abc
foobar_abc Foo::Bar::abc foobar_abc
X::Y::foobar_def X::Y::foobar_def X::Y::foobar_def
ALIAS와 달리 XSUB 이름은 생성된 C 함수의 이름으로만 쓰여요. 위 예에서 arith()라는 Perl 함수가 만들어지지 않아요.
INTERFACE를 T_PTROBJ typemap과 함께 쓰는 완전한 예는 "T_PTROBJ and opaque handles"을 보세요. 하지만 xsubpp 3.60 이전에는 INTERFACE가 T_PTROBJ가 쓰는 Perlish 반환 타입과 함께 쓰는 XSUB에서 제대로 작동하지 않았음을 유의하세요. 예:
Foo::Bar
foo(...)
....
3.60부터 대부분 고쳐졌지만, C_ARGS 키워드가 있는 XSUB에 대해서는(C_ARGS 값이 단순한 매개변수 이름 목록이 아니면) 잘못된 C 코드(특히 잘못된 함수 포인터 캐스트)를 생성할 수 있어요.
INTERFACE는 ALIAS나 ATTRS와 함께 쓰면 안 된다는 점을 유의하세요.
INTERFACE_MACRO: 키워드
int
arith(int a, int b)
INTERFACE: add subtract divide multiply
INTERFACE_MACRO: MY_FUNC_GET
MY_FUNC_SET
이 키워드는 INTERFACE 키워드에 특정 구현을 가정하므로 폐기됐다는 점을 유의하세요. 그 구현은 미래에 바뀔 수 있어요.
이 키워드는 XSUB의 입력 또는 초기화 부분 어디에나 나타날 수 있어요.
기본적으로 INTERFACE 키워드가 생성하는 C 코드는 XSINTERFACE_FUNC_SET과 XSINTERFACE_FUNC 두 매크로에 대한 호출을 심어요. 전자는 (부트 시점에) CV의 필드를 사용할 C 함수 포인터의 주소로 설정하는 데, 후자는 (런타임에) CV에서 그 값을 꺼내는 데 쓰여요.
INTERFACE_MACRO 매크로로 이 목적에 쓸 두 매크로의 이름을 덮어쓸 수 있어요. INTERFACE_MACRO 키워드 다음 줄의 나머지와 다음 키워드까지의 추가 줄은 (합쳐서) 매크로 이름으로 취급되는 두 단어를 포함해야 해요.
get 매크로는 함수의 반환 타입, 함수의 포인터 값을 보유한 CV, 포인터 값이 있는 CV 내부의 필드 세 매개변수를 받아요. C 함수 포인터를 반환해야 해요. setter 매크로는 CV와 함수 포인터 두 매개변수를 가져요.
위 예에서 multiply(), divide(), add(), subtract() 함수에 대한 포인터가 arith_ptrs[]라는 전역 C 배열에 multiply_off, divide_off, add_off, subtract_off enum 값으로 지정된 오프셋으로 보관된다고 가정해 보세요. 그러면 쓸 수 있어요:
#define MY_FUNC_GET(ret, cv, f) \
((XSINTERFACE_CVT_ANON(ret))arith_ptrs[CvXSUBANY(cv).any_i32])
#define MY_FUNC_SET(cv, f) \
CvXSUBANY(cv).any_i32 = CAT2(f, _off)
이렇게 실제 함수 포인터 대신 배열 인덱스를 CV에 저장할 수 있어요.
CASE: 키워드
int
foo(int a, int b = NO_INIT, int c = NO_INIT)
CASE: items == 1
C_ARGS: 0, a
CASE: items == 2
C_ARGS: b, a
CASE:
CODE:
RETVAL = b > c ? foo(b, a) : bar(b, a);
OUTPUT:
RETVAL
CASE 키워드는 XSUB가 효과적으로 여러 본문을 갖게 하지만 Perl 이름은 하나만 갖게 해요(ALIAS는 여러 이름을 갖는 것과 다름). 어떤 본문이 실행되는지는 어떤 CASE 표현식이 참으로 평가되는 첫 번째인지에 달려 있어요. C의 case 키워드와 달리 실행은 다음 분기로 falling through 하지 않으므로 break 키워드의 XS 동등물은 없어요. 마지막 CASE의 표현식은 선택 사항이고, 없으면 기본 분기로 작용해요.
위 예는 대략 이 C 코드로 번역돼요:
if (items < 1 || items > 3) { croak("..."); }
if (items == 1) {
int RETVAL;
int a = (int)SvIV(ST(0)); int b = /* etc */
RETVAL = foo(0, a);
/* ... return RETVAL as ST(0) ... */
}
else if (items == 2) {
int RETVAL;
int a = (int)SvIV(ST(0)); int b = /* etc */
RETVAL = foo(b, a);
/* ... return RETVAL as ST(0) ... */
}
else {
int RETVAL;
int a = (int)SvIV(ST(0)); int b = /* etc */
RETVAL = b > c ? foo(b, a) : bar(b, a);
/* ... return RETVAL as ST(0) ... */
}
XSRETURN(1);
각 CASE 키워드는 PREINIT부터 CLEANUP까지 모든 키워드를 포함하는 완전한 정상 XSUB 본문 앞에 와요. 일반 XSUB 키워드는 어떤 CASE 본문 안에도 놓일 수 있어요. 각 if/else 분기에 대해 생성되는 코드는 인수 처리와 반환 값 스택 처리를 포함해 완전한 XSUB 본문에 대해 보통 생성되는 거의 모든 코드를 포함해요.
CASE 표현식은 매개변수 변수 선언의 스코프 밖에 있어서 그 값들을 쓸 수 없다는 점을 유의하세요. 스코프 안에 있고 쓸 수 있는 전형적인 값은 몇 개의 인수가 전달됐는지 나타내는 items 변수("Ellipsis: variable-length parameter lists" 참고)와, ALIAS가 있으면 ix 변수예요.
여기 또 다른 예가 있는데, 이번에는 ALIAS와 함께 써서 같은 C 함수를 두 개의 별도 Perl 함수로 감싸고, 두 번째는(아마 이전 버전 호환성 위해) 인수를 역순으로 받아요. 다소 인위적인 예이지만, ALIAS 키워드가 CASE 분기 중 하나 안에 있어야 한다는 것(어느 쪽이든 상관없음)을 보여줘요. CASE는 항상 XSUB 본문의 최외곽 스코프에 나타나야 하니까요:
int
foo(int a, int b)
CASE: ix == 0
CASE: ix == 1
ALIAS: foo_rev = 1
C_ARGS: b, a
INPUT와 함께 옛 스타일 매개변수 선언을 쓰면 분기마다 매개변수 타입이 달라질 수 있다는 점을 유의하세요:
int
foo(a, int b = 0)
CASE: items == 1
INPUT:
short a
CASE: items == 2
INPUT:
long a
실제로 CASE는 각 분기 안에서 모든 인수·반환 값 처리가 복제된 부풀린 코드를 만들어내고, 자주 그렇게 유용하지 않으며, 보통 CODE 블록 안에서 switch 문을 쓰는 것으로 더 잘 쓸 수 있어요.
Typemap 사용하기 (Using Typemaps)
이 섹션은 typemap 사용에 대한 기본 사실을 설명해요. 여러분만의 typemap 만들기에 대한 완전한 정보와 표준 typemap의 포괄적인 목록은 perlxstypemap 문서를 보세요.
Typemap은 int 같은 C 타입을 T_IV 같은 논리적 XS 타입으로, 그리고 거기서 $var = ($type)SvIV($arg)와 sv_setiv($arg, (IV)$var) 같은 INPUT·OUTPUT 템플릿으로 매핑하는 규칙 집합이에요. 이 템플릿은 변수 확장 후 Perl 인수와 C 자동 변수 사이를 앞뒤로 변환하는 C 코드를 생성해요.
흔한 C·Perl 타입에 대한 표준 시스템 typemap 파일이 Perl에 번들돼 있어요. 거기에 더해 여러분만의 typemap 파일을 추가할 수 있어요. xsubpp 3.01부터 XS 파일 안에 추가 typemap 선언을 인라인으로 포함할 수도 있어요.
Typemap 처리의 위치와 순서 (Locations and ordering of typemap processing)
Typemap 정의는 순서대로 처리되고, 더 최근 항목이 이전 항목을 덮어써요. 정의는 먼저 파일에서, 그다음 XS 파일의 TYPEMAP 섹션에서 읽혀요.
파일의 위치·읽기 방식을 볼 때, XS 파서는 처음에 처리할 Foo.xs 파일을 포함하는 디렉터리로 변경 디렉터리할 것이라는 점을 유의하세요. 이는 이후 상대 경로에 영향을 줘요. 그런 다음 typemap 파일이 위치하고 읽혀요. 파일은 표준과 명시적 두 출처에서 와요.
표준 typemap 파일은 항상 typemap이라고 불리고, 표준 위치 집합에서(@INC와 현재 디렉터리 기준) 검색되고, 매치된 파일은 읽혀요. 처리 순서로 이 경로들은:
"$_/ExtUtils/typemap" for reverse @INC
../../../../lib/ExtUtils/typemap
../../../../typemap
../../../lib/ExtUtils/typemap
../../../typemap
../../lib/ExtUtils/typemap
../../typemap
../lib/ExtUtils/typemap
../typemap
typemap
@INC를 역순으로 검색한다는 것은 @INC에서 더 일찍 발견된 typemap 파일이 더 나중에 처리되고, 그래서 더 높은 우선순위를 갖는다는 뜻이에요.
명시적 typemap 파일은 xsubpp -typemap foo ... 명령줄 스위치 또는 프로그래매틱하게 배열로 지정돼요:
ExtUtils::ParseXS::process_file(..., typemap => ['foo',...]);
이 파일들은 순서대로 읽히고, 명시적으로 나열된 파일이 없으면 파서가 die해요.
xsubpp 2.09_01 이전에는 @INC가 검색되지 않았고 표준 파일이 명시적 파일 보다 먼저 검색·처리됐어요. 2.09_01부터 표준 파일이 명시적 파일 다음에 처리됐어요. 3.60부터 명시적 파일이 다시 마지막에 처리되고 그래서 표준 파일보다 우선해요. 별도로 3.01부터 모든 파일이 처리된 후 TYPEMAP 섹션이 순서대로 처리돼요.
또한 ExtUtils::MakeMaker는 보통 xsubpp를 두 개의 -typemap 인수로 호출한다는 점을 유의하세요. 첫 번째는 시스템 typemap, 두 번째는 모듈의 typemap 파일(있으면)이에요. 이는 @INC를 검색하지 않는 오래된 Perl을 보상해요.
전형적인 배포판의 경우, 이 모든 복잡함은 보통 Perl에 번들된 typemap 파일이 먼저 읽히고, 그다음 배포판에 포함된 typemap 파일이 표준 정의를 추가(·덮어쓰기)하고, 그다음 XS 파일의 TYPEMAP: 항목들이 모든 것을 덮어쓰는 결과를 낳아요.
Typemap 항목 재사용·재정의·추가 (Reusing, redefining and adding typemap entries)
typemap 파일과 TYPEMAP 블록 둘 다 최대 세 섹션을 가질 수 있어요. TYPEMAP(파일·블록 시작에서 암시적)과 INPUT, OUTPUT. 세 섹션이 모두 존재해야 하는 요구는 없어요. 존재하는 것은 그 섹션에 대한 전역 상태에 추가되어, 새 항목을 추가하거나 기존 항목을 재정의해요.
추가 typemap 항목의 가장 단순한 사용은 아마 새 C 타입을 기존 XS 타입에 매핑하는 거예요. 예를 들어 이 C 타입이 주어졌을 때:
typedef enum { red, green, blue } colors;
매개변수·반환 타입으로 쓸 때 그런 enum을 단순 정수로 취급하고 싶다면 typemap에 다음 C-to-XS 타입 매핑 항목을 추가하는 것으로 충분해요:
colors T_IV
또는 기존 INPUT·OUTPUT 템플릿 하나만 덮어쓸 수도 있어요. 예:
OUTPUT
T_IV
my_sv_setiv($arg, (IV)$var);
완전히 새로운 타입이라면 세 섹션 모두에 항목을 추가하고 싶을 수 있어요:
foo T_FOO
INPUT
T_FOO
$var = ($type)get_foo_from_sv($arg);
OUTPUT
T_FOO
set_sv_to_foo($arg, $var);
일반적인 typemap (Common typemaps)
이 섹션은 사용할 수 있는 일반 typemap 항목이 무엇인지 개요를 줘요. 완전한 목록은 perlxstypemap 문서를 보거나 Perl 배포판에 번들된 typemap 파일을 살펴보세요. 또한 Perl 객체와 C 핸들 사이의 매핑에 특히 유용한 하나의 typemap에 대한 자세한 탐구는 "T_PTROBJ and opaque handles"을 보세요. XSUB에서 하나 이상의 값을 반환하는 것에 대한 일반 논의는 "Returning Values from an XSUB"를 보세요. 여기서 typemap은 때로 유용하고(때로는 그렇지 않아요).
int, long, short 같은 표준 부호 있는 C 정수 타입은 모두 T_IV XS 타입에 매핑돼요. IV, I32 같은 정수류 Perl 타입도 이에 매핑돼요. 매개변수가 T_IV에 매핑되는 것으로 선언되면 전달된 SV의 IV 값이 추출되고(아마 먼저 "123" 같은 문자열 값을 IV로 변환), 그다음 그 값이 최종 C 타입으로 캐스트되는데, 정수 타입 간 캐스팅의 일반적인 C 규칙을 따라요. 반대로 값을 반환할 때 C 값이 먼저 IV로 캐스트되고 SV가 그 IV 값으로 설정돼요.
유사하게 흔한 C·Perl 부호 없는 타입은 T_UV에 매핑되고, 값은 (UV) 캐스트로 앞뒤로 변환돼요. U16, U32 같은 몇몇 부호 없는 타입은 대신 T_U_SHORT와 T_U_LONG XS 타입에 매핑되는데, 이들은 T_UV와 같은 효과를 가져요.
unsigned char 타입은 다른 T_UV 타입과 유사하게 취급되지만, char는 정수가 아니라 문자열로 취급돼요. char 매개변수는 전달 인수를 문자열로 취급하고 자동 변수를 그 문자열의 첫 바이트로 설정해요(UTF-8 문자열과 함께 이상한 결과를 낼 수 있어요). char 값을 반환하면 한 문자 문자열을 Perl 호출자에게 반환해요.
char * 타입과 그 일반 변형은 T_PV에 매핑돼요. 전달된 매개변수는(SvPV() 등으로) 그 SV를 나타내는 문자열 버퍼를 돌려줘요. 이 버퍼는 SV가 문자열 값이 있으면(또는 문자열 값으로 변환될 수 있으면) SV의 일부일 수 있고, 그렇지 않으면 임시 버퍼일 수 있어요. 예를 들어 배열 참조를 담은 SV는 "ARRAY(0x12345678)" 값의 임시 문자열 버퍼를 돌려줄 수 있어요. XSUB가 T_PV에 매핑되는 반환 타입을 가지면 반환될 임시 SV에 RETVAL의 현재 값이 할당되고, 문자열 길이는 strlen() 또는 그 동등물로 결정돼요.
UTF-8 문자열 처리와 관련된 어려움은 "Unicode and UTF-8"을 보세요.
float, double, NV 타입은 T_FLOAT, T_DOUBLE, T_NV XS 타입에 매핑되고, 모두 적절한 캐스팅으로 sv_setnv()와 SvNV(sv)를 통해 SV로/에서 변환해요.
SV* 타입은 T_SV에 매핑되는데, 기본적으로 아무 처리도 하지 않아 실제 전달된 SV 인수에 접근하게 해줘요.
T_PTROBJ와 불투명 핸들 (T_PTROBJ and opaque handles)
C 라이브러리의 흔한 인터페이스 배열은 어떤 종류의 create 함수가 핸들(불투명 데이터에 대한 포인터)을 만들고 반환하는 것입니다. 다른 함수 호출들은 그 핸들을 인수로 받고, 마지막에 어떤 destroy 함수가 핸들과 그 데이터를 해제해요.
T_PTROBJ typemap은 Perl 객체를 그런 C 라이브러리 핸들에 매핑하는 흔한 방법 중 하나예요. perlxstypemap의 "T_PTROBJ"를 보세요. 뒤에서 스칼라의 정수 값을 핸들의 주소로 설정한 blessed 스칼라 객체를 써요. T_PTROBJ typemap의 INPUT 코드 템플릿은 먼저 전달된 SV 인수가 매개변수의 선언된 타입과 연관된 Perl 클래스(또는 파생 클래스)로 blessed 된 객체를 참조하는 RV인지 확인한 다음, 그 객체의 정수 값에서 포인터를 꺼내요. OUTPUT 템플릿은 그 안에 핸들 주소가 저장된 새 blessed RV-to-SV를 만들어요.
예시 목적으로, 여기 mynum이라는 최소한의 예시 C 라이브러리를 만들고 XS로 감쌀 거예요. 이 라이브러리는 그냥 정수를 불투명 데이터에 저장해요. 실제로는 복소수나 다중 정밀도 정수 같은 더 흥미로운 것을 저장하는 기존 라이브러리를 감싸겠죠.
다음 샘플 라이브러리 코드가 XS 파일의 초기 'C' 부분에 올 수 있어요:
typedef struct { int i; } mynum;
mynum* mynum_new(int i)
{
mynum* x = (mynum*)malloc(sizeof(mynum));
x->i = i;
return x;
}
void mynum_destroy (mynum *x)
{ free((void*)x); }
int mynum_val (mynum *x)
{ return x->i; }
mynum* mynum_add (mynum *x, mynum *y)
{ return mynum_new(x->i + y->i); }
mynum* mynum_subtract (mynum *x, mynum *y)
{ return mynum_new(x->i - y->i); }
mynum* mynum_multiply (mynum *x, mynum *y)
{ return mynum_new(x->i * y->i); }
mynum* mynum_divide (mynum *x, mynum *y)
{ return mynum_new(x->i / y->i); }
mynum 구조체가 불투명 핸들 데이터를 보유해요. mynum_new() 함수는 숫자 값을 만들고 그에 대한 핸들을 반환해요. 다른 함수들은 그런 핸들을 인수로 받으며, 핸들의 데이터를 해제하는 destroy 함수도 있어요.
다음 XS 코드는 이 라이브러리가 어떻게 감싸져 My::Num 객체를 통해 Perl에서 접근 가능해지는지 예를 보여줘요:
typedef mynum *My__Num;
MODULE = My::Num PACKAGE = My::Num PREFIX = mynum_
PROTOTYPES: DISABLE
TYPEMAP: <<EOF
My::Num T_PTROBJ
EOF
My::Num
mynum_new(class, int i)
C_ARGS: i
void
DESTROY(My::Num x)
CODE:
mynum_destroy(x);
int
mynum_val(My::Num x)
My::Num
mynum_add(My::Num x, My::Num y)
ALIAS: subtract = 1
multiply = 2
divide = 3
CODE:
switch (ix) {
case 0: RETVAL = mynum_add(x, y); break;
case 1: RETVAL = mynum_subtract(x, y); break;
case 2: RETVAL = mynum_multiply(x, y); break;
case 3: RETVAL = mynum_divide(x, y); break;
}
OUTPUT:
RETVAL
이 예의 XSUB는 대부분 My::Num 매개변수·반환 타입으로 선언돼요. "Fully-qualified type names and Perl objects"에서 설명한 것처럼 타입 이름은 typemap에서 그대로 조회되지만, XSUB의 자동 변수 선언에 쓰일 때는 s/:/_/g가 적용되어 My__Num C 타입으로 변환돼요.
이 코드를 순서대로 훑어보면: XS 파일의 'C' 절반에 있는 동안, My__Num C 타입이 그 산술 라이브러리의 핸들에 대한 포인터와 동등하다는 typedef를 추가해요.
다음 MODULE 줄은 mynum_ 접두사를 포함해서, Perl 네임스페이스의 XSUB 이름이 My::Num::mynum_new()이 아니라 My::Num::new() 등이 되게 해요.
그다음 TYPEMAP 선언으로 My::Num 의사 타입을 T_PTROBJ XS 타입에 매핑해요.
다음은 new() 클래스 메서드가 와요. Perl에서 My::Num->new(99);처럼 호출될 거예요. 첫 매개변수는 클래스 이름인데 여기서는 쓰지 않고, 두 번째 매개변수는 객체를 초기화할 값이에요. XSUB가 mynum_new() 라이브러리 함수를 i 값으로만 autocall해요. 이것은 핸들을 반환하고, T_PTROBJ OUTPUT 맵이 그것을 핸들을 담은 blessed 스칼라 참조로 변환해요.
다음 DESTROY() 메서드는 mynum_destroy()의 얇은 래퍼일 뿐이고, val()은 객체의 정수 값을 반환해요.
마지막으로 네 개의 이항 함수가 정의되고, 별명을 통해 같은 XSUB 본문을 공유해요. 대안으로 주요 XSUB의 코드는 별명을 쓰는 대신 INTERFACE 키워드로 단순화할 수 있어요:
My::Num
arithmetic_interface(My::Num x, My::Num y)
INTERFACE:
mynum_add
mynum_subtract
mynum_multiply
mynum_divide
다만 INTERFACE는 xsubpp 3.60부터 My::Num 같은 Perlish 반환 타입만 지원한다는 점을 유의하세요.
이 XS 모듈은 Perl에서 이런 코드로 접근될 수 있어요:
use My::Num;
my $i2 = My::Num->new(2);
my $i7 = My::Num->new(7);
my $i13 = My::Num->new(13);
my $x = $i13->add($i7)->divide($i2);
printf "val=%d\n", $x->val(); # prints "val=10"
오버로딩을 사용해 표현식을 ($i13 + $i7)/$i2로 더 간단히 쓸 수 있도록 확장하는 예는 "The OVERLOAD: Keyword"를 보세요.
매우 특별한 경우로, XS 컴파일러가 DESTROY라는 이름의 XSUB에 대한 INPUT typemap 항목을 찾을 때 XS typemap 이름을 s/OBJ$/REF/로 번역한다는 점을 유의하세요. 그래서 그런 서브에 대해서는 T_PTRREF typemap 항목이 대신 쓰여요. 이 typemap은 T_PTROBJ와 비슷하지만 객체의 클래스가 설정되지도 검사되지도 않는다는 점이 달라요.
XS를 C++와 함께 사용 (Using XS With C++)
MODULE = Foo::Bar PACKAGE = Foo::Bar
# Class methods
X::Y*
X::Y::new(int i)
static int
X::Y::foo(int i)
# Object methods
int
X::Y::bar(int i)
int
X::Y::bar2(int i) const
void
X::Y::DESTROY()
# C-linkage function
extern "C" int
baz(int i)
XS는 C(가 아닌)++ 출력 파일을 생성하는 제한된 지원을 제공해요. 이름에 ::가 포함된 어떤 XSUB든 C++ 메서드로 취급돼요. 이것은 XSUB의 코드가 생성되는 방식에 두 가지 주요 변경을 촉발해요:
- 암시적 첫 인수가 추가돼요. 클래스 메서드의 경우
CLASS라고 불리고char *타입이에요. 객체 메서드의 경우THIS라고 불리고X::Y *타입이에요(X::Y::는 XSUB 이름의 접두사). XSUB는 이름이new이거나 반환 타입에static접두사가 있으면 클래스 메서드로 취급돼요. - 어떤 autocall이든 C 함수 호출이 아니라 적절한 C++ 메서드 호출을 생성해요. 특히 위 예에 기반해:
new: RETVAL = new X::Y(i);static foo: RETVAL = X::Y::foo(i);bar (and bar2): RETVAL = THIS->bar(i);DESTROY: delete THIS;
- 추가로 XSUB 선언에 후행
const가 있으면THIS의 타입이const X::Y *로 선언돼요.
이것은 대부분 구문 설탕일 뿐이에요. 위 bar XSUB 선언은 길게 이렇게 쓸 수 있어요:
int
bar(X::Y* THIS, int i)
CODE:
RETVAL = THIS->foo(i);
OUTPUT:
RETVAL
THIS의 타입(그리고 xsubpp 3.55부터 CLASS)은 INPUT 섹션의 줄로 덮어쓸 수 있다는 점을 유의하세요:
int
X::Y::bar(int i)
X::Y::Z *THIS
마지막으로 일반 C XSUB 선언에 extern "C"를 접두해서 그 XSUB에 C 링키지를 줄 수 있어요.
위 메서드 중 일부는 Perl에서 이런 코드로 호출될 수 있어요:
{
my $obj = Foo::Bar->new(1);
$obj->bar(2);
# implicit $obj->DESTROY();
}
이 예는 C++ 클래스 이름이 Perl 패키지 이름을 따를 필요가 없음을 강조하려고 X::Y 대신 Foo::Bar를 써요.
new() 호출은 문자열 "Foo::Bar"를 첫 인수로 전달하는데, 이는 여러 Perl 클래스가 같은 new() 메서드를 공유하게 하는 데 쓸 수 있어요. 아래의 간단한 작업 예에서는 패키지 이름이 하드코딩되고 그 매개변수는 사용되지 않아요. new() 메서드는 어떤 방식으로든 그 안에 기본 C++ 객체에 대한 포인터가 내장된 Perl 객체를 반환할 것으로 예상돼요. 이것은 핸들을 쓰는 C 라이브러리를 감싸는 "T_PTROBJ and opaque handles" 예와 비슷하지만, 아래에서 설명할 미묘한 차이가 있어요.
bar() 호출은 이 Perl 객체를 첫 인수로 전달하고, typemap이 C++ 객체 포인터를 추출해 THIS 자동 변수에 할당해요.
완전한 C++ 예 (A complete C++ example)
먼저 생성된 파일이 C++ 컴파일러로 컴파일되도록 MakeMaker 등에 말해야 해요. 기본 실험을 위해서는 Makefile.PL의 WriteMakefile() 메서드 호출에 다음 두 줄을 추가하는 것만으로 충분할 수 있어요:
CC => 'c++',
LD => '$(CC)',
하지만 생산 사용에서 이식성을 높이려면 ExtUtils::CppGuess 같은 것을 써서 사용 가능한 C++ 컴파일러에 기반해 ExtUtils::MakeMaker 또는 Module::Build에 올바른 옵션을 자동 생성하고 싶을 수 있어요.
그런 다음 이런 .xs 파일을 만들어요:
#define PERL_NO_GET_CONTEXT
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#include "ppport.h"
namespace Paint {
class color {
int c_R;
int c_G;
int c_B;
public:
color(int r, int g, int b) { c_R = r; c_G = g; c_B = b; }
~color() { printf("destructor called\n"); }
int blue() { return c_B; }
void set_blue(int b) { c_B = b; };
// and similar for red, green
};
}
typedef Paint::color Paint__color;
MODULE = Foo::Bar PACKAGE = Foo::Bar
PROTOTYPES: DISABLE
TYPEMAP: <<EOF
Paint::color * T_PKG_OBJ
INPUT
T_PKG_OBJ
SvGETMAGIC($arg);
if (SvROK($arg) && sv_derived_from($arg, "$Package")) {
IV tmp = SvIV((SV*)SvRV($arg));
$var = INT2PTR($type,tmp);
}
else {
const char* refstr = SvROK($arg)
? "" : SvOK($arg) ? "scalar " : "undef";
Perl_croak_nocontext(
"%s: Expected %s to be of type %s; got %s%"
SVf " instead",
${$ALIAS?\q[GvNAME(CvGV(cv))]:\qq["$pname"]},
"$var", "$Package",
refstr, $arg
);
}
T_PKG_REF
SvGETMAGIC($arg);
if (SvROK($arg)) {
IV tmp = SvIV((SV*)SvRV($arg));
$var = INT2PTR($type,tmp);
}
else
Perl_croak_nocontext("%s: %s is not a reference",
${$ALIAS?\q[GvNAME(CvGV(cv))]:\qq["$pname"]},
"$var")
OUTPUT
T_PKG_OBJ
sv_setref_pv($arg, "$Package", (void*)$var);
EOF
Paint::color *
Paint::color::new(int r, int g, int b)
int
Paint::color::blue()
void
Paint::color::set_blue(int b)
void
Paint::color::DESTROY()
XS 파일의 C 부분(이 경우 C++ 부분)에서 사소한 예시 C++ 클래스가 정의돼요. 이는 보통 적절한 #include만 있는 기존 라이브러리일 거예요. 예는 네임스페이스, 클래스 이름, Perl 패키지를 명확히 구분하기 위해 네임스페이스를 포함해요. Perl 패키지는 구분을 강조하려고 Paint::color 대신 Foo::Bar라고 불려요. 원한다면 Perl 패키지를 Paint::color라고 부를 수도 있지만요.
"Fully-qualified type names and Perl objects"에서 설명한 것처럼 XS로 맹글된 클래스 이름을 허용하는 단일 typedef가 따라와요.
그다음 MODULE 줄이 파일의 XS 부분을 시작해요.
그다음 T_PKG_OBJ라는 새 typemap의 완전한 정의가 나와요. 이것은 실제로 시스템 typemap 파일에서 발견되는 T_PTROBJ typemap의 직접 복사본인데, $ntype의 모든 출현이 $Package로 바뀌었어요. T_PTROBJ와 같은 기본 목적을 서빙해요. 새 blessed Perl 객체 안에 포인터를 내장하고, 나중에 객체에서 그 포인터를 꺼내는 것이죠. 차이는 객체가 어떤 패키지로 blessed 되는지에 있어요. T_PTROBJ는 타입 이름(Paint::color)이 이미 포인터 타입일 것을 기대하지만, C++ XSUB에서는 암시적 THIS 인수가 자동으로 Paint::color * 타입으로 선언되므로 Paint::color 자체가 꼭 포인터 타입일 필요는 없어요. 추가로 Perl과 C++ 클래스 이름이 다를 때 객체를 C++ 클래스 이름이 아니라 Perl 패키지 이름으로 blessed 하고 싶어요. 이 예에서 typemap 템플릿이 eval될 때 두 변수의 실제 값은:
$ntype = "Paint::colorPtr";
$Package = "Foo::Bar";
typemap에는 T_PKG_REF에 대한 INPUT 정의도 포함되는데, 이는 T_PTRREF의 정확한 복사본이에요. 이는 최적화로 XS 파서가 XSUB 이름이 DESTROY이면 INPUT typemap을 s/OBJ$/REF/로 자동 이름 바꾸기 때문인데, 클래스가 올바른지 검사할 필요가 없기 때문이에요.
마지막으로 XS 파일에는 클래스 메서드에 대한 래퍼인 몇 개의 XSUB가 포함돼요.
이 클래스는 이렇게 쓰일 수 있어요:
use Foo::Bar;
my $color = Foo::Bar->new(0x10, 0x20, 0xff);
printf "blue=%d\n", $color->blue(); # prints 255
$color->set_blue(0x80);
printf "blue=%d\n", $color->blue(); # prints 128
XS에서 정적 데이터 안전하게 저장 (Safely Storing Static Data in XS)
일반적으로 XS 파일 안에서 static 변수와 유사한 변경 가능한 데이터를 선언하는 것은 피해야 해요. Perl 인터프리터 바이너리는 흔히 여러 인터프리터 구조를 허용하도록 구성되고, 각 인터프리터 구조마다 완전한 인터프리터 상태 세트가 있어요. 그 경우 보통 "static" 데이터가 프로세스 전역의 단일 공유 값이 아니라 인터프리터당이어야 해요.
이것은 여러 스레드가 있을 때 더 중요해져요. use threads를 통하거나, Perl 인터프리터가 자기 스레드를 관리하고 적절하다고 생각하는 대로 스레드에 인터프리터를 할당할 수 있는 다른 애플리케이션(예: 웹 서버)에 내장된 경우요.
단일 C 구조체를 선언하고 안전하게 접근하게 하는 매크로 프레임워크가 XS 코드에 제공돼요. 뒤에서 그 구조체는 인터프리터 또는 스레드당 할당돼요. 스레드가 아닌 Perl 인터프리터 빌드에서는 매크로가 단일 전역 인스턴스로 우아하게 퇴화해요. 이 매크로들은 이름에 MY_CXT("my context")를 포함해요.
그래서 static 데이터를 쓰는 모든 XS 모듈은 이 매크로들을 쓰는 것이 강력히 권장돼요.
새 골격 Foo.xs 파일을 만들 때 h2xs의 --global 옵션으로 골격 매크로 세트도 포함할 수 있어요. 예:
h2xs -A --global -n Foo::Bar
아래는 매크로를 쓰는 완전한 예시 모듈이에요. 최대 세 마리 눈먼 쥐의 이름을 추적해요.
#define PERL_NO_GET_CONTEXT
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#include "ppport.h"
#define MAX_NAME_LEN 100
/* Global Data */
#define MY_CXT_KEY "BlindMice::_guts" XS_VERSION
typedef struct {
int count;
char name[3][MAX_NAME_LEN+1];
} my_cxt_t;
START_MY_CXT
MODULE = BlindMice PACKAGE = BlindMice
PROTOTYPES: DISABLE
BOOT:
{
MY_CXT_INIT;
MY_CXT.count = 0;
}
int
AddMouse(char *name)
PREINIT:
dMY_CXT;
CODE:
if (strlen(name) > MAX_NAME_LEN)
croak("Mouse name too long\n");
if (MY_CXT.count >= 3) {
warn("Already have 3 blind mice");
RETVAL = 0;
}
else {
RETVAL = ++MY_CXT.count;
strcpy(MY_CXT.name[MY_CXT.count - 1], name);
}
OUTPUT:
RETVAL
char *
get_mouse_name(int mouse_num)
PREINIT:
dMY_CXT;
CODE:
if (mouse_num < 1 || mouse_num > MY_CXT.count)
croak("There are only %d blind mice.", MY_CXT.count);
else
RETVAL = MY_CXT.name[mouse_num - 1];
OUTPUT:
RETVAL
void
CLONE(...)
CODE:
MY_CXT_CLONE;
이 예의 핵심 포인트는:
my_cxt_t구조체가 여러분의 "static" 데이터 전부를 보유할 거예요.MY_CXT_KEY와START_MY_CXT는 매크로 시스템이 작동하게 하는 보일러플레이트예요. 전자는 여러분의 모듈에 고유해야 하는 문자열이에요.BOOT섹션의MY_CXT_INIT은 모듈이 로드될 때 구조체를 할당해요. 필요한 초기화(예:count설정)를 하는 추가 부트 코드를 더할 수 있어요.BOOT는 인터프리터당 최대 한 번, 그 인터프리터 인스턴스의 코드가 처음use BlindMice를 할 때 호출돼요.- 각 XSUB는
dMY_CXT선언을 포함하고, 이는 현재 인터프리터와 연관된 구조체에 대한 포인터를 꺼내 숨은 자동 변수에 저장해요.MY_CXT로 이 구조체 안의 필드에 접근할 수 있어요. MY_CXT_CLONE은 현재 구조체의 바이트-대-바이트 복사본을 만들어요. 이것은 특별한CLONEXSUB에서 호출되어, 각 새 스레드가 기본적으로 공유되는 데이터의 자기 복사본을 갖도록 보장해요.
MY_CXT 매크로 참조 (MY_CXT macros reference)
-
MY_CXT_KEY — 이 매크로는 XS 모듈의 정적 데이터를 참조하는 고유 키를 정의하는 데 쓰여요. h2xs가 쓰는 권장 명명 규칙은 모듈 이름, 문자열
::_guts, 모듈 버전 번호를 연결한 문자열을 쓰는 것이에요:#define MY_CXT_KEY "MyModule::_guts" XS_VERSION -
my_cxt_t — "static" 값은 반드시 항상
my_cxt_t라고 불리는 구조체 typedef 안에 저장돼야 해요. 다른*MY_CXT*매크로들은my_cxt_ttypedef 이름의 존재를 가정해요. 예:typedef struct { int some_value; int some_other_value; } my_cxt_t; -
START_MY_CXT — 이 매크로는 숨은 보일러플레이트 코드를 담고 있어요.
my_cxt_t선언 바로 뒤에START_MY_CXT매크로를 항상 두세요. -
MY_CXT_INIT —
MY_CXT_INIT매크로는my_cxt_t구조체를 위한 저장 공간을 초기화해요. 정확히 한 번 호출해야 해요. 보통 BOOT 섹션에서요. 여러 인터프리터를 유지한다면 각 인터프리터 인스턴스에서 한 번씩, 기존 인터프리터에서 클론된 것은 제외하고 호출해야 해요. (아래 "MY_CXT_CLONE" 참고.) -
dMY_CXT —
MY_CXT에 접근하는 모든 XSUB(및 다른 함수)의 시작에서dMY_CXT매크로(선언)를 쓰세요. -
MY_CXT —
my_cxt_t구조체의 멤버에 접근하려면MY_CXT매크로를 써요. 예를 들어my_cxt_t가:typedef struct { int index; } my_cxt_t;이라면
index멤버에 접근하려면 이렇게 써요:dMY_CXT; MY_CXT.index = 2; -
aMY_CXT/pMY_CXT —
dMY_CXT는 계산이 꽤 비쌀 수 있어서, 각 함수에서 호출하는 오버헤드를 피하려면 인자/매개변수aMY_CXT/pMY_CXT매크로로 선언을 다른 함수에 전달할 수 있어요. 예:void sub1() { dMY_CXT; MY_CXT.index = 1; sub2(aMY_CXT); } void sub2(pMY_CXT) { MY_CXT.index = 2; }pTHX와 유사하게, 매크로가 여러 인수에서 첫 번째 또는 마지막일 때 동등한 형태가 있고, 적절할 때 밑줄이 쉼표로 확장돼요. 즉_aMY_CXT,aMY_CXT_,_pMY_CXT,pMY_CXT_요. 이는 그 매크로들이 방치된 쉼표를 남기지 않고 실제 인수를 최적화해 버릴 가능성을 허용해요. -
MY_CXT_CLONE — 새 인터프리터가 기존 인터프리터의 복사본으로 생성될 때(예:
threads->create()), 기본적으로 두 인터프리터는 같은 실제 my_cxt_t 구조체를 공유해요.MY_CXT_CLONE을 호출하면(보통 패키지의CLONE()함수를 통해) 구조체의 바이트-대-바이트(깊은 복사는 아님) 복사본이 만들어지고, 이후의dMY_CXT는 그 복사본에 접근하게 돼요. 보통 인터프리터가 복사될 때마다(보통 새 스레드를 만들 때) 호출되는CLONE메서드 안에서 쓰여요. 구조체 안의 항목을 깊은 복사하려면CLONE()에 다른 코드를 더할 수 있어요. -
MY_CXT_INIT_INTERP(my_perl) —
MY_CXT_INIT과dMY_CXT매크로의 변형으로, 명시적 perl 인터프리터를 인수로 받아요. -
dMY_CXT_INTERP(my_perl) — 위 참고.
이 매크로들은 같은 소스 파일 안에서만 함께 작동한다는 점을 유의하세요. 즉 한 소스 파일의 dMY_CXT는 다른 소스 파일의 dMY_CXT와 다른 구조체에 접근해요.
예시 (EXAMPLES)
꽤 완전한 XS 파일 예시는 이 문서의 다른 곳에 있어요:
- "T_PTROBJ and opaque handles"
- "A complete C++ example"
- "Safely Storing Static Data in XS"
"SYNOPSIS"에는 XS 파일의 개요가 있고 perlxstut에는 여러 작업 예시가 있어요.
물론 영감을 위해 CPAN의 기존 XS 배포판을 볼 수도 있어요. 다만 그중 상당수는 이 문서가 2025년에 다시 쓰이기 전에 만들어졌으므로 현재 모범 사례를 따르지 않을 수 있다는 점을 유의하세요.
실제 라이브러리를 감쌀 때 .xs 파일에 이런 줄을 추가해야 하는 경우가 많다는 점을 유의하세요:
#include <foobar.h>
그리고 Makefile.PL 등에 이런 항목을 추가해요:
LIBS => ['-lfoo', '-lbar'],
그리고 t/ 아래에 테스트 스크립트를 추가하는 것도 잊지 마세요.
주의사항 (CAVEATS)
표준 C 라이브러리 함수 사용 (Use of standard C library functions)
종종 Perl API에는 표준 C 라이브러리 함수 대신 써야 하는 함수가 있어요. perlclib를 보세요. 더 나아가 많은 libc 함수는 perl과 함께 쓰면 문제가 있거나 특별한 주의가 필요해요. perlclib이 구체적인 내용과 힌트를 담고 있어요.
Perl이 정의하고 XS 코드에 보이는 심볼 (Symbols defined by Perl that are visible to XS code)
#include "perl.h"를 하면 perl 인터프리터가 정의한 수천 개의 함수와 매크로에 접근할 수 있어요. 그중 일부는 분명히 원하는 것들이에요. perlapi에 설명된 일을 더 쉽게 해주는 요소들처럼요. 하지만 일부는 의도치 않게 보여 네임스페이스를 오염시킨다고 볼 수 있어요. Perl v5.44부터 이런 새 심볼 추가를 방지하는 데 더 신경 쓰고 있고, 기존 것 중 많은 것을 점진적으로 정리할 계획이에요.
실제로 이것은 큰 문제가 아니었어요. 가능한 이름의 우주는 방대하고, perl과 충돌을 발견하면 동의어나 다른 철자를 고를 수 있으니까요.
또한 기능이 만들어졌지만 문서화된 적이 없어 perlapi에 나타나지 않는 것이 있어요. 다른 사람도 필요했을 것 같은 걸 찾았다면, regen/embed.pl 상단 근처의 배열 @unresolved_visibility_overrides를 훑어보고 그럴듯한 후보를 찾은 다음 조사해 보세요. 어떤 것이 원하는 것이라면 https://github.com/Perl/perl5/issues 로 보고해서 알려주거나, 더 좋게는 패치를 제출하세요. 그 시점쯤이면 아마 문서에서 무엇을 말해야 할지 다른 누구보다 잘 알 준비가 돼 있을 거예요!
@unresolved_visibility_overrides는 여러분의 네임스페이스에 보이지만 우리가 어떻게 처리할지 결정하지 않은 모든 매크로를 나열해요. 이 목록을 훑어 자신이 쓰지 말아야 할 이름을 찾을 수도 있어요.
Perl은 PL_로 시작하거나 안에 어떤 형태의 perl을 가진(/^Perl_/ 같은, 양 끝이 어떤 방식으로든 구분된 대문자·소문자) 이름을 스스로 예약해요. 정확한 철자를 주는 패턴도 regen/embed.pl 파일 상단 근처에 있어요.
perl이 제공하는 함수는 오랫동안 Perl_로 시작하는 것과 그 접두사를 생략한 짧은 이름 두 형태로 나와 왔어요. 짧은 이름이 여러분의 심볼과 충돌하면 긴 이름을 쓰면 돼요. 단, 그렇게 하면 짧은 이름이 스레드 문맥 매개변수의 필요성(여부)을 숨기고, ppport.h가 필요할 때 고정 또는 이전 버전 호환 버전을 제공하기 위해 짧은 이름에 의존한다는 단점이 있어요. perlapi는 각 API 요소의 사용 가능한 형태에 대한 프로토타입을 줘요.
이벤트 루프와 제어 흐름 (Event loops and control flow)
일부 모듈은 사용자 입력을 기다리는 이벤트 루프를 가져요. 그런 모듈 두 개가 단일 Perl 애플리케이션에서 제대로 함께 작동할 가능성은 매우 낮아요.
일반적으로 perl 인터프리터는 Perl 프로그램에 관한 한 자신을 세계의 중심으로 봐요. XS 코드는 perl이 하지 않거나 충분히 빠르게 하지 않는 일을 성취하는 조력자로 보여지며, 항상 perl에 종속돼요. XS 코드가 이 모델에 가까이 부합할수록 충돌이 일어날 가능성이 낮아져요.
XS 버전 (XS VERSION)
이 문서는 xsubpp 3.63이 지원하는 기능을 다뤄요.
저자 진단 (AUTHOR DIAGNOSTICS)
xsubpp 3.49부터 몇몇 파서 경고가 기본적으로 비활성화돼 있어요. 개발하는 동안 환경 또는 Makefile.PL에서 $ENV{AUTHOR_WARNINGS}를 참으로 설정하거나, 코드로 $ExtUtils::ParseXS::AUTHOR_WARNINGS를 참으로 설정하거나, process_file()에 author_warnings=>1을 명시적으로 전달할 수 있어요. 현재 이것은 더 엄격한 별명 검사를 활성화하지만 미래에는 더 많은 경고가 추가될 수 있어요. 이것이 활성화할 종류의 경고는 XS 파일의 작성자에게만 유용하고, 생성된 진단은 설치별 세부 내용을 포함하지 않으므로 XS 코드 자체의 유지보수자에게만 유용해요.
저자 (AUTHOR)
원래 Dean Roehrich <[email protected]>가 작성했고, 2025년에 완전히 다시 쓰여졌어요.