A FreeBSD-only port of Apple's notifyd + libnotify — Apple's lightweight named-event pub/sub bus. Drops Mach IPC; replaces the wire layer with GNUstep Distributed Objects + AF_UNIX socket; preserves the libnotify C API surface so Apple-derived applications (gershwin desktop apps, ported third-party Cocoa apps) Just Work without source changes. Companion to launchd, configd, kmodloader, asl.
notifyd/ at the top, alongside src/ (launchd), configd/, kmodloader/, asl/. Single build pipeline, single CI, single boot test.notify_post(), notify_register_dispatch(), notify_get_state() — same C API — and the daemon does the fan-out. Promotes cross-process event coordination from "build a bespoke channel each time" to "just pick a name."Libnotify-98.5 (latest tag at apple-oss-distributions/Libnotify, APSL). 48 source files, ~17k LOC. 12 files have <mach/> includes; 2 MIG .defs files (notify_ipc.defs, notify_old_ipc.defs). Phase 1 amputation drops the Mach plumbing.libnotify linkage, OR (b) configd's DO-callback fan-out becomes a bottleneck for many-subscriber events and we want the cheaper notifyd model. Phase 7+ work, sequenced after configd ships and before/after ASL based on demand.NSConnection over AF_UNIX for the daemon-client channel; shm_open(2) + mmap(2) for the state-value pages (preserves Apple's "reads are zero-IPC" performance characteristic).libdispatch dispatch sources only. DISPATCH_SOURCE_TYPE_VNODE for path-watch notifications (already what Apple does — pathwatch.c uses kqueue under the hood). DISPATCH_SOURCE_TYPE_READ on the AF_UNIX listener.notify_post, notify_register_dispatch, notify_register_signal, notify_register_file_descriptor, notify_register_check, notify_get_state, notify_set_state, notify_cancel all keep their existing signatures and semantics. Apple-derived apps link our libnotify.so without source changes.NOTIFY_TYPE_PORT (Mach port wakeup) is removed. Apps using it would need to switch to NOTIFY_TYPE_DISPATCH or NOTIFY_TYPE_FD — semantically equivalent, just different kernel mechanism.Provide a working libnotify + notifyd on FreeBSD so Apple-derived apps (gershwin desktop, ported Cocoa apps, third-party Apple-OSS-distributions tools) can use named pub/sub events without porting work. Replicate the API surface byte-for-byte; replace Mach-tied IPC + shared-memory mechanisms with FreeBSD-native equivalents (DO + POSIX shm). Preserve the performance characteristic that reads of state values are zero-IPC — that's what makes notifyd fundamentally different from a sockets-only pub/sub system.
NOTIFY_TYPE_PORT (Mach port delivery). Same project-wide rule. Apps that registered for a Mach port to receive notifications: source change to NOTIFY_TYPE_DISPATCH or NOTIFY_TYPE_FD. Net code change in the consumer is ~5 lines.notify_probes.d is dropped. FreeBSD has DTrace but the probe definitions are Apple-specific.notify.conf.iOSSimulator, notify.conf.iPhone dropped on import.com.apple.notifyd.sb is sandbox(7); FreeBSD has Capsicum but the policy doesn't translate. Hardening pass later.entitlements.plist, notifyutil_entitlements.plist are macOS code-signing concerns; n/a here.Libnotify.xcodeproj, xctests/, notifyd-xctests/ dropped — Xcode-only. We rebuild a minimal test corpus on top of FreeBSD's kyua or just shell scripts.Monorepo, same as the other ports. notifyd source lives under notifyd/ at the top of freebsd-launchd:
freebsd-launchd/
├── src/ launchd Apple-imported source
├── configd/ Apple configd-imported source
├── kmodloader/ clean-room kmodloader
├── asl/ Apple syslog-imported source
├── notifyd/ Apple Libnotify-imported source (this plan)
│ ├── scripts/import-source.sh
│ ├── Makefile top-level: build daemon + libnotify + notifyutil
│ ├── compat/
│ └── src/ forked Apple Libnotify-98.5
│ ├── libnotify.{c,h} the libnotify client library — API surface
│ ├── notify.h public API header
│ ├── notify_keys.h public well-known notification names
│ ├── notify_client.c client-side state management
│ ├── notify_internal.h
│ ├── notify_private.h
│ ├── table.{c,h} hash-table for token registry
│ ├── notifyd/ the notifyd daemon
│ │ ├── notifyd.{c,h} daemon main, dispatch loop
│ │ ├── service.{c,h} service registration + name table
│ │ ├── notify_proc.c per-client process tracking
│ │ ├── pathwatch.{c,h} path-watch notifications via kqueue
│ │ └── notify.conf (MacOSX flavor; iOS variants dropped)
│ ├── notifyutil/ notifyutil(1) CLI tool
│ └── notifybench/ throughput benchmark harness
└── make-notifyd.sh STANDALONE — builds + installs
notifyd's defining feature versus a generic pub/sub system: reads of state values are zero-IPC. Apple does this with shared-memory pages — when a process registers for a name with state, it's mapped a page containing the state's uint64_t slot. The daemon writes the slot directly when state changes; client reads are a memory load. No syscall, no IPC roundtrip.
This is why notifyd works for high-frequency state (volume level updates 60Hz, network reachability checks per HTTP request, etc.) where a sockets-roundtrip-per-read would be too slow.
FreeBSD port preserves this: replace Apple's Mach-allocated shared pages with shm_open(2) POSIX shared memory. The daemon allocates one or more shm segments holding the state-value array; clients mmap() them read-only at registration time. Daemon writes go through atomic store intrinsics (__atomic_store_n) so torn reads aren't possible.
The daemon-client channel uses Mach ports in Apple's source — see notify_ipc.defs + notify_old_ipc.defs (two MIG IDLs; old + current). We replace with:
NSConnection over /var/run/notifyd.sock at startup. Implements an @protocol NotifydService with -registerName:type:client:, -postName:, -getState:returning:, -setState:value:, etc.libnotify.c's wire-format functions are replaced with NSConnection rootProxy + remote message sends. The public notify_* API remains identical; only the implementation guts under it change.NOTIFY_TYPE_DISPATCH — daemon writes a byte to a libdispatch source's mach_port-or-fd; we use kqueue/EVFILT_USER + a unix-pipe-pair as the wakeup mechanism. dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_OR, ...) implements the same semantics.NOTIFY_TYPE_SIGNAL — daemon kill(pid, sig)s the subscriber. Portable as-is.NOTIFY_TYPE_FD — daemon writes a byte to a pipe the subscriber holds the read end of. Portable as-is.NOTIFY_TYPE_PORT (Mach) — dropped. Apps source-port to _DISPATCH.notifyd implements file-path-watch notifications: subscribe to a path, get woken when the file changes. Apple's pathwatch.c uses kqueue under the hood (Darwin natively); we keep the implementation as-is. DISPATCH_SOURCE_TYPE_VNODE in libdispatch.
| Source type | Watches | Reaction |
|---|---|---|
DISPATCH_SOURCE_TYPE_READ | DO listener fd (launchd-opened) | accept new NSConnection clients; their messages flow through the protocol |
DISPATCH_SOURCE_TYPE_VNODE | each registered path | fire path-changed notification to subscribers |
DISPATCH_SOURCE_TYPE_PROC | each registered client PID | auto-cancel registrations on DISPATCH_PROC_EXIT (don't deliver to dead clients) |
DISPATCH_SOURCE_TYPE_VNODE | /etc/notify.conf | reload early-access table on config change |
DISPATCH_SOURCE_TYPE_SIGNAL | SIGTERM, SIGHUP, SIGINFO | SIGTERM: clean shutdown. SIGHUP: reload conf. SIGINFO: dump stats. |
| Artifact | Path | Why |
|---|---|---|
notifyd binary | /usr/libexec/notifyd | Daemon not invoked directly. Same tier as /usr/libexec/getty, /usr/libexec/netconfigd. |
notifyutil CLI | /usr/bin/notifyutil | Admin/dev tool: post a name, watch for posts, set/get state. Standard /usr/bin for user-callable commands. |
libnotify.so | /System/Library/Libraries/libnotify.so | The client library. Apps link against this. |
| Public headers | /System/Library/Headers/notify.h/System/Library/Headers/notify_keys.h | Apps include via #include <notify.h>. |
| DO socket | /var/run/notifyd.sock | Per-host daemon connection. launchd-opened via Sockets={Listeners=...}. |
| State-page directory | /var/run/notifyd/ | Daemon-managed POSIX shm names. Created by daemon at startup. |
| Config | /etc/notify.conf | Apple's "early-access" table — names that get a state slot allocated at daemon startup so they're available before the publisher posts. |
| launchd plist | /System/Library/LaunchDaemons/org.freebsd.notifyd.plist | Project-shipped daemon plist. |
| Decision | Choice |
|---|---|
| Source baseline | Apple Libnotify-98.5. APSL. Latest tag. |
| Mach IPC | None. Both .defs files deleted; client-server channel rebuilt on DO over AF_UNIX. |
| Shared-memory state pages | Replaced with POSIX shm_open(2) + mmap(2). Atomic updates via <stdatomic.h>. |
| Notification delivery types | NOTIFY_TYPE_DISPATCH, NOTIFY_TYPE_SIGNAL, NOTIFY_TYPE_FD, NOTIFY_TYPE_CHECK all preserved. NOTIFY_TYPE_PORT (Mach) dropped. |
| Public API stability | All notify_* functions in notify.h keep their signatures and observable semantics. Source compatibility with Apple-derived apps is mandatory. |
| Event loop | libdispatch dispatch sources only. |
| Path-watch impl | kqueue (already what Apple does on Darwin; preserved). |
| License (top-level) | BSD-2-Clause. Apple's libnotify files retain APSL per-file (mix of 1.1 and 2.0 — preserve whichever the file header carries). |
notifyd/src/)Imported source: Apple Libnotify-98.5. 48 files, ~17k LOC. 12 Mach-tied; 2 MIG .defs.
Libnotify.xcodeproj/, xcodeconfig/, xcodescripts/, notifyd/xcodescripts/ — Xcode build infranotify_ipc.defs, notify_old_ipc.defs — MIG IDL (the entire layer they describe is replaced with DO)notify_register_mach_port.3, related Mach-port-delivery code — drops with NOTIFY_TYPE_PORTnotifyd/com.apple.notifyd.sb — sandbox(7) profile; not portablenotifyd/entitlements.plist, notifyutil/notifyutil_entitlements.plist — code-signing entitlements; not portablenotifyd/notify.conf.iPhone, notifyd/notify.conf.iOSSimulator — iOS variantsnotify_probes.d — DTrace probe definitions (Apple-specific)xctests/, notifyd-xctests/ — XCTest harnessesnotify_private.modulemap, notify.modulemap — Clang module maps for Apple's framework build; not used in our pkg-config-based build| File | Apple LOC | Action | Target LOC |
|---|---|---|---|
libnotify.c | ~3.5k | Substantial rewrite of the IPC layer. Replace Mach-port-allocation + MIG-stub-call with NSConnection proxy calls. Public notify_* API unchanged. Replace shared-memory mapping (Mach vm_allocate + port handoff) with POSIX shm_open + mmap. | ~2k |
notify_client.c | ~2k | Port. Per-token bookkeeping. Mostly Mach-free; minor pruning. | ~1.7k |
table.{c,h}, table.in.c | ~1.5k | Keep. Hash-table impl for token registry. Pure data structure. | ~1.5k |
notify.h, notify_keys.h, notify_internal.h, notify_private.h | ~1k | Keep. Public API + internal protocol declarations. Drop only the NOTIFY_TYPE_PORT token type. | ~900 |
notifyd/notifyd.{c,h} | ~2k | Prune. Replace Mach service-loop init with dispatch_main + libdispatch sources. Drop bootstrap-port registration. Keep config-file load + signal handling. | ~1.2k |
notifyd/service.{c,h} | ~3k | Port. Service registration + name table. Replace Mach-port-based subscriber tracking with PID-keyed table tied to DISPATCH_SOURCE_TYPE_PROC. | ~2.2k |
notifyd/notify_proc.c | ~1k | Port. Per-client process management. Auto-cancel registrations on PROC_EXIT. | ~800 |
notifyd/pathwatch.{c,h} | ~600 | Keep mostly intact. kqueue-based; portable. | ~600 |
notifyutil/notifyutil.c | ~700 | Port. The CLI is mostly libnotify-call-and-print; once libnotify works the CLI follows. | ~600 |
notifybench/ | ~500 | Port. Useful for verifying state-page reads stay zero-IPC after our shm_open swap. | ~500 |
Manpages (notify*.3, notifyd.8, notifyutil.1) | — | Keep mostly intact. Drop notify_register_mach_port.3. | shrinks |
Total post-Phase-2: roughly 11-12k LOC vs Apple's ~17k pre-amputation. About 30% deletion (smaller ratio than configd/asl because notifyd is fundamentally smaller and most of the code is portable already).
| Feature | Apple's notifyd does | This port (FreeBSD-only) |
|---|---|---|
| Daemon-client IPC | Mach ports + MIG-generated stubs (two flavors: current + legacy) | GNUstep DO over AF_UNIX. ~3k LOC of Mach plumbing replaced by ~600 LOC of DO. notify_old_ipc.defs goes away entirely (was for back-compat with pre-Snow-Leopard apps). |
| State-value pages | Mach vm_allocate + port handoff for shared memory | POSIX shm_open(2) + mmap(2). Same zero-IPC-read characteristic; portable across any Unix. |
Subscriber wakeup (NOTIFY_TYPE_DISPATCH) | Mach port message → libdispatch source | EVFILT_USER + libdispatch source. One less indirection layer. |
| Path notifications | kqueue under the hood (already!) | kqueue. Identical. |
| Subscriber bookkeeping | Mach send-rights tracked per-client; subscriber-died detected via Mach port-no-senders notification | DISPATCH_SOURCE_TYPE_PROC on each subscriber's PID. DISPATCH_PROC_EXIT fires; daemon cancels that PID's registrations. Cleaner. |
| Build-system gates | iOS / sim / catalyst #if | Delete. One target. |
What we get when notifyd lands and gershwin's apps can use it:
| Concern | Notification name | Effect |
|---|---|---|
| Theme / appearance change | org.freebsd.appearance.theme-changed | Appearance prefs panel writes the new theme; posts the name. Every running app subscribed re-themes instantly. No per-app D-Bus connection or polling. |
| Application activation / focus | org.freebsd.workspace.app-activated | Other apps de-emphasize selves (dim accent colors, pause animations). |
| Hide / show all (NeXTSTEP signature) | org.freebsd.workspace.hide-others | Workspace single-post; all subscribed apps minimize. |
| Display configuration changed | org.freebsd.display.changed | configd or kmodloader detects framebuffer plug; posts. Workspace reflows windows; full-screen video apps react. |
| Login / logout | org.freebsd.session.user-logged-in / ...logged-out | Per-user launchd agents (mail-checker, dock helpers, calendar-sync) wake. Mirrors Apple's loginwindow flow. |
| Concern | Notification name | Effect |
|---|---|---|
| Battery / power | org.freebsd.power.battery-changed | One powermon daemon (or extension to configd's KernelEventMonitor) reads ACPI; posts current state. Workspace menubar battery icon, lid-close handler, screen-dim policy all subscribe. One source, many subscribers — without notifyd, every consumer would need its own ACPI listener. |
| Network state | org.freebsd.config.network-reachable | configd posts when an interface becomes reachable. Mail, browsers, sync clients subscribe. Apps check on each post — no polling, no per-app socket to configd. |
| Sleep / wake | org.freebsd.power.sleep-requested + ...wake | Apps save state, pause downloads, dim UI before sleep; resume after wake. |
| Time / TZ change | org.freebsd.time.timezone-changed | Calendar, Mail, Clock all subscribe. Currently apps poll or recompute on every operation. |
| Filesystem mount / unmount | org.freebsd.fs.mounted / ...unmounted | Workspace's File Viewer (Finder-equivalent) updates the sidebar. ZFS pool import — mount-aware apps refresh. |
| Keyboard layout change | org.freebsd.input.layout-changed | Apps refresh shortcut displays. |
| Audio volume / mute | org.freebsd.audio.volume-changed | Sound prefs slider follows hardware media keys; menubar volume icon updates. |
This is where notifyd shines compared to alternatives. NSUserDefaults cross-app coordination on macOS is a notifyd story:
[defaults setObject:newValue forKey:@"FontFamily"]cfprefsd-equivalent (the prefs daemon) writes the file, posts org.freebsd.prefs.<domain>.changedWithout notifyd, every app would either poll the prefs file or rely on filesystem-watching primitives like kqueue(EVFILT_VNODE) — clunky compared to a single named post. gershwin's NSUserDefaults gets a noticeable UX upgrade when this works correctly across processes.
Apple uses notifyd to trigger launchd jobs without explicit RPC:
<key>LaunchEvents</key>
<dict>
<key>com.apple.notifyd.matching</key>
<dict>
<key>org.freebsd.network-reachable</key>
<dict/>
</dict>
</dict>
A daemon plist with this LaunchEvents stanza launches its program only when that named notification posts. Auto-update checker idle until network up; backup daemon idle until specific conditions; etc. Replaces both "phase markers" (the proposed RequiresPhase per launchd plan §11.3) and ad-hoc polling with one unified mechanism. Adding LaunchEvents support to launchd alongside this notifyd port unlocks an Apple-native event-driven supervision model.
Practical concrete: any app you'd port from macOS that calls notify_register_dispatch() or notify_post() works without modification. Without notifyd we'd either need to:
With notifyd present, the calls Just Work.
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>Label</key> <string>org.freebsd.notifyd</string>
<key>ProgramArguments</key> <array><string>/usr/libexec/notifyd</string></array>
<key>RunAtLoad</key> <true/>
<key>KeepAlive</key> <true/>
<key>Sockets</key> <dict>
<key>Listeners</key> <dict>
<key>SockPathName</key> <string>/var/run/notifyd.sock</string>
<key>SockType</key> <string>stream</string>
<key>SockPathMode</key> <integer>438</integer> <!-- 0666 -->
</dict>
</dict>
</dict>
</plist>
launchd opens the AF_UNIX listener and hands the fd to notifyd; client connections queue in the kernel even when notifyd is restarting.
notifyd has no dependencies on other daemons — it's a leaf service. Configd may want to use notifyd internally for fan-out (instead of DO callbacks) once both are present, but configd works fine without it. Order it alongside other system daemons; no explicit ordering needed.
Apple's Libnotify source is APSL (1.1 in older files, 2.0 in newer; preserve whichever each file carries). Same family of license as ASL; FSF-approved free software; not GPL-compatible (irrelevant for our project — no GPL deps).
| Source | License | How we handle it |
|---|---|---|
| Apple Libnotify-98.5 (per-file APSL headers) | APSL 1.1 / 2.0 mix | Keep per-file headers verbatim. Our edits inherit APSL via inbound=outbound — those individual files stay APSL regardless of top-level repo license. |
| This repo's new code (DO bridge, FreeBSD shims, integration glue) | BSD-2-Clause | SPDX header on each new file. |
| GNUstep Foundation, libdispatch (linked, not in tree) | LGPL/Apache as applicable | Listed in NOTICE. Same calculus as launchd/configd. |
notifyd/ at the top of freebsd-launchd. Update NOTICE for APSL attribution.notifyd/scripts/import-source.sh that clones apple-oss-distributions/Libnotify at Libnotify-98.5, strips .git, drops it under notifyd/src/.notifyd/Makefile + make-notifyd.sh.notifyd.c, service.c, notify_proc.c: minimum daemon that accepts DO connections, registers names, fans out posts.libnotify.c: client API. Replace Mach IPC layer with NSConnection. notify_post, notify_register_dispatch, notify_register_signal, notify_register_fd, notify_cancel all work end-to-end.notify_get_state, notify_set_state are RPC calls in this phase.notifyutil; verify a posted name reaches a subscriber in another process.notify_get_state with shared-memory mapping. Daemon allocates shm_open'd segments holding the state-value array; client mmaps them read-only on registration.atomic_store_explicit) so torn reads are impossible.notifybench: confirm state reads stay zero-IPC after the swap. Compare to Phase 1 RPC baseline.pathwatch.c: subscribe-to-path-changes notifications. Already kqueue-based; minimal changes.notify_register_check (poll-based; check counter changes via shm).NOTIFY_TYPE_FD delivery (subscriber holds the read end of a pipe) wired up.notifyutil/notifyutil.c: post a name, watch for posts, set/get state. Useful for testing + system administration.notifyutil -p test.foo from one shell; notifyutil -w test.foo from another; confirm wakeup.LaunchEvents integrationLaunchEvents + com.apple.notifyd.matching stanza support to core.m in the launchd source. Job stays in WAITING state until a matching name is posted; spawn happens on first post.RequiresPhase mechanism (launchd plan §11.3) with the more general notifyd-driven approach.LaunchEvents instead of RequiresPhase.org.freebsd.session.user-logged-in.NOTIFY_TYPE_PORT only if some app's port-receiving consumer can't be source-ported. Unlikely.notify_probes.d) when the perf-measurement need shows up.atomic_store_explicit(memory_order_relaxed) is the right primitive but compiler / target-arch-specific. Phase 2 testing covers.
NSDistributedNotificationCenter, which is GNUstep's own pub/sub mechanism (separate from notifyd). Apps using NSDistributedNotificationCenter Just Work today. Apps using notify_post need our notifyd port. Both should coexist — they don't conflict; pick the API that matches the app's expectation. Long-term gershwin question: should new app code prefer one over the other? Decision deferrable.
com.apple.* for system events. We propose org.freebsd.* for our project's posts (network, power, theme, etc.) and reserve com.apple.* for app-emitted compat names. org.gershwin.* for desktop-specific events. Keep these consistent across all FreeBSD-launchd-ecosystem code.
com.apple.system.config.network_change compat. Apple-derived apps registering for that exact name will expect it to fire on network state change. Should configd post both the Apple-canon name and our org.freebsd.config.network-changed? Decision: yes, dual-post — costs nothing, retains source compat with apps that hard-coded the Apple name.
/dev/shm/ on Linux; FreeBSD uses /tmp/... or swap/... behind the scenes. Naming scheme: org.freebsd.notifyd.state.<n> for daemon-managed segments. Per-host quota for state-value count: start at 16k slots; bump if real workloads need more.
LaunchEvents-using plists can be evaluated. If notifyd is itself launched by launchd, there's a chicken-and-egg if the launchd plist using LaunchEvents is loaded before notifyd is up. Decision: notifyd's own plist has highest priority RunAtLoad, no LaunchEvents; other plists' LaunchEvents are evaluated after notifyd's IPC socket is up. Same pattern as configd.
Libnotify-98.5).man 3 notify on macOS, or developer.apple.com/.../notify.3.html.shm_open(2): man 2 shm_open.