freebsd-libxpc — porting Apple XPC onto our mach.koA porting plan for libxpc on FreeBSD, dependent on the now-working freebsd-launchd-mach Mach kmod. XPC is the userland abstraction that every Apple system daemon expects: connection-based IPC with typed dictionaries, dispatch-queue event delivery, and named launchd-bootstrap lookups. Without it, the daemons we want to port (launchd's modern paths, configd, asl, mDNSResponder) have no client surface. This plan surveys the source landscape, maps the dependencies, scopes a launchd-only MVP, and assesses how complete the related daemon ports already are.
Primary goal: drag FreeBSD into the 21st century — modern service IPC, modern daemon model, modern network config. macOS app compatibility is a downstream bonus. GNUstep stays for the application / framework layer in the gershwin desktop overlay; Apple source covers only the lower system-services layer (this ISO). The CoreFoundation engine for system services is swift-corelibs-foundation's pure-C CF, not GNUstep libs-corebase — see launchctl-corefoundation-spike for the audit.
Note on §9.5 / §9.6 / §11 below. These sections originally recommended a hybrid CoreFoundation built from GNUstep libs-corebase + a supplementary libCFRuntime ported from CF-Lite-1153.18. That recommendation is superseded. The current authoritative source for the CF question is freebsd-launchctl-corefoundation-spike, which audited the three candidate CF implementations (libs-base, libs-corebase, swift-corelibs-foundation) against actual launchctl.c usage and found that swift-corelibs-foundation's pure-C CF (built with DEPLOYMENT_RUNTIME_SWIFT=0) is the only viable choice — libs-corebase's plist parser is stubbed, which is fatal for launchctl. The remainder of this libxpc plan — what libxpc does, how it fits with launchd, the Mach integration — remains accurate; only the CF-source recommendation has changed.
XPC is Apple's high-level inter-process communication framework, layered on top of Mach IPC and libdispatch. It has two faces:
xpc_dictionary_t, xpc_array_t, xpc_string_t, xpc_int64_t, xpc_object_t base type), file-descriptor passing, asynchronous event delivery via dispatch queues, and a connection-cancellation lifecycle.NSSecureCoding for arbitrary classes. Out of scope here — GNUstep keeps the framework layer.On macOS, libxpc is invoked by basically anything that talks to a system service: apps talking to configd, mDNSResponder, opendirectoryd, tccd; app extensions; helper tools; launchd activating services on-demand because client connections come in via XPC bootstrap lookups. The whole on-demand-activation story for modern daemons routes through libxpc.
Under the hood it uses Mach IPC for transport, libdispatch for event delivery, and bplist for serialization. The NextBSD / ravynOS reimplementation we inherit substitutes FreeBSD's libnv for bplist — a fundamental wire-format departure that affects nothing on the API surface but means we will never bit-exchange messages with Apple binaries.
libxpc is the glue between mach.ko (now done, Phase B Tier 1 complete) and the daemon layer we actually want to ship. Without libxpc:
freebsd-launchd/configd tree carries 452 xpc_* call sites — the whole client API depends on xpc_connection_create_mach_service.core.c we inherit calls xpc_pipe_try_receive in its main dispatch loop.SCDynamicStore-style notification, every entitlement check, every privilege-separated helper expects XPC.Skipping libxpc means rewriting every daemon's IPC layer in something else — D-Bus, hand-rolled Mach, or AF_UNIX. Ugly either way, and it cuts us off from drop-in source ports of Apple system services.
The lineage of the libxpc implementation we'd actually fork from:
Apple public headers (xpc.h, connection.h: copyright 2009-2011 Apple Inc.)
|
v
iXsystems / Jakub Klama 2014-2015 skeleton C reimplementation
(copyright "2014-2015 iXsystems, Inc." on every .c)
|
v
NextBSD (iXsystems-sponsored, Jordan Hubbard + Kip Macy, 2014-2016)
* lib/libxpc/ + lib/libdispatch/ + lib/liblaunch/ + sbin/launchd/
* github.com/NextBSD/NextBSD — dormant since early 2016
|
v
airyxOS (~2021-2022)
|
v
ravynOS (2022-present, history squashed 2025-09-28)
* Same skeleton, with substantive object-model cleanup
* github.com/ravynsoft/ravynos
Apple has never released libxpc as source. The last published launchd was launchd-842.92.1 in 2014; from OS X 10.10 onward, launchd's job-management logic was absorbed into the closed-source libxpc. Apple's Libsystem repo (apple-oss-distributions/Libsystem, latest tag Libsystem-1356 2025-10-04) lists xpc as a binary dependency on line 28 of requiredlibs — that is the most public mention you get. The headers /usr/include/xpc/*.h ship in the macOS SDK as declarations only; no implementation.
So when we say "port libxpc" we mean port the iXsystems / NextBSD skeleton — built against Apple's public headers but with the implementation written from scratch by a different team. We are not forking an Apple codebase. There is no opensource Apple libxpc to fork.
Small in raw LoC, substantial in semantics. Both trees are at lib/libxpc/ with identical 7-header layout and a shared bundled libnv. diff -rq shows 9 differing files; the XPC-specific changes ravynOS made on top of NextBSD (mostly around 2022, since squashed):
xpc_connection an embedded member of struct xpc_object with proper retain/release. NextBSD mallocs a bare struct. ravynOS routes everything through _xpc_prim_create(_XPC_TYPE_CONNECTION, ...). Pre-requisite for real lifecycle management.xpc_pack calls and obviously wrong message sizes; ravynOS actually calls xpc_pack(xo, NULL, &size) to measure, then xpc_pack to fill, frees properly. Real bug fix.xpc_fd_create / xpc_fd_dup via FreeBSD <sys/fileport.h> (fileport_makeport/fileport_makefd). NextBSD had these commented out.#ifdef AUDIT_BSM guards, xpc_dictionary_create_reply type checking.NextBSD's own commit messages call its tree a "first pass" — one says verbatim "paper over disastrous state of libxpc by disabling Werror". ravynOS is closer to working code. Decision: fork from ravynOS, not NextBSD.
Concrete answers to "what sources can we get it from?":
| Source | What's there | Verdict |
|---|---|---|
Apple opensource.apple.com |
Public headers only (xpc.h, connection.h, etc. via macOS SDK). No implementation, ever. Libsystem repo links xpc as binary. |
unusable for implementation |
github.com/apple-oss-distributions/launchd |
Last tag launchd-842.92.1, 2014-08-13 (OS X 10.9.x). Pre-libxpc-split. No XPC source. |
historical reference only |
ravynOS lib/libxpc/ |
iXsystems skeleton + 2022-era cleanup. ~2,348 LoC of XPC logic + ~4,077 LoC bundled libnv. ~78 functions implemented across 5 .c files. FreeBSD bsd.lib.mk compatible. |
fork from this |
NextBSD lib/libxpc/ |
Same skeleton, dormant since 2015. ~90 LoC behind ravynOS but semantically much less polished. | historical |
| darling-libxpc | NextBSD fork extended for Darling. Useful for newer activity-key declarations and as API cross-check. | reference |
Did Apple ever open-source libxpc? No. Public headers, yes; implementation, never. Every "libxpc" you can find on GitHub is descended from the iXsystems/Klama 2014-2015 reimplementation.
Did iXsystems contribute the source? Yes — via NextBSD, sponsored by iX (Jordan Hubbard was CTO at the time). Every .c file in both NextBSD and ravynOS trees carries Copyright 2014-2015 iXsystems, Inc. The implementation was written from scratch against Apple's public headers. ravynOS inherited from NextBSD (via airyxOS).
Public headers under ravynos/lib/libxpc/xpc/ — 7 files, 169 XPC_EXPORT decorations total. Categorized:
| Header | Lines | Surface |
|---|---|---|
base.h |
165 | Attribute macros only (XPC_EXPORT, XPC_NONNULL*, XPC_MALLOC, XPC_INLINE). No functions. The bootstrap header. |
xpc.h |
2531 | The umbrella header: refcount + type system (xpc_retain/release/copy/equal/hash/get_type/copy_description), XPC_TYPE_* globals, primitive constructors (null, bool, int64, uint64, double, date, data, string, uuid, fd, shmem), 24 array APIs, ~30 dictionary APIs, service-runtime (xpc_main, xpc_transaction_begin/_end). 113 exports. |
connection.h |
718 | Connection lifecycle: xpc_connection_create, xpc_connection_create_mach_service, xpc_connection_create_from_endpoint, set_target_queue/set_event_handler/suspend/resume/send_message/send_barrier/send_message_with_reply[_sync]/cancel, peer-info (get_euid/get_egid/get_pid/get_asid), context/finalizer, error dictionaries. 24 exports. |
activity.h |
427 | Background scheduling (xpc_activity_register, criteria dictionaries, defer/state). 29 exports, stubbed/unimplemented in this tree (no xpc_activity.c). |
endpoint.h |
22 | One function: xpc_endpoint_create. |
debug.h |
23 | xpc_debugger_api_misuse_info. |
launchd.h |
24 | Project-specific surface that substitutes Apple's private xpc/private.h: xpc_strerror, xpc_copy_entitlement_for_token, xpc_pipe_routine_reply, xpc_pipe_try_receive, xpc_call_wakeup, xpc_dictionary_{set_mach_recv,set_mach_send,copy_mach_send}, xpc_dictionary_get_audit_token, xpc_copy_entitlements_for_pid, ld2xpc(launch_data_t). |
Net real entry-point count: ~70. The 169 raw export count is misleading — ~100 of those are constants, error-dictionary globals, and type-token symbols. The 78 functions implemented in the .c files map to roughly the right surface for an MVP.
libxpc cannot exist without libdispatch underneath. Connection event handlers run on dispatch queues; message delivery uses dispatch_source_create with DISPATCH_SOURCE_TYPE_MACH_RECV. From the ravynOS tree, libxpc calls:
dispatch_queue_create, dispatch_get_main_queue, dispatch_get_global_queue, dispatch_async, dispatch_sync, dispatch_main, dispatch_release, dispatch_suspend, dispatch_resume, dispatch_set_context.dispatch_source_create with DISPATCH_SOURCE_TYPE_MACH_RECV, dispatch_source_set_event_handler_f.dispatch_semaphore_create/_signal/_wait, DISPATCH_TIME_FOREVER.Block_copy, Block_release, dispatch_block_t.Per the gershwin-on-freebsd integration, libdispatch is already shipping via gershwin-developer at /System/Library/Libraries. Critical verification needed: the libdispatch we have must actually wire DISPATCH_SOURCE_TYPE_MACH_RECV to Mach ports, not just expose the symbol. If gershwin's libdispatch was built for a pre-Mach FreeBSD, we may need to rebuild it against our mach.ko-exposed primitives.
libxpc calls into Mach from xpc_connection.c and xpc_misc.c. Status against what freebsd-launchd-mach currently wires:
| Mach call | Status in mach.ko | Action needed |
|---|---|---|
mach_task_self() | wired as task_self_trap via libmach | none |
mach_msg_send / mach_msg | wired as mach_msg_trap via libmach (6-arg) | verify MACH_RCV_TRAILER_AUDIT trailer population |
mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &p) | not wired | add _kernelrpc_mach_port_allocate_trap |
mach_port_insert_right(task, name, port, type) | not wired | add _kernelrpc_mach_port_insert_right_trap |
mach_port_deallocate(task, port) | not wired | add _kernelrpc_mach_port_deallocate_trap |
task_get_special_port / task_set_special_port | not wired | typically MIG RPC over task port, not direct trap; needs liblaunch-side stub |
bootstrap_check_in / bootstrap_look_up | not wired | biggest gap: needs a userspace bootstrap server (launchd's bootstrap subsystem) plus MIG-generated _bootstrap_* stubs plus a bootstrap_port task-special-port slot |
Bottom line: we have messaging working from userland. We do not yet have port management from userland, and we do not have a name service at all. The bootstrap-server gap is the single biggest piece — it implies a launchd-Mach-helper running as PID 1 (or thereabouts) that owns the bootstrap port and answers bootstrap_look_up calls. The good news: NextBSD/ravynOS have a working bootstrap server we can inherit.
The NextBSD/ravynOS libxpc uses FreeBSD's libnv (nv-list) for wire serialization, not Apple's bplist. The two libnv files (subr_nvlist.c 2,572 lines, subr_nvpair.c 1,505 lines) are bundled inside libxpc in both trees — they dominate the line count (~62% of total). This is a design departure: XPC objects get converted to nvlist, packed via nvlist_pack_buffer, sent over Mach.
Implication: we can never bit-exchange messages with a real macOS app. That's fine for our use case — everything in the stack will be FreeBSD-native binaries. The compatibility we keep is at the source level (Apple-shape API) not the wire level.
audit_token_to_au32, audit_token_t, au_asid_t. Gated behind #ifdef AUDIT_BSM. FreeBSD has libbsm; should work.fileport_makeport / fileport_makefd for sending fds via Mach. Apple-private syscalls. Need a FreeBSD shim — either extend mach.ko to expose equivalent traps, or use Unix-domain SCM_RIGHTS as a fallback transport.vproc_transaction_begin/_end, launch_data_t (for ld2xpc). The legacy liblaunch API is what freebsd-launchd already has working; integration is straightforward.sbuf, TAILQ_*, FreeBSD-native uuid_to_string, atomic_* from <machine/atomic.h>. Already what NextBSD/ravynOS use; no change needed.The realistic libxpc-for-launchd surface, by tier:
Types: xpc_object_t, xpc_type_t, xpc_dictionary, xpc_array, xpc_string, xpc_int64, xpc_uint64, xpc_bool, plus the _xpc_type_* global symbols.
Lifecycle / introspection: xpc_retain, xpc_release, xpc_get_type, xpc_copy, xpc_copy_description, xpc_strerror.
Dictionary: xpc_dictionary_create, xpc_dictionary_create_reply, xpc_dictionary_get_value/set_value, get_string/set_string, get_int64/set_int64, get_uint64/set_uint64, get_bool.
Array: xpc_array_create, xpc_array_append_value, xpc_array_set_string, xpc_array_set_uint64.
Scalars: xpc_bool_get_value, xpc_uint64_get_value, xpc_int64_get_value.
Mach-tied (depends on mach.ko expansion): xpc_dictionary_set_mach_send, set_mach_recv, copy_mach_send, xpc_dictionary_get_audit_token, xpc_pipe_try_receive, xpc_pipe_routine_reply. These are the chokepoint — the entire NextBSD launchd dispatch loop in runtime.c runs on xpc_pipe_try_receive against a Mach port set.
The major addition is the xpc_connection client API: xpc_connection_create_mach_service, set_event_handler, resume, send_message, send_message_with_reply_sync, cancel, get_pid/euid/egid, set_target_queue, set_context/get_context, set_finalizer_f, xpc_dictionary_get_remote_connection. Plus xpc_create_from_plist, xpc_data_create/get_bytes_ptr/get_length, xpc_dictionary_set_data/get_data, set_uuid/get_uuid, set_bool, xpc_dictionary_apply, get_count, xpc_connection_copy_entitlement_value.
NextBSD's lib/libxpc/xpc_connection.c (501 lines) is the natural starting point and already speaks nvlist-over-Mach.
xpc_process_attach/detach, xpc_process_set_jetsam_band/memory_limit) — Apple kernel-level memory pressure hooks. No FreeBSD analog. Stub to ENOTSUP.xpc_transaction_begin/_end, xpc_track_activity, xpc_set_event_stream_handler) — power-management state machine. Safe to stub as no-ops.xpc_domain_check_in, xpc_domain_load_services, xpc_singleton_domain, etc.) — implemented inside launchd, not in libxpc proper. Not blockers.xpc_event_channel_check_in/look_up) — same, launchd-internal.Honest summary: ~35 functions for launchd-only, ~60 for launchd+configd, ~70 for everything our daemon stack actually needs. NextBSD's tree at ~7k LoC (including bundled libnv) is roughly the right size. The cost is dominated not by libxpc proper but by the prerequisites: (a) bootstrap server + bootstrap_port; (b) Mach port-right management traps in mach.ko; (c) audit-trailer support; (d) fileport shim; (e) verified libdispatch Mach-receive source.
To right-size the libxpc effort it helps to know how complete the daemons we'd build on top are. Three audited here.
Reference Apple launchd-842.92.1 at apple-oss-distributions/launchd — the last open source, 2014. src/core.c alone is ~12k lines. Pre-libxpc-split: even Apple-2014 is behind modern macOS launchd, where job management migrated into closed libxpc.
| Tree | LoC (launchd proper) | Mach / MIG | Stub markers | State |
|---|---|---|---|---|
freebsd-launchd (this project's port) |
1,496 + 1,467 liblaunch | none (intentional) | 2 (documenting #if 0 for the corebase plist parser) |
partial — clean rewrite on libdispatch + AF_UNIX, boots, ~7% of Apple's body, supports daemon subset for FreeBSD as PID 1. Uses GNUstep's NSPropertyListSerialization for plist parsing. |
| NextBSD | 16,391 + 4,661 liblaunch | full (8 MIG .defs) | 34 | near-Apple-2014-parity on NextBSD's xnu-derived kernel. Won't build on stock FreeBSD. Dormant since 2016. |
| ravynOS | 16,469 + 4,661 liblaunch | full | 36 | NextBSD + ~150 lines of FreeBSD-15 fixes: <Availability.h> include, build flags, audit-session workaround, per-user env file. Squashed history (2025-09-28), so per-change provenance gone. |
Critical missing pieces from freebsd-launchd: EnvironmentVariables, StartCalendarInterval, WorkingDirectory, UserName/GroupName, StandardOut/ErrorPath, Umask, ThrottleInterval, launchctl load -w, enable/disable, override-file mechanism. Per freebsd-launchd-plan's matrix — Phase 5 batch is the remaining work.
Decision-critical: The NextBSD/ravynOS launchd is closer to Apple parity but only runs on a Mach-coupled kernel. Now that our mach.ko exists, we have two ways to swap forward to the Apple-shape launchd:
#if 0/FIXME bisect markers.launchd-842.92.1 — same source code as the ravynOS path (everyone descends from this), but with clean provenance, our own patch series, and no inherited cruft. Same model as our configd / libdispatch / libxpc imports. Chosen approach — see Phase 5 below.Either way, the swap is enabled by libxpc and jumps us from 1,500 LoC of hand-coded subset to 16k+ LoC of battle-tested Apple-shape code.
Reference Apple apple-oss-distributions/configd — latest tag configd-1405.100.8 (2026-04-13). The framework (SystemConfiguration) lives inside the configd repo, not a separate one.
| Tree | Source present? | State |
|---|---|---|
freebsd-launchd/configd/ |
Yes — verbatim Apple configd-963.270.3 (133K LoC, 280 source files) |
demo / proof-of-concept — sources imported but only a 95-line GNUstep Obj-C banner stub (NCDaemon.m) compiles. The 133K-LoC Apple tree sits beside it, uncompiled. |
| ravynOS | No | absent — ravynOS has a rich frameworks/ tree (CF, AppKit, Foundation, CFNetwork) but no SystemConfiguration. |
| NextBSD | No | absent — never imported configd. |
Subsystems present in the imported tree: SCDynamicStore (the key-value store), SCPreferences (plist-backed config), SCNetworkConfiguration/Service/Interface/Protocol/Set/Reachability, IPMonitor (19,181 LoC), KernelEventMonitor (FreeBSD ev_ipv4/ipv6/dlil bridge), LinkConfiguration, PreferencesMonitor, scutil CLI, dnsinfo, nwi, libSystemConfiguration (XPC client helper).
Gap to functional:
.defs missing from the import — configd_server.c, notify_server.c, _SCD.c all expect generated stubs.xpc_* call sites). This is why libxpc is the unblocker for configd.bootstrap_* / mach_port_* sites need real Mach support.bmake / cmake build for libSystemConfiguration.so, SystemConfiguration.framework, configd daemon, scutil. Currently zero of 280 source files are wired into the build.PrivateStrings.plist, NetworkConfiguration.plist) need install rules.ev_ipv4.c/ev_ipv6.c) already uses PF_ROUTE/sysctl — FreeBSD-friendly, minor adapting only.963.270.3 (~2020) misses 6+ years of bugfixes / new subsystems (CategoryManager, netconfig, QoSMarking). Jumping to 1405.100.8 adds those but expands surface.The good news: Apple's code in this tree is implemented, not stubbed (0 UNIMPLEMENTED, 1 TODO, 12 XXX). It's a verbatim Apple import. What's missing is everything around it: build glue, libxpc backend, MIG .defs, kernel-event-monitor adapters, launchd plist, the netconfigd driver that wires it together. Per freebsd-configd-plan's phasing.
Reference Apple apple-oss-distributions/bootp — latest tag bootp-534.100.6 (2026-04-17). The tarball ships multiple binaries: IPConfiguration (DHCPv4/v6/IPv4LL/RA/SLAAC client), IPConfiguration_framework, IPConfigurationHelper (XPC), ipconfig CLI, bootplib, bootpd (server, replaces CMU's), dhcp6d, rtadvd, NetBoot/BSDP. Total ~74k LoC for the in-scope client-side surface.
| Tree | Source present? | State |
|---|---|---|
freebsd-launchd |
No (Apple-side) | paper plan only — freebsd-ipconfiguration-plan exists with DEFERRED pill. Today's livecd uses dhcpcd (Roy Marples) via org.freebsd.dhcpcd.plist. configd/src/SystemConfiguration.fproj/DHCP.c exists but it's the client-side SC API that reads DHCP info from the dynamic store — not a client. |
| ravynOS | No | absent — uses CMU bootpd + standard FreeBSD dhclient. |
| NextBSD | No | absent — same as ravynOS. |
Apple deps that don't exist on FreeBSD (per freebsd-ipconfiguration-plan §7):
IORegisterForSystemPower, IOPMConnectionCreate, etc. Sleep-coordinated lease renewal is dropped in v1.wpa_supplicant control-socket reader."FreeBSD".#ifdef __APPLE__ out.bootplib/ipconfig.defs (21 routines) plus server.c (654 LoC) plus three client TUs. Plan: GNUstep Distributed Objects over AF_UNIX, or routed via libxpc once we have it.timer.c + FDSet.c ~494 LoC). Already available.Recommendation: follow the existing plan — defer IPConfiguration porting until configd Phases 2/3 are real and libxpc is in place. For the livecd today, dhcpcd covers DHCPv4 + DHCPv6 + IPv4LL adequately and is mature/cross-platform. The estimated ~6-9 person-month cost of a full IPConfiguration port is best spent after configd's SCDynamicStore is functional, because IPConfiguration consumes SC keys for half its decisions.
Project rule from scope_split: GNUstep owns the framework / application layer (libobjc2, Foundation, Base, GUI, Back). Apple source covers only the system-services layer (Mach, launchd, configd, notifyd, asl, libdispatch, libxpc, liblaunch). The userland stays ELF, not Mach-O. Question: does libxpc's port fit cleanly inside that rule, or does it force adopting Apple's ObjC runtime / Foundation?
libxpc itself fits cleanly. The fork we're using is pure C with zero Objective-C runtime dependency. GNUstep stays on the application side. You will not have to use Apple's libobjc.
But the surrounding stack has real gaps that need work. Audited 2026-05-11 against the gershwin-developer/Library/Sources/ tree:
HAVE_MACH is autodetected from <mach/mach.h> presence, off on FreeBSD, so DISPATCH_SOURCE_TYPE_MACH_RECV is unresolved at link. Needs a new backend (see §9.4 below).The ravynOS / NextBSD libxpc implementation (lib/libxpc/) compiles as plain C. From the API-surface audit:
xpc_object_t is just void * when OS_OBJECT_USE_OBJC is undefined. The <os/object.h> header is pulled in only for ObjC ARC builds. Our build sets OS_OBJECT_USE_OBJC=0 and gets pure C..c file. No CFStringRef, no CFDictionaryRef, no CFRunLoop.NSPropertyListSerialization is not in the picture.NSXPCConnection. The Objective-C connection wrapper from Apple's Foundation is out of scope; scope_split excludes adopting Apple's Foundation.So a libxpc consumer written in pure C (launchd, configd, asl, mDNSResponder) links -lxpc and never touches an Obj-C runtime through libxpc.
A GNUstep / libobjc2 application that wants to talk to a launchd-managed service has two options:
xpc_connection_create_mach_service, set an event handler, send dictionaries. This is what launchd-managed daemons already use internally. No Foundation bridge needed.NSXPCConnection is Apple's Objective-C wrapper around libxpc. It provides protocol-based proxy objects, NSSecureCoding for arbitrary classes, automatic reply blocks, static type-checking at compile time, and invalidation / interruption handlers. Strictly an ergonomic layer over libxpc — doesn't add new system capability, but makes Cocoa code 10x more readable than raw libxpc message-construction.
Apple has never open-sourced NSXPCConnection. swift-corelibs-foundation deliberately skips it (no libxpc on Linux/Windows). It exists only inside macOS's closed-source Foundation.framework. "Importing" the class is not an option.
A clean-room reimplementation against the public Apple API documentation IS possible — same approach GNUstep has used for the rest of Foundation for ~30 years. Estimated scope: 3,000-5,000 lines of new Objective-C across NSXPCConnection, NSXPCInterface, NSXPCListener, the forwardInvocation:-based proxy machinery, the reply-block trampolines, and the NSSecureCoding ↔ xpc_object_t bridge.
Why not in this plan:
scope_split, GNUstep owns the Foundation layer. Adding new Foundation classes belongs upstream in gnustep-base, not in freebsd-launchd-mach.What it unlocks downstream:
NSConnection / NSDistantObject (the NeXTSTEP-era predecessor to NSXPCConnection — same "proxy objects over IPC with type-safe protocols" idea, different transport). NSXPCConnection is the migration target Apple chose.[[conn remoteObjectProxy] doThing:arg] instead of constructing xpc_dictionaries by hand.How to encourage it: open an enhancement issue in gnustep/libs-base citing the libxpc availability on FreeBSD and the protocol/proxy machinery already present in NSDistantObject. Once the libxpc port stabilizes on FreeBSD and Darling-libxpc keeps improving on Linux, the case for a clean-room NSXPCConnection in upstream GNUstep gets stronger. Sequence-wise it doesn't matter when this lands — libxpc's C API works on its own, and the NSXPCConnection wrapper can come along whenever a GNUstep maintainer takes interest.
Audit of gershwin-developer/Library/Sources/swift-corelibs-libdispatch/:
apple/swift-corelibs-libdispatch. No Gershwin patches. Branch main, clean working tree.HAVE_MACH is the single switch, set at CMakeLists.txt:211 by check_include_files("mach/mach.h" HAVE_MACH). Autodetected from header presence only; no user-facing flag. On FreeBSD default: HAVE_MACH is off.DISPATCH_SOURCE_TYPE_MACH_RECV is declared in dispatch/source.h:128 (decorated only with DISPATCH_LINUX_UNAVAILABLE(), not FreeBSD-unavailable). The underlying object _dispatch_source_type_mach_recv is defined exactly once, in src/event/event_kevent.c:3249, inside an #if HAVE_MACH block. No #else fallback, no kqueue emulation, no ENOTSUP path.src/event/event_kevent.c uses EVFILT_MACHPORT (a Darwin-only kevent filter, not in FreeBSD's sys/event.h). src/mach.c (3,250 lines, entirely #if HAVE_MACH) assumes the full XNU Mach ABI: MACH_NOTIFY_DEAD_NAME, MACH_NOTIFY_SEND_POSSIBLE, vouchers, MIG, mach_port_construct. None of which we have in mach.ko.Path forward, ranked:
src/event/event_mach_freebsd.c, gated on __FreeBSD__. Define _dispatch_source_type_mach_recv using either: (a) a polling thread per source that calls mach_msg_trap(MACH_RCV_MSG | MACH_RCV_TIMEOUT) with timeout=0 and a wakeup mechanism, or (b) a kqueue user filter that the kernel signals when a Mach message arrives (would need an EVFILT_MACHPORT shim in mach.ko). Skip src/mach.c entirely — libxpc only uses DISPATCH_SOURCE_TYPE_MACH_RECV, not the dispatch_mach_t channel SPI.EVFILT_MACHPORT to FreeBSD kqueue in mach.ko, then flip HAVE_MACH=1. Still need stubs for mach_port_construct, dead-name notifications, vouchers — most of src/mach.c won't link without them. Multi-week scope.Phase 0 verification (the simple "does it work?" test) won't pass as-is. Gershwin's libdispatch needs at minimum a new backend file (Path 1 above), which is ~500-1000 lines of code. This is real work, not a build-flag flip. Plan accordingly.
Audit of gershwin-developer/Library/Sources/libs-corebase/ vs freebsd-launchd/configd/ (the 126,707-LoC verbatim Apple configd import):
libs-corebase scope: GNUstep CoreBase, version 0.2, LGPL. Standard GNUstep build (configure.ac, Headers/CoreFoundation/, Source/). Last meaningful activity 2021-09 / 2022-09 — primarily Windows build fixes and ICU API updates. Stable, partial-by-design, not actively developed.
| CF area | corebase | configd impact |
|---|---|---|
| CFString / CFDictionary / CFArray / CFNumber / CFData / CFDate / CFTimeZone / CFCalendar | PRESENT ICU-backed, ~5,500 String calls / 1,900 Dict / 1,800 Array in configd should all link | Compiles |
| CFPropertyList (XML + binary plist) | PRESENT 1561 lines, both directions | SCPreferences plist parsing works |
| CFURL / CFCharacterSet / CFUUID / CFXMLParser / CFLocale / CFNumberFormatter | PRESENT | Fine |
| CFRunLoop | PARTIAL sources0 + timers + observers work; CFRunLoopProcessSourcesVersion1 is a literal TODO stub at CFRunLoop.c:567-585 | BREAKS configd IPC — server_loop() in configd_server.c won't dispatch Mach messages |
| CFMachPort | ABSENT no header, no source | BREAKS configd IPC — 80 calls across configd_server.c, session.c, _configopen.c, dns-configuration.c, SCDPlugin.c, SCDPrivate.c, SCHelper_server.c. ~9 functions need implementing (Create, CreateWithPort, CreateRunLoopSource, GetPort, Invalidate, ...). |
| CFPreferences | ABSENT | BREAKS SCPreferences / PreferencesMonitor plugin — ~22 calls |
| CFBundle | PARTIAL 268-line NSBundle-backed shim; ~22 functions but missing CFBundleCreateBundlesFromDirectory, CFBundleCopyLocalizedString, CFBundleCopyNonLocalizedString, _CFBundleCachedInfo* | BREAKS configd plugin loader in plugin_support.c. Also drags in GNUstep ObjC base as transitive dep — contradicts "configd is plain C against CF". |
| CFPlugIn / CFUserNotification / CFHost / CFHTTPMessage / CFXPC | ABSENT | Peripheral subsystems — can #ifdef out for headless configd |
| CFNotificationCenter | ABSENT | configd does not use it — N/A |
Plus Apple-private CF extensions used by configd that exist nowhere open-source except Apple's CF-Lite drop: CFStringIsValidNetBIOSName, CFStringIsValidDNSName, CFStringCreateWithInAddr, CFStringCreateWithIn6Addr, CFDataCopyVMData, CFDataCreateVMData, CFStringGetInstallationEncodingAndRegion, CFXPCCreateCFObjectFromXPCObject, _CFBundleCachedInfo*.
Use libs-corebase for the value-type half (String/Dict/Array/Number/Data/Date/PropertyList/URL/Calendar/Locale). Pull CFMachPort.c, CFRunLoop.c (or just version1 dispatch), CFBundle.c, CFPreferences.c from Apple's last open-source CF-Lite (CF-1153.18, ~2017, opensource.apple.com). Write a thin shim for the half-dozen private _CF helpers configd uses.
CF-Lite is plain C — not Foundation, not Objective-C. Importing it does NOT violate the scope_split rule against adopting Apple's ObjC/Foundation. It's the same shape of import as the configd source you already have at freebsd-launchd/configd/.
This hybrid:
libCoreFoundation.so (with CF-Lite parts winning over corebase's stubs for the overlap); or ship two distinct libraries with explicit linking choices.CFBundle.c (plain C) instead.New work estimate to make configd link: ~3,000-5,000 lines of new/imported code — CF-Lite's CFMachPort + CFRunLoop version1 + CFBundle + CFPreferences + the private CF extension shims. This becomes a separate sub-project (call it libCoreFoundation-freebsd) inside or alongside freebsd-launchd, similar to how the configd source already lives there.
| Layer | Runtime | Source |
|---|---|---|
| libxpc | pure C | iXsystems/Klama reimpl — no ObjC, no CF |
| libdispatch | pure C + blocks | gershwin-developer install |
| libnv (bundled with libxpc) | pure C | BSD libnv, bundled in libxpc tree |
| liblaunch | pure C | freebsd-launchd's existing version |
| launchd daemon | pure C | freebsd-launchd (current minimal); Phase 5 swaps to clean Apple launchd-842.92.1 import |
| configd daemon | C + CoreFoundation | Apple verbatim — CF supplied by hybrid libs-corebase (value types) + CF-Lite (CFMachPort, CFRunLoop v1, CFBundle, CFPreferences); see §9.5 |
| configd's small ObjC shim | GNUstep libobjc2 | NCDaemon.m already uses GNUstep runtime |
| SystemConfiguration framework | C + CoreFoundation | Apple verbatim — same CF dependency as configd |
| asl / mDNSResponder / notifyd | pure C | Apple sources (no CF dependency in these) |
| GUI / app layer / Foundation | GNUstep | Apple source explicitly NOT adopted here |
Where you'd be forced into Apple's ObjC: only if a future downstream wants NSXPCConnection's Cocoa-shape API. None of the daemons in this porting scope (launchd, configd, asl, notifyd, mDNSResponder, IPConfiguration) need it. GNUstep apps that want XPC can call libxpc's C API directly. No conflict with the rule.
Strategic question that comes up once you've ported libxpc and start eyeing the next daemons: should we port Libsystem from macOS? Apple's apple-oss-distributions/Libsystem exists on opensource.apple.com (latest tag Libsystem-1356, 2025-10-04). It sits very low in the macOS stack — below CoreFoundation, below Foundation, below libxpc itself. Tempting to think "if we're porting Apple's userland, we might as well port the base of it."
Verdict: don't port Libsystem as a whole. It's an umbrella, not a library. Port the individual sub-libraries we actually need (libxpc, libnotify, libasl); keep FreeBSD's libc, rtld, malloc, and pthread underneath. No Apple ObjC or Foundation involved at this layer.
On macOS, /usr/lib/libSystem.dylib doesn't have its own code — it's an umbrella that reexports symbols from a dozen sub-libraries:
| Apple sub-library | What it provides | Our equivalent / plan |
|---|---|---|
libsystem_c | Apple's libc fork | FreeBSD libc — never replace |
libsystem_pthread | POSIX threads | FreeBSD pthread — never replace |
libsystem_dyld | Apple's dynamic loader runtime support | FreeBSD rtld — never replace (replacing dyld means switching to Mach-O, breaking ELF userland; scope_split forbids this) |
libsystem_malloc | Apple's nano/scalable malloc | FreeBSD jemalloc — never replace |
libsystem_kernel | Syscall wrappers + Mach trap stubs | This is what our libmach is becoming (Phase C1 done) |
libsystem_dispatch | libdispatch / GCD | swift-corelibs-libdispatch + our Mach backend (Phase 0 of this plan) |
libsystem_xpc | libxpc | This plan (Phases 2-4) |
libsystem_notify | libnotify (notifyd client) | Future port from apple-oss-distributions/Libnotify, after notifyd |
libsystem_asl | libasl (Apple system logger client) | Future port from apple-oss-distributions/libasl |
libsystem_platform | Low-level Apple-specific (atomics, OSSpinLock, pthread_mutex_lock_with_options) | Don't port — FreeBSD equivalents in libc / pthread |
libsystem_blocks | Blocks runtime | libBlocksRuntime (already in ports / gershwin) |
libcommonCrypto | Apple's CommonCrypto API | Don't port — use OpenSSL or FreeBSD's libmd |
The Libsystem repo itself is mostly a linker stub specification — a .dylib stub that says "I reexport these symbols at these versions from these sub-libraries." It's metadata, not code. There's no implementation to port; there's just a list of what to glue together.
No. A process has exactly one libc. The libc your binary loads provides the standard C runtime (malloc, stdio, errno, fork, exec), and you can't have two of those active simultaneously — they'd fight over malloc arenas, signal dispositions, errno location, syscall ABIs (Apple's libc speaks Mach traps + BSD syscalls with Apple's numbering; FreeBSD libc speaks FreeBSD's syscall ABI).
Replacing FreeBSD libc with Apple's libsystem_c would mean:
However, you absolutely can have FreeBSD libc as your libc AND link in Apple's individual API-only sub-libraries (libdispatch, libxpc, libnotify, libasl) alongside. Those sub-libraries call libc through standard C APIs — they work against any libc, whether Apple's or FreeBSD's. That's exactly the model we're already using. Our libmach links FreeBSD libc; libxpc will too; configd will too. All of them happily call malloc(), printf(), open(), etc. through FreeBSD's libc.
No. Libsystem is pure C and sits below Foundation/CF/ObjC in the macOS stack. The dependency direction is:
NSXPCConnection / Cocoa apps (ObjC)
|
v
Foundation.framework (ObjC)
|
v
CoreFoundation.framework (C)
|
v
Libsystem (libxpc, libdispatch, ...) (C) <-- libxpc lives here, below CF/Foundation
|
v
FreeBSD libc / kernel (C)
Foundation builds on top of Libsystem. Libsystem doesn't know Foundation exists. Same for CoreFoundation — CF uses Libsystem's libdispatch and libxpc internally, but Libsystem doesn't depend on CF.
So adopting Libsystem (or rather, individual Libsystem sub-libraries) does NOT pull in Apple's ObjC runtime or Foundation. The scope_split rule (GNUstep for framework layer, Apple source only for system services) is preserved exactly.
No, only the sub-libraries we already plan to port. The daemons we want (configd, mDNSResponder, notifyd, asl, IPConfiguration) link against the individual API libraries on macOS — -lxpc -ldispatch -lc. On FreeBSD we'd patch each daemon's Makefile to do the same with our libraries: -lxpc -ldispatch -lmach -lc (FreeBSD libc instead of Apple's). One-line Makefile change per daemon. We're already touching every daemon's Makefile anyway.
On macOS, daemons sometimes link -lSystem (the umbrella) as shorthand for "I want everything." On FreeBSD that shorthand isn't available, but it's easy to recreate with a tiny stub library if it ever becomes ergonomically painful (~10 lines of a linker script that pulls in our actual libraries). Deferrable indefinitely.
libsystem_xpc by name.libsystem_kernel./usr/local/lib/ matching libmach / libxpc.libSystem.so wrapper: deferred. Add a 10-line linker-script reexport library if and only if downstream daemons' Makefiles become annoying to maintain.Bottom-line rule: when looking at any new Apple-source porting decision, ask "is this a Libsystem sub-library that's an API-only consumer of libc, or is it something deeper that wants to replace libc / dyld / malloc?" The former is portable; the latter is off-limits.
Decision: fork libxpc from ravynos/lib/libxpc/. NextBSD's tree is older and explicitly self-described as a first pass; ravynOS has substantive object-model + serialization fixes. Diff is small enough (~150 lines) that we won't regret missing NextBSD's path.
Cannot reach modern Apple parity. Ever. Even with this port, we are upper-bounded by Apple-2014's public API surface plus whatever third-party reverse-engineering (e.g. Darling's activity-key additions) we adopt. Modern macOS XPC features — xpc_activity beyond keys, post-Yosemite domain model, NSXPCConnection ObjC bridge, codesign-derived entitlement enforcement — are out of reach without either Darling-level Obj-C runtime work or rewriting from scratch.
Acceptable. libnv-over-Mach instead of bplist-over-Mach means we can never bit-exchange messages with real macOS apps. Fine — our stack is FreeBSD-native binaries top-to-bottom. Compatibility is at the source level, not the wire level.
Confirmed broken — needs a new backend. Per the audit in §9.4: gershwin's libdispatch is pristine swift-corelibs-libdispatch with HAVE_MACH off; DISPATCH_SOURCE_TYPE_MACH_RECV is unresolved at link. Force-enabling fails because EVFILT_MACHPORT doesn't exist in FreeBSD kqueue and src/mach.c (3,250 lines) assumes XNU's full Mach ABI. A new backend file (~500-1000 lines, polling thread or kqueue-user-filter wrapping mach_msg_trap(MACH_RCV_TIMEOUT=0)) is required. This is Phase 1 of the plan, not a "verify and proceed" check.
Significant gaps — hybrid with Apple CF-Lite required. Per the audit in §9.5: corebase has CFMachPort absent, CFRunLoop version1 sources unimplemented (TODO stub), CFPreferences absent. Roughly 3,000-5,000 lines of new/imported code needed — pulled from Apple's open-source CF-Lite (plain C, not ObjC). This is its own sub-project, sequenced before Phase 6 (configd functional).
Real work. bootstrap_check_in / bootstrap_look_up need a userspace name service. Apple's launchd is the bootstrap server — the bootstrap subsystem is internal to the launchd daemon. Our current freebsd-launchd minimal rewrite doesn't have it; the clean Apple launchd-842.92.1 import in Phase 5 will. Either we stand up a small standalone bootstrap server in Phase 3 (smaller, faster, gets libxpc tested), or we wait for Phase 5's full Apple launchd to bring the bootstrap server along with it. The plan recommends both: Phase 3 minimal standalone for testing, Phase 5 swap to Apple's full launchd for production.
Already decided: no. The whole project's premise is that Apple's system-service plumbing — including XPC's connection-based-async-with-typed-dictionaries shape — is what we want to modernize FreeBSD toward. Substituting another IPC mechanism breaks source compatibility with every daemon we want to inherit. The cost of libxpc is the price of admission.
This is the first task. The 2026-05-11 audit (§9.4) confirmed gershwin's swift-corelibs-libdispatch has no Mach RECV support on FreeBSD. Without it, libxpc connections can't deliver incoming messages — every xpc_connection_set_event_handler registration would dangle. Everything downstream of libxpc (configd, swapped launchd, ...) depends on this working first.
gershwin-developer already follows a clean patches/clone pattern: Library/Sources/swift-corelibs-libdispatch/ holds a pristine upstream clone, and Library/Patches/swift-corelibs-libdispatch.patch (4 hunks of FreeBSD kqueue/timer/workqueue fixes) gets applied at build time. We add a second patch file alongside the existing one:
gershwin-developer/Library/Patches/
├── swift-corelibs-libdispatch.patch (existing, kqueue fixes)
└── swift-corelibs-libdispatch-mach.patch (NEW, our Mach backend)
The new patch is exactly two hunks:
src/event/event_mach_freebsd.c (~500-1000 lines)CMakeLists.txt's target_sources(...) list, gated on __FreeBSD__Why a separate patch (not folded into the existing one): different bug surface, different reviewers, different upstream pathway. Gershwin's existing patch fixes pre-existing FreeBSD kqueue bugs they already maintain; ours adds a brand-new source-type backend. Keep them apart.
Why patches not in-place edits: gershwin-developer's build re-clones Sources/swift-corelibs-libdispatch/ from upstream each build. Hand-edits to the clone get clobbered. Patches survive.
_dispatch_source_type_mach_recv in event_mach_freebsd.c, gated on __FreeBSD__. Implementation choice:
mach_msg_trap(MACH_RCV_MSG | MACH_RCV_TIMEOUT, timeout=small) and invokes the registered event handler via the source's target queue. Pros: no kernel changes. Cons: per-source thread overhead.HAVE_MACH=1. The 3,250-line src/mach.c assumes XNU Mach ABI we don't have. Keep that codepath excluded; our backend lives entirely in the new event_mach_freebsd.c.libmach (the Phase C1 library) for the syscall wrappers.The patch isn't idempotent — applying twice fails. Each iteration starts from a pristine upstream clone:
WORK=/root/libdispatch-dev
rm -rf "$WORK" # always start clean
mkdir -p "$WORK" && cd "$WORK"
git clone --depth 1 https://github.com/apple/swift-corelibs-libdispatch.git
cd swift-corelibs-libdispatch
# Apply both patches in order:
patch -p1 < /root/Patches/swift-corelibs-libdispatch.patch
patch -p1 < /root/Patches/swift-corelibs-libdispatch-mach.patch
# Build:
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_INSTALL_PREFIX=/usr/local
make -j$(sysctl -n hw.ncpu)
make install # to /usr/local/lib, NOT /System/Library/Libraries
# (avoids collision with gershwin's libdispatch during dev)
# Test: a small C program that:
# 1. mach_reply_port() to get a port
# 2. dispatch_source_create(DISPATCH_SOURCE_TYPE_MACH_RECV, port, 0, queue)
# 3. dispatch_source_set_event_handler(...)
# 4. another thread sends mach_msg to the port
# 5. verify handler fires within timeout
# Link against -lmach -ldispatch
Patches live locally at gershwin-developer/Library/Patches/; sync to the development host via scp before each iteration (rsync may not be available; scp is universally fine).
gershwin-developer forkgershwin-developerbuild.sh clones from pkgdemon's forkPhase 0 isn't done until these all PASS. They're the contract libxpc will build on; ship none of Phase 2 until every test below is green.
Prerequisite (small Phase 1 spillback): the smoke tests need a SEND right on our own port, which means mach_port_allocate and mach_port_insert_right have to be wired in mach.ko first. Those are early Phase 1 items in the original plan; pull them forward into Phase 0 so the smoke is real:
_kernelrpc_mach_port_allocate_trap (sysctl mach.syscall.mach_port_allocate)_kernelrpc_mach_port_insert_right_trap (sysctl mach.syscall.mach_port_insert_right)mach_port_allocate, mach_port_insert_rightThen the libdispatch-Mach-recv test program tests/test_dispatch_mach_recv.c, linked against the patched libdispatch + libmach. The program runs five subtests, each printed as a PASS/FAIL line:
| # | Smoke check | What it proves |
|---|---|---|
| 1 | dispatch_source_create returns non-NULL — create a port via mach_port_allocate(MACH_PORT_RIGHT_RECEIVE), call dispatch_source_create(DISPATCH_SOURCE_TYPE_MACH_RECV, port, 0, queue). Check non-NULL. |
Backend's source-type constructor is wired and returns a real source object. |
| 2 | Handler fires on single message — allocate port, insert MAKE_SEND right on same port, attach source with handler that increments a counter, dispatch_resume the source, mach_msg(SEND) a header-only message to the port. Wait up to 1s. Assert counter == 1. |
Backend correctly polls/waits on the port, decodes the message, and invokes the handler on the target queue. The actual "Mach dispatch works" proof. |
| 3 | Handler fires N times for N messages — same setup, send 100 messages in a tight loop, wait up to 5s for counter to reach 100. | Backend doesn't drop messages under load; queue and serialization are correct. |
| 4 | Cancellation is clean — attach source, register a cancellation handler that sets a flag, dispatch_source_cancel, wait for cancellation handler to fire. Send a message after cancel and confirm the event handler does NOT run. |
Backend supports clean teardown. libxpc relies on this for connection invalidation. |
| 5 | No port-table leak — capture sysctl mach.stats.ports_in_use before, run the previous tests 10 times in a loop, capture after. Difference should be near zero (some baseline noise OK). |
Backend properly deallocates Mach ports on source destruction. Catches the most common refcount-leak class of bug. |
Plus regression: all 21 existing smokes (after the two new port-management syscalls are added) still PASS. The patched libdispatch must not break the existing kqueue path used by the libxpc-less rest of the system.
Total Phase 0 smoke target: 26 checks (21 existing + 5 dispatch-mach-recv). Once green, the libxpc fork (Phase 2) starts.
swift-corelibs-libdispatch-mach.patch with event_mach_freebsd.c + CMakeLists.txt entry. ~1 week.test_dispatch_mach_recv goes 5/5 green. ~2-5 days.test_dispatch_mach_recv into smoke-sysctls.sh. Confirm 26/26.PHASE_C_FINDINGS.md in freebsd-launchd-mach.Add the missing Mach port-management syscalls to freebsd-launchd-mach/src/mach_kmod/, following the wired-trap pattern established for the trap family + mach_msg:
_kernelrpc_mach_port_allocate_trap (sysent mach.syscall.mach_port_allocate)_kernelrpc_mach_port_insert_right_trap_kernelrpc_mach_port_deallocate_traptask_get_special_port / task_set_special_port via MIG RPC over the task port (these are RPCs, not raw traps — need a different wiring strategy)MACH_RCV_TRAILER_AUDIT populates mach_msg_audit_trailer_t on receivemach_port_allocate, mach_port_insert_right, mach_port_deallocate)Deliverable: smoke harness grows from 19/19 to ~26/26.
ravynos/lib/libxpc/ into freebsd-launchd-mach/src/libxpc/. Take the bundled subr_nvlist.c + subr_nvpair.c too (don't try to use FreeBSD's stock libnv until later — the bundled version has XPC-specific tweaks).bsd.lib.mk Makefile. Install path: /usr/local/lib/libxpc.so, headers to /usr/local/include/xpc/ (PREFIX defaults to /usr/local, matching the libmach Makefile from Phase C1). Standard FreeBSD ports convention. Not gershwin-coupled; this ships alongside libmach as an out-of-tree port that works on any FreeBSD with our mach.ko loaded.xpc_dictionary_create, xpc_dictionary_set_int64, xpc_dictionary_get_int64, xpc_release. No Mach yet, just in-process object plumbing. Verify it builds and runs.Three precedents in our ecosystem:
| Path | Where it's used | Notes |
|---|---|---|
/usr/lib/system/libxpc.dylib | Apple macOS | Inside Libsystem composite. Reexported via /usr/lib/libSystem.dylib. Not under /System/Library/. macOS reserves /System for .framework bundles, not loose .dylibs. |
/usr/lib/libxpc.so | ravynOS | Standard FreeBSD base-system. bsd.lib.mk defaults applied (no LIBDIR override). |
/usr/local/lib/libxpc.so | This project (recommended) | Standard FreeBSD ports convention. Matches the libmach Makefile placement chosen in Phase C1. Ships as an out-of-tree port that works on any FreeBSD with our mach.ko loaded. |
/System/Library/Libraries/libxpc.so | (gershwin pattern, not used) | Where gershwin-developer puts libdispatch / libobjc / libgnustep-base / libgnustep-corebase. Rejected: couples this project to gershwin's layout choices; /System on macOS is reserved for .framework bundles, so this path is gershwin-specific rather than Apple-compatible. |
Decision: /usr/local/lib/libxpc.so with headers at /usr/local/include/xpc/. Consistent with libmach (Phase C1) and with standard FreeBSD third-party port placement. Gershwin's ISO build picks this up via its normal package-install pipeline; no special path coupling.
The hard part. Options, in order of effort:
bootstrap_register / bootstrap_look_up over Mach, stores port-name mappings in memory. ~500-1000 lines. Reuses NextBSD's MIG .defs for the bootstrap protocol.job_mig_dispatch + bootstrap port handling out of launchd-842.92.1 source, ship it as a standalone daemon. Maybe 2000-3000 lines. Closer to Apple-shape but heavier.launchd-842.92.1 verbatim from apple-oss-distributions/launchd, build against our libmach + libxpc + libdispatch with a patch series for FreeBSD adaptations. Highest payoff (16k+ LoC of upstream-clean Apple code), highest risk (most surface to debug). This becomes the production launchd.Recommended Phase 3 scope: Option A. Smallest critical path to "libxpc connections actually work end-to-end." The full Apple-launchd import comes in Phase 5 once libxpc is proven.
xpc_connection_create_mach_service, sends a dictionary, receives the reply.mach.stats.ports_in_use.Replace freebsd-launchd's 1,496-LoC minimal rewrite with a clean verbatim import of Apple's last open-source launchd, building against our libmach + libxpc + libdispatch stack and keeping all Mach paths intact (no stripping, unlike freebsd-launchd's original approach).
Why this over a ravynOS fork: same source code either way (ravynOS's launchd descends from NextBSD which descends from Apple launchd-842.x), but a clean import gives us:
#if 0/FIXME bisect markersLayout (mirrors configd's import pattern):
freebsd-launchd-mach/
├── src/sbin/launchd/
│ ├── (Apple verbatim from launchd-842.92.1)
│ ├── core.c (~12k lines)
│ ├── runtime.c, launchd.c, ipc.c, log.c
│ ├── *.defs (MIG protocol_jobmgr, job, job_reply, ...)
│ └── ...
├── scripts/import-launchd.sh (refresh-from-upstream tool)
└── patches/launchd/
├── 0001-freebsd-15-availability-header.patch (~20 lines)
├── 0002-build-flags-pic-no-werror.patch (~10 lines)
├── 0003-audit-session-self-workaround.patch (~30 lines)
├── 0004-link-against-libmach-libxpc-libdispatch.patch
├── 0005-freebsd-mach-compat-shim.patch (the bulk)
├── 0006-mig-defs-for-freebsd-makefile.patch
└── 0007-per-user-env-file.patch (~45 lines; useful ravynOS feature)
Estimated patch size: ~700-1500 lines sitting on top of Apple's ~16,500 lines of clean source. Comparable to gershwin's libdispatch patch in approach, larger in absolute size.
What the patches must do:
bsd.prog.mk Makefile replacing Apple's xcconfig. ~50-100 lines..defs compilation — Apple's protocol_jobmgr.defs, job.defs, etc. via a FreeBSD-host mig tool producing our shape of stubs. ~100-200 lines of build orchestration.bootstrap_*, mach_port_construct, MACH_NOTIFY_DEAD_NAME, voucher APIs). Our libmach exposes a subset (Phase 1 expansion + Phase 3 bootstrap server). Either widen libmach to cover what launchd needs, or write thin compat shims that route to what we have. ~500-1000 lines.<Availability.h> shim — Apple's headers reference __OSX_AVAILABLE_STARTING(...); trivial macro stubs. ~20 lines._audit_session_self() issues ravynOS hit on FreeBSD 15. ~30 lines._read_env_file() that ravynOS added; useful for FreeBSD-style /etc/launchd_user.env. ~45 lines.Dependencies on prior phases:
Reference NOT fork: NextBSD's sbin/launchd/ and ravynOS's sbin/launchd/ are useful prior art — their FreeBSD shims have already solved many of the same problems we'll hit. We can read their patches as guidance, but our patches stay clean-room (commit messages describe the underlying problem, not "ported from NextBSD"). Keep the lineage cleanly "Apple upstream + our patches."
Test gate (matches Phase B Tier 1's smoke-driven completion criterion):
org.freebsd.* launch.plists from /System/Library/LaunchDaemons/launchctl load / launchctl unloadCutover: ISO build switches from freebsd-launchd to freebsd-launchd-mach launchd. Keep freebsd-launchd's minimal daemon around as a fallback for non-Mach-aware deployments (e.g. a stripped-down FreeBSD build that doesn't load mach.ko).
Prerequisite for Phase 6. Per the audit in §9.5, GNUstep libs-corebase has critical gaps that block configd. Build a hybrid CF library that ships with freebsd-launchd:
freebsd-launchd/CoreFoundation/. Plain-C-only; CF-Lite has no Foundation / ObjC content. Drop any sources that overlap cleanly with corebase (CFString, CFDictionary, CFArray, CFNumber, CFData, CFPropertyList) and keep what fills gaps: CFMachPort, CFRunLoop (or just the version1 dispatch path), CFBundle, CFPreferences, plus private CF extensions.CFStringIsValidDNSName, CFDataCopyVMData, etc.). Pull from CF-Lite where present; stub the rest.libCoreFoundation.so linking corebase value-types + CF-Lite runtime parts + shim. Install to /usr/local/lib with headers to /usr/local/include/CoreFoundation/.CFMachPortCreateWithPort, attaches it to CFRunLoopRunInMode, sends a Mach message from another thread, verifies the version1 source callback fires. This is the equivalent of Phase 0 but at the CF layer.Depends on libxpc (Phase 4) and CoreFoundation hybrid (Phase 5.5).
.defs for configd_server, notify_server, _SCD.libSystemConfiguration.so + SystemConfiguration.framework + configd + scutil via bmake. Link against the Phase 5.5 libCoreFoundation.so and our libxpc.ev_ipv4.c/ev_ipv6.c against PF_ROUTE.configd-963.270.3 or rebase to configd-1405.100.8. Recommend staying on 963 for now; rebase becomes a separate plan once SCDynamicStore + SCPreferences are running.Out of scope for this libxpc plan but unblocked by it. Each daemon's existing pkgdemon.github.io plan (ipconfiguration, mdnsresponder, notifyd, asl) takes over once libxpc + configd are real.
freebsd-launchd-mach/ — this project's repo (mach.ko + libmach so far)freebsd-launchd/ — minimal launchd port (1,496 LoC core), configd source import (133K LoC, mostly uncompiled), no IPConfigurationravynos/lib/libxpc/ — fork target for libxpcravynos/sbin/launchd/ — 16,469-LoC near-Apple-2014 launchd, fork target for Phase 5 swapnextbsd/lib/libxpc/ — older version of the same code; reference onlygershwin-on-freebsd/ — ISO build pipeline; consumes gershwin-developer for libdispatchlaunchd-842.92.1 (2014). Reference only.configd-1405.100.8 (2026-04-13). Active.bootp-534.100.6 (2026-04-17). Active. Contains IPConfiguration.Libsystem-1356. Mentions xpc as binary dep; no source.freebsd-launchd-mach — mach.ko (kernel side), now Phase B Tier 1 completefreebsd-launchd — the minimal launchd portfreebsd-configd — configd phasingfreebsd-ipconfiguration — the deferred network config portfreebsd-notifyd — notification daemon planfreebsd-asl — Apple system logging planfreebsd-mdnsresponder — mDNS planLast updated 2026-05-11. Research compiled from six parallel agent surveys against the local NextBSD, ravynOS, and freebsd-launchd trees plus web research against the apple-oss-distributions and ravynsoft GitHub orgs. Every URL has been verified; every LoC number is from a wc -l over the cited path. Where research couldn't determine an answer (e.g. "which exact macOS SDK version were the captured headers from"), the document says so explicitly rather than guessing.