Skip to content

Commit

Permalink
Generic signal support
Browse files Browse the repository at this point in the history
Require macOS 14 for the generic pack support

Would bumping the swift-tools-version to 5.10 be better?

Use generic signal instead of generating helper

Don't use SimpleSignal

Moved GenericSignal to its own file. Removed some unused code, tidied comments.

Fixed popping objects.

Cleaner unpacking, without the need to copy the arguments.

revert unrelated formatting changes

put back SignalSupport to minimise changes

Removed SimpleSignal completely

tweaked names for a cleaner diff

Added test for built in signals
  • Loading branch information
samdeane committed Nov 21, 2024
1 parent e01de36 commit e9ee6dd
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 206 deletions.
118 changes: 27 additions & 91 deletions Generator/Generator/ClassGen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -518,110 +518,22 @@ func generateClasses (values: [JGodotExtensionAPIClass], outputDir: String?) asy
}
}

func generateSignalType (_ p: Printer, _ cdef: JGodotExtensionAPIClass, _ signal: JGodotSignal, _ name: String) -> String {
doc (p, cdef, "Signal support.\n")
doc (p, cdef, "Use the ``\(name)/connect(flags:_:)`` method to connect to the signal on the container object, and ``\(name)/disconnect(_:)`` to drop the connection.\nYou can also await the ``\(name)/emitted`` property for waiting for a single emission of the signal.")

var lambdaFull = ""
p ("public class \(name)") {
p ("var target: Object")
p ("var signalName: StringName")
p ("init (target: Object, signalName: StringName)") {
p ("self.target = target")
p ("self.signalName = signalName")
}
doc (p, cdef, "Connects the signal to the specified callback\n\nTo disconnect, call the disconnect method, with the returned token on success\n - Parameters:\n - callback: the method to invoke when this signal is raised\n - flags: Optional, can be also added to configure the connection's behavior (see ``Object/ConnectFlags`` constants).\n - Returns: an object token that can be used to disconnect the object from the target on success, or the error produced by Godot.")

p ("@discardableResult /* \(name) */")
var args = ""
var argUnwrap = ""
var callArgs = ""
var argIdx = 0
var lambdaIgnore = ""
for arg in signal.arguments ?? [] {
if args != "" {
args += ", "
callArgs += ", "
lambdaIgnore += ", "
lambdaFull += ", "
}
args += getArgumentDeclaration(arg, omitLabel: true, isOptional: arg.type == "Variant")
let construct: String

if let _ = classMap [arg.type] {
argUnwrap += "var ptr_\(argIdx): UnsafeMutableRawPointer?\n"
argUnwrap += "args [\(argIdx)]!.toType (Variant.GType.object, dest: &ptr_\(argIdx))\n"
let handleResolver: String
if hasSubclasses.contains(cdef.name) {
// If the type we are bubbling up has subclasses, we want to create the most
// derived type if possible, so we perform the longer lookup
handleResolver = "lookupObject (nativeHandle: ptr_\(argIdx)!) ?? "
} else {
handleResolver = ""
}

construct = "lookupLiveObject (handleAddress: ptr_\(argIdx)!) as? \(arg.type) ?? \(handleResolver)\(arg.type) (nativeHandle: ptr_\(argIdx)!)"
} else if arg.type == "String" {
construct = "\(mapTypeName(arg.type)) (args [\(argIdx)]!)!.description"
} else if arg.type == "Variant" {
construct = "args [\(argIdx)]"
} else {
construct = "\(getGodotType(arg)) (args [\(argIdx)]!)!"
}
argUnwrap += "let arg_\(argIdx) = \(construct)\n"
callArgs += "arg_\(argIdx)"
lambdaIgnore += "_"
lambdaFull += escapeSwift (snakeToCamel (arg.name))
argIdx += 1
}
p ("public func connect (flags: Object.ConnectFlags = [], _ callback: @escaping (\(args)) -> ()) -> Object") {
p ("let signalProxy = SignalProxy()")
p ("signalProxy.proxy = ") {
p ("args in")
p (argUnwrap)
p ("callback (\(callArgs))")
}
p ("let callable = Callable(object: signalProxy, method: SignalProxy.proxyName)")
p ("let r = target.connect(signal: signalName, callable: callable, flags: UInt32 (flags.rawValue))")
p ("if r != .ok { print (\"Warning, error connecting to signal, code: \\(r)\") }")
p ("return signalProxy")
}

doc (p, cdef, "Disconnects a signal that was previously connected, the return value from calling ``connect(flags:_:)``")
p ("public func disconnect (_ token: Object)") {
p ("target.disconnect(signal: signalName, callable: Callable (object: token, method: SignalProxy.proxyName))")
}
doc (p, cdef, "You can await this property to wait for the signal to be emitted once")
p ("public var emitted: Void "){
p ("get async") {
p ("await withCheckedContinuation") {
p ("c in")
p ("connect (flags: .oneShot) { \(lambdaIgnore) in c.resume () }")
}
}
}
}
return lambdaFull
}

