Skip to content

Instantly share code, notes, and snippets.

@migueldeicaza
Created December 2, 2023 05:32
Show Gist options
  • Save migueldeicaza/1884e40a485ade88eff8b4a4c762e0d9 to your computer and use it in GitHub Desktop.
Save migueldeicaza/1884e40a485ade88eff8b4a4c762e0d9 to your computer and use it in GitHub Desktop.
diff --git a/Package.swift b/Package.swift
index cfc1412..d0a2ff0 100644
--- a/Package.swift
+++ b/Package.swift
@@ -117,8 +117,8 @@ targets.append(contentsOf: [
// Godot runtime as a library
.binaryTarget(
name: "libgodot_tests",
- url: "https://github.com/migueldeicaza/SwiftGodotKit/releases/download/v1.0.1/libgodot.xcframework.zip",
- checksum: "bb6ec0946311a71f1eba7ad393c0adf7b8f34a2389d8234ff500b2764b0c6ba5"
+ url: "https://github.com/migueldeicaza/SwiftGodotKit/releases/download/v4.1.99/libgodot.xcframework.zip",
+ checksum: "c8ddf62be6c00eacc36bd2dafe8d424c0b374833efe80546f6ee76bd27cee84e"
),
// Base functionality for Godot runtime dependant tests
diff --git a/Sources/SwiftGodot/Core/NIOLock.swift b/Sources/SwiftGodot/Core/NIOLock.swift
index 9ef1803..82ada61 100644
--- a/Sources/SwiftGodot/Core/NIOLock.swift
+++ b/Sources/SwiftGodot/Core/NIOLock.swift
@@ -1,8 +1,253 @@
+//===----------------------------------------------------------------------===//
//
-// File.swift
-//
+// This source file is part of the SwiftNIO open source project
//
-// Created by Miguel de Icaza on 12/2/23.
+// Copyright (c) 2017-2022 Apple Inc. and the SwiftNIO project authors
+// Licensed under Apache License v2.0
//
+// See LICENSE.txt for license information
+// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
+//
+// SPDX-License-Identifier: Apache-2.0
+//
+//===----------------------------------------------------------------------===//
+
+#if canImport(Darwin)
+import Darwin
+#elseif os(Windows)
+import ucrt
+import WinSDK
+#elseif canImport(Glibc)
+import Glibc
+#elseif canImport(Musl)
+import Musl
+#else
+#error("The concurrency NIOLock module was unable to identify your C library.")
+#endif
+
+#if os(Windows)
+@usableFromInline
+typealias LockPrimitive = SRWLOCK
+#else
+@usableFromInline
+typealias LockPrimitive = pthread_mutex_t
+#endif
+
+@usableFromInline
+enum LockOperations { }
+
+extension LockOperations {
+ @inlinable
+ static func create(_ mutex: UnsafeMutablePointer<LockPrimitive>) {
+ mutex.assertValidAlignment()
+
+#if os(Windows)
+ InitializeSRWLock(mutex)
+#else
+ var attr = pthread_mutexattr_t()
+ pthread_mutexattr_init(&attr)
+// debugOnly {
+// pthread_mutexattr_settype(&attr, .init(PTHREAD_MUTEX_ERRORCHECK))
+// }
+
+ let err = pthread_mutex_init(mutex, &attr)
+ precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
+#endif
+ }
+
+ @inlinable
+ static func destroy(_ mutex: UnsafeMutablePointer<LockPrimitive>) {
+ mutex.assertValidAlignment()
+
+#if os(Windows)
+ // SRWLOCK does not need to be free'd
+#else
+ let err = pthread_mutex_destroy(mutex)
+ precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
+#endif
+ }
+
+ @inlinable
+ static func lock(_ mutex: UnsafeMutablePointer<LockPrimitive>) {
+ mutex.assertValidAlignment()
+
+#if os(Windows)
+ AcquireSRWLockExclusive(mutex)
+#else
+ let err = pthread_mutex_lock(mutex)
+ precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
+#endif
+ }
+
+ @inlinable
+ static func unlock(_ mutex: UnsafeMutablePointer<LockPrimitive>) {
+ mutex.assertValidAlignment()
+
+#if os(Windows)
+ ReleaseSRWLockExclusive(mutex)
+#else
+ let err = pthread_mutex_unlock(mutex)
+ precondition(err == 0, "\(#function) failed in pthread_mutex with error \(err)")
+#endif
+ }
+}
+
+// Tail allocate both the mutex and a generic value using ManagedBuffer.
+// Both the header pointer and the elements pointer are stable for
+// the class's entire lifetime.
+//
+// However, for safety reasons, we elect to place the lock in the "elements"
+// section of the buffer instead of the head. The reasoning here is subtle,
+// so buckle in.
+//
+// _As a practical matter_, the implementation of ManagedBuffer ensures that
+// the pointer to the header is stable across the lifetime of the class, and so
+// each time you call `withUnsafeMutablePointers` or `withUnsafeMutablePointerToHeader`
+// the value of the header pointer will be the same. This is because ManagedBuffer uses
+// `Builtin.addressOf` to load the value of the header, and that does ~magic~ to ensure
+// that it does not invoke any weird Swift accessors that might copy the value.
+//
+// _However_, the header is also available via the `.header` field on the ManagedBuffer.
+// This presents a problem! The reason there's an issue is that `Builtin.addressOf` and friends
+// do not interact with Swift's exclusivity model. That is, the various `with` functions do not
+// conceptually trigger a mutating access to `.header`. For elements this isn't a concern because
+// there's literally no other way to perform the access, but for `.header` it's entirely possible
+// to accidentally recursively read it.
+//
+// Our implementation is free from these issues, so we don't _really_ need to worry about it.
+// However, out of an abundance of caution, we store the Value in the header, and the LockPrimitive
+// in the trailing elements. We still don't use `.header`, but it's better to be safe than sorry,
+// and future maintainers will be happier that we were cautious.
+//
+// See also: https://github.com/apple/swift/pull/40000
+@usableFromInline
+final class LockStorage<Value>: ManagedBuffer<Value, LockPrimitive> {
+
+ @inlinable
+ static func create(value: Value) -> Self {
+ let buffer = Self.create(minimumCapacity: 1) { _ in
+ return value
+ }
+ let storage = unsafeDowncast(buffer, to: Self.self)
+
+ storage.withUnsafeMutablePointers { _, lockPtr in
+ LockOperations.create(lockPtr)
+ }
+
+ return storage
+ }
+
+ @inlinable
+ func lock() {
+ self.withUnsafeMutablePointerToElements { lockPtr in
+ LockOperations.lock(lockPtr)
+ }
+ }
+
+ @inlinable
+ func unlock() {
+ self.withUnsafeMutablePointerToElements { lockPtr in
+ LockOperations.unlock(lockPtr)
+ }
+ }
+
+ @inlinable
+ deinit {
+ self.withUnsafeMutablePointerToElements { lockPtr in
+ LockOperations.destroy(lockPtr)
+ }
+ }
+
+ @inlinable
+ func withLockPrimitive<T>(_ body: (UnsafeMutablePointer<LockPrimitive>) throws -> T) rethrows -> T {
+ try self.withUnsafeMutablePointerToElements { lockPtr in
+ return try body(lockPtr)
+ }
+ }
+
+ @inlinable
+ func withLockedValue<T>(_ mutate: (inout Value) throws -> T) rethrows -> T {
+ try self.withUnsafeMutablePointers { valuePtr, lockPtr in
+ LockOperations.lock(lockPtr)
+ defer { LockOperations.unlock(lockPtr) }
+ return try mutate(&valuePtr.pointee)
+ }
+ }
+}
+
+extension LockStorage: @unchecked Sendable { }
+
+/// A threading lock based on `libpthread` instead of `libdispatch`.
+///
+/// - note: ``NIOLock`` has reference semantics.
+///
+/// This object provides a lock on top of a single `pthread_mutex_t`. This kind
+/// of lock is safe to use with `libpthread`-based threading models, such as the
+/// one used by NIO. On Windows, the lock is based on the substantially similar
+/// `SRWLOCK` type.
+public struct NIOLock {
+ @usableFromInline
+ internal let _storage: LockStorage<Void>
+
+ /// Create a new lock.
+ @inlinable
+ public init() {
+ self._storage = .create(value: ())
+ }
+
+ /// Acquire the lock.
+ ///
+ /// Whenever possible, consider using `withLock` instead of this method and
+ /// `unlock`, to simplify lock handling.
+ @inlinable
+ public func lock() {
+ self._storage.lock()
+ }
+
+ /// Release the lock.
+ ///
+ /// Whenever possible, consider using `withLock` instead of this method and
+ /// `lock`, to simplify lock handling.
+ @inlinable
+ public func unlock() {
+ self._storage.unlock()
+ }
+
+ @inlinable
+ internal func withLockPrimitive<T>(_ body: (UnsafeMutablePointer<LockPrimitive>) throws -> T) rethrows -> T {
+ return try self._storage.withLockPrimitive(body)
+ }
+}
+
+extension NIOLock {
+ /// Acquire the lock for the duration of the given block.
+ ///
+ /// This convenience method should be preferred to `lock` and `unlock` in
+ /// most situations, as it ensures that the lock will be released regardless
+ /// of how `body` exits.
+ ///
+ /// - Parameter body: The block to execute while holding the lock.
+ /// - Returns: The value returned by the block.
+ @inlinable
+ public func withLock<T>(_ body: () throws -> T) rethrows -> T {
+ self.lock()
+ defer {
+ self.unlock()
+ }
+ return try body()
+ }
+
+ @inlinable
+ public func withLockVoid(_ body: () throws -> Void) rethrows -> Void {
+ try self.withLock(body)
+ }
+}
+
+extension NIOLock: Sendable {}
-import Foundation
+extension UnsafeMutablePointer {
+ @inlinable
+ func assertValidAlignment() {
+ assert(UInt(bitPattern: self) % UInt(MemoryLayout<Pointee>.alignment) == 0)
+ }
+}
diff --git a/Sources/SwiftGodot/Core/Wrapped.swift b/Sources/SwiftGodot/Core/Wrapped.swift
index 1d1b6cd..fb8a9c0 100644
--- a/Sources/SwiftGodot/Core/Wrapped.swift
+++ b/Sources/SwiftGodot/Core/Wrapped.swift
@@ -136,9 +136,10 @@ open class Wrapped: Equatable, Identifiable, Hashable {
/// when a class is initialized with the empty constructor - this means that
/// subclasses will have a different name than the subclass.
public required init () {
- guard let godotObject = gi.classdb_construct_object (&Self.godotClassName.content) else {
+ guard let godotObject = bindingObject ?? gi.classdb_construct_object (&Self.godotClassName.content) else {
fatalError("SWIFT: It was not possible to construct a \(Self.godotClassName.description)")
}
+ bindingObject = nil
handle = UnsafeRawPointer(godotObject)
bindGodotInstance(instance: self)
@@ -178,10 +179,15 @@ func bindGodotInstance(instance: some Wrapped) {
var callbacks: GDExtensionInstanceBindingCallbacks
if frameworkType {
callbacks = Wrapped.frameworkTypeBindingCallback
- liveFrameworkObjects [handle] = instance
} else {
callbacks = Wrapped.userTypeBindingCallback
- liveSubtypedObjects [handle] = instance
+ }
+ tableLock.withLockVoid {
+ if frameworkType {
+ liveFrameworkObjects [handle] = instance
+ } else {
+ liveSubtypedObjects [handle] = instance
+ }
}
gi.object_set_instance_binding(UnsafeMutableRawPointer (mutating: handle), token, retain.toOpaque(), &callbacks)
@@ -197,6 +203,7 @@ func register<T:Wrapped> (type name: StringName, parent: StringName, type: T.Typ
return type.getVirtualDispatcher(name: StringName (fromPtr: name))
}
+ #if legacy
var info = GDExtensionClassCreationInfo ()
info.create_instance_func = createFunc(_:)
info.free_instance_func = freeFunc(_:_:)
@@ -211,6 +218,24 @@ func register<T:Wrapped> (type name: StringName, parent: StringName, type: T.Typ
gi.classdb_register_extension_class (library, namePtr, parentPtr, &info)
}
}
+ #else
+ var info = GDExtensionClassCreationInfo2 ()
+ info.create_instance_func = createFunc(_:)
+ info.free_instance_func = freeFunc(_:_:)
+ info.get_virtual_func = getVirtual
+ info.notification_func = notificationFunc2
+ info.recreate_instance_func = recreateFunc
+ info.is_exposed = 1
+
+ let retained = Unmanaged<AnyObject>.passRetained(type as AnyObject)
+ info.class_userdata = retained.toOpaque()
+
+ withUnsafePointer(to: &name.content) { namePtr in
+ withUnsafePointer(to: &parent.content) { parentPtr in
+ gi.classdb_register_extension_class2 (library, namePtr, parentPtr, &info)
+ }
+ }
+ #endif
}
/// Registers the user-type specified with the Godot system, and allows it to
@@ -229,8 +254,14 @@ public func register<T:Wrapped> (type: T.Type) {
/// Currently contains all instantiated objects, but might want to separate those
/// (or find a way of easily telling appart) framework objects from user subtypes
-var liveFrameworkObjects: [UnsafeRawPointer:Wrapped] = [:]
-var liveSubtypedObjects: [UnsafeRawPointer:Wrapped] = [:]
+fileprivate var liveFrameworkObjects: [UnsafeRawPointer:Wrapped] = [:]
+fileprivate var liveSubtypedObjects: [UnsafeRawPointer:Wrapped] = [:]
+
+// Lock for accessing the above
+var tableLock = NIOLock()
+
+// If not-nil, we are in the process of serially re-creating objects from Godot
+fileprivate var bindingObject: UnsafeMutableRawPointer? = nil
///
/// Looks into the liveSubtypedObjects table if we have an object registered for it,
@@ -239,7 +270,9 @@ var liveSubtypedObjects: [UnsafeRawPointer:Wrapped] = [:]
/// The idioms is that we only need to look up subtyped objects, because those
/// are the only ones that would keep state
func lookupLiveObject (handleAddress: UnsafeRawPointer) -> Wrapped? {
- return liveSubtypedObjects [handleAddress]
+ tableLock.withLock {
+ return liveSubtypedObjects [handleAddress]
+ }
}
///
@@ -249,15 +282,19 @@ func lookupLiveObject (handleAddress: UnsafeRawPointer) -> Wrapped? {
/// We are surfacing this, so that when we recreate an object resurfaced in a collection
/// we do not get the base type, but the most derived one
func lookupFrameworkObject (handleAddress: UnsafeRawPointer) -> Wrapped? {
- return liveFrameworkObjects [handleAddress]
+ tableLock.withLock {
+ return liveFrameworkObjects [handleAddress]
+ }
}
func objectFromHandle (nativeHandle: UnsafeRawPointer) -> Wrapped? {
- if let o = (liveFrameworkObjects [nativeHandle] ?? liveSubtypedObjects [nativeHandle]) {
- return o
+ tableLock.withLock {
+ if let o = (liveFrameworkObjects [nativeHandle] ?? liveSubtypedObjects [nativeHandle]) {
+ return o
+ }
+
+ return nil
}
-
- return nil
}
func lookupObject<T:GodotObject> (nativeHandle: UnsafeRawPointer) -> T? {
@@ -293,6 +330,23 @@ func createFunc (_ userData: UnsafeMutableRawPointer?) -> UnsafeMutableRawPointe
return UnsafeMutableRawPointer (mutating: o.handle)
}
+func recreateFunc (_ userData: UnsafeMutableRawPointer?, godotObjecthandle: UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer? {
+ print ("SWIFT: Recreate object userData:\(String(describing: userData))")
+ guard let userData else {
+ print ("Got a nil userData")
+ return nil
+ }
+ let typeAny = Unmanaged<AnyObject>.fromOpaque(userData).takeUnretainedValue()
+ guard let type = typeAny as? Wrapped.Type else {
+ print ("SWIFT: The wrapped value did not contain a type: \(typeAny)")
+ return nil
+ }
+ bindingObject = godotObjecthandle
+ let o = type.init ()
+ bindingObject = nil
+ return UnsafeMutableRawPointer (mutating: o.handle)
+}
+
//
// This is invoked to release any Subtyped objects we created
//
@@ -308,11 +362,13 @@ func freeFunc (_ userData: UnsafeMutableRawPointer?, _ objectHandle: UnsafeMutab
// #endif
if let key = objectHandle {
let original = Unmanaged<Wrapped>.fromOpaque(key).takeRetainedValue()
- let removed = liveSubtypedObjects.removeValue(forKey: original.handle)
- if removed == nil {
- print ("SWIFT ERROR: attempt to release object we were not aware of: \(original) \(key)")
- } else {
- print ("SWIFT: Removed object from our live SubType list (type was: \(original.self)")
+ tableLock.withLockVoid {
+ let removed = liveSubtypedObjects.removeValue(forKey: original.handle)
+ if removed == nil {
+ print ("SWIFT ERROR: attempt to release object we were not aware of: \(original) \(key)")
+ } else {
+ print ("SWIFT: Removed object from our live SubType list (type was: \(original.self)")
+ }
}
}
}
@@ -321,6 +377,10 @@ func notificationFunc (ptr: UnsafeMutableRawPointer?, code: Int32) {
//print ("SWIFT: Notification \(code) on \(ptr)")
}
+func notificationFunc2 (ptr: UnsafeMutableRawPointer?, code: Int32, reversed: UInt8) {
+ //print ("SWIFT: Notification \(code) on \(ptr)")
+}
+
func userTypeBindingCreate (_ token: UnsafeMutableRawPointer?, _ instance: UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer? {
// Godot-cpp does nothing for user types
//print ("SWIFT: instanceBindingCreate")
@@ -350,10 +410,12 @@ func frameworkTypeBindingCreate (_ token: UnsafeMutableRawPointer?, _ instance:
func frameworkTypeBindingFree (_ token: UnsafeMutableRawPointer?, _ instance: UnsafeMutableRawPointer?, _ binding: UnsafeMutableRawPointer?) {
print ("SWIFT: frameworkBindingFree instance=\(String(describing: instance)) binding=\(String(describing: binding)) token=\(String(describing: token))")
if let key = instance {
- if let removed = liveFrameworkObjects.removeValue(forKey: key) {
- print ("SWIFT: Removed from our live Objects with key \(key), removed: \(removed)")
- } else {
- print ("SWIFT ERROR: attempt to release framework object we were not aware of: \(String(describing: instance))")
+ tableLock.withLockVoid {
+ if let removed = liveFrameworkObjects.removeValue(forKey: key) {
+ print ("SWIFT: Removed from our live Objects with key \(key), removed: \(removed)")
+ } else {
+ print ("SWIFT ERROR: attempt to release framework object we were not aware of: \(String(describing: instance))")
+ }
}
}
if let binding {
diff --git a/Sources/SwiftGodot/EntryPoint.swift b/Sources/SwiftGodot/EntryPoint.swift
index 19599b8..0501a5d 100644
--- a/Sources/SwiftGodot/EntryPoint.swift
+++ b/Sources/SwiftGodot/EntryPoint.swift
@@ -74,6 +74,7 @@ struct GodotInterface {
let classdb_construct_object: GDExtensionInterfaceClassdbConstructObject
let classdb_get_method_bind: GDExtensionInterfaceClassdbGetMethodBind
let classdb_register_extension_class: GDExtensionInterfaceClassdbRegisterExtensionClass
+ let classdb_register_extension_class2: GDExtensionInterfaceClassdbRegisterExtensionClass2
let classdb_register_extension_class_signal: GDExtensionInterfaceClassdbRegisterExtensionClassSignal
let classdb_register_extension_class_method: GDExtensionInterfaceClassdbRegisterExtensionClassMethod
let classdb_register_extension_class_property: GDExtensionInterfaceClassdbRegisterExtensionClassProperty
@@ -164,6 +165,7 @@ func loadGodotInterface (_ godotGetProcAddrPtr: GDExtensionInterfaceGetProcAddre
classdb_construct_object: load ("classdb_construct_object"),
classdb_get_method_bind: load ("classdb_get_method_bind"),
classdb_register_extension_class: load ("classdb_register_extension_class"),
+ classdb_register_extension_class2: load ("classdb_register_extension_class2"),
classdb_register_extension_class_signal: load ("classdb_register_extension_class_signal"),
classdb_register_extension_class_method: load ("classdb_register_extension_class_method"),
classdb_register_extension_class_property: load ("classdb_register_extension_class_property"),
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment