Fixed some wrapper lifetime and leak issues found in PlayDate bindings target.
This commit is contained in:
@@ -118,11 +118,20 @@ extension Graphics {
|
||||
}
|
||||
|
||||
/// The player used for the stream's audio track. Owned by the stream.
|
||||
/// The same wrapper is returned on every access, so callbacks
|
||||
/// registered on it stay valid for the stream's lifetime.
|
||||
public var filePlayer: Sound.FilePlayer? {
|
||||
guard let player = streamAPI.pointee.getFilePlayer.unsafelyUnwrapped(pointer) else { return nil }
|
||||
return Sound.FilePlayer(pointer: player, isOwned: false)
|
||||
if let cached = cachedFilePlayer, cached.pointer == player {
|
||||
return cached
|
||||
}
|
||||
let wrapper = Sound.FilePlayer(pointer: player, isOwned: false)
|
||||
cachedFilePlayer = wrapper
|
||||
return wrapper
|
||||
}
|
||||
|
||||
private var cachedFilePlayer: Sound.FilePlayer?
|
||||
|
||||
/// The player used for the stream's video track. Owned by the stream.
|
||||
public var videoPlayer: VideoPlayer? {
|
||||
guard let player = streamAPI.pointee.getVideoPlayer.unsafelyUnwrapped(pointer) else { return nil }
|
||||
|
||||
@@ -116,6 +116,9 @@ extension JSON {
|
||||
}
|
||||
}
|
||||
guard ok else {
|
||||
// A completed root container may already have been written to
|
||||
// outval before the failure; consume it so its box is not leaked.
|
||||
_ = convert(outval)
|
||||
throw decodeError(context)
|
||||
}
|
||||
return convert(outval)
|
||||
@@ -145,6 +148,9 @@ extension JSON {
|
||||
}
|
||||
}
|
||||
guard ok else {
|
||||
// A completed root container may already have been written to
|
||||
// outval before the failure; consume it so its box is not leaked.
|
||||
_ = convert(outval)
|
||||
throw decodeError(context)
|
||||
}
|
||||
return convert(outval)
|
||||
|
||||
@@ -70,21 +70,21 @@ extension Network {
|
||||
}
|
||||
|
||||
/// Turns the wifi radio on or off. The completion receives `nil` on
|
||||
/// success.
|
||||
/// success. Completions of overlapping calls are delivered in call order.
|
||||
public static func setEnabled(_ enabled: Bool, completion: ((NetError?) -> Void)? = nil) {
|
||||
setEnabledCompletion = completion
|
||||
if completion != nil {
|
||||
if let completion {
|
||||
setEnabledCompletions.append(completion)
|
||||
networkAPI.pointee.setEnabled.unsafelyUnwrapped(enabled, { error in
|
||||
let completion = Network.setEnabledCompletion
|
||||
Network.setEnabledCompletion = nil
|
||||
completion?(Network.optionalError(error))
|
||||
guard !Network.setEnabledCompletions.isEmpty else { return }
|
||||
let completion = Network.setEnabledCompletions.removeFirst()
|
||||
completion(Network.optionalError(error))
|
||||
})
|
||||
} else {
|
||||
networkAPI.pointee.setEnabled.unsafelyUnwrapped(enabled, nil)
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated(unsafe) private static var setEnabledCompletion: ((NetError?) -> Void)?
|
||||
nonisolated(unsafe) private static var setEnabledCompletions: [(NetError?) -> Void] = []
|
||||
|
||||
/// Requests permission to connect to `server`. Shared by HTTP and TCP.
|
||||
fileprivate static func requestAccess(
|
||||
|
||||
@@ -207,11 +207,13 @@ extension Sound {
|
||||
}
|
||||
|
||||
/// The default channel, which sources are added to unless otherwise
|
||||
/// specified.
|
||||
public static var `default`: Channel {
|
||||
/// specified. A single shared wrapper, so resources retained through
|
||||
/// it (sources, effects, modulators) stay alive.
|
||||
public static var `default`: Channel { defaultChannel }
|
||||
|
||||
nonisolated(unsafe) private static let defaultChannel =
|
||||
Channel(pointer: snd.pointee.getDefaultChannel.unsafelyUnwrapped().unsafelyUnwrapped,
|
||||
isOwned: false)
|
||||
}
|
||||
|
||||
nonisolated(unsafe) private static var addedChannels: [Channel] = []
|
||||
|
||||
@@ -246,8 +248,12 @@ extension Sound {
|
||||
@discardableResult
|
||||
public func removeSource(_ source: Source) -> Bool {
|
||||
let removed = Channel.api.pointee.removeSource.unsafelyUnwrapped(pointer, source.pointer) != 0
|
||||
retainedSources.removeAll { $0 === source }
|
||||
CallbackSource.release(source)
|
||||
// Only drop the retentions if the source was actually on this
|
||||
// channel; otherwise another channel may still be pulling it.
|
||||
if removed {
|
||||
retainedSources.removeAll { $0 === source }
|
||||
CallbackSource.release(source)
|
||||
}
|
||||
return removed
|
||||
}
|
||||
|
||||
@@ -322,13 +328,22 @@ extension Sound {
|
||||
}
|
||||
|
||||
/// The channel's output as a source, for feeding into another channel.
|
||||
/// The same wrapper is returned on every access, so callbacks
|
||||
/// registered on it stay valid for the channel's lifetime.
|
||||
public var outputAsSource: Source? {
|
||||
guard let source = Channel.api.pointee.getOutputAsSource.unsafelyUnwrapped(pointer) else {
|
||||
return nil
|
||||
}
|
||||
return Source(pointer: source, isOwned: false)
|
||||
if let cached = cachedOutputSource, cached.pointer == source {
|
||||
return cached
|
||||
}
|
||||
let wrapper = Source(pointer: source, isOwned: false)
|
||||
cachedOutputSource = wrapper
|
||||
return wrapper
|
||||
}
|
||||
|
||||
private var cachedOutputSource: Source?
|
||||
|
||||
private func retain(_ modulator: SignalValue?) {
|
||||
if let modulator, !retainedModulators.contains(where: { $0 === modulator }) {
|
||||
retainedModulators.append(modulator)
|
||||
|
||||
@@ -55,7 +55,12 @@ public final class Sprite {
|
||||
init(pointer: OpaquePointer, isOwned: Bool) {
|
||||
self.pointer = pointer
|
||||
self.isOwned = isOwned
|
||||
spriteAPI.pointee.setUserdata.unsafelyUnwrapped(pointer, Unmanaged.passUnretained(self).toOpaque())
|
||||
// Transient wrappers for sprites created outside the binding must not
|
||||
// store a back-reference: it would dangle once the wrapper is
|
||||
// deallocated, and only owned wrappers clear it in `deinit`.
|
||||
if isOwned {
|
||||
spriteAPI.pointee.setUserdata.unsafelyUnwrapped(pointer, Unmanaged.passUnretained(self).toOpaque())
|
||||
}
|
||||
}
|
||||
|
||||
/// Allocates a new sprite.
|
||||
|
||||
@@ -71,16 +71,17 @@ extension String {
|
||||
#if hasFeature(Embedded) && !os(macOS)
|
||||
/// The Embedded Swift runtime allocates through `posix_memalign(3)`, which
|
||||
/// the Playdate device C library does not provide. Memory comes from
|
||||
/// `malloc`, which the SDK's setup code routes to the firmware allocator;
|
||||
/// that allocator hands out sufficiently aligned blocks, so alignment is
|
||||
/// only asserted, not adjusted.
|
||||
/// `malloc`, which the SDK's setup code routes to the firmware allocator.
|
||||
/// The pointer is later released with plain `free`, so it cannot be offset
|
||||
/// to adjust alignment; the firmware allocator's natural alignment has to
|
||||
/// satisfy the request, which the precondition asserts.
|
||||
@_cdecl("posix_memalign")
|
||||
public func posix_memalign(
|
||||
_ memptr: UnsafeMutablePointer<UnsafeMutableRawPointer?>,
|
||||
_ alignment: Int,
|
||||
_ size: Int
|
||||
) -> CInt {
|
||||
guard let allocation = malloc(size + alignment - 1) else { fatalError() }
|
||||
guard let allocation = malloc(size) else { fatalError() }
|
||||
precondition(Int(bitPattern: allocation) % alignment == 0)
|
||||
memptr.pointee = allocation
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user