⚙️

Zigでlibdispatchを使う

に公開

Zigでlibdispatch(GCD)を使う

こんにちは、あらさんです。今回はAppleプラットフォームでおなじみのlibdispatch(Grand Central Dispatch, GCD)をZigから使ってみた話を書いていきます。

普段はSwiftやObjective-Cから使うことが多いlibdispatchですが、ZigでもCインターフェースを活かして簡単に利用できます。ちょっとした非同期処理や並列タスクをZigで書いてみたいときに便利なので、備忘録も兼ねてまとめました。

libdispatchとは?

libdispatchは、Appleが提供している並列処理ライブラリです。iOSやmacOSのアプリ開発では「GCD」としてObjective-CやSwiftからよく使われていますが、これはApple Platform以外のLinuxやWindowsでも動作します。ZigのようなCインターフェースを持つ言語からも利用できます。

Appleが公開しているlibdispatchのレポジトリはこれのはず...

https://github.com/apple-oss-distributions/libdispatch/

private.hを見るとOS分岐が存在しているためDarwin以外のプラットフォームも考慮していることがわかります。

ref: https://github.com/apple-oss-distributions/libdispatch/blob/main/private/private.h

#ifdef __APPLE__
#include <Availability.h>
#include <os/availability.h>
#include <TargetConditionals.h>
#include <os/base.h>
#elif defined(_WIN32)
#include <os/generic_win_base.h>
#elif defined(__unix__)
#include <os/generic_unix_base.h>
#endif

#if TARGET_OS_MAC
#include <mach/boolean.h>
#include <mach/mach.h>
#include <mach/message.h>
#endif
#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__))
#include <unistd.h>
#endif
#if !defined(_WIN32)
#include <pthread.h>
#endif
#if TARGET_OS_MAC
#include <pthread/qos.h>
#endif

Zigからlibdispatchを使う方法

Zigでは、@cImportを使ってCヘッダをインクルードするだけで、Cの関数や型をそのまま呼び出すことができます。
例えば、以下のように書きます。

const dispatch = @cImport({
    @cInclude("dispatch/dispatch.h");
});

これで、dispatch_asyncやdispatch_groupなど、libdispatchのAPIをZigから呼び出せるようになります。

サンプルコード全文

とりあえず実装例を載せておきます。正しい実装方法なのか、正しいメモリ管理になっているのかはZig力足りなくてわかりません、教えて下さい。

const std = @import("std");
const dispatch = @cImport({
    @cInclude("dispatch/dispatch.h");
});

// コールバック関数1: 単純なメッセージ表示
fn printMessage(context: ?*anyopaque) callconv(.C) void {
    _ = context;
    std.debug.print("キュー1からのメッセージ: 非同期処理実行中\n", .{});
}

pub const TypeIdentifier = enum {
    TaskContext,
    NotTaskContext,
};

// 新しい構造体定義
pub const TaskContext = struct {
    tag: TypeIdentifier = TypeIdentifier.TaskContext,
    value: usize,
    allocator: std.mem.Allocator,
};

pub const NotTaskContext = struct {
    tag: TypeIdentifier = TypeIdentifier.NotTaskContext,
    value: u8,
    allocator: std.mem.Allocator,
};

// 任意の型・tagで安全にキャストするcomptime関数
fn tryCastWithTag(comptime T: type, comptime expected_tag: TypeIdentifier, ptr: ?*anyopaque) ?*T {
    if (ptr) |raw| {
        const t_ptr: *T = @ptrCast(@alignCast(raw));
        if (t_ptr.tag == expected_tag) return t_ptr;
    }
    return null;
}

// コールバック関数2: 引数付きのメッセージ表示
fn printWithContext(context: ?*anyopaque) callconv(.C) void {
    const typed_ctx = tryCastWithTag(
        TaskContext,
        TypeIdentifier.TaskContext,
        context,
    ) orelse return;
    defer {
        std.debug.print("destroying context\n", .{});
        typed_ctx.allocator.destroy(typed_ctx);
    }
    std.debug.print("キュー2からのメッセージ: 値 = {}\n", .{typed_ctx.value});
}