func generateSignals (_ p: Printer,
cdef: JGodotExtensionAPIClass,
signals: [JGodotSignal]) {
p ("// Signals ")
var parameterSignals: [JGodotSignal] = []
var sidx = 0

for signal in signals {
let signalProxyType: String
let lambdaSig: String
if signal.arguments != nil {
parameterSignals.append (signal)

sidx += 1
signalProxyType = "Signal\(sidx)"
lambdaSig = " " + generateSignalType (p, cdef, signal, signalProxyType) + " in"
signalProxyType = getGenericSignalType(signal)
lambdaSig = " \(getGenericSignalLambdaArgs(signal)) in"
} else {
signalProxyType = "SimpleSignal"
signalProxyType = "GenericSignal< /* no args */ >"
lambdaSig = ""
}
let signalName = godotMethodToSwift (signal.name)
Expand All @@ -639,6 +551,30 @@ func generateSignals (_ p: Printer,
}
}

/// Return the type of a signal's parameters.
func getGenericSignalType(_ signal: JGodotSignal) -> String {
var argTypes: [String] = []
for signalArgument in signal.arguments ?? [] {
let godotType = getGodotType(signalArgument)
if !godotType.isEmpty && godotType != "Variant" {
argTypes.append(godotType)
}
}

return argTypes.isEmpty ? "GenericSignal< /* no args */ >" : "GenericSignal<\(argTypes.joined(separator: ", "))>"
}

/// Return the names of a signal's parameters,
/// for use in documenting the corresponding lambda.
func getGenericSignalLambdaArgs(_ signal: JGodotSignal) -> String {
var argNames: [String] = []
for signalArgument in signal.arguments ?? [] {
argNames.append(escapeSwift(snakeToCamel(signalArgument.name)))
}

return argNames.joined(separator: ", ")
}

func generateSignalDocAppendix (_ p: Printer, cdef: JGodotExtensionAPIClass, signals: [JGodotSignal]?) {
guard let signals = signals, signals.count > 0 else {
return
Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.9
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
Expand Down Expand Up @@ -231,7 +231,7 @@ targets.append(contentsOf: [
let package = Package(
name: "SwiftGodot",
platforms: [
.macOS(.v13),
.macOS(.v14),
.iOS (.v15)
],
products: products,
Expand Down
99 changes: 99 additions & 0 deletions Sources/SwiftGodot/Core/GenericSignal.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//
// Created by Sam Deane on 25/10/2024.
//

/// Signal support.
/// Use the ``GenericSignal/connect(flags:_:)`` method to connect to the signal on the container object,
/// and ``GenericSignal/disconnect(_:)`` to drop the connection.
///
/// Use the ``GenericSignal/emit(...)`` method to emit a signal.
///
/// You can also await the ``Signal1/emitted`` property for waiting for a single emission of the signal.
///
public class GenericSignal<each T: VariantStorable> {
var target: Object
var signalName: StringName
public 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 (_ t: repeat each T) -> Void) -> Object {
let signalProxy = SignalProxy()
signalProxy.proxy = { args in
var index = 0
do {
callback(repeat try args.unpack(as: (each T).self, index: &index))
} catch {
print("Error unpacking signal arguments: \(error)")
}
}

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
let signalProxy = SignalProxy()
signalProxy.proxy = { _ in c.resume() }
let callable = Callable(object: signalProxy, method: SignalProxy.proxyName)
let r = target.connect(signal: signalName, callable: callable, flags: UInt32(Object.ConnectFlags.oneShot.rawValue))
if r != .ok { print("Warning, error connecting to signal, code: \(r)") }
}

}

}

}


extension Arguments {
enum UnpackError: Error {
case typeMismatch
case missingArgument
}

/// Unpack an argument as a specific type.
/// We throw a runtime error if the argument is not of the expected type,
/// or if there are not enough arguments to unpack.
func unpack<T: VariantStorable>(as type: T.Type, index: inout Int) throws -> T {
if index >= count {
throw UnpackError.missingArgument
}
let argument = self[index]
index += 1
let value: T?
if argument.gtype == .object {
value = T.Representable.godotType == .object ? argument.asObject(Object.self) as? T : nil
} else {
value = T(argument)
}

guard let value else {
throw UnpackError.typeMismatch
}

return value
}
}
Loading

0 comments on commit e9ee6dd

Please sign in to comment.