Fixed some wrapper lifetime and leak issues found in PlayDate bindings target.
This commit is contained in:
@@ -118,10 +118,19 @@ extension Graphics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The player used for the stream's audio track. Owned by the stream.
|
/// 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? {
|
public var filePlayer: Sound.FilePlayer? {
|
||||||
guard let player = streamAPI.pointee.getFilePlayer.unsafelyUnwrapped(pointer) else { return nil }
|
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.
|
/// The player used for the stream's video track. Owned by the stream.
|
||||||
public var videoPlayer: VideoPlayer? {
|
public var videoPlayer: VideoPlayer? {
|
||||||
|
|||||||
@@ -116,6 +116,9 @@ extension JSON {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
guard ok else {
|
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)
|
throw decodeError(context)
|
||||||
}
|
}
|
||||||
return convert(outval)
|
return convert(outval)
|
||||||
@@ -145,6 +148,9 @@ extension JSON {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
guard ok else {
|
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)
|
throw decodeError(context)
|
||||||
}
|
}
|
||||||
return convert(outval)
|
return convert(outval)
|
||||||
|
|||||||
@@ -70,21 +70,21 @@ extension Network {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Turns the wifi radio on or off. The completion receives `nil` on
|
/// 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) {
|
public static func setEnabled(_ enabled: Bool, completion: ((NetError?) -> Void)? = nil) {
|
||||||
setEnabledCompletion = completion
|
if let completion {
|
||||||
if completion != nil {
|
setEnabledCompletions.append(completion)
|
||||||
networkAPI.pointee.setEnabled.unsafelyUnwrapped(enabled, { error in
|
networkAPI.pointee.setEnabled.unsafelyUnwrapped(enabled, { error in
|
||||||
let completion = Network.setEnabledCompletion
|
guard !Network.setEnabledCompletions.isEmpty else { return }
|
||||||
Network.setEnabledCompletion = nil
|
let completion = Network.setEnabledCompletions.removeFirst()
|
||||||
completion?(Network.optionalError(error))
|
completion(Network.optionalError(error))
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
networkAPI.pointee.setEnabled.unsafelyUnwrapped(enabled, nil)
|
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.
|
/// Requests permission to connect to `server`. Shared by HTTP and TCP.
|
||||||
fileprivate static func requestAccess(
|
fileprivate static func requestAccess(
|
||||||
|
|||||||
@@ -207,11 +207,13 @@ extension Sound {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The default channel, which sources are added to unless otherwise
|
/// The default channel, which sources are added to unless otherwise
|
||||||
/// specified.
|
/// specified. A single shared wrapper, so resources retained through
|
||||||
public static var `default`: Channel {
|
/// 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,
|
Channel(pointer: snd.pointee.getDefaultChannel.unsafelyUnwrapped().unsafelyUnwrapped,
|
||||||
isOwned: false)
|
isOwned: false)
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated(unsafe) private static var addedChannels: [Channel] = []
|
nonisolated(unsafe) private static var addedChannels: [Channel] = []
|
||||||
|
|
||||||
@@ -246,8 +248,12 @@ extension Sound {
|
|||||||
@discardableResult
|
@discardableResult
|
||||||
public func removeSource(_ source: Source) -> Bool {
|
public func removeSource(_ source: Source) -> Bool {
|
||||||
let removed = Channel.api.pointee.removeSource.unsafelyUnwrapped(pointer, source.pointer) != 0
|
let removed = Channel.api.pointee.removeSource.unsafelyUnwrapped(pointer, source.pointer) != 0
|
||||||
|
// 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 }
|
retainedSources.removeAll { $0 === source }
|
||||||
CallbackSource.release(source)
|
CallbackSource.release(source)
|
||||||
|
}
|
||||||
return removed
|
return removed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,12 +328,21 @@ extension Sound {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The channel's output as a source, for feeding into another channel.
|
/// 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? {
|
public var outputAsSource: Source? {
|
||||||
guard let source = Channel.api.pointee.getOutputAsSource.unsafelyUnwrapped(pointer) else {
|
guard let source = Channel.api.pointee.getOutputAsSource.unsafelyUnwrapped(pointer) else {
|
||||||
return nil
|
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?) {
|
private func retain(_ modulator: SignalValue?) {
|
||||||
if let modulator, !retainedModulators.contains(where: { $0 === modulator }) {
|
if let modulator, !retainedModulators.contains(where: { $0 === modulator }) {
|
||||||
|
|||||||
@@ -55,8 +55,13 @@ public final class Sprite {
|
|||||||
init(pointer: OpaquePointer, isOwned: Bool) {
|
init(pointer: OpaquePointer, isOwned: Bool) {
|
||||||
self.pointer = pointer
|
self.pointer = pointer
|
||||||
self.isOwned = isOwned
|
self.isOwned = isOwned
|
||||||
|
// 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())
|
spriteAPI.pointee.setUserdata.unsafelyUnwrapped(pointer, Unmanaged.passUnretained(self).toOpaque())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Allocates a new sprite.
|
/// Allocates a new sprite.
|
||||||
public convenience init() {
|
public convenience init() {
|
||||||
|
|||||||
@@ -71,16 +71,17 @@ extension String {
|
|||||||
#if hasFeature(Embedded) && !os(macOS)
|
#if hasFeature(Embedded) && !os(macOS)
|
||||||
/// The Embedded Swift runtime allocates through `posix_memalign(3)`, which
|
/// The Embedded Swift runtime allocates through `posix_memalign(3)`, which
|
||||||
/// the Playdate device C library does not provide. Memory comes from
|
/// the Playdate device C library does not provide. Memory comes from
|
||||||
/// `malloc`, which the SDK's setup code routes to the firmware allocator;
|
/// `malloc`, which the SDK's setup code routes to the firmware allocator.
|
||||||
/// that allocator hands out sufficiently aligned blocks, so alignment is
|
/// The pointer is later released with plain `free`, so it cannot be offset
|
||||||
/// only asserted, not adjusted.
|
/// to adjust alignment; the firmware allocator's natural alignment has to
|
||||||
|
/// satisfy the request, which the precondition asserts.
|
||||||
@_cdecl("posix_memalign")
|
@_cdecl("posix_memalign")
|
||||||
public func posix_memalign(
|
public func posix_memalign(
|
||||||
_ memptr: UnsafeMutablePointer<UnsafeMutableRawPointer?>,
|
_ memptr: UnsafeMutablePointer<UnsafeMutableRawPointer?>,
|
||||||
_ alignment: Int,
|
_ alignment: Int,
|
||||||
_ size: Int
|
_ size: Int
|
||||||
) -> CInt {
|
) -> CInt {
|
||||||
guard let allocation = malloc(size + alignment - 1) else { fatalError() }
|
guard let allocation = malloc(size) else { fatalError() }
|
||||||
precondition(Int(bitPattern: allocation) % alignment == 0)
|
precondition(Int(bitPattern: allocation) % alignment == 0)
|
||||||
memptr.pointee = allocation
|
memptr.pointee = allocation
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
Reference in New Issue
Block a user