호출 규약
호출 규약 (callconv)
도입: 함수가 호출될 때 인자를 어떻게 넘기고 반환 값을 어떻게 돌려받을지는 ABI와 CPU 아키텍처에 따라 달라져요. 그 규칙을 '호출 규약(calling convention)'이라 하는데, Zig에서는
callconv지정자를 쓰면 함수의 호출 규약을 직접 골라줄 수 있습니다.
본문
함수를 선언할 때 callconv 지정자를 써서 그 함수의 호출 규약을 바꿀 수 있어요. 한국어로는 '호출 규약'이라고 하고, 함수가 호출되는 방식을 규정하는 약속입니다.
callconv는 함수 타입(function type)에서 호출 규약을 지정할 때 쓰입니다.
// The callconv specifier changes the calling convention of the function.
extern "kernel32" fn ExitProcess(exit_code: u32) callconv(.winapi) noreturn;
extern "c" fn atan2(a: f64, b: f64) f64;
위 예시에서 흥미로운 부분을 짚어볼게요. extern 지정자는 함수가 링크 시점(정적 링크)이나 실행 시점(동적 링크)에 해석되도록 선언하고, extern 키워드 뒤의 인용된 식별자는 그 함수가 들어 있는 라이브러리를 가리켜요. 예를 들어 "c"는 libc.so를 뜻하죠. 그리고 callconv(.winapi)처럼 호출 규약을 지정해 주면 됩니다. ExitProcess는 Windows API 함수라서 .winapi 호출 규약을 쓰는 게 자연스럽고요.
또 다른 호출 규약으로 네이키드(naked)가 있어요. 네이키드 호출 규약은 함수에 프롤로그(prologue)나 에필로그(epilogue)가 전혀 붙지 않게 해요. 어셈블리와 통합할 때 유용합니다.
// The naked calling convention makes a function not have any function prologue or epilogue.
// This can be useful when integrating with assembly.
fn _start() callconv(.naked) noreturn {
abort();
}
인라인(inline) 호출 규약도 있어요. 이건 함수가 모든 호출 지점에서 인라인되도록 강제합니다. 함수를 인라인할 수 없다면 컴파일 타임 오류가 나요.
// The inline calling convention forces a function to be inlined at all call sites.
// If the function cannot be inlined, it is a compile-time error.
inline fn shiftLeftOne(a: u32) u32 {
return a << 1;
}
좀 더 규칙적으로 보자면, 문법에서는 callconv가 이렇게 정의됩니다.
CallConv <- KEYWORD_callconv LPAREN Expr RPAREN
즉 callconv 키워드 다음에 괄호 안의 식(Expr)으로 호출 규약을 지정하는 형태예요.
한 가지 짚고 넘어갈 점이 있어요. export 지정자로 내보낸 함수는 기본적으로 C 호출 규약(C ABI)을 따릅니다. 그리고 exported 함수는 생성된 오브젝트 파일에서 외부에 보이고, C ABI를 사용하죠. 반대로 extern 같은 경우에는 callconv 지정자로 호출 규약을 명시적으로 바꿀 수 있다는 점이 다릅니다.
더 알아보기
호출 규약은 함수와 떼려야 뗄 수 없는 주제라서, 함수 선언과 함수 포인터의 차이를 함께 보면 도움이 돼요. 함수 본문(function body)은 컴파일 타임 전용 타입이고, 함수 포인터(function pointer)는 런타임에 값이 알려질 수 있다는 점도 기억해 두면 좋아요. 자세한 내용은 Functions 섹션을 참고하세요.