FreeBSD notifyd — porting plan

A FreeBSD-only port of Apple's notifyd + libnotify — Apple's lightweight named-event pub/sub bus. Retains Mach IPC end-to-end: libnotify connects to notifyd via a MIG-served Mach service com.apple.notifyd; 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.

Filed under: freebsd-launchd-mach (v2) effort SHIPPED

Revision 2026-05-23

This plan originally targeted the sibling freebsd-launchd (AF_UNIX / GNUstep Distributed Objects) repo. Refactored 2026-05-23 to target the freebsd-launchd-mach (v2) Mach-IPC track. notifyd has already SHIPPED in this repo's Phase J1 (libnotify) + Phase J2 iter 1 (notifyd daemon) — Mach IPC retained throughout (DISPATCH_SOURCE_TYPE_MACH_RECV via libdispatch's Mach backend, as documented in the libxpc libdispatch-mach spike). Sections below describing "future work" now describe shipped work; the architecture-decision sentences have been flipped from DO/AF_UNIX to MIG/Mach.

Status: SHIPPED — Phase J1 (libnotify) + Phase J2 iter 1 (notifyd daemon)

1. Goal & non-goals

1.1 Goal

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; retain Apple's Mach-IPC substrate (libnotify ↔ notifyd via MIG over mach_msg); use POSIX shm_open(2)+mmap(2) for the shared state-value pages (the one Mach-specific mechanism with no clean equivalent on FreeBSD's Mach port — same end result, portable substrate). 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.

1.2 Non-goals (this iteration)

2. Repository

Monorepo, same as the other ports on the v2 track. notifyd source lives under src/Libnotify/ at the top of freebsd-launchd-mach:

freebsd-launchd-mach/
├── src/launchd/                  launchd Apple-imported source
├── src/syslog/                   Apple syslog-imported source (ASL)
├── src/libxpc/                   libxpc (Mach IPC client/server)
├── src/libdispatch/              libdispatch (incl. Mach backend: MACH_RECV, MACH_SEND)
├── src/libCoreFoundation/        CoreFoundation port
├── src/Libnotify/                Apple Libnotify-imported source (this plan)
│   ├── 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
│   ├── notify_ipc.defs           MIG IDL (current)
│   ├── notify_old_ipc.defs       MIG IDL (legacy, for back-compat clients)
│   ├── 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)
└── build.sh                      top-level: builds libnotify + notifyd alongside ASL + libxpc + libdispatch

3. Architecture

+------------------------+ | publisher process | | notify_post("name") | +------------+-----------+ | (MIG: notify_server_post over mach_msg) v +----------------+ +--------+--------+ +--------------------+ | /etc/notify. |-->| notifyd |<-->| /var/run/notifyd/ | | conf | | (MIG server, | | state-pages | | (early-access | | libdispatch | | (POSIX shm; one | | table) | | MACH_RECV) | | uint64 per state | +----------------+ +--------+--------+ | value, atomically | | | updated) | | (delivery +--------------------+ | per token type: v _PORT | _DISPATCH | _SIGNAL | _FD) +----------------+----------------+ | subscribers | | (libnotify clients in apps) | | token = notify_register_*("name", ...)| +---------------------------------+ Mach service: com.apple.notifyd (bootstrap_check_in by notifyd; bootstrap_look_up by libnotify)

3.1 The state-value optimization (load-bearing)

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 one place we substitute a portable POSIX primitive for a Mach VM primitive — Apple's vm_allocate+port-handoff has no clean equivalent on FreeBSD's mach.ko port, but the end-user semantic — "shared page, daemon writes, clients read with no syscall" — is identical). 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.

3.2 Mach IPC: MIG over mach_msg

The daemon-client channel uses Mach ports — see notify_ipc.defs + notify_old_ipc.defs (two MIG IDLs; old + current). Both are kept on the v2 track:

3.3 Path-watch notifications

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.

3.4 Event sources (libdispatch)

