Skip to content

Objective-C interop

Ordinary Objective-C starts with an ordinary header import:

import AppKit from "AppKit/AppKit.h";
const app = AppKit.NSApplication.sharedApplication;
const frame = AppKit.NSMakeRect(0.0, 0.0, 800.0, 600.0);
const window = AppKit.NSWindow.alloc().initWithContentRect(
frame,
styleMask: AppKit.NSWindowStyleMaskTitled,
backing: AppKit.NSBackingStoreBuffered,
defer: false
);
window.title = "Native Z";
window.center();
window.makeKeyAndOrderFront(null);
app.activate();
app.run();

Classes, methods, properties, constants, inheritance, nullability, and ARC identity come from Clang. std/objc is not required for ordinary message sends.

A concrete imported class also exposes required methods and properties from its adopted protocols, including inherited protocols. For example, NSMenuItem’s identifier property is available without repeating its protocol declaration. Optional protocol members are not assumed implemented unless the class declares them.

Objective-C identity follows the Clang runtime name rather than the header or Z namespace that surfaced it. If Foundation and a vendor header both mention NSString, they describe the same nominal native class—no wrapper or cast is introduced. A non-null Z String can satisfy either a non-null or nullable NSString argument while remaining non-null itself.

This also works when the same call includes an inline native block: the compiler stages the Z String and bridges it once at the Objective-C call. Notification observer tokens still need explicit removal at the end of their owner’s lifetime; owning a token does not itself unregister the observer. Native types nested in a block signature also belong to the imported namespace: an inferred timer parameter can call timer.invalidate(), and an explicit Foundation.NSTimer annotation names that same type.

Native enum and option-set values retain their nominal types. When an integer is actually needed, use an explicit numeric conversion such as usize(window.styleMask). The conversion uses the imported ABI representation, including when a property exposes the type through a qualified module name. Equality with a constant of the same native enum also works across Z import aliases, without an annotation or numeric cast. For example, inside main-thread code both locals below are inferred as boolean:

const titleVisible = window.titleVisibility == AppKit.NSWindowTitleVisible;
const fullSize = (window.styleMask & AppKit.NSWindowStyleMaskFullSizeContentView)
== AppKit.NSWindowStyleMaskFullSizeContentView;

Different nominal enum types still cannot be compared as if they were the same type; use explicit numeric conversion when comparing their representations is intentional.

An inferred object returned through a forward declaration gains its checked members when you import the complete class header. For example, AppKit returns CADisplayLink, while QuartzCore/CADisplayLink.h defines its run-loop methods. You do not need a cast or a redundant local type annotation. Selector callback signatures also recognize the same native class across these imports, without weakening nullability or capability checks.

A Z callable alias can likewise use a different import namespace from the closure assigned to it. Explicit direct Objective-C parameter and return annotations name the same native class across modules, including nullable returns. The compiler normalizes that identity without a wrapper or cast; parameter modes, nullability, error effects, and executor contracts still have to match. This does not make unrelated Z classes or native subclasses interchangeable.

Imported Objective-C objects behave as shared reference identities. Assigning or storing one applies the imported ownership convention and generated ARC retains/releases. Nullable native references remain T | null and must be narrowed before use.

Header availability annotations do not erase nullability. For example, NSStatusItem.button remains NSStatusBarButton | null, including in hover, even when Clang spells its type through an availability macro. Check it before setting the image or tooltip. Implicit property accessors preserve the same contract.

Arrays of Objective-C references and objc.Connection guards own their elements. Growing their storage preserves ARC ownership; clearing or destroying the array releases those elements and their callback environments.

Short-circuit guards preserve that proof for the expression to their right:

const title: Foundation.NSString | null = window.title;
if (title != null && title.isEqualToString("Ready")) {
const owned: String = title;
publish(move owned);
}

Dynamic id does not become a Z any. Narrow with a native class test:

const body = message.body;
if (body instanceof Foundation.NSString) {
const text: String = body;
route(move text);
}

instanceof lowers to the Objective-C runtime class check. Converting the narrowed NSString to an explicit Z String makes an owned UTF-8 copy with its exact byte length.

Early-return guards work too, in both compiler paths:

if (!(body instanceof Foundation.NSString)) return;
const text: String = body;
route(move text);

The class proof must hold on every continuing path. Merely testing the class without exiting the unmatched path does not make later conversion safe.

Use objc.Object when a signature needs Objective-C’s erased id reference:

