Let’s Bind an IOKit Method by Hand

Chris Hamons
Mar 28, 2018 · 4 min read

Today on the forums a user asked about bindings for IOKit. IOKit is very diverse, and currently unbound in Xamarin.Mac. However, since it’s C APIs, it is straightforward to bind by hand.

There aren’t too many examples online how to approach this however, so let’s break it down here.

We are going to bind this API:

io_service_t IOServiceGetMatchingService(mach_port_t masterPort, CFDictionaryRef matching);

Let’s take a look at the header, which will be our guide.

First open up Xcode, make a test Cocoa app, and add

@import IOKit;

to pull in the framework we are binding. Then we can paste IOServiceGetMatchingService into a function and command click it to jump to definition and bring it up the header.

Now we need to do two thing to bind it in C#:

- Setup the base p/invoke
- Convert each type into the correct C# type

The basic p/invoke would look like:

[DllImport (“/System/Library/Frameworks/IOKit.framework/IOKit”)]
static extern blah IOServiceGetMatchingService (blah masterPort, blah matching);

The important things here are that the function name matches the native symbols, and that we have the exact number of parameters, and a matching return type. You want to be very careful with p/invokes, they are not necessarily hard but mistakes can produce ugly crashes to debug.

Well “blah” won’t compile obviously, so we need to figure out the types to go there. Very often those are either numbers (int or long) or pointers (IntPtr), so let’s jump to Xcode.

Find io_service_t in the IOServiceGetMatchingService and command click it to jump to definition. That gives you

typedef io_object_t io_service_t;

and command clicking io_object_t gives you

typedef mach_port_t io_object_t;

then we continue to __darwin_mach_port_t then __darwin_mach_port_name_t then __darwin_natural_t which finally gives us unsigned int. Whew. Finally

Well I happen to know know that that turns into int but if you aren’t sure, you can run

NSLog (@”%d”, sizeof (unsigned int));

to check the size. 4 in this case, which means int.

So the first blah is int:

[DllImport (“/System/Library/Frameworks/IOKit.framework/IOKit”)]
static extern int IOServiceGetMatchingService (blah masterPort, blah matching);

Hitting the back arrow in Xcode a bunch, we start on master_port_t, which turns into __darwin_mach_port_t which we already know is an int. That wasn’t so bad:

[DllImport (“/System/Library/Frameworks/IOKit.framework/IOKit”)]
static extern int IOServiceGetMatchingService (int masterPort, blah matching);

The last one is easy, for CFDictionaryRef (and pretty much AnythingRef) is a IntPtr, since we need to pass a pointer (reference) to the object in question.

[DllImport (“/System/Library/Frameworks/IOKit.framework/IOKit”)]
static extern int IOServiceGetMatchingService (int masterPort, IntPtr matching);

Now let’s try to use it. Read the header to see what the expected values for each parameter. It notes passing kIOMasterPortDefault if you don’t care about the port, which we don’t. What is a kIOMasterPortDefault? Pasting it into our project and command clicking that gives us:

/*! @const kIOMasterPortDefault
@abstract The default mach port used to initiate communication with IOKit.
@discussion When specifying a master port to IOKit functions, the NULL argument indicates “use the default”. This is a synonym for NULL, if you’d rather use a named constant.
const mach_port_t kIOMasterPortDefault;

So we can use 0 (null) in our example. So we’re looking at something like:

int service = IOServiceGetMatchingService (0, some_pointer);

some_pointer being the last missing piece. We could bind the CFDictionary APIs, but there is a much easier way. Apple has what’s called “toll free bridging” between a number of Obj-C and C types. Reading that document, we can use a NSDictionary. So our invocation looks like:

using (NSDictionary d = new NSDictionary ()) {
int service = IOServiceGetMatchingService (0, d.Handle);

We’re using a using block to make sure that NSDictionary is alive for the duration of the call.

Now, let’s run it and see what happens. Service gives us a number and nothing crashed, which are good signs. However, we likely need to fill in the dictionary with information to describe what we’re looking for.

Doing some searching for “IOServiceGetMatchingService example” brings up multiple samples using a IOServiceMatching API, which looks like:

const char * name ) CF_RETURNS_RETAINED;

Using what we learned, and knowing that char * -> string we get:

[DllImport ("/System/Library/Frameworks/IOKit.framework/IOKit")]
static extern IntPtr IOServiceMatching (string name);

But notice the CF_RETURNS_RETAINED bit. That means we normally have to clean it up with

[DllImport ("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
extern static void CFRelease (IntPtr obj);

but if we keep reading we get

The dictionary is commonly passed to IOServiceGetMatchingServices or IOServiceAddNotification which will consume a reference, otherwise it should be released with CFRelease by the caller.

So we must not clean up the IOServiceMatching return. We should clean up the service though, with:

[DllImport ("/System/Library/Frameworks/IOKit.framework/IOKit")]
extern static int IOServiceClose (int service);

All together we get:

IntPtr serviceDescription = IOServiceMatching ("IOPCIDevice");
int service = IOServiceGetMatchingService (0, serviceDescription);
Console.WriteLine (service);
IOServiceClose (service);

Which appear to work! We’ll need more APIs bound it use it, but the process is the same as what we did here.

Chris Hamons

Written by

C# Monkey, Xamarin.Mac Lead