Source typeWatchesReaction
DISPATCH_SOURCE_TYPE_MACH_RECVnotifyd's MIG service port (com.apple.notifyd, registered via bootstrap_check_in)incoming mach_msg is dispatched through the MIG-generated server stubs (notify_ipc_server.c) to the daemon's handler functions
DISPATCH_SOURCE_TYPE_VNODEeach registered pathfire path-changed notification to subscribers
DISPATCH_SOURCE_TYPE_PROCeach registered client PIDauto-cancel registrations on DISPATCH_PROC_EXIT (don't deliver to dead clients)
DISPATCH_SOURCE_TYPE_VNODE/etc/notify.confreload early-access table on config change
DISPATCH_SOURCE_TYPE_SIGNALSIGTERM, SIGHUP, SIGINFOSIGTERM: clean shutdown. SIGHUP: reload conf. SIGINFO: dump stats.

4. Install paths

ArtifactPathWhy
notifyd binary/usr/libexec/notifydDaemon not invoked directly. Same tier as /usr/libexec/getty, /usr/libexec/netconfigd.
notifyutil CLI/usr/bin/notifyutilAdmin/dev tool: post a name, watch for posts, set/get state. Standard /usr/bin for user-callable commands.
libnotify.so/System/Library/Libraries/libnotify.soThe 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>.
Mach servicecom.apple.notifydPer-host daemon connection. notifyd checks in via bootstrap_check_in("com.apple.notifyd"); clients look up via bootstrap_look_up. launchd publishes the service via MachServices in the daemon's plist.
State-page directory/var/run/notifyd/Daemon-managed POSIX shm names. Created by daemon at startup.
Config/etc/notify.confApple'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/com.apple.notifyd.plistProject-shipped daemon plist. Uses Apple's canonical label so apps registering for system events match Apple's bootstrap naming.

5. Locked architectural decisions

DecisionChoice
Source baselineApple Libnotify-98.5. APSL. Latest tag.
Mach IPCRetained. Both .defs files (notify_ipc.defs, notify_old_ipc.defs) kept; MIG client + server stubs compiled in-tree. Service name com.apple.notifyd published via launchd's MachServices dict.
Shared-memory state pagesPOSIX shm_open(2) + mmap(2) substituted for Apple's vm_allocate+port-handoff. Atomic updates via <stdatomic.h>. (Same end-user semantic; one Mach VM primitive without a clean FreeBSD-mach.ko equivalent.)
Notification delivery typesAll four preserved: NOTIFY_TYPE_PORT (Mach), NOTIFY_TYPE_DISPATCH, NOTIFY_TYPE_SIGNAL, NOTIFY_TYPE_FD, NOTIFY_TYPE_CHECK.
Public API stabilityAll notify_* functions in notify.h keep their signatures and observable semantics. Source compatibility with Apple-derived apps is mandatory.
Event looplibdispatch dispatch sources, incl. DISPATCH_SOURCE_TYPE_MACH_RECV (Mach backend on FreeBSD — see the libxpc libdispatch-mach spike for the enabling work).
Path-watch implkqueue (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).

6. File-by-file plan (src/Libnotify/)

Imported source: Apple Libnotify-98.5. 48 files, ~17k LOC. 12 Mach-tied (retained); 2 MIG .defs (retained).

6.1 Deleted on import (out-of-scope; Mach files retained)

Retained (was deleted in the v1 / AF_UNIX-DO plan): notify_ipc.defs, notify_old_ipc.defs, notify_register_mach_port.3, all Mach-tied .c/.h files. The Mach plumbing is the substrate on the v2 track, not an amputation target.

6.2 Retained — Phase 2 fate

FileApple LOCActionTarget LOC
libnotify.c~3.5kKeep IPC layer mostly intact — Mach-port-allocation + MIG-stub-call paths run on FreeBSD via mach.ko + libxpc. Public notify_* API unchanged. Replace only the shared-memory mapping (Apple's vm_allocate + port handoff) with POSIX shm_open + mmap.~3.2k
notify_client.c~2kKeep. Per-token bookkeeping. Mach-aware; runs unchanged.~2k
notify_ipc.defs, notify_old_ipc.defsKeep. MIG IDL files; the FreeBSD mig(1) port generates client + server stubs from these (same as Apple's build).
table.{c,h}, table.in.c~1.5kKeep. Hash-table impl for token registry. Pure data structure.~1.5k
notify.h, notify_keys.h, notify_internal.h, notify_private.h~1kKeep. Public API + internal protocol declarations. All token types (incl. NOTIFY_TYPE_PORT) retained.~1k
notifyd/notifyd.{c,h}~2kKeep. Mach service-loop init runs on FreeBSD's mach.ko; bootstrap_check_in("com.apple.notifyd") + libdispatch DISPATCH_SOURCE_TYPE_MACH_RECV. Keep config-file load + signal handling.~2k
notifyd/service.{c,h}~3kKeep. Service registration + name table. Mach-port-based subscriber tracking runs as-is, augmented with DISPATCH_SOURCE_TYPE_PROC for PID-exit cleanup.~3k
notifyd/notify_proc.c~1kKeep. Per-client process management. Auto-cancel registrations on PROC_EXIT.~1k
notifyd/pathwatch.{c,h}~600Keep mostly intact. kqueue-based; portable.~600
notifyutil/notifyutil.c~700Port. The CLI is mostly libnotify-call-and-print; once libnotify works the CLI follows.~700
notifybench/~500Port. Useful for verifying state-page reads stay zero-IPC after our shm_open swap.~500
Manpages (notify*.3, notifyd.8, notifyutil.1)Keep intact, incl. notify_register_mach_port.3.same

Total post-port: roughly 16-17k LOC, ~Apple's pre-port size. On the v2 (Mach-IPC) track we keep nearly all of Apple's source unchanged; only the Mach-VM shared-page handoff is substituted with POSIX shm. Compare: the abandoned v1 (AF_UNIX/DO) plan would have rewritten ~30% of the source to swap the wire layer.

7. FreeBSD port: substrate parity with Apple (v2 / Mach track)

FeatureApple's notifyd doesThis port (freebsd-launchd-mach)
Daemon-client IPCMach ports + MIG-generated stubs (two flavors: current + legacy)Identical: Mach ports + MIG-generated stubs (both notify_ipc.defs + notify_old_ipc.defs retained, generated via FreeBSD's mig(1)). Substrate provided by mach.ko + libxpc + libdispatch's Mach backend.
State-value pagesMach vm_allocate + port handoff for shared memoryPOSIX shm_open(2) + mmap(2). The one substitution. Same zero-IPC-read characteristic; portable across any Unix; matches Apple's user-visible semantic.
Subscriber wakeup (NOTIFY_TYPE_PORT)Mach mach_msg to registered portIdentical. Native Mach mechanism on the v2 substrate.
Subscriber wakeup (NOTIFY_TYPE_DISPATCH)Mach port message → libdispatch sourceIdentical. Subscriber holds a libdispatch DISPATCH_SOURCE_TYPE_MACH_RECV source; daemon sends a Mach message.
Path notificationskqueue under the hood (already!)kqueue. Identical.
Subscriber bookkeepingMach send-rights tracked per-client; subscriber-died detected via Mach port-no-senders notificationBoth retained: Mach port-no-senders notification on the v2 substrate (the natural mechanism), augmented with DISPATCH_SOURCE_TYPE_PROC on each subscriber's PID as a belt-and-suspenders catch for clients that crash mid-message.
Build-system gatesiOS / sim / catalyst #ifDelete. One target.

8. Use cases for gershwin

What we get when notifyd lands and gershwin's apps can use it:

8.1 Workspace (the desktop manager)

ConcernNotification nameEffect
Theme / appearance changeorg.freebsd.appearance.theme-changedAppearance 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 / focusorg.freebsd.workspace.app-activatedOther apps de-emphasize selves (dim accent colors, pause animations).
Hide / show all (NeXTSTEP signature)org.freebsd.workspace.hide-othersWorkspace single-post; all subscribed apps minimize.
Display configuration changedorg.freebsd.display.changedconfigd or kmodloader detects framebuffer plug; posts. Workspace reflows windows; full-screen video apps react.
Login / logoutorg.freebsd.session.user-logged-in / ...logged-outPer-user launchd agents (mail-checker, dock helpers, calendar-sync) wake. Mirrors Apple's loginwindow flow.

8.2 System-state fanout

ConcernNotification nameEffect
Battery / powerorg.freebsd.power.battery-changedOne 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 stateorg.freebsd.config.network-reachableconfigd 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 / wakeorg.freebsd.power.sleep-requested + ...wakeApps save state, pause downloads, dim UI before sleep; resume after wake.
Time / TZ changeorg.freebsd.time.timezone-changedCalendar, Mail, Clock all subscribe. Currently apps poll or recompute on every operation.
Filesystem mount / unmountorg.freebsd.fs.mounted / ...unmountedWorkspace's File Viewer (Finder-equivalent) updates the sidebar. ZFS pool import — mount-aware apps refresh.
Keyboard layout changeorg.freebsd.input.layout-changedApps refresh shortcut displays.
Audio volume / muteorg.freebsd.audio.volume-changedSound prefs slider follows hardware media keys; menubar volume icon updates.

8.3 Defaults / preferences plumbing

This is where notifyd shines compared to alternatives. NSUserDefaults cross-app coordination on macOS is a notifyd story:

  1. App A calls [defaults setObject:newValue forKey:@"FontFamily"]
  2. cfprefsd-equivalent (the prefs daemon) writes the file, posts org.freebsd.prefs.<domain>.changed
  3. App B (subscribed) gets woken, reloads the relevant key
  4. App B's UI updates without a restart

Without 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.

8.4 launchd-event-driven service activation

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.

8.5 Third-party Apple-derived apps "just work"

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.

9. launchd integration

9.1 The plist

<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
    <key>Label</key>             <string>com.apple.notifyd</string>
    <key>ProgramArguments</key>  <array><string>/usr/libexec/notifyd</string></array>
    <key>RunAtLoad</key>         <true/>
    <key>KeepAlive</key>         <true/>
    <key>MachServices</key>      <dict>
        <key>com.apple.notifyd</key>        <true/>
        <key>com.apple.system.notification_center</key> <true/>
    </dict>
</dict>
</plist>

launchd registers the Mach service names on notifyd's behalf; clients bootstrap_look_up them and obtain a send right; notifyd calls bootstrap_check_in at startup to receive the receive right. Pending client messages queue on the port even when notifyd is restarting.

9.2 Boot ordering

notifyd has no dependencies on other daemons — it's a leaf service. ASL's libsystem_asl uses notifyd internally for fan-out (every log write posts com.apple.system.logger); configd, PowerManagement, IPConfiguration, DiskArbitration, IOKitUser all post / register for named events. Order it alongside other system daemons; no explicit ordering needed (launchd's bootstrap machinery handles "client looks up service before service has checked in" via held messages).

10. Licensing

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).

SourceLicenseHow we handle it
Apple Libnotify-98.5 (per-file APSL headers)APSL 1.1 / 2.0 mixKeep 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 (FreeBSD shims, build glue, shm-replacement of vm_allocate)BSD-2-ClauseSPDX header on each new file.
libdispatch, libxpc, libCoreFoundation (in-tree, in src/)Apache 2.0 / APSLListed in NOTICE. Same calculus as launchd/configd/ASL.

11. Phased delivery

On the v2 (Mach-IPC) track notifyd ships as part of the ASL block — see the ASL plan's Phase J1/J2 for the joint build. Reproduced here for completeness:

Phase J1 — libnotify SHIPPED

Phase J2 iter 1 — notifyd daemon SHIPPED

Phase J2 iter 2+ — pathwatch + notifyutil + launch-events FOLLOW-UP

Phase K+ — downstream consumers IN-FLIGHT

12. Open questions

Q1. Atomic uint64 across architectures. POSIX shm + atomic store works on amd64 native. arm64 + 32-bit hosts we'd want to verify torn-read semantics — atomic_store_explicit(memory_order_relaxed) is the right primitive but compiler / target-arch-specific. Phase 2 testing covers.
Q2. Coexistence with NSDistributedNotificationCenter. GNUstep ships 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.
Q3. Naming convention for project-emitted events. Apple uses 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.
Q4. Apple's 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.
Q5. State-page location and quotas. POSIX shm names default to /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.
Q6. Bootstrapping order with launchd. notifyd needs to be running before 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; the launchd bootstrap machinery holds client bootstrap_look_up("com.apple.notifyd") calls until notifyd has called bootstrap_check_in. Same pattern as configd / WindowServer / any other Mach-service-providing daemon.
Q7. Per-user notifyd vs single system-wide daemon. Apple runs one notifyd per session domain (system + each gui/<uid>). For our scope: start with system-wide only. Per-user notifyd waits until per-user launchd is implemented (post-Phase 6 of the launchd plan).

13. References


Revision 2026-05-23. Refactored from the v1 (AF_UNIX / GNUstep DO) target to the v2 (freebsd-launchd-mach / Mach-IPC) target. notifyd SHIPPED in Phase J1 (libnotify) + Phase J2 iter 1 (notifyd daemon) under src/Libnotify/; MIG client/server stubs from notify_ipc.defs, libdispatch DISPATCH_SOURCE_TYPE_MACH_RECV on the service port, POSIX shm_open+mmap the sole substitution for Apple's vm_allocate+port-handoff. NOTIFY_TYPE_PORT retained (it was dropped in the v1 plan). Companion ASL plan at freebsd-asl-plan.html.