Python에서 Zig 함수 사용

발행: (2026년 2월 17일 오후 02:17 GMT+9)
12 분 소요
원문: Dev.to

Source: Dev.to

위에 제공된 텍스트를 한국어로 번역하려면, 실제 번역할 내용이 필요합니다. 번역하고자 하는 전체 텍스트(코드 블록을 제외한 본문)를 제공해 주시면, 요청하신 대로 마크다운 형식과 기술 용어를 유지하면서 한국어로 번역해 드리겠습니다.

Overview

이 문서는 Zig로 작은 HTTP 클라이언트를 작성하고, 이를 C에 노출한 뒤 Python에서 호출하는 방법을 보여줍니다.
핵심 내용은 다음과 같습니다:

  • Zig의 [:0]const u8 (null‑terminated 문자열)를 직접 사용하는 방법.
  • 불필요한 복사를 피하는 방법.
  • Python(또는 다른 언어)에서 링크할 수 있는 간단한 C 래퍼 제공.

Zig 구현

const std = @import("std");

/// Perform an HTTP GET request and return a null‑terminated byte slice.
///
/// * `allocator` – Zig allocator used for all temporary allocations.  
/// * `url`       – Null‑terminated UTF‑8 string (`[:0]const u8`).
///
/// Returns `![:0]u8` – on success a pointer to a C‑compatible string,
/// on error an error union.
fn request(allocator: std.mem.Allocator, url: [:0]const u8) ![:0]u8 {
    // Strip the trailing NUL to get a normal slice for parsing.
    const len = std.mem.len(url);
    const url_slice = url[0..len];
    const uri = try std.Uri.parse(url_slice);

    // Create an HTTP client that uses the supplied allocator.
    var client = std.http.Client{ .allocator = allocator };
    defer client.deinit();

    // Build a GET request.
    var req = try client.request(.GET, uri, .{});
    defer req.deinit();

    // Send the request without a body.
    try req.sendBodiless();

    // Buffer for possible redirects (up to 4 KB).
    var redirect_buffer: [4096]u8 = undefined;
    var response = try req.receiveHead(&redirect_buffer);

    // Collect the response body into an unmanaged ArrayList.
    var list = std.ArrayListUnmanaged(u8){};
    errdefer list.deinit(allocator);

    var reader = response.reader(&.{});
    try reader.appendRemainingUnlimited(allocator, &list);

    // Append a NUL terminator – required for C strings.
    try list.append(allocator, 0);

    // Convert the list into an owned slice and cast to a C pointer.
    const owned = try list.toOwnedSlice(allocator);
    return @ptrCast(owned.ptr);
}

핵심 포인트

단계무슨 일인가
URL 처리[:0]const u8 를 일반 슬라이스로 변환하여 std.Uri.parse 에 전달합니다.
클라이언트 생성std.http.Client 를 호출자가 제공한 allocator 로 초기화합니다.
요청client.request(.GET, uri, .{}) 로 GET 요청을 생성합니다.
전송req.sendBodiless() 로 본문 없이 요청을 보냅니다.
수신req.receiveHead(&redirect_buffer) 로 응답 헤더를 읽어옵니다.
본문 수집ArrayListUnmanaged(u8) 가 필요에 따라 성장하고, appendRemainingUnlimited 가 전체 본문을 읽습니다.
NUL 종단자list.append(allocator, 0) 로 C 스타일 문자열에 필요한 널 종단자를 추가합니다.
반환소유된 슬라이스를 [:0]u8 로 캐스팅하여 char* 호환 포인터를 반환합니다.

Zig 단위 테스트

test "Request" {
    const allocator = std.testing.allocator;
    const url = "http://localhost";
    // `url.ptr` is a `[:0]const u8` (null‑terminated) pointer.
    const response = try request(allocator, url.ptr);
    defer {
        // Include the NUL terminator when freeing.
        const len = std.mem.len(response) + 1;
        allocator.free(response[0..len]);
    }
    try std.testing.expect(std.mem.len(response) > 0);
}

테스트 실행:

$ zig test request.zig
All 1 tests passed.

Note: 테스트가 성공하려면 localhost에서 HTTP 서버가 실행 중이어야 합니다.

C Wrapper

Zig는 C에서 호출할 수 있는 함수를 내보낼 수 있습니다.
내보낸 시그니처는 동일한 널 종료 타입을 사용하지만, 옵션 반환(?[:0]u8)은 NULL이 될 수 있는 일반 포인터로 변환됩니다.

/// Public wrapper that returns `NULL` on any error.
export fn request_wrapper(url: [:0]const u8) ?[:0]u8 {
    const allocator = std.heap.page_allocator;
    return request(allocator, url) catch return null;
}

/// Free the memory allocated by `request_wrapper`.
export fn request_deallocate(result: [:0]u8) void {
    const allocator = std.heap.page_allocator;
    const len = std.mem.len(result) + 1; // include NUL
    allocator.free(result[0..len]);
}

Wrapper Test

test "Wrappers" {
    const url = "http://localhost";
    const body = request_wrapper(url.ptr);
    try std.testing.expect(std.mem.len(body.?) > 0);
    request_deallocate(body.?);
}

두 테스트를 함께 실행:

$ zig test request.zig
All 2 tests passed.

Header File (request.h)

#ifndef _REQUEST_H
#define _REQUEST_H

/* Returns a heap‑allocated C string on success, or NULL on failure. */
char *request_wrapper(const char *url);

/* Frees a string returned by `request_wrapper`. */
void request_deallocate(char *content);

#endif // _REQUEST_H
  • C에서는 Zig의 [:0]const u8const char *에 매핑됩니다.
  • 옵션(?) 타입은 사라지고, NULL 포인터가 실패를 나타냅니다.

예제 C 프로그램

#include <stdio.h>
#include <stdlib.h>
#include "request.h"

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <url>\n", argv[0]);
        return 1;
    }

    char *content = request_wrapper(argv[1]);
    if (!content) {
        fprintf(stderr, "Request failed\n");
        return 1;
    }

    printf("%s\n", content);
    request_deallocate(content);
    return 0;
}

공유 라이브러리는 다음과 같습니다:

  • librequest.so on Linux
  • librequest.dylib on macOS
  • request.dll on Windows

파이썬에서 래퍼 사용하기

import ctypes
from pathlib import Path

# Load the shared library (adjust the suffix for your platform).
_lib_path = Path(__file__).with_name('librequest.so')
lib = ctypes.CDLL(str(_lib_path))

# Declare argument and return types.
lib.request_wrapper.argtypes = [ctypes.c_char_p]
lib.request_wrapper.restype = ctypes.c_char_p   # NULL on error

lib.request_deallocate.argtypes = [ctypes.c_char_p]
lib.request_deallocate.restype = None

def fetch(url: str) -> str:
    """Fetch `url` using the Zig HTTP client."""
    raw = lib.request_wrapper(url.encode('utf-8'))
    if not raw:
        raise RuntimeError("Request failed")
    try:
        # Convert the C string (null‑terminated) to Python `str`.
        return ctypes.string_at(raw).decode('utf-8')
    finally:
        lib.request_deallocate(raw)

# Example usage
if __name__ == "__main__":
    print(fetch("http://localhost"))
  • 파이썬 코드는 C용으로 빌드된 동일한 공유 라이브러리를 로드합니다.
  • request_wrapperrequest_deallocate로 해제되는 char *를 반환합니다.
  • 오류는 NULL 반환값으로 보고됩니다.

요약

구성 요소목적
request (Zig)핵심 HTTP GET 구현으로, C‑호환 NUL‑종료 문자열을 반환합니다.
request_wrapper / request_deallocate (Zig)내보낸 C API – 간단하고, 복사 없이, 추가 할당 없이 동작하는 래퍼.
request.h두 함수를 노출하는 C 헤더.
Example C program공유 라이브러리의 네이티브 사용을 보여주는 예제 C 프로그램.
Python snippet같은 공유 라이브러리를 Python에서 호출하는 방법을 보여줍니다.