import objc from "std/objc";
import Foundation from "Foundation/Foundation.h";
function textLength(in value: objc.Object | null): usize {
if (value instanceof Foundation.NSString) {
const text: String = value;
return text.byteLength;
}
return 0;
}

objc.Object is non-null; objc.Object | null also admits nil. A null guard narrows the latter to the former, but does not prove a particular class. instanceof supplies that class proof before member access or NSString-to-Z String copying. Module aliases and exported type aliases preserve the same native identity.

This is native id, not NSObject *: alternate-root objects such as NSProxy are included. It is not a Z any, a raw pointer, or a constructible class. Ordinary Z values cannot be assigned to it; convert a Z string to a concrete NSString first when that bridge is intended. Erasing the class adds no wrapper allocation or deep copy. Owned bindings still have normal native ARC costs; in borrows a reference. It grants no cross-thread transfer or executor-access permission.

Inherited native overrides can now name a nullable sender accurately:

override function zoom(inout this, in sender: objc.Object | null): void as "zoom:" {
super.zoom(sender);
}

For this checked synchronous full-expression delegation, the compiler keeps a strong ARC snapshot of sender, then releases only the current receiver entry’s access. It restores access before releasing the snapshot. Native reentry can therefore reach a guarded override without turning the sender into an escaping storage loan. Synchronous native subclasses can also live in supported async task frames; their methods do not become async and native-object thread transfer stays restricted.

String bridging also composes with conditional values:

const reply: Foundation.NSString = accepted ? "accepted" : "rejected";
const snapshot: String = reply;

The selected String result crosses the native boundary; the boolean condition does not. Converting back to String creates an independent owned UTF-8 copy.

Passing a named Z String to an NSString property or parameter borrows its bytes for the native copy; you may keep using the source. move text still consumes text: its Z buffer is released after that copy, as are fresh call results, templates, and explicit copy temporaries. A String-valued conditional transfers its selected result under ordinary Z ownership rules. The bridge does not share backing storage or require you to write copy merely to preserve a named source.

Main-thread requirements enter the type system

Section titled “Main-thread requirements enter the type system”

Imported Apple metadata and reviewed API notes can mark declarations on thread.main. A call from an unproven executor is rejected before native compilation.

import { thread } from "std/thread";
async function publish(in window: AppKit.NSWindow): void {
await on thread.main window.makeKeyAndOrderFront(null);
}

The placement is useful even when the function may be called from different executors: the call site proves where the native operation runs rather than depending on a convention in documentation.

An inherited optional protocol callback can use the same checked override syntax as a method declared on the superclass. For example, a WebView subclass can implement draggingEntered: with an in sender: AppKit.NSDraggingInfo parameter and an AppKit.NSDragOperation result. This retains the native protocol-qualified parameter and does not introduce a wrapper.

The protocol’s identity does not depend on the local header-import name. For example, a borrowed AppKit.NSDraggingInfo can be passed to a Z function whose parameter names that same native protocol through a WebKit import. An unrelated protocol is still rejected; matching an import spelling does not replace checking the native declaration.

Optional does not mean safe to call without an implementation: unimplemented optional members remain unavailable to ordinary calls and super delegation. If no superclass implementation is promised, callbacks during construction or teardown fail closed under the existing native-entry lifecycle guard.

Use std/objc when forwarding to an optional superclass implementation:

import objc from "std/objc";
// Inside the corresponding native override:
const inherited = objc.optionalCall(super.draggingEntered(sender));
return match (in inherited) {
some(operation) => operation;
none => AppKit.NSDragOperationNone;
};

optionalCall is compiler-recognized: it checks the lexical superclass before evaluating arguments, then invokes the method only if implemented. Results are Option<T>; void callbacks return void. some(false) and some(null) remain distinct from none. Native ARC, thread affinity, availability, and receiver guards still apply. A direct synchronous super call is required, and native initialization cannot be conditional. It does not allocate a closure or treat custom message forwarding as a concrete implementation.

Choose the fallback for the particular callback. For example, an absent draggingUpdated: uses the previously accepted drag operation rather than an unconditional rejection. This API does not automatically implement a file-drop policy or grant access to dropped paths.

A protocol adapter does not become an NSWindow. When native virtual dispatch is required, the first subclass tier creates a real native ARC object:

class SmoothWindow extends AppKit.NSWindow on thread.main {
constructor(frame: AppKit.NSRect) {
super.initWithContentRect(
frame,
styleMask: AppKit.NSWindowStyleMaskTitled | AppKit.NSWindowStyleMaskResizable,
backing: AppKit.NSBackingStoreBuffered,
defer: false
);
this.releasedWhenClosed = false;
}
override function animationResizeTime(frame: AppKit.NSRect): f64
as "animationResizeTime:" {
return super.animationResizeTime(frame);
}
}

Import AppKit and thread as above, then construct with new SmoothWindow(frame). The override shown preserves the superclass behavior; it does not implement a resize algorithm. The compiler checks the inherited selector and ABI. super invokes the superclass implementation on the same object. There is no separate Z ARC allocation or protocol-adapter wrapper.

The superclass designated initializer must be first and non-nullable. Generated code checks the returned identity and the main thread, without scheduling a hop. Its generated constructor helper explicitly transfers an owned native reference; new does not defer that ownership to an enclosing autorelease pool. Constructor parameters can include concrete imported Objective-C references, such as a WKWebViewConfiguration, alongside scalar and native record values. Passing a reference does not consume the caller’s alias; storing it retains it. Virtual calls during construction and destruction invoke the superclass, not the Z override. Z overrides become active only after the constructor body and its local cleanup finish, including cleanup of native ARC temporaries. This also applies to this.method() calls from the constructor after super.init. It is Z’s deliberate lifecycle rule, not ordinary Objective-C dispatch; libraries requiring subclass callbacks during initialization need a later explicit tier. The gate adds one byte-sized native field (plus possible layout padding) and an entry branch, not another heap object.

Ready entries also enforce receiver exclusivity across native callbacks. Nested in this reads are allowed; reentry involving an active or incoming inout this traps before the conflicting Z body runs. This is a nonblocking contract check, not a lock or executor hop. Virtual callbacks made by a superclass check the same access rule.

Access remains held during local cleanup, including ARC destructors and early returns. A scoped strong native reference keeps the receiver alive through the check’s cleanup. The current cost is one additional pointer-sized native field, a stack guard, entry/exit checks, and a native retain/release—not another heap allocation. This conservative safety tier is not zero overhead.

There is one checked delegation exception: a synchronous super call used as a whole statement, local initializer, or return can temporarily release the current entry’s access. It permits scalar, enum, and plain native record values, plus native object arguments, including borrowed nullable senders and field references. Arguments are evaluated first while guarded; object references get precise-lifetime strong snapshots; the receiver stays alive; access is restored before argument destruction, Z continuation, or local cleanup. Other active readers remain protected. This lets AppKit’s super.zoom(sender) reenter a mutable setFrame override without disabling its guard. Nested expressions, blocks, and writable arguments remain outside the tier, and live receiver-field storage loans are rejected.

Statement bodies inside match arms follow the same rule, including direct expression arms such as none => super.sendEvent(event);. The match subject and superclass calls nested inside a larger expression remain guarded.

Reentry may change fields. Retained or copied locals remain snapshots, not proof that the receiver is unchanged. This boundary adds stack state and access checks plus native ARC retain/release for object arguments, not another heap allocation.

Synchronous native subclasses can coexist with async Z programs. Their identity remains a native Objective-C reference in task storage, including owned parameters and references inside value structs. Supported main-isolated timer and continuation frames retain that identity across suspension and release it on completion or cancellation. No extra Z object wrapper is allocated. Subclass methods themselves remain synchronous and main-isolated; this does not enable native-object transfer through thread.spawn or remove the native compiler’s broader async composition limits.

Private stored scalars, imported enums/plain native records, owned String, and strong imported Objective-C references, and Weak<T> / Option<Weak<T>> for ordinary Z classes are supported. They live inline in the native object, not a second wrapper. Native reference fields may be nullable: private link: QuartzCore.CADisplayLink | null;. Initialize every field directly after super.init, before other receiver use or control flow. Access is direct this.field inside a guarded entry; writes require inout this. Copy snapshots when a value needs to outlive the entry. Native reference snapshots retain identity; clearing the original nullable field with null does not destroy another retained snapshot. Native field replacement remains protected against reentrant receiver access.

Weak Z state can break a window/observer cycle: the observer owns the native window, while the window stores Option<Weak<Observer>>. Inspect it with match (in this.observer) and upgrade the borrowed handle normally. A direct weak snapshot retains only the weak control block; field replacement and teardown release it. This does not allow Weak<NativeSubclass>, arbitrary managed aggregate fields, or escaping field loans.

Synchronous instance methods without as use ordinary Z types and do not become Objective-C selectors. Normal public, internal, and private visibility applies:

// An external Z owner can install its observer without a native Weak<T> ABI.
function observe(inout this, observer: Weak<Observer>): void {
this.observer = Option.some(observer);
}
private function currentObserver(): Option<Observer> {
return match (in this.observer) {
some(handle) => match (attempt handle.upgrade()) {
success(value) => Option.some(value);
failure(_) => Option.none;
}
none => Option.none;
};
}

Direct this.currentObserver() calls reuse the enclosing receiver loan; calls through another binding acquire their own access guard. Writes still require inout this, and native reentry remains checked through local cleanup. objc.selector only accepts explicit native entries, never these helpers. Public/package visibility does not make a Z method selector-addressable. Keep constructor/deinit field work inline and super calls in native entries. Async, throwing, consuming, static, and generic helpers are not supported yet.

Existing deinit { ... } runs after the readiness gate closes. Its local cleanup finishes before automatic reverse-order field cleanup. You may inspect/copy fields during teardown, but cannot publish or call through dying this, and must never call super.dealloc. Automatic cleanup also works without deinit. Strong fields are cleared before superclass teardown. For registrations such as display links, explicitly invalidate during close/shutdown: ARC cannot break a cycle that prevents deinit from running, and Z does not infer native shutdown methods from an object’s type. See the stateful compiler fixture for construction, replacement, and cleanup examples.

For an application-shaped example, the display-driven window probe implements an AppKit zoom/restore controller in Z, with private geometry state, checked display-link registration, retargeting, and explicit close-time invalidation. It uses AppKit’s full application event loop and removes its close observer after the loop returns. Bounded tests in both compilers cover native geometry/cleanup and real WebView readiness, intermediate viewport layout, and shutdown. Visual presentation quality and framework integration remain separate from those tests. The correctness follow-up covers intended zoom state, saved user restore frames, and Reduce Motion changes during a transition. The link != active guard distinguishes a stale display-link callback from the current source, even during a newer animation.

Native behavior remains part of the contract: if windowShouldZoom:toFrame: vetoes a request, an unchanged target must stay unchanged—even when the Z controller has a saved restore frame. The probe tests that case before both zoom and restore. Compiler ownership checks do not replace these platform semantics tests.

== and != on compatible native class references compare object identity, without consuming either reference or calling isEqual:. Same-class, base/derived, erased objc.Object, and nullable reference comparisons are supported; unrelated concrete classes are rejected. A native NSString uses identity here, whereas Z String uses UTF-8 content equality. Comparing two named references needs only a pointer comparison; nontrivial operands keep temporary references alive through the comparison and clean them up afterward.

Nullable instancetype factories preserve both the receiver’s class and the possibility of null, including inherited factories. Check the result before accessing members; for example, NSURL.URLWithString(...) may return null.

A native subclass can also introduce a method that the superclass does not have:

function onDisplay(
inout this,
in link: QuartzCore.CADisplayLink
): void as "display:" {
const timestamp = link.timestamp;
}

Use override only for an inherited selector; omitting it for an inherited selector is an error. New methods use a checked source-declared ABI. This tier supports scalar/enum values and in concrete imported object parameters, with scalar/enum or void returns. Parameter labels must match the selector.

New entries enforce the same main-thread and receiver-exclusivity checks as overrides. Before Ready or during teardown they trap—there is no superclass method to forward to. No adapter or closure allocation is introduced.

This declaration does not register a display link. With an adjacent selector callback contract, registration uses an explicit declaration reference:

import objc from "std/objc";
const link = window.displayLinkWithTarget(
window,
selector: objc.selector(SmoothWindow.onDisplay)
);

The first tier requires the reference inline at the contracted call. Z checks the method signature, visibility, target type, and main context; it does not call the method, capture a receiver, or allocate a closure. Known constructing/dying target aliases are rejected, and a direct Ready check runs before registration even through a helper. A bare SEL cannot bypass this. Native ARC retention remains native behavior. Add the display link to the main run loop for delivery, and explicitly invalidate it during live shutdown to break any target/link cycle. Registration itself does not schedule a hop.

Broader owned resource fields, escaping field views/captures, Weak, fallible constructors, object-returning or async overrides, and general suspension composition remain future work. Ordinary Z-to-Z inheritance is not supported. Both the TypeScript Stage 0 compiler and the native compiler execute this tier. The native frontend parses these declarations and checks imported declaration contracts, including restricted bases, inherited override signatures, and designated/nonnullable initializer selection. Structural checks also reject repeated initialization, receiver use in initializer arguments, captured or extracted super, and constructor returns hidden in expression blocks. A nested closure may still return to its own caller. The first native body-typing tier checks names, argument/result types, inherited properties, nominal base conversions, and receiver alias exclusivity using the ordinary checker. It also requires the imported thread.main capability. Native Objective-C references retain shared identity on ordinary assignment; explicit move does not apply to these references. Direct this capture is still outside the initial closure tier. A named alias can be retained in a typed owned closure, with the appropriate executor contract. Runtime tests verify that such a callback keeps the native receiver alive until the callback is destroyed. Native subclass checks also use the existing z.json deployment target to reject unavailable APIs and APIs introduced after, or obsoleted by, that minimum version. This includes superclass declarations, overrides, calls, and properties. Other platforms’ restrictions are ignored. Without a configured minimum, version checks remain deferred just as in Stage 0; platform-only unavailability still applies. The native checker also rejects borrowed captures becoming owned function values, including through generic arguments, containers, returns, and nested owned captures. Both frontends also check a known synchronous Z helper’s body before passing it a stack-backed callback. Direct invocation and proven named forwarding remain supported; returning, storing, or capturing the callback requires an owned environment at the call site. This does not forbid retaining received Objective-C blocks whose adapters supply ARC-backed retain/release hooks. Native deprecation warnings remain pending. Dedicated native lowering emits real method entries and constructor wrappers, not a Z ARC record. The same 200-iteration dispatch and lifetime fixture passes both compilers at -O0 and -O2 under strict Clang checks and UBSan, including construction/teardown callbacks and constructor-local ARC cleanup. Owned native captures keep their receiver alive until the closure’s final release. Internal generated class and constructor names, plus lifecycle and owned-field storage, cannot be referenced from raw source. This is a bounded storage checkpoint, not support for arbitrary resource graphs or general async bodies.

AppKit controls commonly expose a weak target plus selector. Binding only the target would make closure lifetime a hidden footgun, so std/objc turns the relationship into one owned token:

import objc from "std/objc";
const connection = objc.connect(button, move () => {
saveDocument();
});

objc.connect installs a generated adapter and returns objc.Connection. Keep that value alive for as long as the action should fire. Its cleanup clears the installed target/action before releasing the control and closure environment. Discarding the connection is a compile-time error.

A Z class can implement Objective-C protocols without becoming an Objective-C class itself:

class TableController implements
AppKit.NSTableViewDataSource,
AppKit.NSTableViewDelegate
{
function rowCount(
in tableView: AppKit.NSTableView
): isize as "numberOfRowsInTableView:" {
return 0;
}
function viewForRow(
in tableView: AppKit.NSTableView,
in tableColumn: AppKit.NSTableColumn | null,
row: isize
): AppKit.NSView | null as "tableView:viewForTableColumn:row:" {
return null;
}
}

The as "selector:" clause is checked against the imported protocol requirement. objc.bind installs generated adapters into weak delegate or data source properties and owns their lifetime:

Protocol-qualified callback values keep their native protocol surface:

function start(
in webView: WebKit.WKWebView,
in task: WebKit.WKURLSchemeTask
): void as "webView:startURLSchemeTask:" {
task.didFinish();
}

The first tier borrows task only for the synchronous callback. Z rejects storing, returning, or suspending with it, while hover and autocomplete expose the imported WKURLSchemeTask methods.

Received Objective-C blocks are typed Z callables:

function decide(
in webView: WebKit.WKWebView,
in action: WebKit.WKNavigationAction,
in decisionHandler: (policy: WebKit.WKNavigationActionPolicy) => void
): void as "webView:decidePolicyForNavigationAction:decisionHandler:" {
decisionHandler(WebKit.WKNavigationActionPolicyAllow);
}

in expresses the callback-time borrow. Stage 0 can copy the function value into owned Z storage, where its ARC bridge retains the native block safely. The fixed-point backend currently executes synchronous invocation and rejects explicit callable copy, preserving the non-escaping boundary.

Imported completion-handler parameters accept captured Z closures directly:

webView.evaluateJavaScript(
move source,
completionHandler: move (value, error): void => {
if (error != null) report(error);
}
);

The compiler uses the exact imported block ABI, keeps the closure environment alive under ARC, and preserves nullable Objective-C values for ordinary Z narrowing. Both incoming and outgoing block adapters run through Stage 0 and the fixed-point native compiler.

Z byte storage can become independently owned Foundation data explicitly:

const bytes = Array<u8>(90, 32, 68, 97, 116, 97);
const data = Foundation.NSData.copy(bytes);

copy accepts Slice<u8>-compatible storage, evaluates it once, and performs one native byte copy into ARC-managed NSData. The source remains owned by Z; no source lifetime escapes into Foundation.

An embedded asset has a stronger, process-lifetime proof and can use the zero-copy bridge:

import embed from "std/embed";
const icon = embed.bytes("./assets/icon.png");
const data = Foundation.NSData.borrow(icon);

NSData.borrow accepts only nominal embed.StaticBytes. Foundation does not copy or free the binary’s immutable static storage. Ordinary Arrays and Slices must use NSData.copy.

const controller = new TableController({});
const binding = objc.bind(tableView, {
dataSource: controller,
delegate: controller,
});

Some APIs retain or store an id<Protocol> argument directly rather than exposing a weak property. Use objc.adapt to create an owned protocol carrier:

const handler = new SchemeHandler({});
const adapter = objc.adapt<WebKit.WKURLSchemeHandler>(handler);
configuration.setURLSchemeHandler(
adapter,
forURLScheme: "zapp"
);

The handler must be a stored Z class implementing the exact imported protocol. The result is objc.Adapter<WebKit.WKURLSchemeHandler>, which can be stored in an ARC class field. Its native representation is exactly id<WKURLSchemeHandler>: one generated NSObject adapter retains the Z controller, and normal ARC retains that adapter. Passing an adapter for a different protocol is a compile error.

Generated protocol callbacks keep their adapter and Z receiver alive through return and local cleanup, even if the callback unregisters its last native owner. This adds a scoped native retain/release, not another heap allocation or executor hop. It does not permit calls through a stale native pointer after teardown.

Optional adapter fields use ordinary Z Option:

class WindowResources on thread.main {
readonly handler: Option<objc.Adapter<WebKit.WKURLSchemeHandler>>;
}
const resources = new WindowResources({ handler: Option.some(adapter) });
const empty = new WindowResources({ handler: Option.none });

Use a borrowed match (in resources.handler) to access a present adapter. Destroying the Z owner releases its optional reference, without creating an additional native adapter. Other native owners can keep the adapter alive.

Imported protocol values also widen safely from id<P> to an Objective-C method parameter typed as ordinary id. This is a zero-cost native widening, not a wrapper or unchecked cast. Free C functions whose signatures directly contain Objective-C objects or id<Protocol> remain outside the checked header-only function tier for now; prefer an imported Objective-C method or a reviewed declaration at that boundary.

This first tier is conservatively thread.main-bound. It is distinct from objc.bind, which owns adapters installed in zeroing weak properties, and from objc.register, which owns a checked add/remove lifetime.

For retained protocol registrations that use separate add/remove methods, objc.register makes teardown deterministic:

const registration = objc.register({
add: contentController.addScriptMessageHandler(handler, "nativeBridge"),
remove: contentController.removeScriptMessageHandlerForName("nativeBridge"),
});

The returned token retains the receiver, adapter, and Z controller, then invokes the checked remove call exactly once during cleanup.

Objective-C references stored in a Z class are strong by default. This applies to ordinary imported objects and objc.Registration, so a class returned from a helper keeps its native graph alive after the helper’s locals end. If a native class also releases itself on a lifecycle event, disable that native policy when Z is intended to remain the sole owner; AppKit windows use window.releasedWhenClosed = false for this shape.

Native shape Preferred Z surface
Normal class, property, method, constant, nullable reference, or typed block Direct header import
Lexically bounded temporary Objective-C work objc.pool
Weak target plus selector objc.connect
Protocol adapter stored by weak properties objc.bind
Retained or stored id<Protocol> argument objc.adapt<P>
Retained handler with paired add/remove calls objc.register

std/objc models runtime conventions missing from ordinary declarations. It is not a generic message-send wrapper and should not sit between Z and APIs Clang already describes safely.

const result = try objc.pool((): WorkResult throws WorkError => {
return try performObjectiveCWork();
});

objc.pool is synchronous and non-escaping. It lowers directly to @autoreleasepool, drains before returning or propagating a typed error, and rejects borrowed/view results or async closure bodies.

Runnable examples: native window, target/action, and protocol conformance.

Next: C++ interop.