dxd - dynax driver framework 3.0.2
cross platform open source driver development framework
Loading...
Searching...
No Matches
Driver architecture (future): one shape on every platform
Todo
What is marked "decided" is settled, "proposed" still to be agreed; what of it is implemented, the repository says.

Goal and constraints

  • The same shape on every platform. A driver looks and works the same on Windows and macOS; a product supplies its desc, its composition and its entry.
  • The OS architecture takes precedence. Where it forces a different organization, it is followed; the deviation is kept as small as possible and hidden in the framework.
  • User and kernel mode correspond and share as much code as runtime and OS allow.
  • The virtual device is one device kind among many. Hardware devices (USB, PCI, FireWire, ...) fit into the same scheme.
  • Non-audio drivers remain possible, as with the KMDF device: audio is an optional layer.
  • The location determines the scope: kernel/ is platform-free, kernel/<platform>/ platform-specific. A product names only platform-free names and needs no #ifdef.

Layers

Layer Kernel User-mode counterpart shared
0 platform dxd_platform.h, sync, memory, dxd::clock per platform directory dx_platform.h, dx_sync.h, dx::clock parallel classes of one shape (kernel: no STL, no exceptions)
1 driver model dxd::driver, dxd::device, their clients dxd::driver::client, dxd::device::client dx::driver, dx::device; the client end is dx::proxy::device IOCTL selectors and payloads
2 device kinds dxd::usb::stream::device<super_device_t, desc_t>, PCI, FireWire, virtual dx::usb::stream::device, dx::virtuel::stream::device, ... dx::stream::desc, dx::circular, dx::stream::cursor, the ring rules
3 audio, optional PortCls, AudioDriverKit CoreAudio server plugin, service/ASIO -
4 product desc, composition, entry compositions the desc

A non-audio driver uses layers 0 to 2.

Driver model (layer 1)

Three roles, the same on every platform:

  • dxd::driver, the driver singleton. It creates devices when the OS reports them (PnP, matching) or the host requests them, holds the driver-wide values (log, status, version) and answers the driver-wide IOCTLs (version, log). client_create() returns {} where the platform gives the driver no access point of its own.
  • dxd::device, the OS-visible device user mode finds and opens; its life follows dx::object: create, launch, [unplugged], conclude, delete. What it does not answer goes to the driver.
  • dxd::device::client, one per open of a device, and dxd::driver::client, one per open of the driver itself; what a client does not answer goes to its device or its driver. The dispatch lives in the client on every platform; IRP table, WDF file object and iig shell are only the OS's line to it.
Platform Driver Device Client
KMDF kmdf::driver kmdf::device dxd::device::client; the WDFFILEOBJECT is only the line
WDM platform::driver with the IRP table platform::device dxd::device::client
PortCls portcls::adapter, derived from platform::driver portcls::adapter::device dxd::device::client
DriverKit platform::driver: a service with a device container, its driver::client creates and removes devices platform::device: one service per device IOUserClient on the device or driver service
AudioDriverKit audiodriverkit::driver (IOUserAudioDriver) audiodriverkit::device as DriverKit
IOKit platform::driver: each device shell owns one, as DriverKit's hardware dext platform::device: one IOService shell per device IOUserClient shell on the device service

Every implementation has a dxd::driver (decided).** dxd::device holds its driver by reference. The OS dictates a driver object under WDM, PortCls and KMDF (DRIVER_OBJECT, WDFDRIVER), under AudioDriverKit (IOUserAudioDriver, the provider of every IOUserAudioDevice) and under Linux (pci_driver, usb_driver); under DriverKit a virtual device requires one, since the dext creates its devices itself. The one combination without an OS reason, hardware under DriverKit without audio - the OS matches the device service directly on IOUSBHostDevice/IOPCIDevice - gets its driver all the same, so the contract is one. Open: the form there. A device shell takes its driver from its provider, the driver shell (hosting()), and a hardware provider is none; either the device shell owns a driver of its own and deletes it after its device, or one dxd::platform::driver per dext process stands for the DRIVER_OBJECT.

