Skip to content

Commit 6ab21a2

Browse files
committed
Add Looper.Handle.pollOnce()
1 parent d147e0f commit 6ab21a2

File tree

1 file changed

+153
-8
lines changed

1 file changed

+153
-8
lines changed

Sources/AndroidLooper/Looper.swift

Lines changed: 153 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ public extension Looper {
112112
public extension Looper {
113113

114114
/// Access the underlying opaque pointer.
115-
func withUnsafePointer<E, Result>(_ body: (OpaquePointer) throws(E) -> Result) throws(E) -> Result where E: Error {
115+
func withUnsafePointer<E, Result>(_ body: (OpaquePointer) throws(E) -> Result) throws(E) -> Result where E: Swift.Error {
116116
try body(handle.pointer)
117117
}
118118

@@ -125,13 +125,82 @@ public extension Looper {
125125
func wake() {
126126
handle.wake()
127127
}
128+
129+
/**
130+
* Removes a previously added file descriptor from the looper.
131+
*
132+
* When this method returns, it is safe to close the file descriptor since the looper
133+
* will no longer have a reference to it. However, it is possible for the callback to
134+
* already be running or for it to run one last time if the file descriptor was already
135+
* signalled. Calling code is responsible for ensuring that this case is safely handled.
136+
* For example, if the callback takes care of removing itself during its own execution either
137+
* by returning 0 or by calling this method, then it can be guaranteed to not be invoked
138+
* again at any later time unless registered anew.
139+
*
140+
* Returns 1 if the file descriptor was removed, 0 if none was previously registered
141+
* or -1 if an error occurred.
142+
*
143+
* This method can be called on any thread.
144+
* This method may block briefly if it needs to wake the poll.
145+
*/
146+
func remove(fileDescriptor: FileDescriptor) throws(AndroidLooperError) {
147+
try handle.remove(fileDescriptor: fileDescriptor).map(fileDescriptor).get()
148+
}
128149
}
129150

130151
// MARK: - Supporting Types
131152

132153
public extension Looper {
133154

155+
156+
}
157+
158+
internal extension Looper.Handle {
159+
134160
typealias Callback = @convention(c) (CInt, CInt, UnsafeMutableRawPointer?) -> CInt
161+
162+
/// 1 if the file descriptor was removed, 0 if none was previously registered or -1 if an error occurred.
163+
enum RemoveFileDescriptorResult: CInt, Sendable, CaseIterable {
164+
165+
/// File descriptor was not previously registered.
166+
case invalid = 0
167+
168+
/// File descriptor was removed.
169+
case removed = 1
170+
171+
/// Error ocurred
172+
case error = -1
173+
}
174+
175+
struct PollResult: Identifiable {
176+
public let id: CInt
177+
public let fd: FileDescriptor
178+
public let events: Looper.Events
179+
public let data: UnsafeRawPointer?
180+
}
181+
}
182+
183+
internal extension Looper.Handle.RemoveFileDescriptorResult {
184+
185+
init(_ raw: RawValue) {
186+
guard let value = Self.init(rawValue: raw) else {
187+
assertionFailure("Invalid \(Self.self): \(raw)")
188+
self = .error
189+
return
190+
}
191+
self = value
192+
}
193+
194+
func map(_ value: FileDescriptor) -> Result<Void, AndroidLooperError> {
195+
switch self {
196+
case .removed:
197+
return .success(())
198+
case .invalid:
199+
return .failure(.fileDescriptorNotRegistered(value))
200+
case .error:
201+
return .failure(.removeFileDescriptor(value))
202+
}
203+
}
135204
}
136205

137206
internal extension Looper {
@@ -223,15 +292,24 @@ internal extension Looper.Handle {
223292
* This method may block briefly if it needs to wake the poll.
224293
*/
225294
func add(
226-
fileDescriptor fd: FileDescriptor,
295+
fileDescriptor: FileDescriptor,
227296
id: CInt = CInt(ALOOPER_POLL_CALLBACK),
228297
events: Looper.Events = .input,
229-
callback: Looper.Callback? = nil,
298+
callback: Callback? = nil,
230299
data: UnsafeMutableRawPointer? = nil
231-
) -> Bool {
300+
) -> Result<Void, AndroidLooperError> {
232301
let id = callback != nil ? CInt(ALOOPER_POLL_CALLBACK) : id
233-
let result = ALooper_addFd(pointer, fd.rawValue, id, Int32(events.rawValue), callback, data)
234-
return result == 1
302+
let result = ALooper_addFd(
303+
pointer, fileDescriptor.rawValue,
304+
id,
305+
Int32(events.rawValue),
306+
callback,
307+
data
308+
)
309+
guard result == 1 else {
310+
return .failure(.addFileDescriptor(fileDescriptor))
311+
}
312+
return .success(())
235313
}
236314

237315
/**
@@ -251,7 +329,74 @@ internal extension Looper.Handle {
251329
* This method can be called on any thread.
252330
* This method may block briefly if it needs to wake the poll.
253331
*/
254-
func remove(fileDescriptor: FileDescriptor) -> CInt { // TODO: Create Looper.RemoveResult
255-
ALooper_removeFd(pointer, fileDescriptor.rawValue)
332+
func remove(fileDescriptor: FileDescriptor) -> RemoveFileDescriptorResult {
333+
let result = ALooper_removeFd(pointer, fileDescriptor.rawValue)
334+
return .init(result)
335+
}
336+
337+
/// Waits for events to be available, with optional timeout in milliseconds.
338+
@available(macOS 13.0, *)
339+
func pollOnce(duration: Duration? = nil) -> Result<PollResult?, AndroidLooperError> {
340+
pollOnce(milliseconds: duration?.milliseconds)
341+
}
342+
343+
/**
344+
* Waits for events to be available, with optional timeout in milliseconds.
345+
* Invokes callbacks for all file descriptors on which an event occurred.
346+
*
347+
* If the timeout is zero, returns immediately without blocking.
348+
* If the timeout is negative, waits indefinitely until an event appears.
349+
*
350+
* Returns ALOOPER_POLL_WAKE if the poll was awoken using ALooper_wake() before
351+
* the timeout expired and no callbacks were invoked and no other file
352+
* descriptors were ready. **All return values may also imply
353+
* ALOOPER_POLL_WAKE.**
354+
*
355+
* Returns ALOOPER_POLL_CALLBACK if one or more callbacks were invoked. The poll
356+
* may also have been explicitly woken by ALooper_wake.
357+
*
358+
* Returns ALOOPER_POLL_TIMEOUT if there was no data before the given timeout
359+
* expired. The poll may also have been explicitly woken by ALooper_wake.
360+
*
361+
* Returns ALOOPER_POLL_ERROR if the calling thread has no associated Looper or
362+
* for unrecoverable internal errors. The poll may also have been explicitly
363+
* woken by ALooper_wake.
364+
*
365+
* Returns a value >= 0 containing an identifier (the same identifier `ident`
366+
* passed to ALooper_addFd()) if its file descriptor has data and it has no
367+
* callback function (requiring the caller here to handle it). In this (and
368+
* only this) case outFd, outEvents and outData will contain the poll events and
369+
* data associated with the fd, otherwise they will be set to NULL. The poll may
370+
* also have been explicitly woken by ALooper_wake.
371+
*
372+
* This method does not return until it has finished invoking the appropriate callbacks
373+
* for all file descriptors that were signalled.
374+
*/
375+
func pollOnce(milliseconds: Double? = nil) -> Result<PollResult?, AndroidLooperError> {
376+
var outFd: CInt = -1
377+
var outEvents: CInt = 0
378+
var outData: UnsafeMutableRawPointer?
379+
let timeoutMillis: CInt = milliseconds.map { CInt($0) } ?? 0
380+
381+
let err = ALooper_pollOnce(timeoutMillis, &outFd, &outEvents, &outData)
382+
switch Int(err) {
383+
case ALOOPER_POLL_WAKE:
384+
fallthrough
385+
case ALOOPER_POLL_CALLBACK:
386+
return .success(nil)
387+
case ALOOPER_POLL_TIMEOUT:
388+
return .failure(.pollTimeout)
389+
case ALOOPER_POLL_ERROR:
390+
return .failure(.pollError)
391+
default:
392+
return .success(
393+
PollResult(
394+
id: err,
395+
fd: .init(rawValue: outFd),
396+
events: Looper.Events(rawValue: Int(outEvents)),
397+
data: outData
398+
)
399+
)
400+
}
256401
}
257402
}

0 commit comments

Comments
 (0)