Made File.Handle a non-copyable struct in the library to adopt Swift 6.4.
This commit is contained in:
@@ -1,11 +1,13 @@
|
|||||||
internal import CPlaydate
|
internal import CPlaydate
|
||||||
|
|
||||||
extension File {
|
extension File {
|
||||||
/// An open file. Wraps `SDFile`. The file is closed on deinit if it has
|
/// An open file. Wraps `SDFile`. The file is closed when the handle goes
|
||||||
/// not been closed explicitly.
|
/// out of scope, unless it was closed explicitly with `close()`.
|
||||||
public final class Handle {
|
///
|
||||||
|
/// The handle is non-copyable: it has a single owner, so it cannot be
|
||||||
|
/// used after `close()` and no heap allocation backs it.
|
||||||
|
public struct Handle: ~Copyable {
|
||||||
let pointer: UnsafeMutableRawPointer
|
let pointer: UnsafeMutableRawPointer
|
||||||
private var isClosed = false
|
|
||||||
|
|
||||||
/// Opens the file at `path`.
|
/// Opens the file at `path`.
|
||||||
public init(path: String, mode: Options) throws(PlaydateError) {
|
public init(path: String, mode: Options) throws(PlaydateError) {
|
||||||
@@ -17,15 +19,16 @@ extension File {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
if !isClosed {
|
|
||||||
_ = fileAPI.pointee.close.unsafelyUnwrapped(pointer)
|
_ = fileAPI.pointee.close.unsafelyUnwrapped(pointer)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Closes the file. Further operations are invalid.
|
/// Closes the file, consuming the handle.
|
||||||
public func close() throws(PlaydateError) {
|
// `@export(interface)` lets `discard` compile in Embedded Swift with
|
||||||
guard !isClosed else { return }
|
// the 6.4 release toolchain; later toolchains accept it without.
|
||||||
isClosed = true
|
@export(interface)
|
||||||
|
public consuming func close() throws(PlaydateError) {
|
||||||
|
let pointer = self.pointer
|
||||||
|
discard self
|
||||||
if fileAPI.pointee.close.unsafelyUnwrapped(pointer) != 0 { throw lastFileError() }
|
if fileAPI.pointee.close.unsafelyUnwrapped(pointer) != 0 { throw lastFileError() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ extension Graphics {
|
|||||||
/// Wraps `LCDStreamPlayer`.
|
/// Wraps `LCDStreamPlayer`.
|
||||||
public final class StreamPlayer {
|
public final class StreamPlayer {
|
||||||
let pointer: OpaquePointer
|
let pointer: OpaquePointer
|
||||||
/// Retains the active source so it outlives the stream.
|
/// Retains the active source so it outlives the stream. A file
|
||||||
|
/// handle is owned outright, as it cannot be shared.
|
||||||
private var retainedSource: AnyObject?
|
private var retainedSource: AnyObject?
|
||||||
|
private var retainedFile: File.Handle?
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
pointer = streamAPI.pointee.newPlayer.unsafelyUnwrapped().unsafelyUnwrapped
|
pointer = streamAPI.pointee.newPlayer.unsafelyUnwrapped().unsafelyUnwrapped
|
||||||
@@ -24,22 +26,26 @@ extension Graphics {
|
|||||||
streamAPI.pointee.setBufferSize.unsafelyUnwrapped(pointer, Int32(video), Int32(audio))
|
streamAPI.pointee.setBufferSize.unsafelyUnwrapped(pointer, Int32(video), Int32(audio))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Streams from an open file.
|
/// Streams from an open file. The stream takes ownership of the
|
||||||
public func setFile(_ file: File.Handle) {
|
/// handle and closes it when the source changes or the stream is freed.
|
||||||
retainedSource = file
|
public func setFile(_ file: consuming File.Handle) {
|
||||||
streamAPI.pointee.setFile.unsafelyUnwrapped(pointer, file.pointer)
|
streamAPI.pointee.setFile.unsafelyUnwrapped(pointer, file.pointer)
|
||||||
|
retainedFile = consume file
|
||||||
|
retainedSource = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Streams from an HTTP connection.
|
/// Streams from an HTTP connection.
|
||||||
public func setHTTPConnection(_ connection: Network.HTTPConnection) {
|
public func setHTTPConnection(_ connection: Network.HTTPConnection) {
|
||||||
retainedSource = connection
|
|
||||||
streamAPI.pointee.setHTTPConnection.unsafelyUnwrapped(pointer, connection.pointer)
|
streamAPI.pointee.setHTTPConnection.unsafelyUnwrapped(pointer, connection.pointer)
|
||||||
|
retainedSource = connection
|
||||||
|
retainedFile = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Streams from a TCP connection.
|
/// Streams from a TCP connection.
|
||||||
public func setTCPConnection(_ connection: Network.TCPConnection) {
|
public func setTCPConnection(_ connection: Network.TCPConnection) {
|
||||||
retainedSource = connection
|
|
||||||
streamAPI.pointee.setTCPConnection.unsafelyUnwrapped(pointer, connection.pointer)
|
streamAPI.pointee.setTCPConnection.unsafelyUnwrapped(pointer, connection.pointer)
|
||||||
|
retainedSource = connection
|
||||||
|
retainedFile = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
|
|||||||
@@ -118,28 +118,22 @@ extension JSON {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes JSON read from an open file into a `Value` tree.
|
/// Decodes JSON read from an open file into a `Value` tree.
|
||||||
public static func decode(file: File.Handle) throws(PlaydateError) -> Value {
|
public static func decode(file: borrowing File.Handle) throws(PlaydateError) -> Value {
|
||||||
let context = DecodeContext()
|
let context = DecodeContext()
|
||||||
var decoder = makeDecoder(context: Unmanaged.passUnretained(context))
|
var decoder = makeDecoder(context: Unmanaged.passUnretained(context))
|
||||||
var reader = json_reader()
|
var reader = json_reader()
|
||||||
reader.userdata = Unmanaged.passUnretained(file).toOpaque()
|
// The handle is borrowed for the whole call, so its `SDFile` stays
|
||||||
|
// open while the decoder reads through it.
|
||||||
|
reader.userdata = file.pointer
|
||||||
reader.read = { userdata, buffer, size in
|
reader.read = { userdata, buffer, size in
|
||||||
guard let userdata, let buffer else { return -1 }
|
guard let userdata, let buffer else { return -1 }
|
||||||
let file = Unmanaged<File.Handle>.fromOpaque(userdata).takeUnretainedValue()
|
let count = fileAPI.pointee.read.unsafelyUnwrapped(userdata, buffer, UInt32(size))
|
||||||
var destination = UnsafeMutableBufferPointer(start: buffer, count: Int(size)).mutableSpan
|
return count > 0 ? count : -1
|
||||||
do {
|
|
||||||
let count = try file.read(into: &destination)
|
|
||||||
return count > 0 ? Int32(count) : -1
|
|
||||||
} catch {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var outval = json_value()
|
var outval = json_value()
|
||||||
let ok = withExtendedLifetime(context) {
|
let ok = withExtendedLifetime(context) {
|
||||||
withExtendedLifetime(file) {
|
|
||||||
jsonAPI.pointee.decode.unsafelyUnwrapped(&decoder, reader, &outval) != 0
|
jsonAPI.pointee.decode.unsafelyUnwrapped(&decoder, reader, &outval) != 0
|
||||||
}
|
}
|
||||||
}
|
|
||||||
guard ok else {
|
guard ok else {
|
||||||
// A completed root container may already have been written to
|
// A completed root container may already have been written to
|
||||||
// outval before the failure; consume it so its box is not leaked.
|
// outval before the failure; consume it so its box is not leaked.
|
||||||
|
|||||||
@@ -422,6 +422,19 @@ enum Mock {
|
|||||||
private static func installJSON() {
|
private static func installJSON() {
|
||||||
jsonAPI.initialize(to: playdate_json())
|
jsonAPI.initialize(to: playdate_json())
|
||||||
|
|
||||||
|
// Pulls one chunk through the reader, as the OS would, and decodes
|
||||||
|
// it as `null`.
|
||||||
|
jsonAPI.pointee.decode = { _, reader, outval in
|
||||||
|
var buffer = [UInt8](repeating: 0, count: 16)
|
||||||
|
let count = buffer.withUnsafeMutableBufferPointer { buffer in
|
||||||
|
reader.read?(reader.userdata, buffer.baseAddress, Int32(buffer.count)) ?? -1
|
||||||
|
}
|
||||||
|
Mock.record("decode(\(count))")
|
||||||
|
outval?.pointee = json_value()
|
||||||
|
outval?.pointee.type = CChar(kJSONNull.rawValue)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
jsonAPI.pointee.decodeString = { decoder, _, outval in
|
jsonAPI.pointee.decodeString = { decoder, _, outval in
|
||||||
guard let decoder else { return 0 }
|
guard let decoder else { return 0 }
|
||||||
|
|
||||||
|
|||||||
@@ -345,6 +345,26 @@ struct WrapperTests {
|
|||||||
#expect(Mock.eventCount("close") == 1)
|
#expect(Mock.eventCount("close") == 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func jsonDecodeFileReadsThroughTheHandleAndClosesIt() throws {
|
||||||
|
Mock.fileReadLimit = 5
|
||||||
|
let value = try JSON.decodeFile(path: "save.json")
|
||||||
|
guard case .null = value else {
|
||||||
|
Issue.record("expected .null, got \(value)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
#expect(Mock.events.contains("decode(5)"))
|
||||||
|
#expect(Mock.eventCount("close") == 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func fileHandleClosesWhenItGoesOutOfScope() throws {
|
||||||
|
do {
|
||||||
|
let handle = try File.Handle(path: "save.dat", mode: .write)
|
||||||
|
_ = try handle.write([1])
|
||||||
|
#expect(Mock.eventCount("close") == 0)
|
||||||
|
}
|
||||||
|
#expect(Mock.eventCount("close") == 1)
|
||||||
|
}
|
||||||
|
|
||||||
@Test func fileHandleReadLengthReturnsOnlyTheBytesRead() throws {
|
@Test func fileHandleReadLengthReturnsOnlyTheBytesRead() throws {
|
||||||
let handle = try File.Handle(path: "save.dat", mode: [.read, .readData])
|
let handle = try File.Handle(path: "save.dat", mode: [.read, .readData])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user