The driver's own access point** (dxd::driver::client) exists under DriverKit alone, because macOS has no software device API and the driver creates virtual devices itself. On Windows every device arrives through PnP (a software device by SwDeviceCreate from the service, hardware from its bus) and the driver's client_create() stays {} (decided): a control device would stand outside PnP and keep the driver in memory; a driver-wide access point without a device would be a root-enumerated devnode ROOT\<URID>. User mode reaches version and log through the device handle on every platform - there is no second channel for the same IOCTLs (decided).

One base (decided).** Every platform's driver derives from the platform-free dxd::driver:

dxd::driver, dxd::device, their clients kernel/dxd_driver.h, dxd_driver_client.h, dxd_device.h, dxd_device_client.h
+- dxd::platform::driver/device (WDM) kernel/wdm/dxd_platform_device.h IRP table
| +- dxd::portcls::adapter kernel/wdm/portcls/dxd_portcls_adapter.h
+- dxd::kmdf::driver/device kernel/wdm/kmdf/dxd_kmdf_device.h beside WDM
+- dxd::platform::driver/device (DriverKit) kernel/driverkit/dxd_platform_device.h
| +- dxd::audiodriverkit::driver/device kernel/driverkit/dxd_audiodriverkit_...h
+- dxd::platform, Linux kernel/linux/dxd_platform_device.h
the device the OS publishes and user mode finds and opens
Definition dxd_device.h:41
device(class driver &driver)
Definition dxd_device.h:63
the driver: one per driver binary, holding and answering what is driver-wide
Definition dxd_driver.h:333
the WDF driver, one per driver object: the framework device objects with their queue and interface,...
Definition dxd_kmdf_device.h:197
DriverKit driver: the plain C++ object its shell holds, making the devices the host asks for.
Definition driverkit/dxd_platform_driver.h:45
PortCls adapter driver.
Definition dxd_portcls_adapter.h:116
the platform branch: DriverKit
Definition driverkit/dxd_platform_device.h:40

The base holds nothing OS-specific: the shape of the factory create(), the life-cycle sequence, log. GUID, registry Parameters and device interface belong to the Windows branches, Info.plist matters to DriverKit; Linux fits in, its bus core calling probe() per device as PnP calls create(). PortCls derives from the WDM branch (decided): the context on the DRIVER_OBJECT, the registry Parameters, the unload hook, the client dispatch (Create/Cleanup/Close/DeviceControl, complete(), removed) and the surprise removal and remove sequence are general WDM in dxd::platform::driver; the adapter replaces registration (PcInitializeAdapterDriver), device creation and start (PcAddAdapterDevice, miniports), the forwarding of foreign IRPs (PcDispatchIrp) and the offset of the device context.

KMDF stands beside WDM (decided).** KMDF brings what a common WDM base would offer (WdfDriverOpenParametersRegistryKey, WdfDeviceCreateDeviceInterface, WdfDeviceQueryProperty, WDFIOTARGET, WdfUsbTargetDevice) and derives from dxd::driver/dxd::device directly. The WDM helpers (wdm::registry, wdm::irp, wdm::unicode) stay free tools. A device kind's OS half therefore exists per driver model, and all of them are made (decided):

Device kind WDM (dxd::platform, PortCls) KMDF DriverKit
USB URBs to wdm.bus WDFUSBDEVICE/WDFUSBINTERFACE/WDFUSBPIPE USBDriverKit: IOUSBHostDevice/IOUSBHostInterface/IOUSBHostPipe
PCI CmResourceTypeMemory/CmResourceTypeInterrupt from the start IRP, IoConnectInterruptEx, IoGetDmaAdapter dxd_kmdf_pci_device.h PCIDriverKit: IOPCIDevice, IOInterruptDispatchSource, IODMACommand
FireWire IRB to wdm.bus (dxd_firewire_platform_device.h) IRB through the WDFIOTARGET (dxd_kmdf_firewire_platform_device.h), both on dxd_firewire_platform.h a client of dxd::firewire::controller - a unit a child service or the controller's IOCTLs (open)

The platform-free part of each kind (usb::device, pci::device, firewire::device: descriptors, configuration, register layout) is extracted from the Windows headers. FireWire's is dxd::firewire::device (kernel/dxd_firewire_device.h): a unit's transactions over its platform's transport, firewire::platform::device - the row above, and IOKit's on IOFireWireUnit. A transport offers read(), write() and lock() - a transaction split into packets, across a bus reset, timeout its bound where the transport takes one -, busreset(), the address spaces address and phy, and host_property().

