C 가변 인자 함수

C 가변 인자 함수 (C Variadic Functions)

Zig는 외부 C 함수처럼 가변 인자를 받는 함수를 선언할 수 있고, 가변 인자 함수를 직접 구현하는 것도 가능해요. C의 printf 같은 함수를 연결해서 쓰는 것부터, 우리가 직접 ...을 다루는 함수를 만드는 것까지 두 가지 측면을 차례로 볼게요.

출처: Zig Documentation

본문

Zig는 extern 가변 인자 함수를 지원해요. 외부 함수 선언에는 타입 목록 끝에 ...을 붙여서 "여기 뒤로는 개수가 정해지지 않은 인자가 온다"고 알려줄 수 있죠. 가장 대표적인 예가 C의 printf예요.

const std = @import("std");
const testing = std.testing;

pub extern "c" fn printf(format: [*:0]const u8, ...) c_int;

test "variadic function" {
    try testing.expectEqual(14, printf("Hello, world!\n"));
    try testing.expect(@typeInfo(@TypeOf(printf)).@"fn".attrs.varargs);
}

여기서 눈여겨볼 점 두 가지가 있어요. 첫째, extern "c" fn printf(format: [*:0]const u8, ...)처럼 ...이 가변 인자를 뜻한다는 점이에요. 둘째, @typeInfo(@TypeOf(printf)).@"fn".attrs.varargs로 함수 타입 정보를 들여다보면 attrs.varargs가 참인지 확인하는데, 이 값이 가변 인자 함수인지 알려주는 플래그예요.

컴파일할 때는 C 라이브러리를 함께 링크해야 하므로 -lc를 붙여줘요. printf"Hello, world!\n"를 출력하고 그 길이인 14를 돌려주는 것까지 확인할 수 있어요.

$ zig test test_variadic_function.zig -lc
1/1 test_variadic_function.test.variadic function...OK
All 1 tests passed.
Hello, world!

이번에는 우리가 직접 가변 인자 함수를 구현해 볼게요. 가변 인자 목록을 다루는 내장 함수인 @cVaStart, @cVaEnd, @cVaArg, @cVaCopy를 사용하면 돼요. C에서 va_start, va_end, va_arg를 쓰는 것과 같은 흐름이에요.

const std = @import("std");
const testing = std.testing;
const builtin = @import("builtin");

fn add(count: c_int, ...) callconv(.c) c_int {
    var ap = @cVaStart();
    defer @cVaEnd(&ap);
    var i: usize = 0;
    var sum: c_int = 0;
    while (i < count) : (i += 1) {
        sum += @cVaArg(&ap, c_int);
    }
    return sum;
}

test "defining a variadic function" {
    if (builtin.cpu.arch == .aarch64 and builtin.os.tag != .macos) {
        // https://github.com/ziglang/zig/issues/14096
        return error.SkipZigTest;
    }
    if (builtin.cpu.arch == .x86_64 and builtin.os.tag == .windows) {
        // https://github.com/ziglang/zig/issues/16961
        return error.SkipZigTest;
    }
    if (builtin.cpu.arch == .s390x) {
        // https://github.com/ziglang/zig/issues/21350#issuecomment-3543006475
        return error.SkipZigTest;
    }

    try std.testing.expectEqual(@as(c_int, 0), add(0));
    try std.testing.expectEqual(@as(c_int, 1), add(1, @as(c_int, 1)));
    try std.testing.expectEqual(@as(c_int, 3), add(2, @as(c_int, 1), @as(c_int, 2)));
}

함수 본문의 흐름을 따라가 볼게요. @cVaStart()로 가변 인자 목록을 시작해서 ap에 담고, defer @cVaEnd(&ap)로 함수가 끝날 때 반드시 정리되게 해요. 그다음 반복문 안에서 @cVaArg(&ap, c_int)를 호출할 때마다 가변 인자를 하나씩 c_int 타입으로 꺼내서 sum에 더해요. count는 정해진 인자로 받은 개수니까, 그만큼만 반복하면 되죠.

테스트를 보면 아키텍처에 따라 error.SkipZigTest로 건너뛰는 분기가 있어요. 특정 CPU 아키텍처에서 가변 인자 구현의 동작이 정해지지 않은 이슈가 있어서(주석의 링크 참고) 실제 검증은 다른 환경에서 실행해야 하는 거예요. 일반적인 환경이라면 add(2, @as(c_int, 1), @as(c_int, 2))처럼 인자를 몇 개든 넘겨서 합을 구할 수 있어요.

$ zig test test_defining_variadic_function.zig
1/1 test_defining_variadic_function.test.defining a variadic function...OK
All 1 tests passed.

더 알아보기

  • @cVaStart, @cVaEnd, @cVaArg, @cVaCopy — 가변 인자 목록을 다루는 내장 함수 (Builtin Functions)
  • Exporting a C Library — C ABI로 라이브러리를 내보내는 방법