Skip to content

Commit 7e4c34c

Browse files
migueldeicazarolandvighkonczg
authored
New memory management system for SwiftGodot for Godot Objects. (#663)
* New memory management system for SwiftGodot for Godot Objects. In this change we deal with proper ownership of objects in Godot, the RefCounted objects that Godot releases via ref/unref operations, and those that are queued for destruction. To free objects: * If you have a Node subclass, use `queueFree` * If you have a RefCounted object, just clear your Swift Reference * Otherwise, call `free` In this change, we take advantage of Godot's system that notifies us when a Godot object has been released so we clear our "handle", and the `isValid` method that we have had for a while is now correct on all cases. It is important because otherwise we could have a situation where Godot frees an object, but we still keep a pointer to it. And due to how Godot's object allocation works, as Godot created new objects, rather than a crash to an invalid memory address, we would just have a pointer to the wrong object and things would seem to work. To implement this, we also had to change an old SwiftGodot design principle - in the past, we did not care if multiple Swift peers were created for the underlying object, because we use to consider them purely peers that could be recreated on demand. But now, when we take advantage of Godot notifying us of the object being deallocated, we would only deallocate one instance, not all the instances. So to fix this, we now only surface one SwiftGodot object wrapper for a given Godot Object. Some special handling is done for different objects in the Godot hierarchy. The way we now handle the lifecycle of Godot objects in Swift is the following: If an object is not a RefCounted, we keep a strong reference to it, so that only a call to the appropriate free method can delete the object. If an object is a RefCounted, we keep track of the reference()/unreference() calls to it, and we keep a weak reference to it if there are no references to it in Godot, otherwise we keep a strong reference to it. This guarantees that the object can still be deleted when no reference to it exists anymore, while making sure that subtyped objects keep their state on the Swift side throughout the Godot object's lifetime. As the Swift wrapper also increases the reference count of the RefCounted, this means that once a RefCounted is passed to the Swift side, that Godot object will always be destroyed by the Swift wrapper eventually. This also introduces a cute method called `attemptToUseObjectFreedByGodot`, so that if you see it on a stack trace, it is a clear indicator that this took place. This can happen on code that kept references to long-term held objects, in those cases, you must call `isValid` on the object. The system now can toggle if we need to keep a strong reference to the underlying object via the WrappedReference class, which can strongify or weakify the changes. While I did the initial work on making the objects be unique and clearing of the handle, the real hard work and the correct tracking of object ownership was done by Gabor Koncz and Gergely Kis at Migeran who worked on all the various and difficult corner cases. In particular the new object lifecycle is tracked like this: - Every time godot returns a RefCounted object in a Ref<> wrapper for a ptrcall, its reference count is incremeneted. As object identity results in the return of the same Swift proxy for the same RefCounted object, this means that every subsequent return should result in an unreference() call, so that the existence of the Swift proxy object always results in a single increment of the reference count. - On the other hand, if a RefCounted object was returned through a non-RefCounted static return type (e.g. as an Object), then Godot did not increment its reference count. This means that on the first return of such an object, the reference count should be incremented by a reference() call, so that similarly the existence of the Swift proxy object always results in a single increment of the reference count. - The ownsRef parameter is true iff Godot can pass ownership of a Ref<> wrapper to SwiftGodot, e.g. with a Ref<> return value of a ptrcall. This work was the result of using a large SwiftGodot-based application on production through real life testing, where we slowly uncovered the original design flaws and worked to fix each of those one by one. That is why this commit is a bundle of fixes, as the original history contains various attempts at solving the problem until the real solution emerged. * Incorporate feedback from Elijah Semyonov from #663 * I don't think that we should be using this teardown that goes behind the back of the system. * It is possible to call the extensionInterface, regardless of whether the caller has provided an extensionInitCallback. This is the scenario for the synthetic SwiftGodotTestability/GodotRuntime. * For now, non-Refcounted, non-Node are released with free(), discussing whether this should be automatic in deinit * Fix RefCounted initialization in case of failed cast. Bug #1024. * Fix unregister order of inheritance chains * Provide an API to externally terminate all the RefCounted objects slated for destruction, so we can use it in the test suite. While doing this, also optimized this process by queuing a bunch of operations in one go, rather than create one Callable per release. * Use cached values for public SwiftGodot * Optimization, should go to main, do not create a new StringName for each use of the class * We already own this StringName, so do not leak * This was deadlocking, caught it once * Second attempt to fix the deadlock * Shorter version, also I think this fixes a leak * Release pending objects before unregistering * Adjust the validateProperty callback code after merging from main, to take into account the new memory usage pattern * Add updated documentation * Small doc update * Follow my own guidance, which fixes one of the tests * frameworkTypeBindingFree: By the time this method is called, accessing the wrapped object's value returns nil, because this is why the deinit for RefCounted was triggered by - so we can just use the instance parameter directly. This fixes a leak caused by these objects not being removed from our internal table. * Wrapped: reuse the callable, there is no need to create this for every deinit, hides the fact that creating a Callable is leaking * Add more tests that I used while debugging * Nasty crash: if we do not call `free` on the objects, Godot's debug code to dump leaked objects attempts to dereference fields in the object that have already been destroyed. A proper fix is likely to add a callback to Godot to wipe those objects from the tables when the extension is terminated. But at least to get the test suite going, release our custom objects, rather than leaking them. --------- Co-authored-by: Roland Vigh <[email protected]> Co-authored-by: Gabor Koncz <[email protected]>
1 parent 087b260 commit 7e4c34c

File tree

18 files changed

+650
-218
lines changed

18 files changed

+650
-218
lines changed

Generator/Generator/ClassGen.swift

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ func generateVirtualProxy (_ p: Printer,
9393
if let arguments = method.arguments, arguments.count > 0 {
9494
p ("guard let args else { return }")
9595
}
96-
p ("let swiftObject = Unmanaged<\(cdef.name)>.fromOpaque(instance).takeUnretainedValue()")
96+
p ("let reference = Unmanaged<WrappedReference>.fromOpaque(instance).takeUnretainedValue()")
97+
p ("guard let swiftObject = reference.value as? \(cdef.name) else { return }")
9798

9899
var argCall = ""
99100
var argPrep = ""
@@ -114,17 +115,12 @@ func generateVirtualProxy (_ p: Printer,
114115
// This idiom guarantees that: if this is a known object, we surface this
115116
// object, but if it is not known, then we create the instance
116117
//
117-
argPrep += "let resolved_\(i) = args [\(i)]!.load (as: UnsafeRawPointer.self)\n"
118-
let handleResolver: String
119-
if hasSubclasses.contains(cdef.name) {
120-
// If the type we are bubbling up has subclasses, we want to create the most
121-
// derived type if possible, so we perform the longer lookup
122-
handleResolver = "lookupObject (nativeHandle: resolved_\(i))!"
118+
argPrep += "let resolved_\(i) = args [\(i)]!.load (as: UnsafeRawPointer?.self)\n"
119+
if isMethodArgumentOptional(className: cdef.name, method: methodName, arg: arg.name) {
120+
argCall += "resolved_\(i) == nil ? nil : lookupObject (nativeHandle: resolved_\(i)!, ownsRef: false) as? \(arg.type)"
123121
} else {
124-
// There are no subclasses, so we can create the object right away
125-
handleResolver = "\(arg.type) (nativeHandle: resolved_\(i))"
122+
argCall += "lookupObject (nativeHandle: resolved_\(i)!, ownsRef: false) as! \(arg.type)"
126123
}
127-
argCall += "lookupLiveObject (handleAddress: resolved_\(i)) as? \(arg.type) ?? \(handleResolver)"
128124
} else if let storage = builtinClassStorage [arg.type] {
129125
argCall += "\(mapTypeName (arg.type)) (content: args [\(i)]!.assumingMemoryBound (to: \(storage).self).pointee)"
130126
} else {
@@ -589,13 +585,9 @@ func processClass (cdef: JGodotExtensionAPIClass, outputDir: String?) async {
589585
p (typeDecl) {
590586
if isSingleton {
591587
p ("/// The shared instance of this class")
592-
p.staticVar(visibility: "public ", name: "shared", type: cdef.name) {
588+
p.staticVar(visibility: "public ", cached: true, name: "shared", type: cdef.name) {
593589
p ("return withUnsafePointer (to: &\(cdef.name).godotClassName.content)", arg: " ptr in") {
594-
if hasSubclasses.contains(cdef.name) {
595-
p ("lookupObject (nativeHandle: gi.global_get_singleton (ptr)!)!")
596-
} else {
597-
p ("\(cdef.name) (nativeHandle: gi.global_get_singleton (ptr)!)")
598-
}
590+
p ("lookupObject (nativeHandle: gi.global_get_singleton (ptr)!, ownsRef: false)!")
599591
}
600592
}
601593
}
@@ -609,8 +601,6 @@ func processClass (cdef: JGodotExtensionAPIClass, outputDir: String?) async {
609601
}
610602
p ("public required init(nativeHandle: UnsafeRawPointer)") {
611603
p ("super.init (nativeHandle: nativeHandle)")
612-
p ("reference()")
613-
p ("ownsHandle = true")
614604
}
615605
}
616606
var referencedMethods = Set<String>()

Generator/Generator/MethodGen.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -497,11 +497,12 @@ func generateMethod(_ p: Printer, method: MethodDefinition, className: String, c
497497
let inlineAttribute: String?
498498
let documentationVisibilityAttribute: String?
499499
if let methodHash = method.optionalHash {
500-
let staticVarVisibility = if bindName != "method_get_class" { "fileprivate " } else { "" }
500+
// get_class and unreference are also called by Wrapped
501+
let staticVarVisibility = if bindName != "method_get_class" && bindName != "method_unreference" { "fileprivate " } else { "" }
501502
assert (!method.isVirtual)
502503
switch generatedMethodKind {
503504
case .classMethod:
504-
p.staticVar(visibility: staticVarVisibility, name: bindName, type: "GDExtensionMethodBindPtr") {
505+
p.staticVar(visibility: staticVarVisibility, cached: true, name: bindName, type: "GDExtensionMethodBindPtr") {
505506
p ("let methodName = StringName(\"\(method.name)\")")
506507

507508
p ("return withUnsafePointer(to: &\(className).godotClassName.content)", arg: " classPtr in") {
@@ -511,7 +512,7 @@ func generateMethod(_ p: Printer, method: MethodDefinition, className: String, c
511512
}
512513
}
513514
case .utilityFunction:
514-
p.staticVar(visibility: staticVarVisibility, name: bindName, type: "GDExtensionPtrUtilityFunction") {
515+
p.staticVar(visibility: staticVarVisibility, cached: true, name: bindName, type: "GDExtensionPtrUtilityFunction") {
515516
p ("let methodName = StringName(\"\(method.name)\")")
516517
p ("return withUnsafePointer(to: &methodName.content)", arg: " ptr in") {
517518
p ("return gi.variant_get_ptr_utility_function(ptr, \(methodHash))!")
@@ -669,7 +670,7 @@ func generateMethod(_ p: Printer, method: MethodDefinition, className: String, c
669670
return "return Variant(takingOver: _result)"
670671
} else if frameworkType {
671672
//print ("OBJ RETURN: \(className) \(method.name)")
672-
return "guard let _result else { \(returnOptional ? "return nil" : "fatalError (\"Unexpected nil return from a method that should never return nil\")") } ; return lookupObject (nativeHandle: _result)!"
673+
return "guard let _result else { \(returnOptional ? "return nil" : "fatalError (\"Unexpected nil return from a method that should never return nil\")") } ; return lookupObject (nativeHandle: _result, ownsRef: true)\(returnOptional ? "" : "!")"
673674
} else if godotReturnType?.starts(with: "typedarray::") ?? false {
674675
let defaultInit = makeDefaultInit(godotType: godotReturnType!, initCollection: "content: _result")
675676
return "return \(defaultInit)"
@@ -746,6 +747,9 @@ func generateMethod(_ p: Printer, method: MethodDefinition, className: String, c
746747
}
747748

748749
p ("\(declarationTokens)(\(argumentsList))\(returnClause)") {
750+
if staticAttribute == nil {
751+
p("if handle == nil { Wrapped.attemptToUseObjectFreedByGodot() }")
752+
}
749753
if method.optionalHash == nil {
750754
if let godotReturnType {
751755
p(makeDefaultReturn(godotType: godotReturnType))

Generator/Generator/Printer.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ class Printer {
7070
}
7171

7272
// Prints a variable definition
73-
func staticVar(visibility: String = "", name: String, type: String, block: () -> ()) {
73+
func staticVar (visibility: String = "", cached: Bool = true, name: String, type: String, block: () -> ()) {
74+
if !cached {
75+
b ("\(visibility)static var \(name): \(type)", suffix: "", block: block)
76+
return
77+
}
7478
if generateResettableCache {
7579
p ("fileprivate static var _c_\(name): \(type)? = nil")
7680
p ("fileprivate static var _g_\(name): UInt16 = 0")

Sources/ExtensionApi/ApiJsonModel.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ public struct JGodotBuiltinClassConstant: Codable {
189189
public let name: String
190190
public let type: JGodotTypeEnum
191191
public let value: String
192-
public let description: String
192+
public let description: String?
193193

194194
public init(name: String, type: JGodotTypeEnum, value: String, description: String) {
195195
self.name = name
@@ -227,7 +227,7 @@ public struct JGodotBuiltinClassEnum: Codable {
227227
public struct JGodotValueElement: Codable {
228228
public let name: String
229229
public let value: Int
230-
public let description: String
230+
public let description: String?
231231

232232
public init(name: String, value: Int, description: String) {
233233
self.name = name
@@ -242,7 +242,7 @@ public struct JGodotBuiltinClassMethod: Codable {
242242
public let returnType: String?
243243
public let isVararg, isConst, isStatic: Bool
244244
public let hash: Int
245-
public let description: String
245+
public let description: String?
246246
public let arguments: [JGodotArgument]?
247247

248248
enum CodingKeys: String, CodingKey {
@@ -332,8 +332,8 @@ public struct JGodotExtensionAPIClass: Codable {
332332
public let isRefcounted, isInstantiable: Bool
333333
public let inherits: String?
334334
public let apiType: JGodotAPIType
335-
public let brief_description: String
336-
public let description: String
335+
public let brief_description: String?
336+
public let description: String?
337337
public let enums: [JGodotGlobalEnumElement]?
338338
public let methods: [JGodotClassMethod]?
339339
public let properties: [JGodotProperty]?
@@ -458,7 +458,7 @@ public struct JGodotProperty: Codable {
458458
public struct JGodotSignal: Codable {
459459
public let name: String
460460
public let arguments: [JGodotArgument]?
461-
public let description: String
461+
public let description: String?
462462

463463
public init(name: String, description: String, arguments: [JGodotArgument]?) {
464464
self.name = name

Sources/SwiftGodot/Core/ClassServices.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,9 @@ func bind_call (_ udata: UnsafeMutableRawPointer?,
255255
guard let classInstance else { return }
256256

257257
let finfo = udata.assumingMemoryBound(to: ClassInfo.FunctionInfo.self).pointee
258-
let object = Unmanaged<Object>.fromOpaque(classInstance).takeUnretainedValue()
258+
let ref = Unmanaged<WrappedReference>.fromOpaque(classInstance).takeUnretainedValue()
259+
guard let object = ref.value as? Object else { return }
260+
259261

260262
let ret = withArguments(pargs: variantArgs, argc: argc) { arguments in
261263
let bound = finfo.function(object)

Sources/SwiftGodot/Core/ObjectCollection.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public class ObjectCollection<Element: Object>: Collection, ExpressibleByArrayLi
128128
fatalError("Could not unwrap variant as object.")
129129
}
130130

131-
return lookupObject(nativeHandle: handle)
131+
return lookupObject(nativeHandle: handle, ownsRef: false)
132132
}
133133

134134
// If I make this optional, I am told I need to implement an internal _read method

Sources/SwiftGodot/Core/SignalProxy.swift

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,20 @@
1919
/// ```
2020
public class SignalProxy: Object {
2121
public static var proxyName = StringName("proxy")
22-
static var initClass: Bool = {
22+
23+
public static func initClass() {
2324
register(type: SignalProxy.self)
2425

2526
let s = ClassInfo<SignalProxy>(name: "SignalProxy")
2627

2728
s.registerMethod(name: SignalProxy.proxyName, flags: .default, returnValue: nil, arguments: [], function: SignalProxy.proxyFunc)
28-
return true
29-
}()
30-
29+
}
30+
3131
/// The code invoked when Godot invokes the `proxy` method on this object.
3232
public typealias Proxy = (borrowing Arguments) -> ()
3333
public var proxy: Proxy?
34-
35-
public required init() {
36-
let _ = SignalProxy.initClass
34+
35+
public required init () {
3736
super.init()
3837
}
3938

Sources/SwiftGodot/Core/VariantStorable.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,7 @@ extension VariantStorable {
6969

7070
extension String: VariantStorable {
7171
public func toVariantRepresentable() -> GString {
72-
let r = GString()
73-
gi.string_new_with_utf8_chars (&r.content, self)
74-
return r
72+
return GString(self)
7573
}
7674

7775
public init?(_ variant: Variant) {

0 commit comments

Comments
 (0)