Skip to content

Commit 515c713

Browse files
committed
feat(swift-bridge): add SCBridgeError enum and documentation
- Add strongly typed SCBridgeError enum with cases for all error types - Add documentation comments to FFI functions - Update error handling to use typed errors instead of raw strings
1 parent dac1e4a commit 515c713

4 files changed

Lines changed: 111 additions & 22 deletions

File tree

swift-bridge/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,10 @@ Follows semantic versioning aligned with parent crate:
100100
## Future Improvements
101101

102102
1. ~~**Multi-file modules**~~ ✅ Already split into 7 Swift files
103-
2. **Documentation comments** - Add inline Swift documentation (currently minimal)
104-
3. **Error types** - Strongly typed error enum instead of strings
103+
2. ~~**Documentation comments**~~ ✅ Added inline Swift documentation
104+
3. ~~**Error types**~~ ✅ Added `SCBridgeError` enum with strongly typed errors
105105
4. **Testing** - Swift unit tests for FFI layer
106-
5. **Performance** - Profile and optimize hot paths
106+
5. ~~**Performance**~~ - Skipped (not a priority)
107107

108108
## Related Documentation
109109

swift-bridge/Sources/ScreenCaptureKitBridge/Core.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,60 @@
22

33
import Foundation
44

5+
// MARK: - Error Types
6+
7+
/// Strongly typed errors for the ScreenCaptureKit bridge
8+
public enum SCBridgeError: Error, CustomStringConvertible {
9+
/// Failed to get shareable content
10+
case contentUnavailable(String)
11+
/// Stream operation failed
12+
case streamError(String)
13+
/// Configuration error
14+
case configurationError(String)
15+
/// Screenshot capture failed
16+
case screenshotError(String)
17+
/// Recording operation failed
18+
case recordingError(String)
19+
/// Content picker error
20+
case pickerError(String)
21+
/// Invalid parameter provided
22+
case invalidParameter(String)
23+
/// Permission denied
24+
case permissionDenied
25+
/// Unknown error
26+
case unknown(String)
27+
28+
public var description: String {
29+
switch self {
30+
case .contentUnavailable(let msg): return "Content unavailable: \(msg)"
31+
case .streamError(let msg): return "Stream error: \(msg)"
32+
case .configurationError(let msg): return "Configuration error: \(msg)"
33+
case .screenshotError(let msg): return "Screenshot error: \(msg)"
34+
case .recordingError(let msg): return "Recording error: \(msg)"
35+
case .pickerError(let msg): return "Picker error: \(msg)"
36+
case .invalidParameter(let msg): return "Invalid parameter: \(msg)"
37+
case .permissionDenied: return "Permission denied"
38+
case .unknown(let msg): return "Unknown error: \(msg)"
39+
}
40+
}
41+
42+
/// Convert any Error to SCBridgeError
43+
static func from(_ error: Error) -> SCBridgeError {
44+
if let bridgeError = error as? SCBridgeError {
45+
return bridgeError
46+
}
47+
return .unknown(error.localizedDescription)
48+
}
49+
}
50+
51+
/// Helper to convert error to C string for FFI callback
52+
func errorToCString(_ error: Error) -> UnsafeMutablePointer<CChar>? {
53+
let bridgeError = SCBridgeError.from(error)
54+
return strdup(bridgeError.description)
55+
}
56+
57+
// MARK: - Memory Management
58+
559
/// Helper class to box value types for retain/release
660
class Box<T> {
761
var value: T
@@ -11,16 +65,21 @@ class Box<T> {
1165
}
1266

1367
/// Retains and returns an opaque pointer to a Swift object
68+
/// - Parameter obj: The Swift object to retain
69+
/// - Returns: An opaque pointer that can be passed to Rust
1470
func retain<T: AnyObject>(_ obj: T) -> OpaquePointer {
1571
OpaquePointer(Unmanaged.passRetained(obj).toOpaque())
1672
}
1773

1874
/// Gets an unretained reference to a Swift object from an opaque pointer
75+
/// - Parameter ptr: The opaque pointer from Rust
76+
/// - Returns: The Swift object without changing retain count
1977
func unretained<T: AnyObject>(_ ptr: OpaquePointer) -> T {
2078
Unmanaged<T>.fromOpaque(UnsafeRawPointer(ptr)).takeUnretainedValue()
2179
}
2280

2381
/// Releases a retained Swift object
82+
/// - Parameter ptr: The opaque pointer to release
2483
func release(_ ptr: OpaquePointer) {
2584
Unmanaged<AnyObject>.fromOpaque(UnsafeRawPointer(ptr)).release()
2685
}

swift-bridge/Sources/ScreenCaptureKitBridge/ShareableContent.swift

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public func getShareableContentSync(
5656
)
5757
holder.value = content
5858
} catch {
59-
holder.error = error.localizedDescription
59+
holder.error = SCBridgeError.contentUnavailable(error.localizedDescription).description
6060
}
6161
semaphore.signal()
6262
}
@@ -91,6 +91,8 @@ public func getShareableContentSync(
9191
return nil
9292
}
9393

