Skip to content

Commit 7bd7d25

Browse files
authored
feat: expose macOS exclusive device open mode (#24)
* feat: expose macOS exclusive device open mode Add hidDarwinSetOpenExclusive and hidDarwinGetOpenExclusive wrappers for the macOS-specific hid_darwin_set_open_exclusive and hid_darwin_get_open_exclusive functions. On non-macOS platforms the wrappers catch the missing-symbol error and return safe defaults. * ci: add macOS test runner and skip darwin tests on Linux Platform-specific darwin exclusive mode tests now skip on non-macOS. Add a test-macos CI job so those tests still run on every push/PR.
1 parent 4e8c897 commit 7bd7d25

5 files changed

Lines changed: 126 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,13 @@ jobs:
2222
- run: dart analyze --fatal-infos
2323
- run: dart format --output=none --set-exit-if-changed .
2424
- run: dart test
25+
26+
test-macos:
27+
runs-on: macos-latest
28+
steps:
29+
- uses: actions/checkout@v4
30+
- uses: dart-lang/setup-dart@v1
31+
with:
32+
sdk: '3.10.0'
33+
- run: dart pub get
34+
- run: dart test

SPEC.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,36 @@ let release-please derive the correct version bump automatically.
109109

110110
---
111111

112-
## 6. Publishing
112+
## 6. macOS exclusive device access
113+
114+
*Status: in progress*
115+
116+
On macOS, hidapi defaults to opening devices in exclusive mode
117+
(`kIOHIDOptionsTypeSeizeDevice`). This is set by `hid_init()` calling
118+
`hid_darwin_set_open_exclusive(1)` for backward compatibility.
119+
120+
Exclusive mode prevents a second `hid_open_path` call on the same device
121+
path within the same process — the `IOHIDDeviceOpen` call fails. This
122+
breaks applications that open separate read and write handles from
123+
different Dart isolates (isolates share process memory, so the C-level
124+
`device_open_options` global is visible to all threads).
125+
126+
The package shall expose the macOS-specific exclusive mode control:
127+
128+
- `hidDarwinSetOpenExclusive(bool exclusive)` — sets the global open mode.
129+
No-op on Linux and Windows where the symbol does not exist.
130+
- `hidDarwinGetOpenExclusive()` — returns the current setting.
131+
Returns `false` on non-macOS platforms.
132+
133+
The FFI bindings file shall include `hid_darwin_set_open_exclusive` and
134+
`hid_darwin_get_open_exclusive`. The Dart wrappers shall guard calls with
135+
a platform check (attempt the native call; catch the symbol-not-found
136+
error on non-macOS) so that consuming code does not need conditional
137+
imports.
138+
139+
---
140+
141+
## 7. Publishing
113142

114143
*Status: not started*
115144

lib/hidapi.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,32 @@ String hidVersionStr() {
398398
return ptr.cast<Utf8>().toDartString();
399399
}
400400

401+
/// Set the macOS exclusive device open mode.
402+
///
403+
/// When `true`, subsequent [hidOpen] and [hidOpenPath] calls seize the device
404+
/// exclusively (`kIOHIDOptionsTypeSeizeDevice`). Defaults to `true` after
405+
/// [hidInit].
406+
///
407+
/// No-op on Linux and Windows where the symbol does not exist.
408+
void hidDarwinSetOpenExclusive(bool exclusive) {
409+
try {
410+
ffi.hid_darwin_set_open_exclusive(exclusive ? 1 : 0);
411+
} on ArgumentError {
412+
// Symbol not available on this platform.
413+
}
414+
}
415+
416+
/// Get the current macOS exclusive device open mode.
417+
///
418+
/// Returns `false` on non-macOS platforms where the symbol does not exist.
419+
bool hidDarwinGetOpenExclusive() {
420+
try {
421+
return ffi.hid_darwin_get_open_exclusive() != 0;
422+
} on ArgumentError {
423+
return false;
424+
}
425+
}
426+
401427
// --- Internal helpers ---
402428

403429
HidDeviceInfo _deviceInfoFromNative(ffi.hid_device_info info) {

lib/src/hidapi_bindings.g.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,11 @@ external Pointer<hid_api_version> hid_version();
202202

203203
@Native<Pointer<Char> Function()>()
204204
external Pointer<Char> hid_version_str();
205+
206+
// --- macOS-only (hidapi_darwin.h) ---
207+
208+
@Native<Void Function(Int32)>()
209+
external void hid_darwin_set_open_exclusive(int openExclusive);
210+
211+
@Native<Int32 Function()>()
212+
external int hid_darwin_get_open_exclusive();

test/hidapi_test.dart

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:ffi';
2+
import 'dart:io' show Platform;
23

34
import 'package:ffi/ffi.dart';
45
import 'package:hidapi/hidapi.dart';
@@ -87,4 +88,55 @@ void main() {
8788
}
8889
});
8990
});
91+
92+
group('darwin exclusive mode', () {
93+
test('hidDarwinGetOpenExclusive returns bool', () {
94+
hidInit();
95+
try {
96+
final val = hidDarwinGetOpenExclusive();
97+
expect(val, isA<bool>());
98+
} finally {
99+
hidExit();
100+
}
101+
});
102+
103+
test('hidDarwinSetOpenExclusive accepts bool without throwing', () {
104+
hidInit();
105+
try {
106+
expect(() => hidDarwinSetOpenExclusive(false), returnsNormally);
107+
} finally {
108+
hidExit();
109+
}
110+
});
111+
112+
test(
113+
'hidDarwinSetOpenExclusive round-trips value',
114+
() {
115+
hidInit();
116+
try {
117+
hidDarwinSetOpenExclusive(false);
118+
expect(hidDarwinGetOpenExclusive(), isFalse);
119+
hidDarwinSetOpenExclusive(true);
120+
expect(hidDarwinGetOpenExclusive(), isTrue);
121+
} finally {
122+
hidExit();
123+
}
124+
},
125+
skip: !Platform.isMacOS ? 'macOS-only' : null,
126+
);
127+
128+
test(
129+
'hidDarwinGetOpenExclusive defaults to true after init',
130+
() {
131+
hidInit();
132+
try {
133+
// Per upstream docs, hid_init sets exclusive mode to true.
134+
expect(hidDarwinGetOpenExclusive(), isTrue);
135+
} finally {
136+
hidExit();
137+
}
138+
},
139+
skip: !Platform.isMacOS ? 'macOS-only' : null,
140+
);
141+
});
90142
}

0 commit comments

Comments
 (0)