diff --git a/SourceDocs/User Guide.md b/SourceDocs/User Guide.md index ac61b06..e8db26d 100644 --- a/SourceDocs/User Guide.md +++ b/SourceDocs/User Guide.md @@ -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 @@ -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 diff --git a/Sources/RubyGateway/RbBlockCall.swift b/Sources/RubyGateway/RbBlockCall.swift index 801d9e3..ea04277 100644 --- a/Sources/RubyGateway/RbBlockCall.swift +++ b/Sources/RubyGateway/RbBlockCall.swift @@ -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 diff --git a/Sources/RubyGateway/RbClass.swift b/Sources/RubyGateway/RbClass.swift index 7605514..9b44aed 100644 --- a/Sources/RubyGateway/RbClass.swift +++ b/Sources/RubyGateway/RbClass.swift @@ -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( + public func defineClass( _ name: String, under: RbObject? = nil, initializer: @escaping () -> SwiftPeer) throws -> RbObject { diff --git a/Sources/RubyGateway/RbFailableAccess.swift b/Sources/RubyGateway/RbFailableAccess.swift index 5faeed9..52b18bb 100644 --- a/Sources/RubyGateway/RbFailableAccess.swift +++ b/Sources/RubyGateway/RbFailableAccess.swift @@ -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 = [:], + 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:)`. @@ -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 = [:], - 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) } @@ -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 = [:], + 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:)`. @@ -123,7 +158,7 @@ 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 @@ -131,7 +166,7 @@ extension RbFailableAccess { args: [(any RbObjectConvertible)?] = [], kwArgs: KeyValuePairs = [:], 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) } diff --git a/Sources/RubyGateway/RbGlobalVar.swift b/Sources/RubyGateway/RbGlobalVar.swift index b20f379..f95e805 100644 --- a/Sources/RubyGateway/RbGlobalVar.swift +++ b/Sources/RubyGateway/RbGlobalVar.swift @@ -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(_ 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) @@ -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(_ 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) diff --git a/Sources/RubyGateway/RbMethod.swift b/Sources/RubyGateway/RbMethod.swift index f54072b..9cf2351 100644 --- a/Sources/RubyGateway/RbMethod.swift +++ b/Sources/RubyGateway/RbMethod.swift @@ -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. /// @@ -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. @@ -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) -> (RbMethod) throws -> Return +public typealias RbBoundMethodCallback = + @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. @@ -78,8 +75,8 @@ public typealias RbBoundMethodCallback = - (SwiftPeer) -> (RbMethod) throws -> Void +public typealias RbBoundMethodVoidCallback = + @Sendable (SwiftPeer) -> (RbMethod) throws -> Void // MARK: - Dispatch gorpy implementation @@ -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 @@ -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( + public func defineMethod( _ name: String, argsSpec: RbMethodArgsSpec = RbMethodArgsSpec(), method: @escaping RbBoundMethodCallback) throws { @@ -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( + public func defineMethod( _ name: String, argsSpec: RbMethodArgsSpec = RbMethodArgsSpec(), method: @escaping RbBoundMethodVoidCallback) throws { diff --git a/Sources/RubyGateway/RbObject.swift b/Sources/RubyGateway/RbObject.swift index b0bd0de..630d428 100644 --- a/Sources/RubyGateway/RbObject.swift +++ b/Sources/RubyGateway/RbObject.swift @@ -256,6 +256,11 @@ 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. /// @@ -263,15 +268,14 @@ extension RbObject { /// 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 = [:], - 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, @@ -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 = [:], + 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 { diff --git a/Sources/RubyGateway/RbObjectAccess.swift b/Sources/RubyGateway/RbObjectAccess.swift index 2094668..dd1ca11 100644 --- a/Sources/RubyGateway/RbObjectAccess.swift +++ b/Sources/RubyGateway/RbObjectAccess.swift @@ -273,13 +273,47 @@ extension RbObjectAccess { return try doCall(id: methodId, args: args, kwArgs: kwArgs) } + /// Call a Ruby object method passing Swift code as a block used immediately. + /// + /// This version is for something like `Enumerable#each` where the block is used + /// only in the context of this method and never again. The Swift closure does not have + /// to be escaping or sendable. + /// + /// If the method you're calling retains the block in some way, associating it with the called + /// or returned object for future use, then the Swift closure must be both escaping and sendable + /// and you must call the method with `call(_:args:kwArgs:blockRetention:blockCall:)`. + /// + /// - parameter methodName: 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: The result of calling the method. + /// - throws: `RbError.rubyException(_:)` if there is a Ruby exception. + /// `RbError.duplicateKwArg(_:)` if there are duplicate keywords in `kwArgs`. + /// + /// For a version that does not throw, see `failable`. + @discardableResult + public func call(_ methodName: String, + args: [(any RbObjectConvertible)?] = [], + kwArgs: KeyValuePairs = [:], + blockCall: RbBlockCallback) throws -> RbObject { + try Ruby.setup() + let methodId = try Ruby.getID(for: methodName) + return try withoutActuallyEscaping(blockCall) { newBlockCall in + try doCall(id: methodId, + args: args, kwArgs: kwArgs, + blockRetention: .none, + blockCall: newBlockCall) + } + } + /// Call a Ruby object method passing Swift code as a block. /// /// - parameter methodName: 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 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: The result of calling the method. /// - throws: `RbError.rubyException(_:)` if there is a Ruby exception. @@ -290,8 +324,8 @@ extension RbObjectAccess { public func call(_ methodName: String, args: [(any RbObjectConvertible)?] = [], kwArgs: KeyValuePairs = [:], - blockRetention: RbBlockRetention = .none, - blockCall: @escaping RbBlockCallback) throws -> RbObject { + blockRetention: RbBlockRetention, + blockCall: @escaping @Sendable RbBlockCallback) throws -> RbObject { try Ruby.setup() let methodId = try Ruby.getID(for: methodName) return try doCall(id: methodId, @@ -343,13 +377,46 @@ extension RbObjectAccess { } } + /// Call a Ruby object method using a symbol passing Swift code as a block used immediately. + /// + /// This version is for something like `Enumerable#each` where the block is used + /// only in the context of this method and never again. The Swift closure does not have + /// to be escaping or sendable. + /// + /// If the method you're calling retains the block in some way, associating it with the called + /// or returned object for future use, then the Swift closure must be both escaping and sendable + /// and you must call the method with `call(symbol:args:kwArgs:blockRetention:blockCall:)`. + /// + /// - parameter symbol: The symbol for 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: The result of calling the method. + /// - throws: `RbError.rubyException(_:)` if there is a Ruby exception. + /// `RbError.badType(_:)` if `symbol` is not a symbol. + /// `RbError.duplicateKwArg(_:)` if there are duplicate keywords in `kwArgs`. + /// + /// For a version that does not throw, see `failable`. + @discardableResult + public func call(symbol: any RbObjectConvertible, + args: [(any RbObjectConvertible)?] = [], + kwArgs: KeyValuePairs = [:], + blockCall: RbBlockCallback) throws -> RbObject { + try Ruby.setup() + return try withoutActuallyEscaping(blockCall) { realBlockCall in + try symbol.rubyObject.withSymbolId { methodId in + try doCall(id: methodId, args: args, kwArgs: kwArgs, blockRetention: .none, blockCall: realBlockCall) + } + } + } + /// Call a Ruby object method using a symbol passing Swift code as a block. /// /// - parameter symbol: The symbol for 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 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: The result of calling the method. /// - throws: `RbError.rubyException(_:)` if there is a Ruby exception. @@ -361,8 +428,8 @@ extension RbObjectAccess { public func call(symbol: any RbObjectConvertible, args: [(any RbObjectConvertible)?] = [], kwArgs: KeyValuePairs = [:], - blockRetention: RbBlockRetention = .none, - blockCall: @escaping RbBlockCallback) throws -> RbObject { + blockRetention: RbBlockRetention, + blockCall: @escaping @Sendable RbBlockCallback) throws -> RbObject { try Ruby.setup() return try symbol.rubyObject.withSymbolId { methodId in try doCall(id: methodId, args: args, kwArgs: kwArgs, blockRetention: blockRetention, blockCall: blockCall) @@ -419,7 +486,7 @@ extension RbObjectAccess { // Do call - more complicated if block is involved return try argObjects.withRubyValues { argValues -> RbObject in - if let blockCall = blockCall { + if let blockCall { let (context, value) = try RbBlock.doBlockCall(value: getValue(), methodId: id, argValues: argValues, @@ -434,7 +501,7 @@ extension RbObjectAccess { case .returned: retObject.associate(object: context) } return retObject - } else if let blockObj = blockObj { + } else if let blockObj { return RbObject(rubyValue: try blockObj.withRubyValue { blockValue in try RbBlock.doBlockCall(value: getValue(), methodId: id, argValues: argValues, diff --git a/Sources/RubyGateway/RbThread.swift b/Sources/RubyGateway/RbThread.swift index 0689f25..abb8860 100644 --- a/Sources/RubyGateway/RbThread.swift +++ b/Sources/RubyGateway/RbThread.swift @@ -66,7 +66,7 @@ public enum RbThread { /// - parameter callback: Callback to make on the new thread /// - returns: The Ruby `Thread` object, or `nil` if there was a problem. /// See `RbError.history` for details of any error. - public static func create(callback: @Sendable @escaping () -> Void) -> RbObject? { + public static func create(callback: @escaping @Sendable () -> Void) -> RbObject? { RbObject(ofClass: "Thread", retainBlock: true) { args in callback() return .nilObject diff --git a/Tests/RubyGatewayTests/TestRbObject.swift b/Tests/RubyGatewayTests/TestRbObject.swift index 2a31739..9678baf 100644 --- a/Tests/RubyGatewayTests/TestRbObject.swift +++ b/Tests/RubyGatewayTests/TestRbObject.swift @@ -74,7 +74,7 @@ class TestRbObject: XCTestCase { XCTFail("Managed to create object of odd class: \(obj)") } - if let obj = RbObject(ofClass: "DoesNotExist", retainBlock: false, blockCall: { args in .nilObject }) { + if let obj = RbObject(ofClass: "DoesNotExist", blockCall: { args in .nilObject }) { XCTFail("Managed to create object of odd class: \(obj)") } }