在 Python 中使用 Zig 函数

发布: (2026年2月17日 GMT+8 13:17)
10 分钟阅读
原文: Dev.to

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
  • 将 Python URL 编码为字节(C 字符串)。
  • 调用 Zig 函数并检查是否为 NULL
  • 手动定位终止的 \0 字节。
  • 切片取到该位置的字节并解码为 Python 字符串。
  • 调用 request_deallocate 释放 Zig 分配的内存。

使用包装器

import request

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

所有 FFI 细节都隐藏在 Request 中;使用者只需使用普通的 Python 对象即可。

Source:

扩展示例

您可以在此基础上进行多种扩展:

  • 添加更多 HTTP 方法 – POST、PUT、DELETE 等(相同模式)。
  • 暴露配置选项 – 超时、 自定义头部、身份验证。
  • 集成用 Zig 编写的计算密集型算法
  • 封装其他与操作系统交互的 Zig 库

关键要点

  1. 内存管理是显式的 – 你必须明确谁分配内存、谁释放内存。Zig 强制你显式处理,这有助于避免泄漏,但需要自律。
  2. 类型必须匹配 – 字符串和数组在每种语言中的表示不同;转换必须精确。
  3. 错误处理需要转换 – Zig 的错误模型与 C 不同。在本例中我们使用 NULL 来表示失败,但也可以采用其他策略。
  4. 分层测试 – 首先单独测试 Zig 代码,然后从 C 调用,最后从 Python 调用。这可以将问题来源隔离。

相同的方法可以应用于 HTTP 或任何其他你想要从 Zig 暴露给 Python 的功能。

注意: 本文最初用西班牙语撰写。

0 浏览
Back to Blog

相关文章

阅读更多 »

Python 中的实例变量和实例方法

Python 中的实例变量与实例方法 在面向对象编程(Object‑Oriented Programming,OOP)中,你必须掌握的两个概念是: - 实例变量 - 实例方法