Scarlet Industries

BunKit bridge

Send any message to any Objective-C object from TypeScript.

Use the bridge when BunKit's own classes do not wrap the part of AppKit you need. It needs no wrapper per method, because it reads each method's type encoding from the Objective-C runtime and packs the arguments to match.

Calling any class

Every property of objc is a class, and every property of an object is a method. Each colon in a selector becomes an underscore, so setTitle: is setTitle_.

import { objc } from "bunkit/objc";
import {
  NSBackingStoreBuffered,
  NSWindowStyleMaskClosable,
  NSWindowStyleMaskTitled,
} from "bunkit/constants";

const win = objc.NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(
  { x: 0, y: 0, width: 420, height: 180 },
  NSWindowStyleMaskTitled | NSWindowStyleMaskClosable,
  NSBackingStoreBuffered,
  false,
);
win.setTitle_("Native AppKit, from TypeScript.");
win.center();

Strings, arrays, plain objects and numbers become NSString, NSMutableArray, NSMutableDictionary and NSNumber on the way in, and a CGRect is { x, y, width, height }. Constants such as NSWindowStyleMaskTitled are generated from the installed SDK by bun run gen. A misspelt method or an Objective-C exception comes back as an Error that names the selector:

win.setTitel_("x");
// Error: -[NSWindow setTitel:] is not implemented (unrecognized selector)
//   did you mean: setTimeMachineDelegate:, setTitle:, setTitleHidden:,
//                 setTitleMode:, setTitlePosition:, setTitleVisibility:

Delegates and blocks

AppKit reports events by calling methods on a delegate, and createDelegate builds one from plain functions:

import { createDelegate } from "bunkit/objc";

const delegate = createDelegate(
  {
    windowDidResize_: (notification) => { resized(notification); },
    windowShouldClose_: () => true,
  },
  { protocols: ["NSWindowDelegate"], name: "MainWindowDelegate" },
);

win.setDelegate_(delegate);

A block is Objective-C's closure, and the runtime cannot say what one will be called with, so createBlock takes the signature from you: the return type, @? for the block itself, then the arguments.

import { createBlock } from "bunkit/objc";

// void (^)(id obj, NSUInteger idx, BOOL *stop)
array.enumerateObjectsUsingBlock_(
  createBlock("v@?@Q^B", (obj, idx, stop) => {
    console.log(idx, String(obj));
  }),
);

BunKit never frees a block on its own, so call dispose() on one when you are done with it.

Memory

Each JavaScript wrapper holds one retain on its object and gives it back when the wrapper is garbage collected. Object pointers are BigInts, because macOS keeps some small objects inside the pointer itself, so compare them against 0n.

The run loop

JavaScript owns the main loop and lends the thread to AppKit a few milliseconds at a time. Anything in AppKit that runs a loop of its own, such as a modal dialog or a live window resize, freezes JavaScript until it ends.