Layering (decided, user mode the model):** contract (dxd::device) -> OS platform or driver model -> OS half of the device kind (usb::platform::device) -> device kind, platform-free (usb::device) -> streaming/product.

Names (decided).** The cross-platform base carries the plainest names, dxd::driver, dxd::device, with the clients nested as in user mode; bases are super, template parameters super_<what>_t, never "base". A platform namespace level exists only where a platform-free class of the same name derives from it (dxd::platform::device under dxd::device, dx::usb::platform::device under dx::usb::device); an OS half without one is the name itself (dxd::pci::device) until its platform-free class is extracted. An OS's platform branch is dxd::platform in dxd_platform_device.h; the search paths separate Windows, DriverKit, IOKit and Linux. The forms taking over the call line carry their framework as namespace and prefix: dxd::kmdf, dxd::portcls (adapter), dxd::audiodriverkit; a plain WDM or DriverKit product takes dxd::platform directly. Headers (decided):** a bare name or _platform is unique only where the search paths separate the platforms; KMDF and PortCls share kernel/wdm with the WDM branch and carry their prefix (dxd_kmdf_*.h, dxd_portcls_*.h, likewise dxd_audiodriverkit_*.h), the OS half of a device kind is dxd_<kind>_platform_device.h (WDM, DriverKit, IOKit) beside its platform-free class, dxd_<kind>_device.h without one, or dxd_kmdf_<kind>_device.h; a header all Windows forms share that differs from macOS/Linux is _platform, like dxd_sync_platform.h. portcls is namespace and part of file names only; in prose it is PortCls.

IOKit is a platform with its own search path (decided).** Kext and dext are separate builds against different SDKs, never meet in one translation unit, and live apart at runtime; IOKit follows the header rule without a prefix (iokit/dxd_platform_device.h, iokit/dxd_<kind>_platform_device.h). IOKit carries real dxd namespaces - erasing them by #define dxd goes, and so do the global token macros (#define device ...); what must stay unique is the OSMetaClass name the kernel keeps flat across all kexts, so every OSObject class is itself named DX_CAT(DX_PRODUCT_ID,_dxd_<name>) with the planned name its alias. To check: whether kmutil loads kexts whose classes live in namespaces.

Power is a driver's option (decided):** dxd::power<super_driver_t> binds the OS's power management to its devices' sleep()/wake() (dxd::device::powered()), composed over the driver's form - class driver: public dxd::power<dxd::platform::driver>, dxd::power<dxd::portcls::adapter>; one specialization per form (dxd_power.h per platform, dxd_<form>_power.h). A driver without it leaves the device in the working state as far as it knows; a driver of hardware composes it, as a device loses its state in a system sleep whether told or not.

Device kinds (layer 2)

Audio binding (layer 3)

Windows PortCls DriverKit AudioDriverKit
driver form portcls::adapter (WDM, PcInitializeAdapterDriver) audiodriverkit::driver (IOUserAudioDriver), one service per dext
per device PortCls subdevices on the FDO (PcRegisterSubdevice) one IOUserAudioDevice per audiodriverkit::device, attached by AddObject()
per pin WaveRT stream IOUserAudioStream
per tick to the OS WaveRT advance() and notification events UpdateCurrentZeroTimestamp()
data WaveRT buffer, cyclic HAL IO buffer, IO operation handler
  • The binding is a subclass of the device kind, as portcls::device<...> is; IOKit's audio object talking through the device's IOCTL contract is not carried over. Under DriverKit the device holds its IOUserAudioDevice and attaches it to the audio driver by reference.
  • One endpoint participant logic for both: cursor, claim(), transport or client role and mixing as a writing client move from wavert::stream into a platform-free kernel base; only the buffer model stays platform-specific.
  • The tick hook lives in the binding, the same interface for both.

