在 Python 中使用 Zig 函数
I’m happy to translate the article for you, but I need the full text of the article itself. Could you please paste the content you’d like translated (excluding the source link you’ve already provided)? Once I have the text, I’ll translate it into Simplified Chinese while preserving the formatting, markdown, and any code blocks.
概述
本文档展示了如何在 Zig 中编写一个小型 HTTP 客户端,将其导出为 C 接口,并在 Python 中调用它。
重点包括:
- 直接使用 Zig 的
[:0]const u8(以空字符结尾的字符串)。 - 避免不必要的拷贝。
- 提供一个简易的 C 包装层,可从 Python(或任何其他语言)链接使用。
Source:
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。 |
| 请求 | 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.
注意: 必须在
localhost上有一个 HTTP 服务器在监听,测试才能成功。
C 包装器
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]);
}
包装器测试
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.so在 Linux 上librequest.dylib在 macOS 上request.dll在 Windows 上
使用 Python 包装器
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"))
- Python 代码加载为 C 构建的相同共享库。
request_wrapper返回一个char *,需要使用request_deallocate进行释放。- 错误通过返回
NULL值来报告。
摘要
| 组件 | 用途 |
|---|---|
request (Zig) | 核心 HTTP GET 实现,返回一个兼容 C 的以 NUL 结尾的字符串。 |
request_wrapper / request_deallocate (Zig) | 导出的 C API —— 简单、零拷贝、零额外分配的包装器。 |
request.h | 暴露上述两个函数的 C 头文件。 |
| 示例 C 程序 | 演示如何在本地使用该共享库。 |
| Python 代码片段 | 展示同一共享库如何从 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)
所有 FFI 细节都隐藏在 Request 中;使用者只需使用普通的 Python 对象即可。
Source: …
扩展示例
您可以在此基础上进行多种扩展:
- 添加更多 HTTP 方法 – POST、PUT、DELETE 等(相同模式)。
- 暴露配置选项 – 超时、 自定义头部、身份验证。
- 集成用 Zig 编写的计算密集型算法。
- 封装其他与操作系统交互的 Zig 库。
关键要点
- 内存管理是显式的 – 你必须明确谁分配内存、谁释放内存。Zig 强制你显式处理,这有助于避免泄漏,但需要自律。
- 类型必须匹配 – 字符串和数组在每种语言中的表示不同;转换必须精确。
- 错误处理需要转换 – Zig 的错误模型与 C 不同。在本例中我们使用
NULL来表示失败,但也可以采用其他策略。 - 分层测试 – 首先单独测试 Zig 代码,然后从 C 调用,最后从 Python 调用。这可以将问题来源隔离。
相同的方法可以应用于 HTTP 或任何其他你想要从 Zig 暴露给 Python 的功能。
注意: 本文最初用西班牙语撰写。