Skip to content

Commit 0b4b615

Browse files
committed
Change NativeEventStream to use JNI+Rust instead of JNA with the least amount of changes (and remove unused code)
1 parent 6f72a15 commit 0b4b615

File tree

4 files changed

+148
-408
lines changed

4 files changed

+148
-408
lines changed

src/main/java/engineering/swat/watch/impl/mac/NativeEventStream.java

Lines changed: 6 additions & 218 deletions
Original file line numberDiff line numberDiff line change
@@ -26,40 +26,9 @@
2626
*/
2727
package engineering.swat.watch.impl.mac;
2828

29-
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCreateFlag.FILE_EVENTS;
30-
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCreateFlag.NO_DEFER;
31-
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCreateFlag.WATCH_ROOT;
32-
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_CREATED;
33-
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_INODE_META_MOD;
34-
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_MODIFIED;
35-
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_REMOVED;
36-
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.ITEM_RENAMED;
37-
import static engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamEventFlag.MUST_SCAN_SUB_DIRS;
38-
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
39-
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
40-
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
41-
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
42-
4329
import java.io.Closeable;
4430
import java.io.IOException;
45-
import java.nio.file.Files;
4631
import java.nio.file.Path;
47-
import java.util.Arrays;
48-
49-
import org.checkerframework.checker.nullness.qual.Nullable;
50-
51-
import com.sun.jna.Memory;
52-
import com.sun.jna.Native;
53-
import com.sun.jna.Pointer;
54-
import com.sun.jna.platform.mac.CoreFoundation;
55-
import com.sun.jna.platform.mac.CoreFoundation.CFArrayRef;
56-
import com.sun.jna.platform.mac.CoreFoundation.CFIndex;
57-
import com.sun.jna.platform.mac.CoreFoundation.CFStringRef;
58-
59-
import engineering.swat.watch.impl.mac.apis.DispatchObjects;
60-
import engineering.swat.watch.impl.mac.apis.DispatchQueue;
61-
import engineering.swat.watch.impl.mac.apis.FileSystemEvents;
62-
import engineering.swat.watch.impl.mac.apis.FileSystemEvents.FSEventStreamCallback;
6332