DriverKit

  • The device is a service. Hardware: the personality matches the provider (IOUSBHostDevice, IOPCIDevice) and the matched service makes the device service. Virtual: the driver matched on IOUserResources makes device services with IOService::Create(provider, "DeviceProperties", &service) and RegisterService() and removes them with Terminate(). Device and clients look the same either way.
  • User clients: IOServiceOpen on a device service -> NewUserClient -> the device's client_create(); ExternalMethod -> client::ioctl -> device::ioctl (iig shell dxd_user_client); dx::proxy::driver finds the devices by matching, as under IOKit.
  • Creation: the host sends its request to the driver service's user client, the counterpart of SwDeviceCreate (dx::proxy::driver::create/destroy); the dext's bundle ID is the product URID, by which the proxy finds driver and devices (decided).
  • Desc: a property of the device service before Start runs (to check), else handed over by the driver.
  • Memory to the client: a dext cannot map into its client, so map<dx::user> has no address and the client maps by IOConnectMapMemory64(type) against CopyClientMemoryForType, the type the id the buffer was opened with - 32 bits, macOS' memoryType (decided).
  • Signals to the client: event<dx::user> is the OSAction completion of the registering async call (IOConnectCallAsyncStructMethod).
  • Namespaces: DriverKit has real ones; the kext relic #define client ... in kernel/dxd_sync.h applies under __MACH__ here too - a clean-up candidate.

IOKit

  • DriverKit's form: a template is no OSMetaClass, so the device and its client are plain C++ objects their shells hold, as under DriverKit - the shells are the product's classes (dxd_iokit_class(device), (user_client), named after DX_PRODUCT_ID), the kext's platform unit iokit/dxd_platform.cpp includes the product's version.h; dxd::create(driver, service) and platform::driver::owned(shell) are DriverKit's.
  • User clients: externalMethod -> session::dispatch() -> client::ioctl; clientMemoryForType hands the client's mapping by type (map<dx::user> carries no address, the client maps as from a dext); a user event takes the async reference of the call that registered it, its wake port's reference with it - IOKit asks it released once per call, so what no event took goes as the call returns, and async calls of a client run one at a time; the entitlement dx::ioctl::privileged from the opener's task.
  • Waiting: a kext's thread sleeps (IOLockSleep), process is a kernel thread; irq() runs in the interrupt's filter and takes no lock, isr() on the device's workloop, so spinlock is a mutex, as under DriverKit.
  • Finding the devices: both macOS shells state their product as dxd (DriverKit its IOUserServerName, IOKit DX_PRODUCT_URID), which dx::proxy::driver matches.

User and kernel mode

  • Shared: dx::stream::desc, dx::circular, dx::stream::cursor, the format, the IOCTL selectors and payloads, the ring rules.
  • Parallel, for runtime reasons: promoted, clock, sync.
  • Goal: the endpoint participant logic as one implementation where user-mode engine and kernel endpoints apply the same rules; the device tree semantics - the user-mode host persists the tree and pushes it into the kernel by create/destroy, on Windows through SwDeviceCreate, under DriverKit by the IOCTL to the driver; the virtual device of one name and structure in both modes.

OS-imposed deviations

Building block Windows DriverKit Reason
entry DriverEntry Info.plist personality + iig Start the OS's entry model
creating a software device through the OS (SwDeviceCreate -> PnP) through the driver (IOCTL -> IOService::Create) macOS has no software device API; the same API outward
driver foundation KMDF or plain WDM; PortCls on WDM IOService, AudioDriverKit on it PortCls owns the WDM dispatch; KMDF brings everything itself
memory to the client the kernel maps the client maps by type a dext cannot map into its client
signal to the client KEVENT by handle OSAction completion the OS's event model
clock KTIMER thread IOTimerDispatchSource hidden in the platform's dxd::clock
audio per tick WaveRT position + notification zero timestamp the audio API's model
driver access point (driver::client) none, client_create() stays {} user client type ioctl::driver::client on the driver service see Driver model (layer 1)

Open points

  • Does a service made by IOService::Create take properties before its Start runs? Whether the device reads its desc from the OS object as on Windows depends on it.
  • Are such services visible to IOServiceMatching and openable by IOServiceOpen from user mode? The SDK says yes; untested.
  • Under DriverKit a device the driver made passes every unanswered ioctl on to platform::driver::ioctl, which answers dx::ioctl::driver::create/destroy too: every client of a device can create and remove devices. The proxy uses the driver client for that; the path through the device is to be closed.
  • A hardware device under AudioDriverKit does not come about: platform::driver::start() makes a device only on a pending host request; a hardware audio dext needs the matching path there.
  • Runtime selection of the sync reference pin, after the stream level's pattern (device.sync_reference).
  • Client mappings beyond the ring (channel buffers, control, monitor) under DriverKit: a second type dimension, or the address-less map<dx::user>?
  • The platform-free include of the audio binding: products include dxd_audio.h by its bare name (kernel/wdm/portcls, kernel/driverkit, both naming dxd::audio::base/device), while by the header rule the forms carry their prefix; proposed: dxd_audio_platform.h per OS directory, including the form and naming dxd::audio::base/device. The alias name base is no framework term (super, super_<what>_t) and is decided with it.

