Skip to content

Commit

Permalink
Updates for sendable closures
Browse files Browse the repository at this point in the history
  • Loading branch information
johnfairh committed Jul 22, 2024
1 parent 673ec70 commit 304fed4
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 47 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ jobs:
run: |
xcrun llvm-cov export -format lcov .build/debug/RubyGatewayPackageTests.xctest/Contents/MacOS/RubyGatewayPackageTests -instr-profile .build/debug/codecov/default.profdata -ignore-filename-regex "(Test|checkouts)" > coverage.lcov
- name: Coverage upload
uses: codecov/codecov-action@v2
uses: codecov/codecov-action@v4
with:
files: ./coverage.lcov
token: ${{ secrets.CODECOV_TOKEN }}
verbose: true
- name: Tests (Xcodebuild)
run: |
Expand Down
12 changes: 5 additions & 7 deletions SourceDocs/User Guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ Outside of the very first time, it's not possible to call Ruby on a random
thread created either directly by your program or by the Swift concurrency /
Dispatch runtime.
A reasonable pattern is to call `RbGateway.setup()` during system startup on
A reasonable pattern is to call some Ruby method during system startup on
the Swift `@MainActor` and then treat Ruby calls as requiring isolation to
that actor. If you take calls _from_ Ruby on Ruby-created threads, and
servicing these requires access to your Swift concurrency executors, then you
Expand Down Expand Up @@ -599,13 +599,11 @@ immediately crashes unless you are running inside `rb_protect()` or equivalent.
## Swift Concurrency
Sendable annotations and checking are mostly complete. The parts remaining are
* `RbBlockCallback` - Swift doesn't understand @Sendable typealiases.
* `RbMethodCallback` and related - Swift doesn't understand Sendable method
references.
Sendable annotations and checking are thought to be complete.
Despite the lack of `Sendable` requirement on these closure types they should
be treated as such if you are using Ruby across multiple threads.
That said it's probably possible to defeat these checks with enough effort
because of the way Swift types are lost and reapplied either side of the C
layer.
### Garbage collection
Expand Down
6 changes: 3 additions & 3 deletions Sources/RubyGateway/RbBlockCall.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ public typealias RbBlockCallback = ([RbObject]) throws -> RbObject
/// `RbObjectAccess.call(_:args:kwArgs:blockRetention:blockCall:)`, RubyGateway
/// needs some help to understand how Ruby will use the closure.
///
/// The easiest thing to get wrong is using the default of `.none` when
/// Ruby retains the block for use later. This causes a hard crash in
/// `RbBlockContext.from(raw:)` when Ruby tries to call the block.
/// The easiest thing to get wrong is using `.none` when Ruby retains the
/// block for use later. This causes a hard crash in `RbBlockContext.from(raw:)`
/// when Ruby tries to call the block.
public enum RbBlockRetention {
/// Do not retain the closure. The default, appropriate when the block
/// is used only during execution of the method it is passed to. For
Expand Down
2 changes: 1 addition & 1 deletion Sources/RubyGateway/RbClass.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ extension RbGateway {
/// module. `RbError.rubyException(...)` if Ruby is unhappy with the definition,
/// for example when the class already exists with a different parent.
@discardableResult
public func defineClass<SwiftPeer: AnyObject>(
public func defineClass<SwiftPeer: AnyObject & Sendable>(
_ name: String,
under: RbObject? = nil,
initializer: @escaping () -> SwiftPeer) throws -> RbObject {
Expand Down
45 changes: 40 additions & 5 deletions Sources/RubyGateway/RbFailableAccess.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,23 @@ extension RbFailableAccess {
try? access.call(method, args: args, kwArgs: kwArgs)
}

/// Call a method of a Ruby object passing Swift code as a block used immediately.
///
/// This is a non-throwing version of `RbObjectAccess.call(_:args:kwArgs:blockCall:)`.
/// See `RbError.history` to retrieve error details.
///
/// - parameter method: The name of the method to call.
/// - parameter args: The positional arguments to the method, none by default.
/// - parameter kwArgs: The keyword arguments to the method, none by default.
/// - parameter blockCall: Swift code to pass as a block to the method.
/// - returns: An `RbObject` for the result of the method, or `nil` if an error occurred.
public func call(_ method: String,
args: [(any RbObjectConvertible)?] = [],
kwArgs: KeyValuePairs<String, (any RbObjectConvertible)?> = [:],
blockCall: RbBlockCallback) -> RbObject? {
try? access.call(method, args: args, kwArgs: kwArgs, blockCall: blockCall)
}

/// Call a method of a Ruby object passing Swift code as a block.
///
/// This is a non-throwing version of `RbObjectAccess.call(_:args:kwArgs:blockRetention:blockCall:)`.
Expand All @@ -70,14 +87,14 @@ extension RbFailableAccess {
/// - parameter args: The positional arguments to the method, none by default.
/// - parameter kwArgs: The keyword arguments to the method, none by default.
/// - parameter blockRetention: Should the `blockCall` closure be retained for
/// longer than this call? Default `.none`. See `RbBlockRetention`.
/// longer than this call? See `RbBlockRetention`.
/// - parameter blockCall: Swift code to pass as a block to the method.
/// - returns: An `RbObject` for the result of the method, or `nil` if an error occurred.
public func call(_ method: String,
args: [(any RbObjectConvertible)?] = [],
kwArgs: KeyValuePairs<String, (any RbObjectConvertible)?> = [:],
blockRetention: RbBlockRetention = .none,
blockCall: @escaping RbBlockCallback) -> RbObject? {
blockRetention: RbBlockRetention,
blockCall: @escaping @Sendable RbBlockCallback) -> RbObject? {
try? access.call(method, args: args, kwArgs: kwArgs, blockRetention: blockRetention, blockCall: blockCall)
}

Expand Down Expand Up @@ -114,6 +131,24 @@ extension RbFailableAccess {
try? access.call(symbol: symbol, args: args, kwArgs: kwArgs)
}

/// Call a method of a Ruby object using a symbol passing Swift code as a block used immediately.
///
/// This is a non-throwing version of `RbObjectAccess.call(symbol:args:kwArgs:blockCall:)`.
/// See `RbError.history` to retrieve error details.
///
/// - parameter symbol: A symbol for the method to call.
/// - parameter args: The positional arguments to the method, none by default.
/// - parameter kwArgs: The keyword arguments to the method, none by default.
/// - parameter blockCall: Swift code to pass as a block to the method.
/// - returns: An `RbObject` for the result of the method, or `nil` if an error occurred.
@discardableResult
public func call(symbol: any RbObjectConvertible,
args: [(any RbObjectConvertible)?] = [],
kwArgs: KeyValuePairs<String, (any RbObjectConvertible)?> = [:],
blockCall: RbBlockCallback) -> RbObject? {
try? access.call(symbol: symbol, args: args, kwArgs: kwArgs, blockCall: blockCall)
}

/// Call a method of a Ruby object using a symbol passing Swift code as a block.
///
/// This is a non-throwing version of `RbObjectAccess.call(symbol:args:kwArgs:blockRetention:blockCall:)`.
Expand All @@ -123,15 +158,15 @@ extension RbFailableAccess {
/// - parameter args: The positional arguments to the method, none by default.
/// - parameter kwArgs: The keyword arguments to the method, none by default.
/// - parameter blockRetention: Should the `blockCall` closure be retained for
/// longer than this call? Default `.none`. See `RbBlockRetention`.
/// longer than this call? See `RbBlockRetention`.
/// - parameter blockCall: Swift code to pass as a block to the method.
/// - returns: An `RbObject` for the result of the method, or `nil` if an error occurred.
@discardableResult
public func call(symbol: any RbObjectConvertible,
args: [(any RbObjectConvertible)?] = [],
kwArgs: KeyValuePairs<String, (any RbObjectConvertible)?> = [:],
blockRetention: RbBlockRetention = .none,
blockCall: @escaping RbBlockCallback) -> RbObject? {
blockCall: @escaping @Sendable RbBlockCallback) -> RbObject? {
try? access.call(symbol: symbol, args: args, kwArgs: kwArgs, blockRetention: blockRetention, blockCall: blockCall)
}

Expand Down
6 changes: 3 additions & 3 deletions Sources/RubyGateway/RbGlobalVar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ extension RbGateway {
/// - throws: `RbError.badIdentifier(type:id:)` if `name` is bad; some other kind of error if Ruby is
/// not working.
public func defineGlobalVar<T: RbObjectConvertible>(_ name: String,
get: @Sendable @escaping () -> T) throws {
get: @escaping @Sendable () -> T) throws {
try setup()
try name.checkRubyGlobalVarName()
RbGlobalVar.create(name: name, get: get, set: nil)
Expand All @@ -120,8 +120,8 @@ extension RbGateway {
/// - throws: `RbError.badIdentifier(type:id:)` if `name` is bad; some other kind of error if Ruby is
/// not working.
public func defineGlobalVar<T: RbObjectConvertible>(_ name: String,
get: @Sendable @escaping () -> T,
set: @Sendable @escaping (T) throws -> Void) throws {
get: @escaping @Sendable () -> T,
set: @escaping @Sendable (T) throws -> Void) throws {
try setup()
try name.checkRubyGlobalVarName()
RbGlobalVar.create(name: name, get: get, set: set)
Expand Down
19 changes: 8 additions & 11 deletions Sources/RubyGateway/RbMethod.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ internal import RubyGatewayHelpers
// dynamic dispatch order. So we can search this property looking for a match.
// OK - not THAT bad!

// XXX these guys all ought to be Sendable, but Swift is broken wrt Sendable and
// XXX method references....

/// The function signature for a Ruby method implemented as a Swift free function
/// or closure.
///
Expand All @@ -46,7 +43,7 @@ internal import RubyGatewayHelpers
///
/// See `RbBoundMethodCallback` and `RbBoundMethodVoidCallback` for use with
/// custom Ruby classes that are bound to Swift types.
public typealias RbMethodCallback = (RbObject, RbMethod) throws -> RbObject
public typealias RbMethodCallback = @Sendable (RbObject, RbMethod) throws -> RbObject

/// The function signature for a Ruby method implemented as a Swift method of
/// a Swift bound object that returns a value.
Expand All @@ -62,8 +59,8 @@ public typealias RbMethodCallback = (RbObject, RbMethod) throws -> RbObject
/// You can throw an `RbException` to raise a Ruby exception instead of returning
/// normally from the method. Throwing another type gets wrapped up in an
/// `RbException` and raised as a Ruby runtime exception.
public typealias RbBoundMethodCallback<SwiftPeer: AnyObject, Return: RbObjectConvertible & Sendable> =
(SwiftPeer) -> (RbMethod) throws -> Return
public typealias RbBoundMethodCallback<SwiftPeer: AnyObject & Sendable, Return: RbObjectConvertible & Sendable> =
@Sendable (SwiftPeer) -> (RbMethod) throws -> Return

/// The function signature for a Ruby method implemented as a Swift method of
/// a Swift bound object that does not return a value.
Expand All @@ -78,8 +75,8 @@ public typealias RbBoundMethodCallback<SwiftPeer: AnyObject, Return: RbObjectCon
/// You can throw an `RbException` to raise a Ruby exception instead of returning
/// normally from the method. Throwing another type gets wrapped up in an
/// `RbException` and raised as a Ruby runtime exception.
public typealias RbBoundMethodVoidCallback<SwiftPeer: AnyObject> =
(SwiftPeer) -> (RbMethod) throws -> Void
public typealias RbBoundMethodVoidCallback<SwiftPeer: AnyObject & Sendable> =
@Sendable (SwiftPeer) -> (RbMethod) throws -> Void

// MARK: - Dispatch gorpy implementation

Expand Down Expand Up @@ -258,7 +255,7 @@ public struct RbMethod: Sendable {
/// Call the overridden version of the current method.
///
/// The current active block, if any, is passed on to the superclass method.
/// There is no RubyBridge equivalent to Ruby's 'raw super' keyword, you must
/// There is no RubyGateway equivalent to Ruby's 'raw super' keyword, you must
/// always explicitly specify the arguments to pass on.
///
/// If there is no matching superclass method to call then Ruby raises a
Expand Down Expand Up @@ -667,7 +664,7 @@ extension RbObject {
/// - method: The Swift method to call to fulfill the Ruby method.
/// - Throws: `RbError.badIdentifier(type:id:)` if `name` is bad.
/// `RbError.badType(...)` if the object is not a class.
public func defineMethod<SwiftPeer: AnyObject, Return: RbObjectConvertible & Sendable>(
public func defineMethod<SwiftPeer: AnyObject & Sendable, Return: RbObjectConvertible & Sendable>(
_ name: String,
argsSpec: RbMethodArgsSpec = RbMethodArgsSpec(),
method: @escaping RbBoundMethodCallback<SwiftPeer, Return>) throws {
Expand Down Expand Up @@ -710,7 +707,7 @@ extension RbObject {
/// - method: The Swift method to call to fulfill the Ruby method.
/// - Throws: `RbError.badIdentifier(type:id:)` if `name` is bad.
/// `RbError.badType(...)` if the object is not a class.
public func defineMethod<SwiftPeer: AnyObject>(
public func defineMethod<SwiftPeer: AnyObject & Sendable>(
_ name: String,
argsSpec: RbMethodArgsSpec = RbMethodArgsSpec(),
method: @escaping RbBoundMethodVoidCallback<SwiftPeer>) throws {
Expand Down
44 changes: 38 additions & 6 deletions Sources/RubyGateway/RbObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,22 +256,26 @@ extension RbObject {

/// Create an instance of a given Ruby class passing a Swift closure as a block.
///
/// This version is really for cases where Ruby retains the block rather than using
/// it only synchronously during the exection of the `new` method. For the synchronous
/// case see `init(ofClass:args:kwArgs:blockCall:)` which does not require
/// an `@escapable` or `@Sendable` block closure.
///
/// Fails (returns `nil`) if anything goes wrong along the way - check `RbError.history` to
/// find out what failed.
///
/// - parameter ofClass: Name of the class to instantiate. Can contain `::` to drill
/// down into module/etc. scope.
/// - parameter args: positional arguments to pass to `new` call for the object. Default none.
/// - parameter kwArgs: keyword arguments to pass to the `new` call for the object. Default none.
/// - parameter retainBlock: Should `blockCall` be retained by the object? Default `false`. Set
/// `true` if Ruby uses the block after this call. For example creating a Proc object
/// using `Proc#new`.
/// - parameter retainBlock: Should `blockCall` be retained by the object? Set `true` if
/// Ruby uses the block after this call. For example creating a Proc object using `Proc#new`.
/// - parameter blockCall: Swift code to pass as a block to the method.
public convenience init?(ofClass className: String,
args: [(any RbObjectConvertible)?] = [],
kwArgs: KeyValuePairs<String, (any RbObjectConvertible)?> = [:],
retainBlock: Bool = false,
blockCall: @escaping RbBlockCallback) {
retainBlock: Bool,
blockCall: @escaping @Sendable RbBlockCallback) {
let retention: RbBlockRetention = retainBlock ? .returned : .none
guard let obj = try? Ruby.get(className).call("new",
args: args, kwArgs: kwArgs,
Expand All @@ -282,12 +286,40 @@ extension RbObject {
self.init(obj)
}

/// Create an instance of a given Ruby class passing a Swift closure as a block.
///
/// The closure is used only synchronously during the `new` method. For a version appropriate
/// for use with things like `Proc#new` that retain the block, see `init(ofClass:args:kwArgs:retainBlock:blockCall:)`
///
/// Fails (returns `nil`) if anything goes wrong along the way - check `RbError.history` to
/// find out what failed.
///
/// - parameter ofClass: Name of the class to instantiate. Can contain `::` to drill
/// down into module/etc. scope.
/// - parameter args: positional arguments to pass to `new` call for the object. Default none.
/// - parameter kwArgs: keyword arguments to pass to the `new` call for the object. Default none.
/// - parameter blockCall: Swift code to pass as a block to the method.
public convenience init?(ofClass className: String,
args: [(any RbObjectConvertible)?] = [],
kwArgs: KeyValuePairs<String, (any RbObjectConvertible)?> = [:],
blockCall: RbBlockCallback) {
guard let obj = withoutActuallyEscaping(blockCall, do: { newBlockCall in
try? Ruby.get(className).call("new",
args: args, kwArgs: kwArgs,
blockRetention: .none,
blockCall: newBlockCall)
}) else {
return nil
}
self.init(obj)
}

/// Create a Ruby Proc object from a Swift closure.
///
/// - parameter blockCall: The callback for the proc.
/// - warning: You must not allow this `RbObject` to be deallocated before Ruby has
/// finished with the block, or the process will crash when Ruby calls it.
public convenience init(blockCall: @escaping RbBlockCallback) {
public convenience init(blockCall: @escaping @Sendable RbBlockCallback) {
if let obj = try? Ruby.get("Proc").call("new", blockRetention: .returned, blockCall: blockCall) {
self.init(obj)
} else {
Expand Down
Loading

0 comments on commit 304fed4

Please sign in to comment.