6433
// Note: This file is designed to be the only place in this package where JNA is
6534
// used and/or the native APIs are invoked. If the need to do so arises outside
@@ -79,23 +48,14 @@
7948
* </p>
8049
*/
8150
class NativeEventStream implements Closeable {
82-
83-
// Native APIs
84-
private static final CoreFoundation CF = CoreFoundation.INSTANCE;
85-
private static final DispatchObjects DO = DispatchObjects.INSTANCE;
86-
private static final DispatchQueue DQ = DispatchQueue.INSTANCE;
87-
private static final FileSystemEvents FSE = FileSystemEvents.INSTANCE;
88-
89-
// Native memory
90-
private @Nullable FSEventStreamCallback callback; // Keep reference to avoid premature GC'ing
91-
private @Nullable Pointer stream;
92-
private @Nullable Pointer queue;
93-
// Note: These fields aren't volatile, as all reads/write from/to them are
94-
// inside synchronized blocks. Be careful to not break this invariant.
51+
static {
52+
NativeLibrary.load();
53+
}
9554

9655
private final Path path;
9756
private final NativeEventHandler handler;
9857
private volatile boolean closed;
58+
private volatile long nativeWatch;
9959

10060
public NativeEventStream(Path path, NativeEventHandler handler) throws IOException {
10161
this.path = path.toRealPath(); // Resolve symbolic links
@@ -110,88 +70,7 @@ public synchronized void open() {
11070
closed = false;
11171
}
11272

113-
// Allocate native memory
114-
callback = createCallback();
115-
stream = createFSEventStream(callback);
116-
queue = createDispatchQueue();
117-
118-
// Start the stream
119-
var streamNonNull = stream;
120-
if (streamNonNull != null) {
121-
FSE.FSEventStreamSetDispatchQueue(streamNonNull, queue);
122-
FSE.FSEventStreamStart(streamNonNull);
123-
}
124-
}
125-
126-
private FSEventStreamCallback createCallback() {
127-
return new FSEventStreamCallback() {
128-
@Override
129-
public void callback(Pointer streamRef, Pointer clientCallBackInfo,
130-
long numEvents, Pointer eventPaths, Pointer eventFlags, Pointer eventIds) {
131-
// This function is called each time native events are issued by
132-
// macOS. The purpose of this function is to perform the minimal
133-
// amount of processing to hide the native APIs from downstream
134-
// consumers, who are offered native events via `handler`.
135-
136-
var paths = eventPaths.getStringArray(0, (int) numEvents);
137-
var flags = eventFlags.getIntArray(0, (int) numEvents);
138-
139-
for (var i = 0; i < numEvents; i++) {
140-
var context = path.relativize(Path.of(paths[i]));
141-
142-
// Note: Multiple "physical" native events might be
143-
// coalesced into a single "logical" native event, so the
144-
// following series of checks should be if-statements
145-
// (instead of if/else-statements).
146-
if (any(flags[i], ITEM_CREATED.mask)) {
147-
handler.handle(ENTRY_CREATE, context);
148-
}
149-
if (any(flags[i], ITEM_REMOVED.mask)) {
150-
handler.handle(ENTRY_DELETE, context);
151-
}
152-
if (any(flags[i], ITEM_MODIFIED.mask | ITEM_INODE_META_MOD.mask)) {
153-
handler.handle(ENTRY_MODIFY, context);
154-
}
155-
if (any(flags[i], MUST_SCAN_SUB_DIRS.mask)) {
156-
handler.handle(OVERFLOW, null);
157-
}
158-
if (any(flags[i], ITEM_RENAMED.mask)) {
159-
// For now, check if the file exists to determine if the
160-
// event pertains to the target of the rename (if it
161-
// exists) or to the source (else). This is an
162-
// approximation. It might be more accurate to maintain
163-
// an internal index (but getting the concurrency right
164-
// requires care).
165-
if (Files.exists(Path.of(paths[i]))) {
166-
handler.handle(ENTRY_CREATE, context);
167-
} else {
168-
handler.handle(ENTRY_DELETE, context);
169-
}
170-
}
171-
}
172-
}
173-
174-
private boolean any(int bits, int mask) {
175-
return (bits & mask) != 0;
176-
}
177-
};
178-
}
179-
180-
private Pointer createFSEventStream(FSEventStreamCallback callback) {
181-
try (var pathsToWatch = new Strings(path.toString())) {
182-
var allocator = CF.CFAllocatorGetDefault();
183-
var context = Pointer.NULL;
184-
var sinceWhen = FSE.FSEventsGetCurrentEventId();
185-
var latency = 0.15;
186-
var flags = NO_DEFER.mask | WATCH_ROOT.mask | FILE_EVENTS.mask;
187-
return FSE.FSEventStreamCreate(allocator, callback, context, pathsToWatch.toCFArray(), sinceWhen, latency, flags);
188-
}
189-
}
190-
191-
private Pointer createDispatchQueue() {
192-
var label = "engineering.swat.watch";
193-
var attr = Pointer.NULL;
194-
return DQ.dispatch_queue_create(label, attr);
73+
nativeWatch = NativeLibrary.start(path.toString(), handler);
19574
}
19675

19776
// -- Closeable --
@@ -204,97 +83,6 @@ public synchronized void close() {
20483
closed = true;
20584
}
20685

207-
var streamNonNull = stream;
208-
var queueNonNull = queue;
209-
if (streamNonNull != null && queueNonNull != null) {
210-
211-
// Stop the stream
212-
FSE.FSEventStreamStop(streamNonNull);
213-
FSE.FSEventStreamSetDispatchQueue(streamNonNull, Pointer.NULL);
214-
FSE.FSEventStreamInvalidate(streamNonNull);
215-
216-
// Deallocate native memory
217-
DO.dispatch_release(queueNonNull);
218-
FSE.FSEventStreamRelease(streamNonNull);
219-
queue = null;
220-
stream = null;
221-
callback = null;
222-
}
223-
}
224-
}
225-
226-
/**
227-
* Array of strings in native memory, needed to create a new native event stream
228-
* (i.e., the {@code pathsToWatch} argument of {@code FSEventStreamCreate} is an
229-
* array of strings).
230-
*/
231-
class Strings implements AutoCloseable {
232-
233-
// Native APIs
234-
private static final CoreFoundation CF = CoreFoundation.INSTANCE;
235-
236-
// Native memory
237-
private final CFStringRef[] strings;
238-
private final CFArrayRef array;
239-
240-
private volatile boolean closed = false;
241-
242-
public Strings(String... strings) {
243-
// Allocate native memory
244-
this.strings = createCFStrings(strings);
245-
this.array = createCFArray(this.strings);
246-
}
247-
248-
public CFArrayRef toCFArray() {
249-
if (closed) {
250-
throw new IllegalStateException("Strings are already deallocated");
251-
} else {
252-
return array;
253-
}
254-
}
255-
256-
private static CFStringRef[] createCFStrings(String[] pathsToWatch) {
257-
return Arrays.stream(pathsToWatch)
258-
.map(CFStringRef::createCFString)
259-
.toArray(CFStringRef[]::new);
260-
}
261-
262-
private static CFArrayRef createCFArray(CFStringRef[] strings) {
263-
var n = strings.length;
264-
var size = Native.getNativeSize(CFStringRef.class);
265-
266-
// Create a temporary array of pointers to the strings (automatically
267-
// freed when `values` goes out of scope)
268-
var values = new Memory(n * size);
269-
for (int i = 0; i < n; i++) {
270-
values.setPointer(i * size, strings[i].getPointer());
271-
}
272-
273-
// Create a permanent array based on the temporary array
274-
var alloc = CF.CFAllocatorGetDefault();
275-
var numValues = new CFIndex(n);
276-
var callBacks = Pointer.NULL;
277-
return CF.CFArrayCreate(alloc, values, numValues, callBacks);
278-
}
279-
280-
// -- AutoCloseable --
281-
282-
@Override
283-
public void close() {
284-
if (closed) {
285-
throw new IllegalStateException("Strings are already deallocated");
286-
} else {
287-
closed = true;
288-
}
289-
290-
// Deallocate native memory
291-
for (var s : strings) {
292-
if (s != null) {
293-
s.release();
294-
}
295-
}
296-
if (array != null) {
297-
array.release();
298-
}
86+
NativeLibrary.stop(nativeWatch);
29987
}
30088
}

0 commit comments

Comments
 (0)