例えば、AppKit
のNSAlert
のメソッド、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には、アプリケーションコンテキストの型を不可視にしたい。
アプリケーションコンテキストを不可視にするために幽霊型を導入する。
幽霊型のcontext
フィールドにはBlock(...).Context
のポインタが入る。
fn ApiBlock(comptime Args: type) type {
_ = Args;
return struct {
context: *anyopaque,
};
}
NSAlert
のbeginSheetModalForWindow
を以下のように定義する。
幽霊型の型引数には、元のブロックの関数宣言を記述した(おそらく型を捨てているため、ビルドできたと思われる)。
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});
}
ここで、NSAlert
とNSWindow
はobjc.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", .{});
}
- 幽霊型のフィールドには任意のポインタを渡せるため、いくらでも偽装できる(おそらく訳のわからないエラーで落ちる)
- コンパイル時に型チェックするコード追加すれば、なんとかなったりするかな?