Python에서 Zig 함수 사용
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 u8가const 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.soon Linuxlibrequest.dylibon macOSrequest.dllon 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_wrapper는request_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 |
|
래퍼 사용하기
import request
req = request.Request()
body = req.get("http://localhost")
print(body)
Request 내부에 모든 FFI 세부 사항이 숨겨져 있어, 사용자는 일반 Python 객체만 사용하면 됩니다.
예제 확장
이 기반 위에 다양한 방법으로 확장할 수 있습니다:
- 더 많은 HTTP 메서드 추가 – POST, PUT, DELETE 등 (동일한 패턴).
- 구성 옵션 노출 – 타임아웃, 커스텀 헤더, 인증.
- Zig로 작성된 계산 집약적인 알고리즘 통합.
- 운영 체제와 상호 작용하는 다른 Zig 라이브러리 래핑.
핵심 요점
- 메모리 관리는 명시적이어야 합니다 – 누가 메모리를 할당하고 누가 해제하는지 알아야 합니다. Zig는 명시성을 강제하여 메모리 누수를 방지하지만, 그만큼 규율이 필요합니다.
- 타입은 일치해야 합니다 – 문자열과 배열은 각 언어마다 다른 표현 방식을 갖습니다; 변환은 정확히 이루어져야 합니다.
- 오류 처리는 번역이 필요합니다 – Zig의 오류 모델은 C와 동일하지 않습니다. 이 예제에서는
NULL을 사용해 실패를 알리지만, 다른 전략도 가능합니다. - 계층별 테스트 – 먼저 Zig 코드만 테스트하고, 그 다음 C에서, 마지막으로 Python에서 테스트합니다. 이렇게 하면 문제의 원인을 쉽게 파악할 수 있습니다.
같은 접근 방식을 HTTP와 같은 다른 기능에도 적용하여 Zig에서 Python으로 노출할 수 있습니다.
Note: This article was originally written in Spanish.