You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用P/Invoke调用IOKit时无法触发回调通知

.NET跨平台库macOS下IOKit串口插拔通知未触发问题

我正在开发一个.NET跨平台库,在macOS平台尝试用IOKit监听串口插拔事件,后续用标准.NET串口库处理操作。所有Native方法返回值均正常,但设备插拔时publishedNotificationCallback和terminatedNotificationCallback始终不触发,代码无崩溃,只是通知完全没反应。


配置通知的代码

using NM = Native.NativeMethods;

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

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

        _self = this;
        nint 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);
        var typeKey = NM.CFStringCreateWithCharacters(NM.kIOSerialBSDTypeKey);
        var allTypes = NM.CFStringCreateWithCharacters(NM.kIOSerialBSDAllTypes);
        NM.CFDictionarySetValue(matchingDict, typeKey, allTypes);
        NM.CFRelease(typeKey);
        NM.CFRelease(allTypes);

        var notificationPortRef = NM.IONotificationPortCreate(masterPort);

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

        var runLoopSource = NM.IONotificationPortGetRunLoopSource(notificationPortRef);
        NM.CFRunLoopAddSource(NM.CFRunLoopGetCurrent(), runLoopSource, 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(IntPtr context, nint iterator)
    {
        var device = NM.IOIteratorNext(iterator);
        while (device != 0)
        {
            // 处理设备逻辑

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

        _self.refreshSerialDevices();
    }

    private static void terminatedNotificationCallback(IntPtr context, nint iterator)
    {
        var device = NM.IOIteratorNext(iterator);
        while (device != 0)
        {
            // 处理设备逻辑

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

        _self.refreshSerialDevices();
    }

    private void refreshSerialDevices()
    {
        // 刷新串口列表逻辑
    }
}

原NativeMethods类(问题点标注)

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

    // 错误:这些常量需为CFString的IntPtr,而非C#字符串
    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";

    // 错误:未指定调用约定,IOKit回调用Cdecl,默认StdCall会导致调用失败
    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")]
    // 错误:masterPort需为nint,macOS是64位,int会截断句柄
    public static extern IOReturn IOMasterPort(int bootstrapPort, out int masterPort);

    [DllImport(IOKit, EntryPoint = "IONotificationPortCreate")]
    // 错误:masterPort需为nint
    public static extern IntPtr IONotificationPortCreate(int masterPort);

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

    [DllImport(IOKit, EntryPoint = "IOIteratorNext")]
    // 错误:iterator和返回值需为nint,io_iterator_t是64位类型
    public static extern int IOIteratorNext(int iterator);

    [DllImport(IOKit, EntryPoint = "IOObjectRelease")]
    // 错误:object需为nint
    public static extern IOReturn IOObjectRelease(int @object);

    [DllImport(IOKit, EntryPoint = "IOServiceAddMatchingNotification")]
    // 错误:notificationType需为IntPtr(CFStringRef),iterator参数类型错误
    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);
}

关键修复点

  1. 回调委托调用约定:给IOServiceMatchingCallback添加[UnmanagedFunctionPointer(CallingConvention.Cdecl)]属性,IOKit的C回调使用Cdecl调用约定,默认StdCall会导致调用栈异常,回调无法触发。

  2. 64位类型匹配:macOS是64位系统,io_iterator_t、io_object_t、mach_port_t都是64位整数,C#里要用nint替代int,避免句柄截断失效。

  3. CFString参数修正:IOServiceAddMatchingNotification的第二个参数是CFStringRef,不能直接传C#字符串,需将常量预转为CFString的IntPtr,并在程序退出时释放:

public static readonly IntPtr kCFRunLoopDefaultMode = CFStringCreateWithCharacters("kCFRunLoopDefaultMode");
public static readonly IntPtr kIOPublishNotification = CFStringCreateWithCharacters("IOServicePublish");
public static readonly IntPtr kIOTerminatedNotification = CFStringCreateWithCharacters("IOServiceTerminate");

// 程序退出时释放资源
public static void Cleanup()
{
    CFRelease(kCFRunLoopDefaultMode);
    CFRelease(kIOPublishNotification);
    CFRelease(kIOTerminatedNotification);
    // 其他CFString常量同理释放
}
  1. 内存泄漏修复:创建CFString后必须调用CFRelease释放,比如原代码中CFDictionarySetValue、CFRunLoopAddSource时创建的临时CFString都需要释放。

修改后的NativeMethods核心部分

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 IntPtr kCFRunLoopDefaultMode = CFStringCreateWithCharacters("kCFRunLoopDefaultMode");
    public static readonly IntPtr kIOSerialBSDServiceValue = CFStringCreateWithCharacters("IOSerialBSDClient");
    public static readonly IntPtr kIOSerialBSDTypeKey = CFStringCreateWithCharacters("IOSerialBSDClientType");
    public static readonly IntPtr kIOSerialBSDAllTypes = CFStringCreateWithCharacters("IOSerialStream");
    public static readonly IntPtr kIOPublishNotification = CFStringCreateWithCharacters("IOServicePublish");
    public static readonly IntPtr kIOTerminatedNotification = CFStringCreateWithCharacters("IOServiceTerminate");

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate void IOServiceMatchingCallback(IntPtr context, nint iterator);

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

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

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

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

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

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

    // 其他方法保持类型修正后的版本...

    public static void Cleanup()
    {
        CFRelease(kCFRunLoopDefaultMode);
        CFRelease(kIOSerialBSDServiceValue);
        CFRelease(kIOSerialBSDTypeKey);
        CFRelease(kIOSerialBSDAllTypes);
        CFRelease(kIOPublishNotification);
        CFRelease(kIOTerminatedNotification);
    }
}

以上修改解决了类型不匹配、调用约定错误、CFString参数传递错误等核心问题,应该能触发串口插拔的回调通知。

内容的提问来源于stack exchange,提问作者sonic1015

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 02:54:54