A kext (kernel extension) is a loadable bundle of kernel code. On darwinOS, kexts are the primary way drivers hook into IOKit. This guide walks through a no-op kext that loads, logs a banner, and registers itself as a service. It’s the smallest IOKit driver you can write that actually does something.
Before you start, make sure you’ve read the Architecture overview — specifically the IOKit section — and have a built darwinOS to test against.
Skeleton
Create a directory my-kext/ anywhere under darwinos/drivers/contrib/. The build system auto-discovers contributed drivers there on the next make world.
my-kext/
├── Info.plist
├── my_kext.cpp
├── my_kext.hpp
└── MakefileInfo.plist
The plist advertises the kext to IOKit and tells the loader what class to instantiate and when.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>io.darwinos.contrib.MyKext</string>
<key>IOKitPersonalities</key>
<dict>
<key>MyKext</key>
<dict>
<key>CFBundleIdentifier</key>
<string>io.darwinos.contrib.MyKext</string>
<key>IOClass</key>
<string>MyKext</string>
<key>IOProviderClass</key>
<string>IOResources</string>
<key>IOMatchCategory</key>
<string>MyKextMatchCategory</string>
</dict>
</dict>
<key>OSBundleLibraries</key>
<dict>
<key>com.apple.kpi.iokit</key>
<string>17.0</string>
<key>com.apple.kpi.libkern</key>
<string>17.0</string>
</dict>
</dict>
</plist>IOProviderClass: IOResources means “match against the root nub” — i.e. load unconditionally, not in response to hardware discovery. Real drivers would match against a specific provider (e.g. AppleARMIO).
my_kext.hpp
#pragma once
#include <IOKit/IOService.h>
class MyKext : public IOService {
OSDeclareDefaultStructors(MyKext);
public:
bool start(IOService *provider) override;
void stop(IOService *provider) override;
};my_kext.cpp
#include "my_kext.hpp"
#include <IOKit/IOLib.h>
OSDefineMetaClassAndStructors(MyKext, IOService);
bool MyKext::start(IOService *provider) {
if (!IOService::start(provider)) {
return false;
}
IOLog("MyKext: hello from kernel space\n");
registerService();
return true;
}
void MyKext::stop(IOService *provider) {
IOLog("MyKext: stopping\n");
IOService::stop(provider);
}IOLog writes to the kernel’s ring buffer, which surfaces on the console and in dmesg. registerService() makes the driver visible to userspace via the IOKit registry.
Makefile
KEXT_NAME := MyKext
KEXT_BUNDLE_ID := io.darwinos.contrib.MyKext
KEXT_SOURCES := my_kext.cpp
include $(DARWINOS_TOP)/glue/mk/kext.mkThe included fragment handles everything else — darwinos-clang invocation, Embedded C++ flags, the bundle layout, linking against the kernel’s KPI stubs.
Build it
From the repo root:
make kextsThe output lands in build/<target>/kexts/MyKext.kext/. Copy that directory into the guest’s /Library/Extensions/ (same pattern as copying a userland binary).
Load it
In the guest:
kextload /Library/Extensions/MyKext.kext
dmesg | tail
# ... MyKext: hello from kernel spaceUnload with:
kextunload -b io.darwinos.contrib.MyKextWhat to do next
- Replace
IOProviderClass: IOResourceswith a real provider so your driver only loads against specific hardware. - Match against an IOKit family —
IOEthernetController,IOBlockStorageDriver,IOUSBHostInterface— and override the family’s abstract methods. - Add a user-client class to allow userland to talk to your driver over Mach messages.
The IOKit reference covers families in detail. When something goes wrong, debug the kernel — set a breakpoint on your start() and step in.