전체 흐름은 메모리‑안전(할당과 해제가 동일한 할당자를 사용)하고 오류‑인식(Zig 오류가 호출자에게 NULL 포인터가 됩니다)합니다. 이 패턴은 C‑호환 언어에 노출하고 싶은 모든 Zig 기능에 재사용할 수 있습니다.

개요

이 문서는 ctypes를 사용하여 Zig 라이브러리를 Python에 노출하는 방법을 설명합니다.
모든 FFI(외부 함수 인터페이스) 복잡성은 작은 Python 클래스 안에 캡슐화되어 있어, 라이브러리를 Python 코드에서 쉽게 사용할 수 있습니다.

Python 래퍼

import ctypes

class Request:
    def __init__(self):
        # Load the shared library
        self.lib = ctypes.CDLL("./librequest.so")

        # Declare the function signatures
        self.lib.request_wrapper.argtypes = [ctypes.c_char_p]
        self.lib.request_wrapper.restype = ctypes.POINTER(ctypes.c_char)

    def get(self, url: str) -> str:
        # Call the Zig function
        result = self.lib.request_wrapper(url.encode())
        if not result:
            raise RuntimeError("Request failed")

        # Find the terminating NUL byte
        i = 0
        while result[i] != b'\0':
            i += 1

        # Convert the C string to a Python string
        content = result[:i].decode()

        # Free the memory allocated by Zig
        self.lib.request_deallocate(result)

        return content

작동 방식

단계설명
__init__CDLL을 사용하여 공유 라이브러리를 로드합니다.
argtypes = [ctypes.c_char_p]함수가 C‑스타일 문자열을 기대한다는 것을 선언합니다.
restype = ctypes.POINTER(ctypes.c_char)함수가 문자에 대한 포인터를 반환한다는 것을 선언합니다.
get
  • Python URL을 바이트(C 문자열)로 인코딩합니다.
  • Zig 함수를 호출하고 NULL인지 확인합니다.
  • 종료 \0 바이트를 수동으로 찾습니다.
  • 그 지점까지 바이트를 슬라이스하고 Python 문자열로 디코드합니다.
  • request_deallocate를 호출하여 Zig가 할당한 메모리를 해제합니다.

래퍼 사용하기

import request

req = request.Request()
body = req.get("http://localhost")
print(body)

Request 내부에 모든 FFI 세부 사항이 숨겨져 있어, 사용자는 일반 Python 객체만 사용하면 됩니다.

예제 확장

이 기반 위에 다양한 방법으로 확장할 수 있습니다:

  • 더 많은 HTTP 메서드 추가 – POST, PUT, DELETE 등 (동일한 패턴).
  • 구성 옵션 노출 – 타임아웃, 커스텀 헤더, 인증.
  • Zig로 작성된 계산 집약적인 알고리즘 통합.
  • 운영 체제와 상호 작용하는 다른 Zig 라이브러리 래핑.

핵심 요점

  1. 메모리 관리는 명시적이어야 합니다 – 누가 메모리를 할당하고 누가 해제하는지 알아야 합니다. Zig는 명시성을 강제하여 메모리 누수를 방지하지만, 그만큼 규율이 필요합니다.
  2. 타입은 일치해야 합니다 – 문자열과 배열은 각 언어마다 다른 표현 방식을 갖습니다; 변환은 정확히 이루어져야 합니다.
  3. 오류 처리는 번역이 필요합니다 – Zig의 오류 모델은 C와 동일하지 않습니다. 이 예제에서는 NULL을 사용해 실패를 알리지만, 다른 전략도 가능합니다.
  4. 계층별 테스트 – 먼저 Zig 코드만 테스트하고, 그 다음 C에서, 마지막으로 Python에서 테스트합니다. 이렇게 하면 문제의 원인을 쉽게 파악할 수 있습니다.

같은 접근 방식을 HTTP와 같은 다른 기능에도 적용하여 Zig에서 Python으로 노출할 수 있습니다.

Note: This article was originally written in Spanish.

0 조회
Back to Blog

관련 글

더 보기 »