fix: use class matching and double-pointer refs

IOServiceMatching on idVendor/idProduct numbers is unreliable (single-key
matches fail), so match the IOUSBDevice class and filter VID/PID in user
space via the idVendor/idProduct properties.

QI'd interfaces are double-pointer references: *ref is the interface
struct and methods are called as (*ref)->Method(ref, ...). The old code
treated the slot as the struct and crashed on the first method call.

Verified on a physical dongle (PID 0x02E6): probe opens the device,
enumerates EP 0x04 IN/OUT and EP 0x05 IN, and register reads via
DeviceRequest return valid values (MT7612 ASIC version).

Co-Authored-By: qwen3.8-27b@q2_k_xl: fixed USB transport for hardware
This commit is contained in:
portersky
2026-08-17 16:06:53 +02:00
parent 4f0f1ff75d
commit aab44fd7aa
2 changed files with 84 additions and 76 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ if(NOT CMAKE_GENERATOR MATCHES "^(Ninja|Xcode)$")
endif() endif()
cmake_minimum_required(VERSION 3.21) cmake_minimum_required(VERSION 3.21)
project(xone_macos VERSION 0.1.3 LANGUAGES CXX Swift) project(xone_macos VERSION 0.1.4 LANGUAGES CXX Swift)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+77 -69
View File
@@ -27,14 +27,35 @@ constexpr std::size_t num_in_reads = 4;
namespace { namespace {
auto vid_match_dict() -> CFMutableDictionaryRef // Numeric property matching on "idVendor"/"idProduct" is unreliable on some
// macOS versions (single-key matches fail while combined matches work), so
// we match the IOUSBDevice class and filter VID/PID in user space. The
// numeric values live in the "idVendor"/"idProduct" registry properties.
struct vid_pid {
std::uint16_t vid = 0;
std::uint16_t pid = 0;
};
auto read_vid_pid(io_service_t service) -> std::optional<vid_pid>
{ {
CFMutableDictionaryRef match = IOServiceMatching("IOUSBDevice"); CFMutableDictionaryRef props = nullptr;
SInt32 vid_value = static_cast<SInt32>(vid); if (IORegistryEntryCreateCFProperties(service, &props, kCFAllocatorDefault, 0)
CFNumberRef number = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &vid_value); != kIOReturnSuccess
CFDictionaryAddValue(match, CFSTR(kUSBVendorString), number); || !props)
CFRelease(number); return std::nullopt;
return match;
auto *vid_number = static_cast<CFNumberRef>(CFDictionaryGetValue(props, CFSTR("idVendor")));
auto *pid_number = static_cast<CFNumberRef>(CFDictionaryGetValue(props, CFSTR("idProduct")));
SInt32 v = 0, p = 0;
std::optional<vid_pid> result;
if (vid_number && pid_number
&& CFNumberGetValue(vid_number, kCFNumberSInt32Type, &v)
&& CFNumberGetValue(pid_number, kCFNumberSInt32Type, &p)) {
result = { static_cast<std::uint16_t>(v), static_cast<std::uint16_t>(p) };
}
CFRelease(props);
return result;
} }
auto is_supported_pid(std::uint16_t pid) -> bool auto is_supported_pid(std::uint16_t pid) -> bool
@@ -43,25 +64,9 @@ auto is_supported_pid(std::uint16_t pid) -> bool
|| pid == pid_builtin_asus_lenovo || pid == pid_surface_book_2; || pid == pid_builtin_asus_lenovo || pid == pid_surface_book_2;
} }
// Read the "kUSBProductString" property (a CFNumber) of a USB device service. auto is_dongle(vid_pid const &vp) -> bool
// Returns 0 if the property is missing or unreadable.
auto read_pid(io_service_t service) -> std::uint16_t
{ {
CFMutableDictionaryRef props = nullptr; return vp.vid == vid && is_supported_pid(vp.pid);
if (IORegistryEntryCreateCFProperties(service, &props, kCFAllocatorDefault, 0) != kIOReturnSuccess
|| !props)
return 0;
auto *number = static_cast<CFNumberRef>(CFDictionaryGetValue(props, CFSTR(kUSBProductString)));
std::uint16_t pid = 0;
if (number) {
SInt32 value = 0;
if (CFNumberGetValue(number, kCFNumberSInt32Type, &value))
pid = static_cast<std::uint16_t>(value);
}
CFRelease(props);
return pid;
} }
} // namespace } // namespace
@@ -69,7 +74,7 @@ auto read_pid(io_service_t service) -> std::uint16_t
struct transport::read_slot { struct transport::read_slot {
transport *owner = nullptr; transport *owner = nullptr;
std::uint8_t ep = 0; std::uint8_t ep = 0;
IOUSBInterfaceInterface190 *iface = nullptr; IOUSBInterfaceInterface190 **iface_ref = nullptr;
UInt8 pipe_ref = 0; UInt8 pipe_ref = 0;
std::vector<std::uint8_t> buf; std::vector<std::uint8_t> buf;
}; };
@@ -78,21 +83,21 @@ struct transport::state {
std::uint16_t pid = 0; std::uint16_t pid = 0;
// Device connection (port of the usb_device + usb_interface binding). // Device connection (port of the usb_device + usb_interface binding).
// Plug-in interface references are double pointers; QI'd interfaces are // All interface references are double pointers: *ref is the interface
// single struct pointers. // struct, and methods are called as (*ref)->Method(ref, ...).
io_service_t service = 0; io_service_t service = 0;
IOCFPlugInInterface **dev_iodev = nullptr; IOCFPlugInInterface **dev_iodev = nullptr;
IOUSBDeviceInterface197 *dev = nullptr; IOUSBDeviceInterface197 **dev_ref = nullptr;
bool dev_opened = false; bool dev_opened = false;
struct iface_conn { struct iface_conn {
IOCFPlugInInterface **iodev = nullptr; IOCFPlugInInterface **iodev = nullptr;
IOUSBInterfaceInterface190 *iface = nullptr; IOUSBInterfaceInterface190 **iface_ref = nullptr;
}; };
std::vector<iface_conn> ifaces; std::vector<iface_conn> ifaces;
struct pipe_ref { struct pipe_ref {
IOUSBInterfaceInterface190 *iface = nullptr; IOUSBInterfaceInterface190 **iface_ref = nullptr;
UInt8 ref = 0; UInt8 ref = 0;
}; };
std::optional<pipe_ref> in_cmd_pipe; std::optional<pipe_ref> in_cmd_pipe;
@@ -143,7 +148,7 @@ transport::~transport()
state_->stopping = true; state_->stopping = true;
} }
for (auto &slot : state_->slots) for (auto &slot : state_->slots)
slot.iface->AbortPipe(slot.iface, slot.pipe_ref); (*slot.iface_ref)->AbortPipe(slot.iface_ref, slot.pipe_ref);
if (state_->thread_started) { if (state_->thread_started) {
CFRunLoopRef runloop = nullptr; CFRunLoopRef runloop = nullptr;
@@ -168,16 +173,16 @@ transport::~transport()
// Close the interfaces and their endpoint pipes. // Close the interfaces and their endpoint pipes.
for (auto &conn : state_->ifaces) { for (auto &conn : state_->ifaces) {
conn.iface->USBInterfaceClose(conn.iface); (*conn.iface_ref)->USBInterfaceClose(conn.iface_ref);
conn.iface->Release(conn.iface); (*conn.iface_ref)->Release(conn.iface_ref);
IODestroyPlugInInterface(conn.iodev); IODestroyPlugInInterface(conn.iodev);
} }
// Close the device connection. // Close the device connection.
if (state_->dev) { if (state_->dev_ref) {
if (state_->dev_opened) if (state_->dev_opened)
state_->dev->USBDeviceClose(state_->dev); (*state_->dev_ref)->USBDeviceClose(state_->dev_ref);
state_->dev->Release(state_->dev); (*state_->dev_ref)->Release(state_->dev_ref);
} }
if (state_->dev_iodev) if (state_->dev_iodev)
IODestroyPlugInInterface(state_->dev_iodev); IODestroyPlugInInterface(state_->dev_iodev);
@@ -192,9 +197,9 @@ auto transport::open(frame_callback frames, disconnect_callback disconnected) ->
state_->frames = std::move(frames); state_->frames = std::move(frames);
state_->disconnected = std::move(disconnected); state_->disconnected = std::move(disconnected);
// Find the dongle service (VID match, filter by PID). // Find the dongle service (class match, filter by VID/PID).
io_iterator_t iter = 0; io_iterator_t iter = 0;
kern_return_t kr = IOServiceGetMatchingServices(0, vid_match_dict(), &iter); kern_return_t kr = IOServiceGetMatchingServices(0, IOServiceMatching("IOUSBDevice"), &iter);
if (kr != kIOReturnSuccess) if (kr != kIOReturnSuccess)
return false; return false;
@@ -203,10 +208,10 @@ auto transport::open(frame_callback frames, disconnect_callback disconnected) ->
io_service_t service = IOIteratorNext(iter); io_service_t service = IOIteratorNext(iter);
if (!service) if (!service)
break; break;
std::uint16_t pid = read_pid(service); auto vp = read_vid_pid(service);
if (is_supported_pid(pid)) { if (vp && is_dongle(*vp)) {
state_->service = service; state_->service = service;
state_->pid = pid; state_->pid = vp->pid;
found = true; found = true;
} else { } else {
IOObjectRelease(service); IOObjectRelease(service);
@@ -225,17 +230,16 @@ auto transport::open(frame_callback frames, disconnect_callback disconnected) ->
return false; return false;
} }
auto *dev = static_cast<IOUSBDeviceInterface197 *>(nullptr); void *slot = nullptr;
HRESULT hr = (*state_->dev_iodev)->QueryInterface(state_->dev_iodev, HRESULT hr = (*state_->dev_iodev)->QueryInterface(state_->dev_iodev,
CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID197), CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID197), &slot);
reinterpret_cast<void **>(&dev)); if (hr != S_OK || !slot) {
if (hr != S_OK || !dev) {
xone::log_msg(log_level::error, "usb: query device interface failed"); xone::log_msg(log_level::error, "usb: query device interface failed");
return false; return false;
} }
state_->dev = dev; state_->dev_ref = static_cast<IOUSBDeviceInterface197 **>(slot);
kr = dev->USBDeviceOpen(dev); kr = (*state_->dev_ref)->USBDeviceOpen(state_->dev_ref);
if (kr != kIOReturnSuccess) { if (kr != kIOReturnSuccess) {
xone::log_msg(log_level::error, "usb: open device failed (%d)", kr); xone::log_msg(log_level::error, "usb: open device failed (%d)", kr);
return false; return false;
@@ -267,7 +271,8 @@ auto transport::open(frame_callback frames, disconnect_callback disconnected) ->
// Async completion dispatch for the opened interfaces. // Async completion dispatch for the opened interfaces.
for (auto &conn : state_->ifaces) { for (auto &conn : state_->ifaces) {
CFRunLoopSourceRef source = nullptr; CFRunLoopSourceRef source = nullptr;
if (conn.iface->CreateInterfaceAsyncEventSource(conn.iface, &source) == kIOReturnSuccess if ((*conn.iface_ref)->CreateInterfaceAsyncEventSource(conn.iface_ref, &source)
== kIOReturnSuccess
&& source) && source)
state_->sources.push_back(source); state_->sources.push_back(source);
} }
@@ -276,7 +281,8 @@ auto transport::open(frame_callback frames, disconnect_callback disconnected) ->
// matching dictionary is consumed by this call. // matching dictionary is consumed by this call.
state_->notify_port = IONotificationPortCreate(0); state_->notify_port = IONotificationPortCreate(0);
kr = IOServiceAddMatchingNotification(state_->notify_port, kIOTerminatedNotification, kr = IOServiceAddMatchingNotification(state_->notify_port, kIOTerminatedNotification,
vid_match_dict(), on_dongle_terminated, this, IOServiceMatching("IOUSBDevice"),
on_dongle_terminated, this,
&state_->termination_iter); &state_->termination_iter);
if (kr != kIOReturnSuccess) { if (kr != kIOReturnSuccess) {
xone::log_msg(log_level::error, "usb: add termination notification failed (%d)", kr); xone::log_msg(log_level::error, "usb: add termination notification failed (%d)", kr);
@@ -293,7 +299,7 @@ auto transport::open(frame_callback frames, disconnect_callback disconnected) ->
std::uint8_t ep, std::size_t buf_len) -> bool { std::uint8_t ep, std::size_t buf_len) -> bool {
for (std::size_t i = 0; i < num_in_reads; i++) { for (std::size_t i = 0; i < num_in_reads; i++) {
state_->slots.push_back( state_->slots.push_back(
{ this, ep, pipe->iface, pipe->ref, std::vector<std::uint8_t>(buf_len) }); { this, ep, pipe->iface_ref, pipe->ref, std::vector<std::uint8_t>(buf_len) });
if (!submit_read(&state_->slots.back())) if (!submit_read(&state_->slots.back()))
return false; return false;
} }
@@ -318,17 +324,17 @@ auto transport::open_interface(io_service_t child) -> bool
!= kIOReturnSuccess) != kIOReturnSuccess)
return false; return false;
auto *iface = static_cast<IOUSBInterfaceInterface190 *>(nullptr); void *slot = nullptr;
if ((*conn.iodev)->QueryInterface(conn.iodev, CFUUIDGetUUIDBytes(kIOUSBInterfaceInterfaceID190), if ((*conn.iodev)->QueryInterface(conn.iodev, CFUUIDGetUUIDBytes(kIOUSBInterfaceInterfaceID190),
reinterpret_cast<void **>(&iface)) != S_OK &slot) != S_OK
|| !iface) { || !slot) {
IODestroyPlugInInterface(conn.iodev); IODestroyPlugInInterface(conn.iodev);
return false; return false;
} }
conn.iface = iface; conn.iface_ref = static_cast<IOUSBInterfaceInterface190 **>(slot);
if (iface->USBInterfaceOpen(iface) != kIOReturnSuccess) { if ((*conn.iface_ref)->USBInterfaceOpen(conn.iface_ref) != kIOReturnSuccess) {
iface->Release(iface); (*conn.iface_ref)->Release(conn.iface_ref);
IODestroyPlugInInterface(conn.iodev); IODestroyPlugInInterface(conn.iodev);
return false; return false;
} }
@@ -337,24 +343,24 @@ auto transport::open_interface(io_service_t child) -> bool
// Scan the interface's pipes for the endpoints we need. // Scan the interface's pipes for the endpoints we need.
UInt8 num_pipes = 0; UInt8 num_pipes = 0;
if (iface->GetNumEndpoints(iface, &num_pipes) != kIOReturnSuccess) if ((*conn.iface_ref)->GetNumEndpoints(conn.iface_ref, &num_pipes) != kIOReturnSuccess)
return true; return true;
for (UInt8 i = 1; i <= num_pipes; i++) { for (UInt8 i = 1; i <= num_pipes; i++) {
UInt8 direction = 0, number = 0, type = 0; UInt8 direction = 0, number = 0, type = 0;
UInt16 max_packet_size = 0; UInt16 max_packet_size = 0;
UInt8 interval = 0; UInt8 interval = 0;
if (iface->GetPipeProperties(iface, i, &direction, &number, &type, if ((*conn.iface_ref)->GetPipeProperties(conn.iface_ref, i, &direction, &number, &type,
&max_packet_size, &interval) != kIOReturnSuccess) &max_packet_size, &interval) != kIOReturnSuccess)
continue; continue;
std::uint8_t ep = static_cast<std::uint8_t>(number); std::uint8_t ep = static_cast<std::uint8_t>(number);
if (direction == kUSBIn && ep == ep_in_cmd && !state_->in_cmd_pipe.has_value()) if (direction == kUSBIn && ep == ep_in_cmd && !state_->in_cmd_pipe.has_value())
state_->in_cmd_pipe = { iface, i }; state_->in_cmd_pipe = { conn.iface_ref, i };
else if (direction == kUSBIn && ep == ep_in_wlan && !state_->in_wlan_pipe.has_value()) else if (direction == kUSBIn && ep == ep_in_wlan && !state_->in_wlan_pipe.has_value())
state_->in_wlan_pipe = { iface, i }; state_->in_wlan_pipe = { conn.iface_ref, i };
else if (direction == kUSBOut && ep == ep_out && !state_->out_pipe.has_value()) else if (direction == kUSBOut && ep == ep_out && !state_->out_pipe.has_value())
state_->out_pipe = { iface, i }; state_->out_pipe = { conn.iface_ref, i };
} }
return true; return true;
@@ -378,7 +384,7 @@ auto transport::send_vendor_request(vendor_request req, bool is_read, std::uint1
request.wLength = static_cast<UInt16>(len); request.wLength = static_cast<UInt16>(len);
request.pData = data; request.pData = data;
IOReturn ret = state_->dev->DeviceRequest(state_->dev, &request); IOReturn ret = (*state_->dev_ref)->DeviceRequest(state_->dev_ref, &request);
if (ret != kIOReturnSuccess || request.wLenDone != len) { if (ret != kIOReturnSuccess || request.wLenDone != len) {
xone::log_msg(log_level::error, "usb: vendor request 0x%02x failed (%d)", xone::log_msg(log_level::error, "usb: vendor request 0x%02x failed (%d)",
static_cast<int>(req), ret); static_cast<int>(req), ret);
@@ -391,7 +397,7 @@ auto transport::send_vendor_request(vendor_request req, bool is_read, std::uint1
auto transport::bulk_write(void const *data, std::size_t len) -> int auto transport::bulk_write(void const *data, std::size_t len) -> int
{ {
auto &pipe = state_->out_pipe.value(); auto &pipe = state_->out_pipe.value();
IOReturn ret = pipe.iface->WritePipe(pipe.iface, pipe.ref, IOReturn ret = (*pipe.iface_ref)->WritePipe(pipe.iface_ref, pipe.ref,
const_cast<void *>(data), static_cast<UInt32>(len)); const_cast<void *>(data), static_cast<UInt32>(len));
if (ret != kIOReturnSuccess) { if (ret != kIOReturnSuccess) {
xone::log_msg(log_level::error, "usb: bulk write failed (%d)", ret); xone::log_msg(log_level::error, "usb: bulk write failed (%d)", ret);
@@ -410,7 +416,7 @@ auto transport::submit_read(read_slot *slot) -> bool
state_->in_flight++; state_->in_flight++;
} }
IOReturn ret = slot->iface->ReadPipeAsync(slot->iface, slot->pipe_ref, IOReturn ret = (*slot->iface_ref)->ReadPipeAsync(slot->iface_ref, slot->pipe_ref,
slot->buf.data(), static_cast<UInt32>(slot->buf.size()), slot->buf.data(), static_cast<UInt32>(slot->buf.size()),
on_read_completion, slot); on_read_completion, slot);
if (ret != kIOReturnSuccess) { if (ret != kIOReturnSuccess) {
@@ -453,7 +459,7 @@ void transport::handle_read(read_slot *slot, IOReturn result, std::size_t len)
return; return;
} }
IOReturn ret = slot->iface->ReadPipeAsync(slot->iface, slot->pipe_ref, IOReturn ret = (*slot->iface_ref)->ReadPipeAsync(slot->iface_ref, slot->pipe_ref,
slot->buf.data(), static_cast<UInt32>(slot->buf.size()), slot->buf.data(), static_cast<UInt32>(slot->buf.size()),
on_read_completion, slot); on_read_completion, slot);
if (ret != kIOReturnSuccess) { if (ret != kIOReturnSuccess) {
@@ -494,7 +500,8 @@ void transport::on_dongle_terminated(void *refcon, io_iterator_t iter)
io_service_t service = IOIteratorNext(iter); io_service_t service = IOIteratorNext(iter);
if (!service) if (!service)
break; break;
if (read_pid(service) == t->state_->pid) auto vp = read_vid_pid(service);
if (vp && vp->vid == vid && vp->pid == t->state_->pid)
ours = true; ours = true;
IOObjectRelease(service); IOObjectRelease(service);
} }
@@ -506,7 +513,7 @@ void transport::on_dongle_terminated(void *refcon, io_iterator_t iter)
auto dongle_present() -> bool auto dongle_present() -> bool
{ {
io_iterator_t iter = 0; io_iterator_t iter = 0;
kern_return_t kr = IOServiceGetMatchingServices(0, vid_match_dict(), &iter); kern_return_t kr = IOServiceGetMatchingServices(0, IOServiceMatching("IOUSBDevice"), &iter);
if (kr != kIOReturnSuccess) if (kr != kIOReturnSuccess)
return false; return false;
@@ -515,7 +522,8 @@ auto dongle_present() -> bool
io_service_t service = IOIteratorNext(iter); io_service_t service = IOIteratorNext(iter);
if (!service) if (!service)
break; break;
if (is_supported_pid(read_pid(service))) auto vp = read_vid_pid(service);
if (vp && is_dongle(*vp))
present = true; present = true;
IOObjectRelease(service); IOObjectRelease(service);
} }