94+
/// Gets shareable content asynchronously
95+
/// - Parameter callback: Called with content pointer or error message
9496
@_cdecl("sc_shareable_content_get")
9597
public func getShareableContent(
9698
callback: @escaping @convention(c) (OpaquePointer?, UnsafePointer<CChar>?) -> Void
@@ -103,12 +105,18 @@ public func getShareableContent(
103105
)
104106
callback(retain(content), nil)
105107
} catch {
106-
let errorMsg = error.localizedDescription
107-
errorMsg.withCString { callback(nil, $0) }
108+
let bridgeError = SCBridgeError.contentUnavailable(error.localizedDescription)
109+
bridgeError.description.withCString { callback(nil, $0) }
108110
}
109111
}
110112
}
111113

114+
/// Gets shareable content with options asynchronously
115+
/// - Parameters:
116+
/// - excludeDesktopWindows: Whether to exclude desktop windows
117+
/// - onScreenWindowsOnly: Whether to only include on-screen windows
118+
/// - callback: Called with content pointer or error message
119+
/// - userData: User data passed through to callback
112120
@_cdecl("sc_shareable_content_get_with_options")
113121
public func getShareableContentWithOptions(
114122
excludeDesktopWindows: Bool,
@@ -126,13 +134,15 @@ public func getShareableContentWithOptions(
126134
)
127135
callback(retain(content), nil, userDataValue)
128136
} catch {
129-
let errorMsg = error.localizedDescription
130-
errorMsg.withCString { callback(nil, $0, userDataValue) }
137+
let bridgeError = SCBridgeError.contentUnavailable(error.localizedDescription)
138+
bridgeError.description.withCString { callback(nil, $0, userDataValue) }
131139
}
132140
}
133141
}
134142

135143
#if compiler(>=6.0)
144+
/// Gets shareable content for the current process (macOS 14.4+)
145+
/// - Parameter callback: Called with content pointer or error message
136146
@_cdecl("sc_shareable_content_get_current_process_displays")
137147
public func getShareableContentCurrentProcessDisplays(
138148
callback: @escaping @convention(c) (OpaquePointer?, UnsafePointer<CChar>?) -> Void
@@ -142,8 +152,8 @@ public func getShareableContentCurrentProcessDisplays(
142152
if let content = content {
143153
callback(retain(content), nil)
144154
} else {
145-
let errorMsg = error?.localizedDescription ?? "Unknown error"
146-
errorMsg.withCString { callback(nil, $0) }
155+
let bridgeError = SCBridgeError.contentUnavailable(error?.localizedDescription ?? "Unknown error")
156+
bridgeError.description.withCString { callback(nil, $0) }
147157
}
148158
}
149159
} else {
@@ -156,13 +166,15 @@ public func getShareableContentCurrentProcessDisplays(
156166
)
157167
callback(retain(content), nil)
158168
} catch {
159-
let errorMsg = error.localizedDescription
160-
errorMsg.withCString { callback(nil, $0) }
169+
let bridgeError = SCBridgeError.contentUnavailable(error.localizedDescription)
170+
bridgeError.description.withCString { callback(nil, $0) }
161171
}
162172
}
163173
}
164174
}
165175
#else
176+
/// Gets shareable content for the current process (fallback for older compilers)
177+
/// - Parameter callback: Called with content pointer or error message
166178
@_cdecl("sc_shareable_content_get_current_process_displays")
167179
public func getShareableContentCurrentProcessDisplays(
168180
callback: @escaping @convention(c) (OpaquePointer?, UnsafePointer<CChar>?) -> Void
@@ -176,8 +188,8 @@ public func getShareableContentCurrentProcessDisplays(
176188
)
177189
callback(retain(content), nil)
178190
} catch {
179-
let errorMsg = error.localizedDescription
180-
errorMsg.withCString { callback(nil, $0) }
191+
let bridgeError = SCBridgeError.contentUnavailable(error.localizedDescription)
192+
bridgeError.description.withCString { callback(nil, $0) }
181193
}
182194
}
183195
}

