5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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のラッパーを作るよ。

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?