// コールバック関数3: 時間のかかる処理をシミュレート
fn longRunningTask(context: ?*anyopaque) callconv(.C) void {
    _ = context;
    std.debug.print("バックグラウンドキュー: 長時間処理開始...\n", .{});
    std.time.sleep(2 * std.time.ns_per_s); // 2秒間スリープ
    std.debug.print("バックグラウンドキュー: 長時間処理完了!\n", .{});
}

pub fn main() !void {

    // 1. dispatch_groupの作成
    const group = dispatch.dispatch_group_create();

    // 2. シリアルキューの作成 - メインスレッドで解放
    const serial_queue = dispatch.dispatch_queue_create("com.example.serialqueue", null);

    // 3. 並列キューの取得
    const concurrent_queue = dispatch.dispatch_get_global_queue(dispatch.DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    // 4. シリアルキューでタスクをグループに追加
    dispatch.dispatch_group_async_f(group, serial_queue, null, &printMessage);

    // 5. コンテキスト付きでタスクをグループに追加
    var general_purpose_allocator: std.heap.GeneralPurposeAllocator(.{}) = .init;
    const gpa = general_purpose_allocator.allocator();
    const context_struct = try gpa.create(TaskContext);
    context_struct.* = TaskContext{ .value = 42, .allocator = gpa };
    dispatch.dispatch_group_async_f(group, serial_queue, @as(*anyopaque, context_struct), &printWithContext);

    // 6. 並列キューで長時間タスクをグループに追加
    dispatch.dispatch_group_async_f(group, concurrent_queue, null, &longRunningTask);
    dispatch.dispatch_group_async_f(group, concurrent_queue, null, &longRunningTask);
    dispatch.dispatch_group_async_f(group, concurrent_queue, null, &longRunningTask);

    // メインスレッドのメッセージを表示
    std.debug.print("メインスレッド: すべてのタスクをディスパッチしました\n", .{});

    // 7. 全タスクの完了を待つ
    _ = dispatch.dispatch_group_wait(group, dispatch.DISPATCH_TIME_FOREVER);

    // 終了メッセージ
    std.debug.print("メインスレッド: 終了します\n", .{});

    // グループの解放(必要に応じて)
    // dispatch.dispatch_release(group); // Zigのlibdispatchバインディングによる
}

考察・工夫したポイント

  • C言語のAPIはZigから直接呼び出せるが、ポインタの型変換やアラインメントには注意が必要
    • ?*anyopaque からZigの構造体への安全なキャストには @ptrCast や @alignCast を適切に使う必要がある
    • Zigにはランタイムが無いためanyopaqueから構造体を安全にキャストするには、tag付きstructを使う必要がある
  • コールバック関数の呼び出し規約(callconv(.C))を明示することで、C側からの呼び出しも安全にできる
  • Objective-CやSwiftでは「クロージャ」を渡せるが、Zigでは関数ポインタ+context(void*)で渡すしかなさそう?Zig力が足りなくてちょっと分からん、libdispatchのテストコードちゃんと見ないと不正確なこと書きそう。
  • libdispatchは使い慣れている非同期を管理できるライブラリなので、Zigでも使えるとちょっとしたことには使いやすいかも、libxev使う方が綺麗にZigの世界で閉じるので使い分けは必要そう。

どうしてZigでlibdispatchを使うの??

メリット

Zigの非同期に関する取り組みは今のところ注力されているところでは無いので、良いライブラリを組み合わせることがよさそう。libdispatchは実績のある、使い慣れたものなのでiOSアプリ、macOSアプリを作ってる人からすると動きがわかりやすい。ZigのThreadライブラリを直接使うより下手なコードだとパフォーマンスは高くできる気がする。

デメリット・注意点

Cを経由するのでインターフェイスが一部利用できないものがあることに注意が必要。Objective-CやSwiftのようなリッチなランタイムはZigには無いため、適切にメモリを扱って型変換でZigの世界に戻さないとダメそう?

Discussion