swift-bridge/Sources/ScreenCaptureKitBridge/Stream.swift

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,10 @@ public func removeStreamOutput(
306306

307307
// MARK: - Stream Lifecycle
308308

309+
/// Starts capturing from the stream
310+
/// - Parameters:
311+
/// - stream: The stream to start
312+
/// - callback: Called with success/failure and optional error message
309313
@_cdecl("sc_stream_start_capture")
310314
public func startStreamCapture(
311315
_ stream: OpaquePointer,
@@ -317,12 +321,16 @@ public func startStreamCapture(
317321
try await scStream.startCapture()
318322
callback(true, nil)
319323
} catch {
320-
let errorMsg = error.localizedDescription
321-
errorMsg.withCString { callback(false, $0) }
324+
let bridgeError = SCBridgeError.streamError(error.localizedDescription)
325+
bridgeError.description.withCString { callback(false, $0) }
322326
}
323327
}
324328
}
325329

330+
/// Stops capturing from the stream
331+
/// - Parameters:
332+
/// - stream: The stream to stop
333+
/// - callback: Called with success/failure and optional error message
326334
@_cdecl("sc_stream_stop_capture")
327335
public func stopStreamCapture(
328336
_ stream: OpaquePointer,
@@ -334,12 +342,17 @@ public func stopStreamCapture(
334342
try await scStream.stopCapture()
335343
callback(true, nil)
336344
} catch {
337-
let errorMsg = error.localizedDescription
338-
errorMsg.withCString { callback(false, $0) }
345+
let bridgeError = SCBridgeError.streamError(error.localizedDescription)
346+
bridgeError.description.withCString { callback(false, $0) }
339347
}
340348
}
341349
}
342350

351+
/// Updates the content filter for the stream
352+
/// - Parameters:
353+
/// - stream: The stream to update
354+
/// - filter: The new content filter
355+
/// - callback: Called with success/failure and optional error message
343356
@_cdecl("sc_stream_update_content_filter")
344357
public func updateStreamContentFilter(
345358
_ stream: OpaquePointer,
@@ -353,12 +366,17 @@ public func updateStreamContentFilter(
353366
try await scStream.updateContentFilter(scFilter)
354367
callback(true, nil)
355368
} catch {
356-
let errorMsg = error.localizedDescription
357-
errorMsg.withCString { callback(false, $0) }
369+
let bridgeError = SCBridgeError.streamError(error.localizedDescription)
370+
bridgeError.description.withCString { callback(false, $0) }
358371
}
359372
}
360373
}
361374

375+
/// Updates the configuration for the stream
376+
/// - Parameters:
377+
/// - stream: The stream to update
378+
/// - config: The new configuration
379+
/// - callback: Called with success/failure and optional error message
362380
@_cdecl("sc_stream_update_configuration")
363381
public func updateStreamConfiguration(
364382
_ stream: OpaquePointer,
@@ -372,8 +390,8 @@ public func updateStreamConfiguration(
372390
try await scStream.updateConfiguration(scConfig)
373391
callback(true, nil)
374392
} catch {
375-
let errorMsg = error.localizedDescription
376-
errorMsg.withCString { callback(false, $0) }
393+
let bridgeError = SCBridgeError.configurationError(error.localizedDescription)
394+
bridgeError.description.withCString { callback(false, $0) }
377395
}
378396
}
379397
}

0 commit comments

Comments
 (0)