Skip to content

Instantly share code, notes, and snippets.

@ritalin
Last active November 30, 2023 17:54
Show Gist options
  • Save ritalin/406e1ebf9dbe96f90e8f847c44c7547b to your computer and use it in GitHub Desktop.
Save ritalin/406e1ebf9dbe96f90e8f847c44c7547b to your computer and use it in GitHub Desktop.
zig言語でObjective-Cのブロック型を扱うための設計方針

概要

例えば、AppKitNSAlertのメソッド、beginSheetModalForWindowの第二引数はブロック型を要求する。 zig言語によるAppKitのバインディングで、ブロック型を要求するAPIを扱えるようにしたい。

ブロック型の実行

mitchellh/zig-objc (https://github.com/mitchellh/zig-objc) にて、2023-10-21のコミットでブロック型のサポートが追加された。

以下のようなコードを記述することで、実行できることは確認できた。

// 呼び出し側
fn test() !void {
    var user_context: *MyContext = ...; // アプリケーションコンテキスト
    var window: objc.Object = ...; // 親ウインドウ
    var alert: objc.Object = ...; // アラートウインドウ

    var block = try objc.Block(struct{context: *MyContext}, .{NSModalResponse}, void).init(user_context, &dispatch);

    var sel = objc.Sel.registerName("beginSheetModalForWindow:completionHandler:");
    alert.msgSend(void, sel, .{window, block.context});
}

// ハンドラ
fn dispatch(block: objc.Block(struct{context: *MyContext}, .{NSModalResponse}, void).Context, response: NSModalResponse) void {
    // ....
}

API設計で満たすべき条件

  • 実際に実行するコールバックハンドラには、利用者の値を共有するためのアプリケーションコンテキストを渡したい。
  • フロントエンドAPIには、アプリケーションコンテキストの型を不可視にしたい。

設計

アプリケーションコンテキストを不可視にするために幽霊型を導入する。 幽霊型のcontextフィールドにはBlock(...).Contextのポインタが入る。

fn ApiBlock(comptime Args: type) type {
    _ = Args;

    return struct {
        context: *anyopaque,
    };
}

NSAlertbeginSheetModalForWindowを以下のように定義する。 幽霊型の型引数には、元のブロックの関数宣言を記述した(おそらく型を捨てているため、ビルドできたと思われる)。

pub fn beginSheetModalForWindow(self: NSAlert, _window: NSWindow, _block: ApiBlock(fn (NSModalResponse) void) void {
    var sel = objc.Sel.registerName("beginSheetModalForWindow:completionHandler:");
    alert.msgSend(void, sel, .{_window._id, _block.context});
}

ここで、NSAlertNSWindowobjc.Objectをラップした型。 一例として、NSWindow

pub const NSWindow = struct {
    _id: objc.Object,
};

なんやかんや型パズルを解き、以下のサポート型を用意した上で、

fn BlockSupport(comptime UserContextType: type) type {
    return struct {
        pub const Handlers = struct {
            const BeginSheetModalForWindowHandler = *const fn (*UserContextType, appKit.NSModalResponse) anyerror!void;
        };

        fn BeginSheetModalForWindowBlock(comptime _handler: handlers.BeginSheetModalForWindowHandler) type {
            return struct {
                const Captures = struct{context: *UserContextType};
                const Block = objc.Block(Captures, .{appKit.NSModalResponse}, void);

                pub fn init(user_context: *UserContextType) !ApiBlock(fn (appKit.NSModalResponse) void) {
                    var block = try Block.init(
                        .{.context = user_context}, 
                        &dispatchRecordBookFinished
                    );

                    return .{
                        .context = block.context,
                    };
                }
                
                fn dispatchRecordBookFinished(x: *const Block.Context, r: appKit.NSModalResponse) callconv(.C) void {
                    defer std.heap.c_allocator.destroy(x);
                    return _handler(x.context, r) catch unreachable;
                }   
            };
        }
    };
}

以下の呼び出しを行うことで、利用者のアプリケーションハンドラを不可視にしつつハンドリングすることができた。

var user_context: *MyContext = ...;
var window: NSWindow = ...;
var alert: NSAlert = ...;

var block: ApiBlock(fn (appKit.NSModalResponse) void) = 
    try BlockSupport(FlightBookContext).BeginSheetModalForWindowBlock(&handleRecordBookFinished).init(user_context);

// alertとwindow引数は、上述したobjc.Objectをラップしたもの
beginSheetModalForWindow(alert, window, block);

ここで、BeginSheetModalForWindowBlock型の型引数には、実際に利用者が公開す以下のような関数のポインタを渡す。

pub fn handleRecordBookFinished(context: *MyContext, r: NSModalResponse) !void {
    _ = context;
    _ = r;
    std.debug.print("Debug: handler invoked !!\n", .{});
}

未対応

  • 幽霊型のフィールドには任意のポインタを渡せるため、いくらでも偽装できる(おそらく訳のわからないエラーで落ちる)
  • コンパイル時に型チェックするコード追加すれば、なんとかなったりするかな?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment