ADR 0001: unsafe OS FFI in ashlar-native, and nowhere else
- Status: accepted (2026-08-18, wave 1)
Context
The workspace sets unsafe_code = "forbid" (AGENTS.md §0). Some OS features every desktop app
needs have no safe binding: registering Apple event handlers (files opened from Finder),
ending an NSProcessInfo activity, SetThreadExecutionState, ITaskbarList3, ShellExecuteW,
SystemParametersInfoW, and attaching a muda menu to a Win32 window. The bindings used
(objc2-*, the official windows crate, muda) are generated
declarations.
Decision
Allow unsafe in exactly one crate, ashlar-native, for OS FFI. Containment rules (the same as
the video editor’s platform crate these were adapted from):
-
The crate does not use
lints.workspace = true. Its own[lints]table copies the workspace lints withunsafe_code = "deny"(notforbid) and addsclippy::undocumented_unsafe_blocks = "deny". Only FFI modules carry#[allow(unsafe_code)]:power(macOSendActivity:, WindowsSetThreadExecutionState)open_files(macOS: the Apple event handler class and its registration)progress(WindowsITaskbarList3)system(WindowsShellExecuteW,SystemParametersInfoW,GetUserPreferredUILanguages(added 2026-11-10 so apps started from the desktop, which have noLANG, find the user’s languages); and forbring_to_front, added 2026-11-10:IsIconic,ShowWindow,SetForegroundWindow,BringWindowToTop,AttachThreadInput)mac_shell(macOS: replacing the implementation of winit’s window delegateobserveValueForKeyPath:ofObject:change:context:with a checked wrapper that calls the original,class_addMethodofapplicationShouldHandleReopen:hasVisibleWindows:on winit’s application delegate, and themsg_send!s that inspect the KVO change); added 2026-11-10 so a nil appearance during screen capture doesn’t end the app and the Dock icon shows the windowmenu::popup(muda’sshow_context_menu_for_nsview/show_context_menu_for_hwndon the window’s own view or HWND, retaining the macOS view, and posting the macOS menu to the main run loop) anddrag_out(macOS: anNSDraggingSourceclass, retaining the window’s view, anNSDraggingItemframe); added 2026-11-10 for context menus and dragging files outsingle_instance(WindowsAllowSetForegroundWindow, one block; added 2026-11-10 so the second launch lets the first come to the front)menu::native::attach_window(muda’sinit_for_hwnd) andmenu::native::accelerators(Windows:SetWindowSubclass,TranslateAcceleratorW,DefSubclassProc; added 2026-11-10 so menu shortcuts work without a hook in winit’s message loop)terminate(Unix, throughlibc:pipe,fcntl,sigaction, and in the handler onlywrite,signalandraise, which are async-signal-safe; added 2026-09-10 so a stdio server can end its telemetry session when it is asked to stop)memory(macOSproc_pid_rusage, glibcmalloc_trim, WindowsGetProcessMemoryInfo; added for idle apps that report their footprint and hand freed memory back; macOSproc_pidinfoand WindowsCreateToolhelp32Snapshot/Process32FirstW/Process32NextW/CloseHandleadded 2026-11-10 for the thread count inapp.stats)display(WindowsGetProcessWindowStation,GetUserObjectInformationW; added 2026-10-27 so an app started where no window can be shown says so and exits instead of panicking; WindowsEnumDisplayMonitorswith its callback,GetMonitorInfoWandGetDpiForMonitoradded 2026-11-10 to list the monitors before the window exists, so a saved placement is checked at launch; macOS uses objc2’s safeNSScreenbindings)notify::macos(theUNUserNotificationCenterDelegateclass, itsinit, andsetUserInfo:) andnotify::windows(SetCurrentProcessExplicitAppUserModelID,CoInitializeEx); added 2026-11-10 so banners show while the app is frontmost, clicks are reported and Windows gets toastssession_end(macOS:class_addMethodofapplicationShouldTerminate:on winit’s application delegate class, and reading that method’s implementation; Windows: a hidden window’s class, window and message loop forWM_QUERYENDSESSION/WM_ENDSESSION); added 2026-11-10 so Dock Quit, logout and shutdown get the “Save changes?” question and the settings, autosave and telemetry flush
Everything else (dialogs, clipboard, secrets, Linux notifications, macOS progress and preferences, single instance off Windows, tray, global shortcuts) uses safe APIs of rfd, arboard, keyring-core, objc2 and zbus.
-
Every
unsafeblock has a// SAFETY:comment saying why it is sound. -
The public API is safe: no
pub unsafe fn, no raw pointers or FFI types in signatures. Window handles come in throughraw_window_handle::HasWindowHandle, so a caller can’t pass a dangling HWND. Failures areResult<_, NativeError>. -
No panic crosses an FFI boundary: the Apple event handler methods and the notification completion block run under
catch_unwind; calls into third-party bindings that could panic (rfd, arboard, muda, keyring) go throughguarded, which turns a panic intoNativeError::Os. -
The crate compiles on every target. OS bindings are target-specific optional dependencies enabled per cargo feature; elsewhere a call returns
NativeError::Unsupported. With default features it passescargo check --target wasm32-unknown-unknown.
Consequences
- Changes to the FFI modules need the same scrutiny as any FFI code. A new use of
unsafegoes into one of the listed modules with a SAFETY comment, or needs an update to this record. - Main-thread requirements (macOS menus, Dock tile, Apple event handlers) are checked at run
time with
MainThreadMarkerand reported as errors instead of crashing.