Steps

Each step builds on its own - macOS locally, Windows on the build server - before the next begins.

  1. Renaming without behaviour change: kernel/wdk -> kernel/wdm, portclass -> portcls, the headers by the rule above.
  2. CMake for every kernel platform, the IDE projects go (decided, IOKit included): kernel/<platform>/CMakeLists.txt with the INTERFACE target dxd and the platform's search paths (IOKit: kext bundle, -mkernel -fapple-kext, Kernel.framework; DriverKit: iig as a build step, dext bundle with Info.plist, entitlements, signature); the dxd projects go once no product refers to them. Measured: the IOKit platform, its sync, memory and PCI half compile against the macOS 27 SDK's Kernel.framework and link into a kext; USB, FireWire and the audio binding still stand on the kext's form before the contract.
  3. The contract dxd::driver, dxd::device and their clients: life cycle, ioctl (version, log), client forwarding.
  4. Windows: dxd::platform, the WDM branch with the adapter's IRP table; portcls::adapter on it; kmdf on the contract directly.
  5. DriverKit: dxd::platform as a service with a device container and a create/remove client; audiodriverkit on it; dx::proxy::driver::create/destroy for macOS.
  6. dxd::virtuel::stream::device into the kernel framework; the product only composes.
  7. IOKit onto the contract: USB, FireWire and the audio binding.
  8. Device kinds: extract the platform-free parts of USB, PCI, FireWire (user-mode dx_usb.h the common descriptor base), then the missing halves of the matrix; without hardware, compile-only.
  9. Extract the endpoint participant logic from wavert::stream, shared by PortCls and AudioDriverKit; the tick hook in the binding.
  10. Runtime test after signing.

Build, signing, installation (decided)

  • One build carries one SDK. On macOS a product's configuration builds the kernel trees as sub-builds (ExternalProject): dxd_driverkit() against the DriverKit SDK, dxd's tree kernel/driverkit/dext out of the sources and kernel libraries the product names (-mtargetos=driverkit21.0, -fno-exceptions -fno-rtti, iig a step of its own), dxd_iokit() against the macOS SDK named by DXD_IOKIT_SDK (empty: the current one) - the macOS 27 SDK ships IOPMAssertionTypes.h empty, so IOUSBHostDevice.h does not compile in a kext and the kext USB branch needs an older SDK. dxd_dext()/ dxd_kext() assemble the bundle and sign it with its entitlements; the sub-builds inherit CMAKE_OSX_ARCHITECTURES.
  • Installation, one shape on both platforms: dx::install::parser with --install/--uninstall - on Windows the driver package (SetupAPI), on macOS the system extension (OSSystemExtensionRequest, interface/macOS/dx_install.h), activated by an .app that carries the dext in Contents/Library/SystemExtensions and alone may activate it. The app is signed without --deep (dxd_target_sign(<target> ENTITLEMENTS <file>)) and needs an embedded provisioning profile for system-extension.install (without one: SIGKILL at launch, measured); dxd_profile() fetches the profiles of dext and app at configure time by bundle ID over the provider's API key, signing ad hoc where there is none. The app is DX_APP_URID, default DX_PRODUCT_URID.app; the identity Developer ID Application: <DX_PROVIDER> from the product's version.h.
  • Future: a product's control panel carries and activates the dext itself, its App ID with system-extension.install. Open: a CoreAudio server plugin fits no app bundle, only an AudioDriverKit dext in its place makes the installer dispensable; macOS activates from /Applications only; when to activate (at launch on a missing or different dext, or on request); who builds the dext; Windows in the same shape (the driver package needs admin); a small carrier for development, so that not every dext test runs through the signed panel.