Mac OS • 48:01
I/O Kit, the Mac OS X driver model, makes creating Macintosh drivers easier than ever. I/O Kit handles standard services and functionality for you, allowing you to focus on the unique aspects of your product. This session covers what makes up an I/O Kit driver, how drivers are dynamically matched and loaded for your devices, and how to access devices from applications.
Speaker: Dean Reece
Unlisted on Apple Developer site
Transcript
This transcript was generated using Whisper, it has known transcription errors. We are working on an improved version.
Good afternoon and welcome to Worldwide Developer Conference. I'm Craig Keithley. I'm the USB and FireWire technology manager in Apple's Worldwide Developer Relations program. And I've been working on USB and FireWire drivers and products probably for the last four years. I've worked on just about every different kind of driver that exists at Apple. And it's been incredible to watch the adoption of I/O Kit over the last year.
I've worked with developers internal to Apple and external to Apple, and all of them come to me and tell me how powerful it is, how useful it is. The object inheritance model that we have makes it easy to write drivers that leverage off of the families that are in the operating system. And so to introduce this to you, describe it to you in depth, I'd like to bring up Dean Reece.
Dean? Hi, good afternoon. I'm Dean Reece. I manage the I/O Kit team at Apple. And I don't know how in-depth we'll be able to get today, because I/O Kit covers a lot of API and a lot of ground. But we'll definitely cover as much of it as we can, and there's a whole lot of sessions for the rest of the week that will hopefully give you the details you need to be able to make your products.
So in introduction, we're going to talk about sort of the design and the scope of I/O Kit. I/O Kit is just one word. It doesn't really tell you what all it covers. And it's actually important to know what fits in I/O Kit and what doesn't fit in I/O Kit. We'll talk about kernel extension some, because that's the basic unit of delivery for I/O Kit components.
And I'm also going to spend a little bit of time talking about how applications can get access to devices. This is very important, obviously. If somebody's porting any kind of application that maybe wants to sync to a handheld device or touch any kind of hardware that's out there where a higher level of abstraction is not appropriate. So we'll spend some time talking about the various models for that.
So it's only a couple sessions into WWDC this year, but I'm sure you're already familiar with this slide. We're going to be focusing in on the bottom, the Darwin capsule there. And to expand on that a little bit, this is the explosion of the Darwin kernel, and there's a lot of components in there. I/O Kit is just one of them, and drivers are built based on I/O Kit primitives and provide services to the rest of the Core OS, the Darwin kernel, and the higher level applications as well.
So, what is I/O Kit? There's really three views of I/O Kit. To driver developers, it's a framework that makes it as easy as possible to write a driver. That's our goal is to make it as easy as possible to write a device driver for Mac OS X. And I've got in my definition here correct Mac OS X drivers.
We've had to spend a lot of time thinking about the threading model and the memory management because it's very different under Mac OS X than it was under classic Mac OS or even under other operating systems out there in the world. So, what we wanted to do was put as much of that knowledge into one framework as possible so you didn't have to go and reinvent all of that. And that's one of the fundamental goals of I/O Kit.
To application developers, I/O Kit presents an abstraction of hardware and a rendezvous mechanism. And by rendezvous mechanism, I mean also arbitration where you can have one device being shared between two applications. There's higher level abstraction services built higher up in the operating system, but all of them ultimately boil down to what I/O Kit allows for mutual exclusion of devices. And to the OS, I/O Kit really serves as an abstraction of each attached device. That's sort of its simplest definition. It tries to provide an abstract object for each device attached to the system.
Now more from a development perspective, I/O Kit is a framework and a collection of families for developing Mac OS X drivers. Simply put, that's what it is. It's a library, but it's a very big one with a lot of APIs. It's delivered on the Mac OS X developer CD, so there's not a separate SDK or KDK that you have to download if you've got the Mac OS X developer CD. You have I/O Kit. Now we will be occasionally posting updates to our website, but basically go to the CD and you'll find the developer package there that contains all of the APIs and I/O Kit.
From a runtime perspective, I/O Kit is part of the Darwin kernel. It's compiled into the kernel, it executes in the kernel address space, and all of the APIs that I/O Kit presents are present in the kernel.framework. That is all the in-kernel APIs are present there. The APIs that we present to application developers are present in the I/O Kit framework.
Now the distinction here is, as has been said in previous sessions, inside the kernel and outside the kernel, development environments are very different. There are different APIs, there are different rules for threading, for memory management, and as a result we have to have two separate sets of APIs for that. The I/O Kit in kernel implementation is an object-oriented design based on a subset of the C++ language. Now, the application level bits are still object-oriented in nature, but they're straight C. We don't use C++ for the app-level APIs.
Now, the question of course comes up, when do I need to use Iocit and when not? Well, if you're developing a driver that must run inside the Mac OS X kernel, and I'll spend a little more time on that later, then you're probably going to be living inside the Iocit framework. You're going to be making use of Iocit APIs.
And if you're developing application that needs to directly access devices or drivers, then you're going to make use of Iocit as well, not necessarily by subclassing from it, but by making use of its user-level APIs to get at the hardware. Now, not all drivers in Mac OS X are Iocit drivers.
This is important. Most of the imaging devices, scanners, printers, cameras, things like that, are not managed inside of Iocit. We really only try to put things in the kernel that need to be there for other reasons. And I'll dive into some of these details a little bit later.
The design goals of I/O Kit are pretty straightforward. It's a very complex task writing a driver for a system that can run on MP and have a preemptive scheduling. You have to deal with multiple address spaces and also ideally be architecture neutral, something that isn't tied to one particular platform because as you know, evolution of platforms tends to maybe render some of those design decisions inappropriate in a few years. So we've spent a lot of time trying to hammer these things out.
Drivers really focus on mechanism. When you write a device driver, you're focusing entirely on the bits of code that are necessary to push data in and out of a physical device. Ideally, we'd like to keep policy out of drivers. The idea is a driver's there to move data between the operating system and a device. And you really want policy user experience issues to be driven at a higher level.
And third-- this is an important point though. Third party developers can extend Mac OS X and I/O Kit just like Apple can. Through the Darwin open source community, you can download 95% of the source code to I/O Kit and its families. You can develop new families. You can develop drivers for existing families. It's a very flexible model. And we don't actually do anything within Apple that we make it impossible for a third party to use.
Now I/O Kit itself is, as I said, a very, very large framework and it contains a lot of API, a lot of surface area. Well, just for us to be able to manage it and think about it, we had to subdivide it. And the way we did this is by dividing it into families and then within families, multiple drivers. So I/O Kit, in and of itself, is just a framework. It doesn't have any intrinsic protocol support. SCSI is not part of the I/O Kit meta-framework. It doesn't understand SCSI commands. The protocol specifics are embedded into families.
The idea here is you would have a SCSI family that understands what it means to be a SCSI device and a USB family that understands what it means to be a USB device. Underneath each one of those families, The I/O Kit are a collection of drivers, some of them written by Apple, some of them written by third parties, and all the device specifics are embedded in those drivers.
Now, I-O-Kit families contain classes that present an abstract view of a device type. That's basically what it's trying to say there is a family presents the abstraction of a type of device to the system. So if all disk drives fundamentally look the same, the storage family would present that abstraction, and everybody that builds a device driver underneath that abstraction would inherit from it and therefore get sort of the same appearance to the upper layers.
In this way, upper-level services can look and find things in I-O-Kit in a common way. A device driver from one vendor doesn't look completely different from a device driver from another vendor if they are for the same type of device. Device drivers inherit the device-independent details from the family, and then as a developer, you fill in the device-dependent pieces.
So just to give you a shopping list here of some of the common families, you can see kind of a naming theme here. And there's plenty more. This is probably a third of them. There's also ADB Family and other somewhat more esoteric ones that you probably won't ever see. These are the ones I get most questions about from developers.
Now, from an object-oriented class perspective, drivers in the I/O Kit universe are leaf classes. That is, they don't intend to have any other class inherit from them. Now, I/O Kit developers, excuse me, the I/O Kit engineers at Apple who have built I/O Kit have had to write all their code with the knowledge that you people are going to be subclassing them. So we've had to put a lot of attention into the subclass interface.
Now, that takes a good bit of discipline and object-oriented experience to be able to design a class that intends to be subclassed. So, fortunately, you don't have to bother with that for drivers. Basically, you write, you inherit from something we've written, you write the bits that you need to, and you don't have to worry about third parties inheriting from you. Unless you want to, and that's a whole other presentation. Your driver really should focus in on the requirements of one particular device, or possibly a collection of devices that are very similar.
Now there's a data structure that's in I/O Kit, it actually exists in the kernel address space, called the I/O Registry. And it's important to understand that the I/O Registry is not something that's brought in from a file. It's never archived on disk. It is literally the network of active I/O Kit objects in the system.
And it's roughly a tree structure. I say roughly, if you browse it through any of our tools, like I/O Registry Explorer or I/O Reg, it will appear as a tree structure. It is actually possible to have one node have two parents. This is done for fan-in devices, like a soft RAID might choose to use that for implementation. So the technical term, I believe it's a directed acyclic graph, but basically a tree structure.
Now what it does is it tracks all of the relationships between the various objects. As I said, the nodes in this graph are the actual objects that make up your drivers and the other family components. And so there are links between them. There are planes within the I/O registry. There's a service plane, which is the one you'll hear about 99% of the time when you're looking at I/O Kit.
The service plane basically says, "This object depends on this other object for some service." And the service is usually named by the class name. So I/O PCI device provides a service to some other driver, and the service it provides is access to an I/O PCI device. I/O Kit tries to mimic the physical world wherever possible.
Also built into I/O Kit is the basis for driver matching. Now, each family can override this and can change the behavior. So the example I'm about to walk through is going to be true, 99% true, for every driver that you see, but there's always going to be some slight variation. So you have to check with the header files for your family and documentation that's available for whatever family you're in to understand how the matching will be different for you.
Dean Reece It's basically four stages, and I'll walk through those in four future slides. But it's fundamentally a score system. So effectively you've got some set of drivers identified as appropriate. We use scores to try to figure out which one is best. It's very powerful and flexible. There's quite a lot that you can do to change the way that matching occurs as needed.
So step one, device discovery. This is really sort of before the matching begins. But during boot, or when a dynamic bus has been rescanned, we notice a new piece of hardware. In this case, I've written it up as a PCI device discovered during boot. Well, the host controller driver is going to create a nub, an IOPCI device nub.
That's a term we kind of invented that represents that piece of hardware. If there's five PCI devices in the system, you'll have five separate IOPCI device nubs, one that represents each piece of hardware. And that nub will register itself for matching. This is how matching gets kicked off.
So in step two, it's called class matching. This is something that's done basically in I/O Kit innards. It's not something that the family typically alters. Basically, I/O Kit starts with a list of all drivers known to the system. Everything that's been installed that we're aware of.
[Transcript missing]
Now, the device driver itself is not going to have adjusted its probe score, but the family may have. The family at this point has got a little more information than it did in the previous step. So it may have decided to promote or move down in priority some driver based on family-specific criteria. I know that the USB family implements the USB matching specification, and it does, in fact, affect the probe score.
[Transcript missing]
So each of the remaining drivers is loaded and probed. And effectively, they're given the opportunity in C or C++ code to be able to talk to the device, find out if it is appropriate for them or not, adjust their own probe score, and return.
They can say, this device is not appropriate for me at all. Or they can say, it is appropriate, but I want to raise my probe score or lower my probe score because I discovered something about it that affects that. It's pretty rare that you would do that, but as I said, this is pretty flexible.
Now all the drivers that failed their probe are unloaded at this point, they're clearly not of interest. But the remaining drivers are started, starting with the highest probe score, and marching down the list until one of them successfully starts. In general if you've passed your probe you will successfully pass your start as well, so it's generally the very first driver on the list at this point. But it is possible that you discover something during start that causes you to bail on this hardware. And at that point, we have a winning driver.
We have a notion of match categories that you might have noticed in some I/O Kit documentation in the tutorials or some other places. In general, you won't need to use that. I bring this up only because I've had a number of questions about it. The idea there was there may be multiple drivers could usefully be attached to one device at any given time. And the idea here is you could have one piece of hardware that maybe is bimodal. It can operate as a tape drive, or it can operate as a floor buffer, whatever.
And effectively, you can have the floor buffer driver be in one category, and it can win for that category. And you can have the tape drive driver be in a different category and win for that category. And you can only typically have one driver actually accessing the device at a time, so you'd have to go through open-close to be able to actually acquire the device and use it.
But that allows you to have multifunction hardware share drivers. So if you need to do something like that, if you have a really interesting multifunction device, this may be one technique you can use. So this matching process, by the way, is repeated independently for each match category. Thank you.
Now, developing inside the kernel is a bit of an art. It's not something that we encourage all of our developers to do. Obviously, we want the kernel to be as lean as possible, but it's also something that isn't necessary in a lot of cases. We'll talk a little bit about what it means to develop inside the kernel.
For now, you have to use Apple-provided tools. If you wanted to use Code Warrior or some other tool for that, you'll have to contact the provider of that tool and ask for that support. Apple is willing to work with other tool vendors to show them what's needed to develop in this space, but presently, you have to use our tools.
When you want to do debugging, you need to use a two-machine debugger. We have a debug environment that uses GDB, and it uses two machines connected over Ethernet. It's a very powerful environment. There's a lot you can do with it, but it's probably different, unless you've already used GDB, it's probably different than anything you're used to. So there's going to be a learning curve there. Once you've learned it, I think you'll find it very powerful.
The fact that you can download source code from Darwin means that you can actually do source code level debugging within the kernel. Now, we have restricted language features. Specifically, a lot of the libraries, the standard POSIX libraries that Unix programmers are used to using, aren't available in the kernel. Basically, it's a mini environment, so to speak.
We would encourage you to go and look at the headers in the kernel framework to see what's available. There's also a tool called NM, which will tell you all the symbols. So if you're a Unix developer, there's some symbol you're used to using and you just want to find out if it's in the kernel, it's a very easy way to find out. Run an NM on the kernel and find out if it's there.
Also, if you're developing an I/O Kit, you're going to be using C++. C++ is available for other types of kernel extensions as well, but it's pretty much required for I/O Kit kernel extensions. We don't support a full-blown C++ implementation. Our initial port of I/O Kit C++ was based on a movement called Embedded C++ from a few years ago, and we're going to be evolving what that means to us.
Basically, some of the higher-level features like multiple inheritance, exceptions, RTTI, these things are not available. Also, you really need to use our constructor metaclasses. We have our own replacement for RTTI that's used to maintain reference counting and automatically unload drivers. It keeps track of a lot of very important things.
If you're writing a driver and you're making use of objects that derive from the OSObject class, which is the root of all I/O Kit objects, you can use the OSObject class to make use of the objects that derive from the OSObject class, which is the root of all I/O Kit objects. If you're writing a driver and you're making use of objects that derive from the OSObject class, you can use the OSObject class to make use of objects that derive from the OSObject class.
I know this has not been well documented, and we're working on improving that, but it is very important. Otherwise, you're going to get into situations that can cause panics and other crashes because we may decide to unload your driver because there's no references of it. Well, there are references of it, but our runtime system doesn't know that.
There's very limited user interaction inside the kernel. You can't just communicate with an app directly. You have to set up explicit communications. Now we do have some basic facilities. We have this kernel user notification center that allows you to pop up simple panels, that allows you to possibly launch an application if you needed to have a GUI.
But the code that actually does that is not in the kernel. The code that would display on the screen is outside of the kernel. So you can have your internal component cooperate with something out in user space, but you can't directly talk to Carbon APIs or toolbox APIs or anything like that.
Inside the kernel, resources are much more costly than they are in user land. Now, the reason this is so is because kernel memory is typically wired. If you allocate memory in the kernel, unless you go out of your way to unwire it, and that's tricky to do, that memory is wired.
Now, that means that that memory is permanently unavailable for other applications, other components in the system to use. If that same amount of memory is being used by some task in user space, when that task is not active, those pages will page out, and that physical memory will become available for some other entity to use.
So basically, you don't want to develop inside the kernel unless you have to. I think that's probably been made clear. The last important reason is failures are fatal. We want Mac OS X to be the most stable operating system on the planet. And obviously, you want to try to contain failures.
If an application crashes, we want the application to crash and not affect anything else. If you've got something inside the kernel and it crashes, it's going to probably bring the whole system down, depending on the nature of the failure. But basically, if you crash inside the kernel, You've crashed the system.
That being said, if you've decided to forge ahead and write code inside the kernel, here's how you do it. You're going to have to write a kernel extension, which is basically a package for binary code to be delivered into the kernel. Now, you can write a kernel extension that doesn't exist inside one of the three well-defined frameworks, but it's not really going to do very much because nobody's going to call it.
So, you really are going to be looking at one of these three universes: the I/O Kit universe, the kernel network extensions, NKE universe, or file system plug-ins. The three of them use fairly different APIs, so I can't go into any specifics for the latter two. But basically, the idea is the kernel extensions are there to deliver binary code into one of these universes, and then the APIs that are appropriate in that space take over from there.
An extension in classic Mac OS is not necessarily a kernel extension on Mac OS X. If you've delivered functionality in the past using an extension, it may not be appropriate to do that on X. You need to stop and think about the best way to package your functionality. So get inside the kernel only if you have to.
So a kernel extension is the way to get into the kernel. If you want to do something a little bit more interesting, kernel extensions can also serve as libraries to other kernel extensions. It's a relatively straightforward layering mechanism. So let's say that you're writing a family of drivers for your company that share a common service library. So you've got a USB version of something and a FireWire version of something, and 90% of the code is the same, but you need to have some bus-specific code as well.
You can write a library, effectively, that contains that 90% common code. And then you can write a kernel extension that depends on that library for the bus-specific details. And when you've done that, you've basically just written a family. That's the way families work. They're just a library that has leaf classes plugged into it.
So how do you actually go about writing a library? There's really only a couple things you have to do that are unique. First off, you have to identify yourself by a CFBundle identifier and a CFBundle version. These are properties in the PList. Not too onerous, you have to do this whether you're writing a library or not, all kernel extensions have that.
But you indicate that you are a, I didn't put it on the slide, you indicate that you're a library by expressing the OS bundle compatible version, which basically says this is the oldest version to which I'm binary compatible. And that is the clue to the system that, aha, this is a library and somebody can try to link against this.
Somebody wanting to make use of a library indicates this through the OS Bundle Libraries property. Basically, you list all the libraries on which you depend when you write a kernel extension. Here's a list of some of them. The list is very long. The first five on this list are components of the kernel itself. You could say I depend on com.apple.kernel and pick a version number based on one that we've shipped. And that basically means you're dependent on some version of the whole kernel.
Most of you won't need to do that because you're only going to be including API from I/O Kit and maybe from Mach and Libkern as well. But basically we've subdivided the kernel into four other pieces: Mach, BSD, I/O Kit, and Libkern. And you need to list these explicitly in your kernel extension when you're writing an I/O Kit driver.
We recommend that I/O Kit drivers list com.apple.kernel I/O Kit as a dependency, as well as libkern because that's where all the OS objects are defined, and Mach because you're going to be using primitives from Mach for return codes and things like that. You're also going to want to list the specific family on which you depend if you are writing a driver that depends on a family, which most of you will be. So for example, if you're writing a driver for a USB controller card that sits on a PCI bus, you would need to include the PCI family and the USB family. They're the two families that you depend on.
So now to actually talk about kernel extensions and how they exist on the file system, we've got a series of slides here that explodes it. If you look at this, this is a directory hierarchy. And you'll see that there's only two files in here. The rest of it's all structure. We basically chose to mirror the application bundle format, because all of our tools are there to deal with it.
Finder already knows how to deal with these, and they have some nice features about them. It's probably a little bit more than strictly necessary for delivering drivers, but it doesn't really cost anything. So there are really two files in a typical kernel extension that are going to be interesting to you. The first one is this Info.plist.
Every bundle of any description on Mac OS X has one of these files. And basically, it is the-- The I/O Kit is a table of contents for the kernel extension. It provides information on what it is, why it would need to be loaded. This is where the I/O Kit personalities with the matching information goes. I've listed four basic properties here that are pretty much going to be necessary for all I/O Kit drivers. The first is the C/F Bundle Identifier. The C/F Bundle Identifier identifies your driver.
We've gone with a reverse DNS namespace because this doesn't really require Apple to put some kind of a program together and police it. Basically, if you can sign up for a domain name, put it in reverse order. For Apple, we ship drivers and other components under the name com.apple.whatever.
Obviously, unless you work for Apple, you shouldn't be doing that. You should be providing your own company name there. Once you get within your company database, you can use the C/F Bundle Identifier to identify your driver. The other thing that's important is that within your company domain, you can decide whatever namespace within that that you want. It's actually pretty flexible. It tends to be a little verbose, but that's okay. This information is usually only thrown around when we're loading drivers. Once they're loaded, it's just sitting around as information.
Your CFBundle version is also going to be important. Our version checking code makes use of this to know that it's getting the latest and greatest piece of software. So right now the format is specified as a classic Mac OS Verse resource, something you're probably familiar with already. OS bundle libraries, this is where you list all of the libraries on which you depend. So com.apple I/O Kit.whateverfamily, all the libraries that you care about. It's very important to understand that Mac OS X as it's shipped so far doesn't enforce all of these properties.
It makes note of them and it does its best to use them, but we didn't have time to get all of our drivers into compliance, so we have to allow them to be a little bit fuzzy. Future releases of the Mac OS X operating system are going to require that these be in order or your driver will not load. So for example, if you don't have a CFBundle identifier, it won't load. I don't think it would load. It wouldn't load today actually because that's key to a lot of things. If you don't have a bundle version, it won't load.
If you don't list any bundle libraries, it won't load. Because by definition, if all you're doing is calling into the kernel, the kernel is acting as your library. You have to list at least one thing as a dependency. Otherwise your kernel extension could not call any API. And if nobody's calling you and you're not calling anybody, it's kind of a useless kernel extension.
And the fourth property here, I/O Kit personalities, this is the only one that's I/O Kit specific. This is where you would list all of the personalities of your driver. This is a little subtle. By and large, your drivers will have a single personality. That is, one set of matching criteria, a few properties to help your driver get oriented when it gets loaded.
But you could also have multiple personalities. You could have, say, two PCI cards that you ship that are similar but slightly different, and they need different match criteria. Well, one driver could drive them, but you could list two personalities, one for each of those flavors. It's very powerful.
We use this ourselves. We have several multi-personality drivers. As a very useful resource, go into the extensions folder on 10 and poke around and see what's there. Though I will warn you, as I said, not all of our drivers are in spec. So we'll also look at the log that comes out and see all the warnings that get posted. The warnings are on our drivers as well. well. The next file that's important here is the binary that the KEXT is delivering. In this case I've exploded the USB family, but this is the actual binary file.
First and foremost, it's optional, which may seem a little bit odd. The kernel extension is a way to deliver binaries. What does it mean to have it be optional? We have a notion of a driverless driver that is used occasionally, and this is where personalities come in. You're in your personality, your I/O Kit personality, you will specify the class, the basic class for your driver, and you'll also specify the CFBundle identifier for the kernel extension that contains the binary code.
You could write a driver that depends on some other driver, and actually just instantiates its classes directly. There are a few cases where this is useful. Basically, you would put in some new property in the personality. Maybe it's as simple as changing the probe score. I want to use this driver over here, but I want to make it slightly different matching, and I want to have a higher probe score.
Okay, enough said about that. If you do have a binary there, it's going to be in the MacO format, which is the only format the kernel will load. Please run strip-x on it before shipping it. I've looked at some of the drivers that have been posted. Obviously, I'm interested.
Some developers have forgotten to run this, and it means that their download size is about 10 times larger than it needs to be. They're giving away all their debugging symbols. It doesn't affect the runtime at all. We only load the important pieces into the kernel. But if you want to have faster downloads, strip-x is a good thing to do.
Okay, last is this plugins folder inside the kernel extension contents. And this is kind of neat. Again, it's optional. If you have a plugins folder, it gives you a place to deliver other kernel extensions. The reason you'd want to do this is for the drag and drop functionality. Let's say you've got a family, well, family collection of three or four drivers that you always want to move around together as a collection.
What you can do is pick one that is sort of the definitive KEXT and then put all of the other pieces as subkexts, as we call them, inside the plugin folder. The advantage there is to the user, it appears to be one kernel extension. To our loading system, however, we consider every kernel extension equal, so we don't care whether it's embedded or at the top level. That's actually important to note. The fact that it's embedded doesn't mean anything at all. It's just a way to deliver it.
Now, the Plugins folder can contain other texts. It can also contain device interface libraries. This would be typically if you're writing a driver with some sort of a custom interface that applications would need to get at directly. Or if you're writing a family and you want to provide a user client or device interface for an application, that's a handy place to store the bundle for that. And in the future, there will probably be other things of interest that could go in that Plugins folder. It's sort of a generic place to put add-ons.
Only one level of nesting is supported, though. You can't have a kernel extension inside a kernel extension inside a kernel extension. Well, we could have made this work. But there was very little use for it. The user sees the top level. That's our intent. You can package things internally. If we want to have more complex relationships and organizations, send us notes on how you'd like to have that done. I don't think creating a massive tree inside the extensions folder is necessarily the right way to do that.
All right, now I want to talk about the user kernel boundary. This is new, I think, to developers just coming in from 9, looking at 10. There are different places on Mac OS X, different address spaces. Drivers live inside the kernel. Applications live outside the kernel. So anytime your app needs to talk to a driver, you've got to cross that boundary. It's as simple as that. There's no way around it.
Mac OS X and the Darwin kernel and infrastructure provide a large number of ways to get across that boundary. If you can use a sufficiently high abstraction, like a file system, the file system provides that bridge for you. It makes use of BSD to cross that bridge and get data out of the kernel and into your application. Networking, Sockets does that for you as well. But if you can't use a high level abstraction and you have to use a low level abstraction, you've got to get directly at a device, you can use, basically it's a CFPlugin interface that we use called a device interface.
Oh, and the class, the in-kernel class that you derive from is I/O User Client. The name derives from the fact that from the kernel's perspective, the client is a user. Therefore, the object is a user client. So your code belongs outside the kernel unless you must process interrupts. If you have to get at an interrupt, you need to be inside the kernel.
Unless you have to deliver services to an in-kernel component, for example, file systems live inside the kernel. So if you're writing a disk driver and the disk is most likely going to be used by file systems, it makes sense for that driver to live inside the kernel. It is possible, of course, to put almost anything you want outside the kernel, but then moving data means more user kernel boundary crossings, and that costs a lot of time, so you're not going to get a huge amount of performance. But if it's a really slow device, you can have drivers live outside and tunnel back and forth across the user kernel boundary.
The other reason for putting something inside the kernel would be if it provides a service that's needed by a large number of clients and it's needed much of the time. Now, this is kind of a judgment call here, but basically the kernel is sort of the only guaranteed rendezvous mechanism on the system. An app running in Darwin or Cocoa or Carbon can pretty much guarantee it's there and there are APIs to get at it.
So if you've got a service that's available inside the kernel, it's a convenient rendezvous mechanism. But if you only have one client that needs to get at your device, or if it's only used 1% of the time, why put it in the kernel? That memory is going to be used all of the time, not just when it's in use.
So an example there might be a backup driver, a device driver that's only used to do backups. Most people aren't backing up all of the time. You could have one application that could load such a driver when it needs it, or it could process it in user space and then unload it when it's no longer needed.
So the device interface model, this is sort of the app view of I/O Kit. The device interface model provides API access for many devices in the system. It doesn't provide access for all devices. This is important. We don't provide access for applications to directly get at the PCI bus.
We do let you get at devices hanging off the PCI bus, so if you've got a SCSI controller there, you can talk to the SCSI bus, but you can't talk to the PCI bus. It's actually a security issue there. Also, there are some devices it really doesn't make sense to get at from user space, so we don't create the ability to cross that boundary for every kind of device. But the ability is there if we need to make use of it in the future.
The device interface model wraps a number of Mach mechanisms. Basically, since Mach is the IPC model, everything that we do is simply a wrapper for that. And our idea there is for I/O Kit to make the most common uses of the Mach infrastructure easy to use. I/O User Client provides this. So, system calls.
[Transcript missing]
So to a driver, this boundary crossing layer really looks like an in-kernel client, a subclass of I/O user client. The object I/O user client is inside the kernel. It's talking to the driver like any other in-kernel component. It's just moving that data across the user kernel boundary as its primary goal. To an application, it looks like typically a CFPlugin, generally Maco binary, that's the preferred format.
But basically, it is There's proxies in both space. The proxy in the user space is a CFPlugin, and the proxy in the kernel space is an I/O user client. They're geared to talk to each other, and your app talks to the CFBundle, the CFPlugin, and the I/O Kit driver is going to talk to the user client. So, I'm going to go through a four-step sequence here of how this works.
So from an application, the first thing to do is find the device of interest. And the way you do that is by searching the I-O registry. There's a number of different ways to search the I-O registry and discover the device or devices of interest, but probably the most common and most useful one is I-O service get matching services. This basically allows you to find all of the instances of I-O SCSI device or all the instances of I-O USB device.
And you can provide some further match criteria that is driven actually very similarly to the way driver matching works. But basically, you find the devices of interest, and you get a list back, and you use I-O Iterator Next to actually walk through that list. And when you find the device you're interested in, you make note of it. And in step two... You're going to need to call I/O Create Plugin Interface for Service, if this is a CFPlugin style user client.
The technique here is to take the cookie that you got from searching the I/O registry and request a session here. So you basically create a plugin interface. This loads the CFBundle into your application, and then you call Query Interface, which actually returns an interface to the desired device.
All right, at this point, you actually don't own the device. You just have an interface to the device. I/O Kit has multiple layers here. Because we also have multiple clients competing for devices. They might all want to have a session going to the device, but they don't want to keep it open all the time. They want to be a good citizen and close it periodically to allow others the opportunity to use it.
So in this case, the details are family specific. But basically, it follows a pretty basic open I/O close procedure here. There's nothing really surprising there. Ideally, you call open as late as possible. And you call close as early and as often as possible. Because as long as you have a device open, nobody else is going to be able to get at it. Each family will instrument its own arbitration policy.
But in general, assume mutual exclusion, unless you've heard something else. And ideally, if you're doing something to a device that only takes a few seconds, open it. Do that I/O and close it. You only keep it open if you really need to. Because it's in some volatile state that you can't easily restore. Or it doesn't make sense for it to trade owners briefly.
And the last step is to release the interface to the device. This just cleans things up. If your app crashes, this gets done for you. But ideally, you explicitly clean this up. There is a release note. I'm sorry, I don't have a more specific reference, but if you go to the ADC website, you should be able to search for this and find it. CFBundle and CFPlugins release note describe a lot of the APIs, the app-level APIs.
Now, I want to talk for a little while about the different app models. I gave you a rundown of how to do that, assuming that your app could talk to a Mako plug-in. But there's a number of different application spaces on Mac OS X. So for starters, let's take a classic application. Well, there's no direct I/O Kit access for a classic application. It wouldn't be a compatibility environment if it included all the I/O Kit APIs. So basically, whatever is present in classic, whatever abstractions are there, that's what you get. All communications go through classic itself.
The same is true for a pure Carbon app. If you're writing a Carbon app in CFM and you're keeping it pure, you're only calling Carbon APIs, you only get what's in Carbon. I mean, that's kind of the definition of a pure Carbon app. So you go through the Carbon framework and that's how you get down into the kernel and get at drivers. But there's not a tremendous amount there.
Now it starts to get a little bit more interesting when you are willing to do Mac OS X specific code in your application. And in this case we've got a CFM Carbon application. which has basically two ways to talk to I/O Kit. One is through Carbon in places like I/O SCSI Action, where we've actually provided some Carbon APIs for device access. You can make use of that.
But you can also make use of the plugin architecture that I described in the previous slides. Now the coloration here is a little bit of a giveaway. The orange represents parts of the Darwin kernel, things that Apple provides you. The purple is the app environment that you're using. And the blue boxes on the top are the boxes that represent code that you write.
You write the application itself, and you write the CFBundle that provides the interface down into I/O Kit. Now this is actually kind of handy because it allows you to write a different plugin for running on Mac OS 9. So you can write one application that runs on 9 or 10 and it would load an operating system specific plugin to do its I/O.
Now if you're writing a MacO Carbon app, it actually eliminates a layer there. You don't have to have the plug-in because you're not switching over from CFM to MacO. You can directly call the plug-ins that I/O Kit provides. And of course you can still go through the Carbon environment that's always available to you.
And the same is true for a Cocoa app. Basically, you can go through Cocoa or you can go through the CFPlugin. What I don't have up here is a pure BSD application, but a BSD application has all the CFPlugin stuff available to it, plus all the BSD APIs as well.
So I don't have any other details on I/O Kit for you today. What I do have is a rather lengthy roadmap for other sessions, because that's where the details are going to be conveyed to you. There are sessions on graphics, actually right after this session. There is sessions on image capture, sessions on Input devices, Firewire, USB. Spend a little time looking through your guide if you haven't already and figure out which ones are going to have information most relevant to you. There's a lot of details, there's a lot of API that fits under the I/O Kit umbrella. And yet another page of them.