Can't get callback notifications from IOKit to fire using pInvoke

77 views Asked by At

I'm trying to write a .net multiplatform library, and macos is giving me some trouble. I'm attempting to use IOKit to let me know when a serial port has been plugged or unplugged, and from there, I can use the standard .net serial port libraries to do everything else. The following is the code for setting up the notification:

using NM = Native.NativeMethods;

public class SerialPortManager
{
    private static SerialPortManager _self;
    
    private int _portPublishedIterator = 0;
    private int _portTerminatedIterator = 0;

    public SerialPortManager()
    {
        if (_self != null)
        {
            throw new Exception("already substantiated");
        }

        _self = this;
        int masterPort;

        if (NM.IOMasterPort(0, out masterPort) != NM.IOReturn.Success || masterPort == 0)
        {
            throw new Exception("could not get master port");
        }

        var matchingDict = NM.IOServiceMatching(NM.kIOSerialBSDServiceValue);
        NM.CFRetain(matchingDict);
        NM.CFDictionarySetValue(matchingDict, NM.CFStringCreateWithCharacters(NM.kIOSerialBSDTypeKey), NM.CFStringCreateWithCharacters(NM.kIOSerialBSDAllTypes));

        var notificationPortRef = NM.IONotificationPortCreate(masterPort);

        if(notificationPortRef == IntPtr.Zero)
        {
            throw new Exception("could not create notification port");
        }

        NM.CFRunLoopAddSource(NM.CFRunLoopGetCurrent(), NM.IONotificationPortGetRunLoopSource(notificationPortRef), NM.CFStringCreateWithCharacters(NM.kCFRunLoopDefaultMode));

        var result = NM.IOServiceAddMatchingNotification
                (notificationPortRef,
                NM.kIOPublishNotification,
                matchingDict,
                publishedNotificationCallback,
                IntPtr.Zero,
                out _portPublishedIterator);

        if (result != NM.IOReturn.Success)
        {
            if (_portPublishedIterator != 0)
                NM.IOObjectRelease(_portPublishedIterator);

            NM.CFRelease(matchingDict);
            throw new Exception("could not create notification matching service");
        }

        var device = NM.IOIteratorNext(_portPublishedIterator);
        while (device != 0)
        {
            NM.IOObjectRelease(device);
            device = NM.IOIteratorNext(_portPublishedIterator);
        }

        result = NM.IOServiceAddMatchingNotification
                (notificationPortRef,
                NM.kIOTerminatedNotification,
                matchingDict,
                terminatedNotificationCallback,
                IntPtr.Zero,
                out _portTerminatedIterator);

        if (result != NM.IOReturn.Success)
        {
            if (_portTerminatedIterator != 0)
                NM.IOObjectRelease(_portTerminatedIterator);

            throw new Exception("could not create notification matching service");
        }

        device = NM.IOIteratorNext(_portTerminatedIterator);
        while (device != 0)
        {
            NM.IOObjectRelease(device);
            device = NM.IOIteratorNext(_portTerminatedIterator);
        }
    }

    private static void publishedNotificationCallback(nint context, int iterator)
    {
        var device = NM.IOIteratorNext(iterator);
        while (device != 0)
        {
            //do something with device

            NM.IOObjectRelease(device);
            device = NM.IOIteratorNext(iterator);
        }

        _self.refreshSerialDevices();
    }

    private static void terminatedNotificationCallback(nint context, int iterator)
    {
        var device = NM.IOIteratorNext(iterator);
        while (device != 0)
        {
            //do something with device

            NM.IOObjectRelease(device);
            device = NM.IOIteratorNext(iterator);
        }

        _self.refreshSerialDevices();
    }
}

And here is the class that invokes all the methods from IOKit:

public static class NativeMethods
{
    const string CoreFoundation = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";
    const string IOKit = "/System/Library/Frameworks/IOKit.framework/IOKit";

    public static readonly string kCFRunLoopDefaultMode = "kCFRunLoopDefaultMode";
    public static readonly string kIOSerialBSDServiceValue = "IOSerialBSDClient";
    public static readonly string kIOSerialBSDTypeKey = "IOSerialBSDClientType";
    public static readonly string kIOSerialBSDAllTypes = "IOSerialStream";
    public static readonly string kIOPublishNotification = "IOServicePublish";
    public static readonly string kIOTerminatedNotification = "IOServiceTerminate";

    public delegate void IOServiceMatchingCallback(IntPtr context, int iterator);

    public enum IOReturn
    {
        Success = 0,
        ExclusiveAccess = -536870203,
        NotSupported = -536870201,
        Offline = -536870185,
        NotPermitted = -536870174
    }

    [DllImport(CoreFoundation, EntryPoint = "CFDictionarySetValue")]
    public static extern void CFDictionarySetValue(IntPtr dict, IntPtr key, IntPtr value);

    [DllImport(CoreFoundation, CharSet = CharSet.Unicode, EntryPoint = "CFStringCreateWithCharacters")]
    public static extern IntPtr CFStringCreateWithCharacters(IntPtr allocator, char[] buffer, IntPtr length);

    public static IntPtr CFStringCreateWithCharacters(string str)
    {
        return CFStringCreateWithCharacters(IntPtr.Zero, str.ToCharArray(), (IntPtr)str.Length);
    }

    [DllImport(CoreFoundation, EntryPoint = "CFRunLoopGetCurrent")]
    public static extern IntPtr CFRunLoopGetCurrent();

    [DllImport(CoreFoundation, EntryPoint = "CFRunLoopAddSource")]
    public static extern void CFRunLoopAddSource(IntPtr runLoop, IntPtr source, IntPtr mode);

    [DllImport(CoreFoundation, EntryPoint = "CFRelease")]
    public static extern void CFRelease(IntPtr obj);

    [DllImport(CoreFoundation, EntryPoint = "CFRetain")]
    public static extern void CFRetain(IntPtr obj);

    [DllImport(IOKit, EntryPoint = "IOMasterPort")]
    public static extern IOReturn IOMasterPort(int bootstrapPort, out int masterPort);

    [DllImport(IOKit, EntryPoint = "IONotificationPortCreate")]
    public static extern IntPtr IONotificationPortCreate(int masterPort);

    [DllImport(IOKit, EntryPoint = "IONotificationPortGetRunLoopSource")]
    public static extern IntPtr IONotificationPortGetRunLoopSource(IntPtr notifyPort);

    [DllImport(IOKit, EntryPoint = "IOIteratorNext")]
    public static extern int IOIteratorNext(int iterator);

    [DllImport(IOKit, EntryPoint = "IOObjectRetain")]
    public static extern IOReturn IOObjectRetain(int @object);

    [DllImport(IOKit, EntryPoint = "IOObjectRelease")]
    public static extern IOReturn IOObjectRelease(int @object);

    [DllImport(IOKit, EntryPoint = "IOServiceAddMatchingNotification")]
    public static extern IOReturn IOServiceAddMatchingNotification(IntPtr notifyPort, string notificationType, IntPtr matching, IOServiceMatchingCallback callback, IntPtr context, out int iterator);

    [DllImport(IOKit, EntryPoint = "IOServiceMatching")]
    public static extern IntPtr IOServiceMatching(string name);
}

The functions all return correctly, but the published/terminated callbacks are never used. Has anybody else had luck with pInvoking IOKit and getting notification? Or is there something obviously wrong in how I've done the callbacks?

I've pulled this sequence from ORSSerialPort as well as several other examples, and have tried a few different things such as having the callbacks be IntPtr instead of delegate, but I can't seem to get this to work.

Edit: Just adding that the code does not crash, just no notification is ever fired.

0

There are 0 answers