ZigでCラッパーを作るシリーズ
| Part1 translate-c | Part2 手動ラップ | Part3 SQLite |
|---|---|---|
| ✅ Done | 👈 Now | - |
はじめに
前回は@cImportでCライブラリを使う方法を学んだ。
でも、Cの生のAPIって使いにくいことが多いよね。nullポインタだらけ、手動メモリ管理、エラーコード...。
今回は手動でラッピングして、Zigらしい綺麗なAPIを提供する方法を紹介するよ。
なぜ手動ラッピングが必要か
┌─────────────────────────────────────────────────────────────────┐
│ Raw C API vs Idiomatic Zig Wrapper │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Problems with Raw C: │
│ • Null pointers everywhere │
│ • Manual memory management │
│ • Error codes instead of errors │
│ • No RAII / defer support │
│ • Cryptic function names │
│ │
│ Benefits of Zig Wrapper: │
│ • Optional types (?*T) │
│ • Error unions (Error!T) │
│ • defer for cleanup │
│ • Slices instead of pointer+length │
│ • Method syntax (self.method()) │
│ • Compile-time safety │
│ │
└─────────────────────────────────────────────────────────────────┘
例:ファイルAPIのラッピング
CのFILEポインタをラップして、安全なAPIを提供する。
// file_wrapper.zig
const std = @import("std");
const c = @cImport({
@cInclude("stdio.h");
});
pub const FileError = error{
OpenFailed,
ReadFailed,
WriteFailed,
SeekFailed,
CloseFailed,
};
pub const File = struct {
handle: *c.FILE,
const Self = @This();
/// ファイルを開く
pub fn open(path: [:0]const u8, mode: [:0]const u8) FileError!Self {
const handle = c.fopen(path.ptr, mode.ptr);
if (handle == null) {
return FileError.OpenFailed;
}
return Self{ .handle = handle.? };
}
/// ファイルを閉じる
pub fn close(self: *Self) void {
_ = c.fclose(self.handle);
}
/// バッファに読み込む
pub fn read(self: *Self, buffer: []u8) FileError!usize {
const bytes_read = c.fread(
buffer.ptr,
1,
buffer.len,
self.handle,
);
if (bytes_read == 0 and c.ferror(self.handle) != 0) {
return FileError.ReadFailed;
}
return bytes_read;
}
/// 全て読み込む(allocator使用)
pub fn readAll(self: *Self, allocator: std.mem.Allocator) ![]u8 {
// ファイルサイズを取得
if (c.fseek(self.handle, 0, c.SEEK_END) != 0) {
return FileError.SeekFailed;
}
const size = c.ftell(self.handle);
if (size < 0) {
return FileError.SeekFailed;
}
if (c.fseek(self.handle, 0, c.SEEK_SET) != 0) {
return FileError.SeekFailed;
}
// バッファを確保して読み込む
const buffer = try allocator.alloc(u8, @intCast(size));
const bytes_read = try self.read(buffer);
return buffer[0..bytes_read];
}
/// データを書き込む
pub fn write(self: *Self, data: []const u8) FileError!usize {
const bytes_written = c.fwrite(
data.ptr,
1,
data.len,
self.handle,
);
if (bytes_written < data.len) {
return FileError.WriteFailed;
}
return bytes_written;
}
/// 行を読み込む
pub fn readLine(self: *Self, buffer: []u8) ?[]u8 {
const result = c.fgets(buffer.ptr, @intCast(buffer.len), self.handle);
if (result == null) {
return null;
}
// 改行を削除
const len = std.mem.indexOfScalar(u8, buffer, '\n') orelse c.strlen(buffer.ptr);
return buffer[0..len];
}
};
// 使用例
pub fn main() !void {
var file = try File.open("test.txt", "w");
defer file.close();
_ = try file.write("Hello, World!\n");
_ = try file.write("This is a test.\n");
std.debug.print("File written successfully!\n", .{});
}
エラーハンドリングの改善
Cのエラーコードを適切なZigエラーに変換。
// error_handling.zig
const std = @import("std");
const c = @cImport({
@cInclude("errno.h");
@cInclude("string.h");
});
pub const PosixError = error{
PermissionDenied, // EACCES
FileNotFound, // ENOENT
TooManyOpenFiles, // EMFILE
NoSpace, // ENOSPC
IoError, // EIO
InvalidArgument, // EINVAL
Unknown,
};
/// errnoをZigエラーに変換
pub fn translateErrno() PosixError {
const err = c.__errno_location().*;
return switch (err) {
c.EACCES => PosixError.PermissionDenied,
c.ENOENT => PosixError.FileNotFound,
c.EMFILE => PosixError.TooManyOpenFiles,
c.ENOSPC => PosixError.NoSpace,
c.EIO => PosixError.IoError,
c.EINVAL => PosixError.InvalidArgument,
else => PosixError.Unknown,
};
}
/// エラーメッセージを取得
pub fn getErrorMessage(err: PosixError) []const u8 {
return switch (err) {
.PermissionDenied => "Permission denied",
.FileNotFound => "File not found",
.TooManyOpenFiles => "Too many open files",
.NoSpace => "No space left on device",
.IoError => "I/O error",
.InvalidArgument => "Invalid argument",
.Unknown => "Unknown error",
};
}
リソース管理:RAII風パターン
// raii_wrapper.zig
const std = @import("std");
const c = @cImport({
@cInclude("stdlib.h");
});
/// CのmallocをラップしてZigのスライスとして使う
pub fn ManagedBuffer(comptime T: type) type {
return struct {
ptr: [*]T,
len: usize,
const Self = @This();
pub fn init(size: usize) !Self {
const byte_size = size * @sizeOf(T);
const raw_ptr = c.malloc(byte_size);
if (raw_ptr == null) {
return error.OutOfMemory;
}
return Self{
.ptr = @ptrCast(@alignCast(raw_ptr)),
.len = size,
};
}
pub fn deinit(self: *Self) void {
c.free(self.ptr);
self.* = undefined;
}
pub fn slice(self: Self) []T {
return self.ptr[0..self.len];
}
};
}
pub fn main() !void {
// RAII風にリソース管理
var buffer = try ManagedBuffer(u8).init(1024);
defer buffer.deinit();
// スライスとして使える
const data = buffer.slice();
@memset(data, 0);
data[0] = 'H';
data[1] = 'i';
std.debug.print("Data: {s}\n", .{data[0..2]});
}
コールバックの扱い
// callback_wrapper.zig
const std = @import("std");
const c = @cImport({
@cInclude("stdlib.h");
});
// Cのqsortをラップ
pub fn sort(comptime T: type, items: []T, compareFn: fn (*const T, *const T) std.math.Order) void {
const Context = struct {
compare: fn (*const T, *const T) std.math.Order,
};
const ctx = Context{ .compare = compareFn };
// Cのコールバック用ラッパー
const wrapper = struct {
fn cmp(a: ?*const anyopaque, b: ?*const anyopaque) callconv(.C) c_int {
const ptr_a: *const T = @ptrCast(@alignCast(a));
const ptr_b: *const T = @ptrCast(@alignCast(b));
// 注意:実際のコードではcontextを渡す方法が必要
return switch (std.math.order(ptr_a.*, ptr_b.*)) {
.lt => -1,
.eq => 0,
.gt => 1,
};
}
};
c.qsort(
items.ptr,
items.len,
@sizeOf(T),
wrapper.cmp,
);
}
// より良い方法:Zigのstd.sortを使う
pub fn betterSort(comptime T: type, items: []T) void {
std.mem.sort(T, items, {}, struct {
fn lessThan(_: void, a: T, b: T) bool {
return a < b;
}
}.lessThan);
}
pub fn main() void {
var numbers = [_]i32{ 5, 2, 8, 1, 9, 3 };
betterSort(i32, &numbers);
std.debug.print("Sorted: ", .{});
for (numbers) |n| {
std.debug.print("{} ", .{n});
}
std.debug.print("\n", .{});
}
構造体のラッピング
// struct_wrapper.zig
const std = @import("std");
// Cの構造体定義(extern struct)
const c_point = extern struct {
x: c_int,
y: c_int,
};
extern fn c_create_point(x: c_int, y: c_int) c_point;
extern fn c_distance(p1: *const c_point, p2: *const c_point) f64;
// Zigらしいラッパー
pub const Point = struct {
x: i32,
y: i32,
const Self = @This();
pub fn init(x: i32, y: i32) Self {
return Self{ .x = x, .y = y };
}
pub fn distance(self: Self, other: Self) f64 {
const dx = @as(f64, @floatFromInt(other.x - self.x));
const dy = @as(f64, @floatFromInt(other.y - self.y));
return @sqrt(dx * dx + dy * dy);
}
// C構造体との変換
pub fn toC(self: Self) c_point {
return c_point{
.x = @intCast(self.x),
.y = @intCast(self.y),
};
}
pub fn fromC(cp: c_point) Self {
return Self{
.x = @intCast(cp.x),
.y = @intCast(cp.y),
};
}
};
文字列の安全なラッピング
// string_wrapper.zig
const std = @import("std");
const c = @cImport({
@cInclude("string.h");
});
pub const CString = struct {
ptr: [*:0]const u8,
const Self = @This();
/// Zigスライスから作成(null終端が必要)
pub fn fromSlice(slice: [:0]const u8) Self {
return Self{ .ptr = slice.ptr };
}
/// 長さを取得
pub fn len(self: Self) usize {
return c.strlen(self.ptr);
}
/// Zigスライスに変換
pub fn toSlice(self: Self) [:0]const u8 {
const length = self.len();
return self.ptr[0..length :0];
}
/// 比較
pub fn eql(self: Self, other: Self) bool {
return c.strcmp(self.ptr, other.ptr) == 0;
}
/// 部分文字列を検索
pub fn contains(self: Self, needle: Self) bool {
return c.strstr(self.ptr, needle.ptr) != null;
}
};
/// 動的に確保されるC文字列
pub const OwnedCString = struct {
ptr: [*:0]u8,
allocator: std.mem.Allocator,
const Self = @This();
pub fn init(allocator: std.mem.Allocator, str: []const u8) !Self {
const ptr = try allocator.allocSentinel(u8, str.len, 0);
@memcpy(ptr, str);
return Self{
.ptr = ptr.ptr,
.allocator = allocator,
};
}
pub fn deinit(self: *Self) void {
const slice = self.toSlice();
self.allocator.free(slice);
self.* = undefined;
}
pub fn toSlice(self: Self) [:0]const u8 {
const length = c.strlen(self.ptr);
return self.ptr[0..length :0];
}
pub fn toCString(self: Self) CString {
return CString{ .ptr = self.ptr };
}
};
オプショナル型の活用
// optional_wrapper.zig
const std = @import("std");
const c = @cImport({
@cInclude("stdlib.h");
});
/// nullを返す可能性のあるC関数をラップ
pub fn getenv(name: [:0]const u8) ?[:0]const u8 {
const result = c.getenv(name.ptr);
if (result == null) {
return null;
}
// null終端文字列として返す
const len = c.strlen(result);
return @as([*:0]const u8, @ptrCast(result))[0..len :0];
}
pub fn main() void {
// 環境変数の取得(安全)
if (getenv("HOME")) |home| {
std.debug.print("HOME = {s}\n", .{home});
} else {
std.debug.print("HOME not set\n", .{});
}
if (getenv("PATH")) |path| {
std.debug.print("PATH = {s}\n", .{path});
}
// 存在しない環境変数
if (getenv("NONEXISTENT")) |_| {
std.debug.print("Found NONEXISTENT\n", .{});
} else {
std.debug.print("NONEXISTENT not set\n", .{});
}
}
ラッパー設計のベストプラクティス
┌─────────────────────────────────────────────────────────────────┐
│ Wrapper Design Best Practices │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Error Handling │
│ ✗ Return codes, null pointers │
│ ✓ Error unions, optional types │
│ │
│ 2. Memory Management │
│ ✗ Manual malloc/free │
│ ✓ Allocator interface, defer │
│ │
│ 3. String Handling │
│ ✗ char*, strlen │
│ ✓ [:0]const u8, slices │
│ │
│ 4. API Design │
│ ✗ Long function names with prefix │
│ ✓ Method syntax with struct │
│ │
│ 5. Safety │
│ ✗ Trust C pointers │
│ ✓ Validate, use optional/error │
│ │
└─────────────────────────────────────────────────────────────────┘
まとめ
手動ラッピングで実現したこと:
| 項目 | Cの生API | Zigラッパー |
|---|---|---|
| エラー | 戻り値、errno |
error型 |
| NULL | 生ポインタ |
?オプショナル |
| 文字列 | char* |
[:0]const u8 |
| メモリ | malloc/free | allocator + defer |
| API | 関数 | メソッド構文 |
次回は実践として、SQLiteのラッパーを作るよ。
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!