-
Notifications
You must be signed in to change notification settings - Fork 74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Unify Swift and Godot Signal Syntax #584
base: main
Are you sure you want to change the base?
Conversation
We have an existing idiom to map Godot signal into Swift idioms, have you seen it? I wonder if we can reuse that, so we do not end up with an additional idiom - check the generated code we have. |
I guess you're talking about support for the syntax described here, generated by ClassGen.generateSignalType? It would indeed make sense to try to use the same underlying mechanism for signals that are defined by the user on the Swift side. It confused me that the syntax in the documentation worked for pre-existing signals declared as part of the generated API, but not for signals we declare using There is also a big potential optimisation I think with // Signals
/// Signal support.
///
///
///
/// Use the ``Signal1/connect(flags:_:)`` method to connect to the signal on the container object, and ``Signal1/disconnect(_:)`` to drop the connection.
///
/// You can also await the ``Signal1/emitted`` property for waiting for a single emission of the signal.
///
public class Signal1 {
var target: Object
var signalName: StringName
init (target: Object, signalName: StringName) {
self.target = target
self.signalName = signalName
}
/// Connects the signal to the specified callback
///
///
///
/// To disconnect, call the disconnect method, with the returned token on success
///
/// - Parameters:
///
/// - callback: the method to invoke when this signal is raised
///
/// - flags: Optional, can be also added to configure the connection's behavior (see ``Object/ConnectFlags`` constants).
///
/// - Returns: an object token that can be used to disconnect the object from the target on success, or the error produced by Godot.
///
@discardableResult /* Signal1 */
public func connect (flags: Object.ConnectFlags = [], _ callback: @escaping (_ bodyRid: RID, _ body: Node2D, _ bodyShapeIndex: Int64, _ localShapeIndex: Int64) -> ()) -> Object {
let signalProxy = SignalProxy()
signalProxy.proxy = {
args in
let arg_0 = RID (args [0])!
var ptr_1: UnsafeMutableRawPointer?
args [1].toType (Variant.GType.object, dest: &ptr_1)
let arg_1 = lookupLiveObject (handleAddress: ptr_1!) as? Node2D ?? Node2D (nativeHandle: ptr_1!)
let arg_2 = Int64 (args [2])!
let arg_3 = Int64 (args [3])!
callback (arg_0, arg_1, arg_2, arg_3)
}
let callable = Callable(object: signalProxy, method: SignalProxy.proxyName)
let r = target.connect(signal: signalName, callable: callable, flags: UInt32 (flags.rawValue))
if r != .ok { print ("Warning, error connecting to signal, code: \(r)") }
return signalProxy
}
/// Disconnects a signal that was previously connected, the return value from calling ``connect(flags:_:)``
public func disconnect (_ token: Object) {
target.disconnect(signal: signalName, callable: Callable (object: token, method: SignalProxy.proxyName))
}
/// You can await this property to wait for the signal to be emitted once
public var emitted: Void {
get async {
await withCheckedContinuation {
c in
connect (flags: .oneShot) { _, _, _, _ in c.resume () }
}
}
}
} in public var bodyShapeEntered: Signal1 { Signal1 (target: self, signalName: "body_shape_entered") } I understand why it does this (it's simple and it works!), but it's quite wasteful, when quite a lot of signals probably have the same signature. (admittedly, the signature in the example above is probably unique, but there are quite a lot of animation-related signals for example which take a single animation name as their parameter, and we're making an identical class for each one) The repeated code will affect compile times, and I'm not sure whether the linker will be smart enough to eliminate the duplication. There's an optimisation in their for the
public var bodyShapeEntered: GenericSignal<RID,Node2D,Int64,Int64> { GenericSignal<RID, Node2d,Int64,Int64> (target: self, signalName: "body_shape_entered") } and it would automatically have I don't know whether the underlying swift compiler would be smart enough to avoid the bloat of repeated expansion of this generic into concrete versions with different signatures... but at least that is someone else's problem 😛. |
Ignoring the optimisations for now, I wonder if I can lift the code from Then using could emit a public class SignalFoo { /*. generated support for Foo signal here... */ }
public var foo: SignalFoo { SignalFoo(target: self, signalName: "foo") } and you could do |
Correct, it is wasteful, and would love to consolidate, that was filed as #42 I am open to the changes if we can harmonize them with the existing signal setup, would this change break existing code that might have used #signal already? |
Better mockup - this code actually runs and the test passes, although it's obviously just a sketch... import ChaosTesting
import Testing
@testable import Scratch
@Test func name() async throws {
struct Foo {
var foo: GenericSignal<Int> { GenericSignal<Int>(name: "foo") }
}
var callbackRan = false
let f = Foo()
f.foo.connect { (i: Int) in
#expect(i == 42)
callbackRan = true
}
f.foo.emit(42)
#expect(callbackRan)
}
struct GenericSignal<each T> {
let name: String
func emit(_ t: repeat each T) {
emitSignal(name, repeat each t)
}
func connect(callback: @escaping (_ t: repeat each T) -> Void) {
sp.proxy = { args in
callback(repeat args.pop(as: (each T).self)!)
}
}
}
nonisolated(unsafe) var sp = SignalProxy()
/// A packed array of arguments that can be popped off one by one,
/// with type checking.
class Arguments {
init(_ args: [Any]) {
self.args = args
}
var args: [Any] = []
func pop<T>(as: T.Type) -> T? {
let v = args.first as? T
args.removeFirst()
return v
}
}
/// A proxy object that can be registered with a signal.
/// When the signal is emitted, the proxy will be called,
/// and will invoke its callback with the supplied arguments.
struct SignalProxy {
var proxy: ((Arguments) -> Void)? = nil
func proxyFunc(args: Arguments) {
proxy?(args)
}
}
func emitSignal<each T>(_ name: String, _ args: repeat each T) {
var a: [Any] = []
repeat a.append(each args)
sp.proxyFunc(args: Arguments(a))
} |
It might be possible to support some kind of backwards compatibility, but I suspect that a cleaner route could be to implement a new solution and then deprecate the old one. I am wondering whether the new solution could use So you'd do something like: @Godot class MyClass {
@Signal var mySignal
and it would expand the body for you, out to: @Godot class MyClass {
var mySignal: GenericSignal< > { GenericSignal< >(target: self, signalName: "mySignal") |
So you could also do: @Godot class MyClass {
@Signal(args: [String.self, Int.self]) var mySignal
and it would expand the body to: @Godot class MyClass {
var mySignal: GenericSignal<String, Int> { GenericSignal<String, Int>(target: self, signalName: "mySignal") and so on... |
I've added a new issue #586, which is written from the perspective of fixing the disparity between the way we call imported signals and the way we call Swift-declared ones. I think I could try to implement #42 using my suggested solution, which should be a non-breaking change if it works. |
510bba2
to
5bed64b
Compare
29304d3
to
c91d51c
Compare
Note that:
|
824a798
to
9afffee
Compare
I've cleaned up the formatting - the PR should now just contain the actual changes I've made. |
630d307
to
1cfdf88
Compare
It looks like the Windows build is broken:
Maybe parameter Packs are not supported on Windows yet? |
Uh-oh. I'll take a look. |
Would bumping the swift-tools-version to 5.10 be better?
Allows using the spelling obj.signal.connect(target, method) which is closer to GDScript than obj.connect(Class.signal, target, method)
Should probably deprecate #signal and/or rename #nusignal
1cfdf88
to
ce226d0
Compare
See also #586, which describes the problem I was trying to solve.
This PR builds on the changes in #587 - which should be tested and merged first.
Motivation
The
#signal
macro defines the signal as a static property on the class that contains it.This means that, given a class with a signal:
when using the signal in SwiftGodot code, the current spelling to connect to it is:
This isn't as intuitive as the GDScript spelling, and it's different from the way we treat signals that are defined in Godot and bridged over to Swift, eg:
Implementation
This PR adds a new
#nusignal
macro expansion which generates a computed property on the class, usingGenericSignal
. This is the same approach that is used to implement the signals imported from the Godot API.We also add an
emit()
method toGenericSignal
, which is of course essential to support emitting signals defined in Swift.This allows the swift connect call to be written as:
It can also be written as:
To Do
I added a new macro to ensure that this isn't a breaking change.
Instead of the
#xxx
style macro, it might be possible to make an attachment-style@Signal
, which could be cleanerIf this change is adopted, it's probably worth updating the documentation and deprecating the old macro.