IOKit and the driver model: how a Mac talks to its hardware
Embedded C++, an object tree, and matching dictionaries — IOKit is how every driver on macOS gets loaded, paired with hardware, and called.
Unix drivers are a list of function pointers. IOKit is an object graph. That single design decision — that drivers are C++ objects held in a runtime-traversable tree, matched against hardware by a dictionary, and instantiated lazily — shapes every weird thing about device support on macOS.
This is what IOKit is, why it looks the way it does, and where to read it.
The tree
Boot a Mac, and the kernel builds a tree of IOService instances: the registry. The root is IORegistryEntry; everything else hangs off it through parent/child relationships, with IOService as the base class for anything that can be matched and started.
apple-oss-distributions/xnuiokit/Kernel/IOService.cppIOService — the base class. Every driver inherits, transitively, from this.View on GitHub(line —) apple-oss-distributions/xnuiokit/Kernel/IORegistryEntry.cppThe registry — the kernel's database of every node IOKit knows about.View on GitHub(line —)
You can look at this tree yourself. From the command line:
ioreg -l
What you're seeing is the live, in-kernel IORegistry, serialized. PCI controllers, USB hubs, sensors, the keyboard, the display, the Wi-Fi chip — every one is a node. Walk the parents of any node and you'll eventually reach the platform expert and, beyond it, the registry root.
Matching: how a driver finds its hardware
When a new node appears (a USB device is plugged in, the platform expert publishes the PCI bus), IOKit probes every personality it knows about for a match. A personality is a dictionary in a driver's Info.plist: IOClass, IOProviderClass, IONameMatch, IOPropertyMatch and friends.
The match goes:
- The provider (e.g.,
IOPCIDevice) publishes its properties — vendor/device IDs, class codes, BARs. - IOKit iterates registered drivers whose
IOProviderClassisIOPCIDevice. - For each candidate, it scores the match using whatever keys the dictionary names (
IOPCIClassMatch,IOPCIMatch, ...). - The highest-scoring driver gets
probe-d. Ifprobereturns a positive score, IOKit callsinit, thenattach, thenstart.
Two things fall out of this:
- You don't write a driver entry point. You write a class. IOKit instantiates it.
- Drivers can compete. If a vendor-specific driver outscores a generic one, the vendor's wins. This is how third-party USB device support layers cleanly under Apple's generic class drivers.
The object model: Embedded C++
IOKit is C++ — but not the C++ you write in userspace. Kernel C++ has no RTTI, no exceptions, no STL. Apple maintains its own object model: OSObject for reference-counted base, OSMetaClass for runtime type info, OSDictionary / OSArray / OSNumber / OSString for value types.
apple-oss-distributions/xnulibkern/c++/OSObject.cppOSObject — the kernel's reference-counted base. retain() / release() pattern.View on GitHub(line —) apple-oss-distributions/xnulibkern/c++/OSMetaClass.cppOSMetaClass — the home-grown RTTI that makes runtime introspection of drivers possible.View on GitHub(line —)
Every IOKit class declares its metaclass with macros (OSDeclareDefaultStructors, OSDefineMetaClassAndStructors). These wire the class into a registry of class names → constructors, so IOKit can instantiate a driver by name from a plist string.
There's a strict reason for the home-grown system: in 2000, when IOKit was designed, no C++ ABI could be safely linked across binary kexts compiled with different toolchains. The metaclass system papers over that — kexts only see a binary-stable C API plus pointer arithmetic that the macros generate, never the compiler's vtable layout.
kexts: how a driver actually gets into the kernel
A kernel extension is a Mach-O bundle that the kernel can load on demand. Each kext has:
- A binary with one or more
IOServicesubclasses. - An
Info.plistwith one or more personalities (the matching dictionaries above). - A list of link dependencies on other kexts.
kextcache (or, modern macOS, kmutil) merges all installed kext plists into the prelinked kernel at install time, so the boot loader has a single artifact to load. At runtime, when a personality matches incoming hardware, the kernel walks the dependency list, links the kext into the kernel address space, and instantiates the class.
The big direction of travel: third-party kexts are dying. Since macOS 10.15, Apple has pushed driver authors to DriverKit — a userspace driver framework where the IOKit object model is preserved but the driver process runs in userspace, isolated, with its own entitlements. The kernel still talks to it over IOKit-style messages, but the driver's crashes don't take the kernel with them.
apple-oss-distributions/xnuiokit/Kernel/IOUserServer.cppIOUserServer — the kernel side of DriverKit. How userland drivers attach to the IORegistry.View on GitHub(line —)Power management is an IOKit responsibility
Every node in the registry can announce its power needs. When the system idles, IOKit walks the tree and tells each driver to drop to a lower power state if it can; on wake, it walks the tree the other way. This is why a graphics driver and a fan controller use the same API — both are IOPMrootDomain children.
User-kernel boundary: IOUserClient
A userspace process that wants to talk to a driver does it through IOUserClient. The driver subclasses IOUserClient and exposes a small set of method numbers; the userspace side opens the driver via IOServiceOpen and calls those methods through IOConnectCallMethod. Under the hood: Mach messages on a special port.
This is why third-party userland tools that need to read sensor data, switch GPUs, or control fans all end up shipping a tiny kext (or now, a DriverKit extension): there's no /dev/whatever for most hardware. There's just an IOUserClient you have to open.
Why this is the way it is
The case for IOKit, made succinctly:
- Hot-plug is the default, not a bolt-on. The same code path handles USB, Thunderbolt, and a Bluetooth pairing.
- Drivers can introspect each other. A power manager doesn't need a kernel API for "list every display" — it walks
IODisplayWrangler's children. - The object model is debuggable.
ioreg,IORegistryExplorer, and lldb's IOKit awareness all read the same registry, live, on a running system.
The case against:
- C++ in the kernel is hard to get right. ABI breakage in any IOKit base class can ripple through every third-party kext. Apple has burned a release window on that more than once.
- Mach-message round-trips are slower than function calls. DriverKit makes the trade explicit; you accept more latency for less risk to the kernel.
What to read next
Pick one piece of hardware you care about — a webcam, the trackpad, the SoC's PMU — and walk the IORegistry to its driver. Then open the driver's source on GitHub:
apple-oss-distributions/xnuiokit/DriversThe in-tree IOKit drivers — small, but instructive.View on GitHub(line —)And read IOService::start once. It's the function every driver writes, and once you've read the base class's version of it, half the IOKit literature in books and WWDC talks suddenly makes sense.