Skip to content

Commit

Permalink
Final updates for Swift 6 and sendable closures (#50)
Browse files Browse the repository at this point in the history
* Updates for sendable closures
* Update tests for split methods
* Updates for Xcode 16 RC
* Linux vs Swift 6
* Document -fmodules workaround
  • Loading branch information
johnfairh authored Sep 19, 2024
1 parent 673ec70 commit c18a461
Show file tree
Hide file tree
Showing 131 changed files with 2,568 additions and 1,300 deletions.
13 changes: 10 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@ jobs:
strategy:
fail-fast: false
matrix:
xcode: ['16.0-beta']
xcode: ['16.0']
steps:
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ matrix.xcode }}
- uses: actions/checkout@v4
with:
submodules: recursive
persist-credentials: false
- name: Ruby
run: |
gem install rouge
Expand All @@ -46,9 +47,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 All @@ -69,6 +71,11 @@ jobs:
- short: '3.3'
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: vapor/[email protected]
with:
toolchain: "6.0"
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.rby.short }}
Expand All @@ -94,4 +101,4 @@ jobs:
run: |
export PKG_CONFIG_PATH=$(pwd)/Packages/CRuby:$PKG_CONFIG_PATH
export LD_LIBRARY_PATH=${RB_PREFIX}/lib:$LD_LIBRARY_PATH
swift test
swift test -Xcc -fmodules
2 changes: 1 addition & 1 deletion .jazzy.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
author: John Fairhurst
author_url: http://github.com/johnfairh
module: RubyGateway
module_version: 5.5.0
module_version: 6.0.0
copyright: Distributed under the MIT license. Maintained by [John Fairhurst](mailto:[email protected]).
readme: README.md
github_url: https://github.com/johnfairh/RubyGateway
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version:5.9
// swift-tools-version:6.0

// Package.swift
// RubyGateway
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ log(object2_to_log, priority: 2)

## Requirements

* Swift 5.9 or later, from swift.org or Xcode 15.3+
* Swift 6.0 or later, from swift.org or Xcode 16+
* macOS (tested on 14.1) or Linux (tested on Ubuntu Jammy)
* Ruby 2.6 or later including development files:
* For macOS, these come with Xcode.
Expand All @@ -172,6 +172,10 @@ For macOS, if you are happy to use the system Ruby then you just need to include
the RubyGateway framework as a dependency. If you are building on Linux or want
to use a different Ruby then you also need to configure CRuby.

## Linux

As of Swift 6, Apple have broken Swift PM such that you must pass "-Xcc -fmodules" to build the project. Check the CI invocation for an example.

### Getting the framework

Carthage for macOS:
Expand All @@ -181,7 +185,7 @@ github "johnfairh/RubyGateway"

Swift package manager for macOS or Linux:
```
.package(url: "https://github.com/johnfairh/RubyGateway", from: "5.5.0")
.package(url: "https://github.com/johnfairh/RubyGateway", from: "6.0.0")
```

CocoaPods for macOS:
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
15 changes: 5 additions & 10 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 Expand Up @@ -149,13 +149,8 @@ private func rbproc_value_block_callback(context: VALUE,
internal enum RbBlock {
/// One-time init to register the callbacks
private static let initOnce: Void = {
// Swift 6 breakage, 'func's apparently don't work for C functions
rbg_register_pvoid_block_proc_callback { a, b, c, d, e in
rbproc_pvoid_block_callback(rawContext: a, argc: b, argv: c, blockArg: d, returnValue: e)
}
rbg_register_value_block_proc_callback { a, b, c, d, e in
rbproc_value_block_callback(context: a, argc: b, argv: c, blockArg: d, returnValue: e)
}
rbg_register_pvoid_block_proc_callback(rbproc_pvoid_block_callback)
rbg_register_value_block_proc_callback(rbproc_value_block_callback)
}()

/// Call a method on an object passing a Swift closure as its block
Expand Down
8 changes: 2 additions & 6 deletions Sources/RubyGateway/RbClass.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,7 @@ internal enum RbClassBinding {

/// One-time init to register the callbacks
private static let initOnce: Void = {
// Swift 6 breakage :(
rbg_register_object_binding_callbacks(
{ rbbinding_alloc(className: $0) },
{ rbbinding_free(className: $0, instance: $1) }
)
rbg_register_object_binding_callbacks(rbbinding_alloc, rbbinding_free)
}()

private static let bindings = LockedDictionary<String, any RbBoundClassProtocol>()
Expand Down Expand Up @@ -168,7 +164,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
10 changes: 4 additions & 6 deletions Sources/RubyGateway/RbGlobalVar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ private enum RbGlobalVar {

/// One-time init to register the callbacks
private static let initOnce: Void = {
// Swift 6 breakage
rbg_register_gvar_callbacks( { rbobject_gvar_get_callback(id: $0) },
{ rbobject_gvar_set_callback(id: $0, newValue: $1, returnValue: $2) })
rbg_register_gvar_callbacks(rbobject_gvar_get_callback, rbobject_gvar_set_callback)
}()

/// Callbacks + store - type-erased at this point
Expand Down Expand Up @@ -97,7 +95,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 +118,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
24 changes: 9 additions & 15 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 @@ -143,10 +140,7 @@ private struct RbMethodExec {
private struct RbMethodDispatch {
/// One-time init to register the callbacks
private static let initOnce: Void = {
// Swift 6 breakage
rbg_register_method_callback {
rbmethod_callback(symbol: $0, targetCount: $1, rawTargets: $2, rubySelf: $3, argc: $4, argv: $5, returnValue: $6)
}
rbg_register_method_callback(rbmethod_callback)
}()

/// List of all method callbacks
Expand Down Expand Up @@ -258,7 +252,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 +661,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 +704,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
Loading

0 comments on commit c18a461

Please sign in to comment.