Skip to content

Unify Swift and Godot Signal Syntax #584

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

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Generator/Generator/MethodGen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ enum GeneratedMethodKind {
case utilityFunction
}

/// The list of static method names to make public.
/// Most of the names are kept fileprivate.
let publicMethodNames = ["method_get_class", "method_emit_signal"]

// To test the design, will use an external file later
// determines whether the className/method returns an optional reference type
func isReturnOptional (className: String, method: String) -> Bool {
Expand Down Expand Up @@ -497,7 +501,7 @@ func generateMethod(_ p: Printer, method: MethodDefinition, className: String, c
let inlineAttribute: String?
let documentationVisibilityAttribute: String?
if let methodHash = method.optionalHash {
let staticVarVisibility = if bindName != "method_get_class" { "fileprivate " } else { "" }
let staticVarVisibility = publicMethodNames.contains(bindName) ? "" : "fileprivate "
assert (!method.isVirtual)
switch generatedMethodKind {
case .classMethod:
Expand Down
59 changes: 56 additions & 3 deletions Sources/SwiftGodot/Core/GenericSignal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,23 @@
// Created by Sam Deane on 25/10/2024.
//

extension VariantStorable {
static func propInfo(name: String) -> PropInfo {
let gType = Self.Representable.godotType
return PropInfo(
propertyType: gType,
propertyName: StringName(name),
className: gType == .object ? StringName(String(describing: Self.self)) : "",
hint: .none,
hintStr: "",
usage: .default
)
}
}

public typealias SignalWithArguments = GenericSignal
public typealias SimpleSignal = GenericSignal< /* no args */ >

/// Signal support.
/// Use the ``GenericSignal/connect(flags:_:)`` method to connect to the signal on the container object,
/// and ``GenericSignal/disconnect(_:)`` to drop the connection.
Expand All @@ -13,9 +30,32 @@
public class GenericSignal<each T: VariantStorable> {
var target: Object
var signalName: StringName
public init(target: Object, signalName: StringName) {
public init(target: Object, signalName: String) {
self.target = target
self.signalName = signalName
self.signalName = StringName(signalName)
}

/// Register this signal with the Godot runtime.
// TODO: the signal macro could probably pass in the argument names, so that we could register them as well
public static func register<C: Object>(_ signalName: String, info: ClassInfo<C>) {
let arguments = expandArguments(repeat (each T).self)
info.registerSignal(name: StringName(signalName), arguments: arguments)
}

/// Expand a list of argument types into a list of PropInfo objects
/// Note: it doesn't currently seem to be possible to constrain
/// the type of the pack expansion to be ``VariantStorable.Type``, but
/// we know that it always will be, so we can force cast it.
static func expandArguments<each ArgType>(_ type: repeat each ArgType) -> [PropInfo] {
var args = [PropInfo]()
var argC = 1
for arg in repeat each type {
let a = arg as! any VariantStorable.Type
args.append(a.propInfo(name: "arg\(argC)"))
argC += 1

}
return args
}

/// Connects the signal to the specified callback
Expand All @@ -26,7 +66,7 @@ public class GenericSignal<each T: VariantStorable> {
/// - 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 */
@discardableResult
public func connect(flags: Object.ConnectFlags = [], _ callback: @escaping (_ t: repeat each T) -> Void) -> Object {
let signalProxy = SignalProxy()
signalProxy.proxy = { args in
Expand All @@ -50,6 +90,19 @@ public class GenericSignal<each T: VariantStorable> {
target.disconnect(signal: signalName, callable: Callable(object: token, method: SignalProxy.proxyName))
}

/// Emit the signal (with required arguments, if there are any)
@discardableResult /* discardable per discardableList: Object, emit_signal */
public func emit(_ t: repeat each T) -> GodotError {
var args = [Variant(signalName)]
for arg in repeat each t {
args.append(Variant(arg))
}
let result = target.emitSignalWithArguments(args)
guard let result else { return .ok }
guard let errorCode = Int(result) else { return .ok }
return GodotError(rawValue: Int64(errorCode))!
}

/// You can await this property to wait for the signal to be emitted once.
public var emitted: Void {
get async {
Expand Down
45 changes: 45 additions & 0 deletions Sources/SwiftGodot/Core/RawCall.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
@_implementationOnly import GDExtension

extension Object {

/// Make a raw call to a method bind.
/// All input arguments must be marshaled into `Variant`s.
/// The result is a `Variant` that must be unmarshaled into the expected type.
@discardableResult /* discardable per discardableList: Object, emit_signal */
final func rawCall(_ p_method_bind: GDExtensionMethodBindPtr, arguments: [Variant]) -> Variant? {
var _result: Variant.ContentType = Variant.zero
// A temporary allocation containing pointers to `Variant.ContentType` of marshaled arguments
withUnsafeTemporaryAllocation(of: UnsafeRawPointer?.self, capacity: arguments.count) { pArgsBuffer in
defer { pArgsBuffer.deinitialize() }
guard let pArgs = pArgsBuffer.baseAddress else {
fatalError("pArgsBuffer.baseAddress is nil")
}

// A temporary allocation containing `Variant.ContentType` of marshaled arguments
withUnsafeTemporaryAllocation(of: Variant.ContentType.self, capacity: arguments.count) { contentsBuffer in
defer { contentsBuffer.deinitialize() }
guard let contentsPtr = contentsBuffer.baseAddress else {
fatalError("contentsBuffer.baseAddress is nil")
}

for i in 0..<arguments.count {
// Copy `content`s of the variadic `Variant`s into `contentBuffer`
contentsBuffer.initializeElement(at: i, to: arguments[i].content)
// Initialize `pArgs` elements following mandatory arguments to point at respective contents of `contentsBuffer`
pArgsBuffer.initializeElement(at: i, to: contentsPtr + i)
}

gi.object_method_bind_call(p_method_bind, UnsafeMutableRawPointer(mutating: handle), pArgs, Int64(arguments.count), &_result, nil)
}
}

return Variant(takingOver: _result)
}

/// Non-variadic variation on the emitSignal method.
/// Used by GenericSignal.
public final func emitSignalWithArguments(_ arguments: [Variant]) -> Variant? {
return rawCall(Object.method_emit_signal, arguments: arguments)
}

}
4 changes: 2 additions & 2 deletions Sources/SwiftGodot/Core/SignalRegistration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -375,8 +375,8 @@ public extension Object {
}
}

private extension PropInfo {
init(
extension PropInfo {
fileprivate init(
propertyType: (some VariantStorable).Type,
propertyName: StringName
) {
Expand Down
21 changes: 21 additions & 0 deletions Sources/SwiftGodot/MacroDefs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -240,4 +240,25 @@ public macro SceneTree(path: String? = nil) = #externalMacro(module: "SwiftGodot
@freestanding(declaration, names: arbitrary)
public macro signal(_ signalName: String, arguments: Dictionary<String, Any.Type> = [:]) = #externalMacro(module: "SwiftGodotMacroLibrary", type: "SignalMacro")

/// Defines a Godot signal on a class.
///
/// The `@Godot` macro will register any #signal defined signals so that they can be used in the editor.
///
/// Usage:
/// ```swift
/// @Godot class MyNode: Node2D {
/// @Signal var gameStarted: SimpleSignal
/// @Signal var livesChanged: SignalWithArguments<Int>
///
/// func startGame() {
/// gameStarted.emit()
/// livesChanged.emit(5)
/// }
/// }
/// ```

@attached(accessor)
public macro Signal() = #externalMacro(module: "SwiftGodotMacroLibrary", type: "SignalAttachmentMacro")


#endif
124 changes: 84 additions & 40 deletions Sources/SwiftGodotMacroLibrary/MacroGodot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -210,13 +210,26 @@ class GodotMacroProcessor {
ctor.append (funcArgs)
ctor.append (" classInfo.registerMethod(name: StringName(\"\(funcName)\"), flags: .default, returnValue: \(retProp ?? "nil"), arguments: \(funcArgs == "" ? "[]" : "\(funcName)Args"), function: \(className)._mproxy_\(funcName))\n")
}


func processVariable(_ varDecl: VariableDeclSyntax, previousGroupPrefix: String?, previousSubgroupPrefix: String?) throws -> Bool {
if varDecl.isGArrayCollection {
try processGArrayCollectionVariable(varDecl, prefix: previousSubgroupPrefix ?? previousGroupPrefix)
} else if hasExportAttribute(varDecl.attributes) {
return try processExportVariable(varDecl, prefix: previousSubgroupPrefix ?? previousGroupPrefix)
} else if hasSignalAttachmentAttribute(varDecl.attributes) {
try processSignalVariable(varDecl, prefix: previousSubgroupPrefix ?? previousGroupPrefix)
}

return false
}


// Returns true if it used "tryCase"
func processVariable (_ varDecl: VariableDeclSyntax, prefix: String?) throws -> Bool {
func processExportVariable (_ varDecl: VariableDeclSyntax, prefix: String?) throws -> Bool {
assert(hasExportAttribute(varDecl.attributes))

var usedTryCase = false
guard hasExportAttribute(varDecl.attributes) else {
return false
}
guard let last = varDecl.bindings.last else {
throw GodotMacroError.noVariablesFound
}
Expand Down Expand Up @@ -254,7 +267,7 @@ class GodotMacroProcessor {
let proxyGetterName = "_mproxy_get_\(varNameWithPrefix)"
let setterName = "_mproxy_set_\(varNameWithoutPrefix)"
let getterName = "_mproxy_get_\(varNameWithoutPrefix)"

if let accessors = last.accessorBlock {
if CodeBlockSyntax (accessors) != nil {
throw MacroError.propertyGetSet
Expand All @@ -263,27 +276,27 @@ class GodotMacroProcessor {
var hasSet = false
var hasGet = false
switch block.accessors {
case .accessors(let list):
for accessor in list {
switch accessor.accessorSpecifier.tokenKind {
case .keyword(let val):
switch val {
case .didSet, .willSet:
hasSet = true
hasGet = true
case .set:
hasSet = true
case .get:
hasGet = true
default:
break
case .accessors(let list):
for accessor in list {
switch accessor.accessorSpecifier.tokenKind {
case .keyword(let val):
switch val {
case .didSet, .willSet:
hasSet = true
hasGet = true
case .set:
hasSet = true
case .get:
hasGet = true
default:
break
}
default:
break
}
default:
break
}
}
default:
throw MacroError.propertyGetSet
default:
throw MacroError.propertyGetSet
}

if hasSet == false || hasGet == false {
Expand Down Expand Up @@ -417,6 +430,38 @@ class GodotMacroProcessor {
}
}


// Returns true if it used "tryCase"
func processSignalVariable (_ varDecl: VariableDeclSyntax, prefix: String?) throws {
guard let last = varDecl.bindings.last else {
throw GodotMacroError.noVariablesFound
}

guard let type = last.typeAnnotation?.type else {
throw GodotMacroError.noTypeFound(varDecl)
}

guard var typeName = type.as (IdentifierTypeSyntax.self)?.name.text else {
throw GodotMacroError.unsupportedType(varDecl)
}

if let genericArgs = type.as (IdentifierTypeSyntax.self)?.genericArgumentClause {
typeName += "\(genericArgs)"
}

for variable in varDecl.bindings {
guard let ips = variable.pattern.as(IdentifierPatternSyntax.self) else {
throw GodotMacroError.expectedIdentifier(variable)
}

let nameWithPrefix = ips.identifier.text
let name = String(nameWithPrefix.trimmingPrefix(prefix ?? ""))

ctor.append("\(typeName).register(\"\(name.camelCaseToSnakeCase())\", info: classInfo)")
}
}


var ctor: String = ""
var genMethods: [String] = []

Expand All @@ -433,29 +478,27 @@ class GodotMacroProcessor {
var needTrycase = false
for member in classDecl.memberBlock.members.enumerated() {
let decl = member.element.decl
let macroExpansion = MacroExpansionDeclSyntax(decl)

if let macroExpansion = MacroExpansionDeclSyntax(decl),
let name = macroExpansion.exportGroupName {
previousGroupPrefix = macroExpansion.exportGroupPrefix ?? ""
if let name = macroExpansion?.exportGroupName {
previousGroupPrefix = macroExpansion?.exportGroupPrefix ?? ""
processExportGroup(name: name, prefix: previousGroupPrefix ?? "")
} else if let macroExpansion = MacroExpansionDeclSyntax(decl),
let name = macroExpansion.exportSubgroupName {
previousSubgroupPrefix = macroExpansion.exportSubgroupPrefix ?? ""
} else if let name = macroExpansion?.exportSubgroupName {
previousSubgroupPrefix = macroExpansion?.exportSubgroupPrefix ?? ""
processExportSubgroup(name: name, prefix: previousSubgroupPrefix ?? "")
} else if let funcDecl = FunctionDeclSyntax(decl) {
try processFunction (funcDecl)
} else if let varDecl = VariableDeclSyntax(decl) {
if varDecl.isGArrayCollection {
try processGArrayCollectionVariable(varDecl, prefix: previousSubgroupPrefix ?? previousGroupPrefix)
} else {
if try processVariable(varDecl, prefix: previousSubgroupPrefix ?? previousGroupPrefix) {
needTrycase = true
}
}
} else if let macroDecl = MacroExpansionDeclSyntax(decl) {
try classInitSignals(macroDecl)
try needTrycase = needTrycase || processVariable(
varDecl,
previousGroupPrefix: previousGroupPrefix,
previousSubgroupPrefix: previousSubgroupPrefix
)
} else if let macroExpansion {
try classInitSignals(macroExpansion)
}
}

if needTrycase {
ctor.append (
"""
Expand Down Expand Up @@ -628,7 +671,8 @@ struct godotMacrosPlugin: CompilerPlugin {
PickerNameProviderMacro.self,
SceneTreeMacro.self,
Texture2DLiteralMacro.self,
SignalMacro.self
SignalMacro.self,
SignalAttachmentMacro.self,
]
}

Expand Down
7 changes: 6 additions & 1 deletion Sources/SwiftGodotMacroLibrary/MacroSharedApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,18 @@ func hasExportAttribute (_ attrs: AttributeListSyntax?) -> Bool {
hasAttribute ("Export", attrs)
}

/// True if the attribtue list syntax has an attribute name 'Signal'
func hasSignalAttachmentAttribute (_ attrs: AttributeListSyntax?) -> Bool {
hasAttribute ("Signal", attrs)
}

/// True if the attribtue list syntax has an attribute name 'Callable'
func hasCallableAttribute (_ attrs: AttributeListSyntax?) -> Bool {
hasAttribute ("Callable", attrs)
}

/// True if the attribtue list syntax has an attribute name 'signal'
func hasSignalAttribute (_ attrs: AttributeListSyntax?) -> Bool {
func hasDeprecatedSignalAttribute (_ attrs: AttributeListSyntax?) -> Bool {
hasAttribute ("signal", attrs)
}

Expand Down
Loading
Loading