From b3da44e9fb783e29fab913369cef3dedc6e580c4 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Tue, 6 Jan 2026 18:37:34 -0600 Subject: [PATCH 01/10] Enable the embedded version of the libary This re-enables the 'BinaryParsingEmbedded' target, which builds from the same sources as the non-embedded version of the library. I've also added a basic "parser" to test that the library is usable. --- EmbeddedExample/EmbeddedParser.swift | 29 ++++++++++++++++++ Package.swift | 30 ++++++++++++------- .../Parser Types/ParserSource.swift | 10 ++++--- Sources/BinaryParsing/Parsers/Data.swift | 2 +- 4 files changed, 55 insertions(+), 16 deletions(-) create mode 100644 EmbeddedExample/EmbeddedParser.swift diff --git a/EmbeddedExample/EmbeddedParser.swift b/EmbeddedExample/EmbeddedParser.swift new file mode 100644 index 0000000..4df6658 --- /dev/null +++ b/EmbeddedExample/EmbeddedParser.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Binary Parsing open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import BinaryParsingEmbedded + +@available(macOS 26.0, *) +@main +struct EmbeddedTest { + static func main() throws(ParsingError) { + let data: InlineArray<_, UInt8> = [0x0, 0x1, 0x2, 0x3, 0x4, 0x5] + var span = ParserSpan(data.span.bytes) + + let a = try Int16(parsingBigEndian: &span) + let b = try Int16(parsingBigEndian: &span) + let c = try Int16(parsingBigEndian: &span) + + precondition(a == 1) + precondition(b == 0x203) + print(c) + } +} diff --git a/Package.swift b/Package.swift index f4e3bc0..8f4d3af 100644 --- a/Package.swift +++ b/Package.swift @@ -17,11 +17,11 @@ import PackageDescription let package = Package( name: "swift-binary-parsing", platforms: [ - .macOS(.v13), .iOS(.v16), .watchOS(.v9), .tvOS(.v16), .visionOS(.v1), + .macOS(.v14), .iOS(.v17), .watchOS(.v10), .tvOS(.v17), .visionOS(.v1), ], products: [ - .library(name: "BinaryParsing", targets: ["BinaryParsing"]) - // .library(name: "BinaryParsingEmbedded", targets: ["BinaryParsingEmbedded"]), + .library(name: "BinaryParsing", targets: ["BinaryParsing"]), + .library(name: "BinaryParsingEmbedded", targets: ["BinaryParsingEmbedded"]), ], dependencies: [ .package( @@ -43,14 +43,14 @@ let package = Package( .strictMemorySafety(), ] ), - // .target( - // name: "BinaryParsingEmbedded", - // dependencies: ["BinaryParsingMacros"], - // swiftSettings: [ - // .enableExperimentalFeature("Embedded"), - // .enableExperimentalFeature("Lifetimes"), - // ] - // ), + .target( + name: "BinaryParsingEmbedded", + dependencies: ["BinaryParsingMacros"], + swiftSettings: [ + .enableExperimentalFeature("Embedded"), + .enableExperimentalFeature("Lifetimes"), + ] + ), .macro( name: "BinaryParsingMacros", dependencies: [ @@ -78,6 +78,14 @@ let package = Package( "ParserTest", .product(name: "ArgumentParser", package: "swift-argument-parser"), ]), + .executableTarget( + name: "EmbeddedExample", + dependencies: ["BinaryParsingEmbedded"], + path: "EmbeddedExample", + swiftSettings: [ + .enableExperimentalFeature("Embedded") + ] + ), .testTarget( name: "BinaryParsingTests", diff --git a/Sources/BinaryParsing/Parser Types/ParserSource.swift b/Sources/BinaryParsing/Parser Types/ParserSource.swift index 05f5f21..d6e02fc 100644 --- a/Sources/BinaryParsing/Parser Types/ParserSource.swift +++ b/Sources/BinaryParsing/Parser Types/ParserSource.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -#if canImport(Foundation) +#if !$Embedded && canImport(Foundation) public import Foundation #endif @@ -65,7 +65,7 @@ extension RandomAccessCollection { public func withParserSpanIfAvailable( _ body: (inout ParserSpan) throws(ThrownParsingError) -> T ) throws(ThrownParsingError) -> T? { - #if canImport(Foundation) + #if !$Embedded && canImport(Foundation) if let data = self as? Foundation.Data { let result = unsafe data.withUnsafeBytes { buffer in var span = unsafe ParserSpan(_unsafeBytes: buffer) @@ -81,7 +81,9 @@ extension RandomAccessCollection { let result = self.withContiguousStorageIfAvailable { buffer in let rawBuffer = UnsafeRawBufferPointer(buffer) var span = unsafe ParserSpan(_unsafeBytes: rawBuffer) - return Result { try body(&span) } + return Result { () throws(ThrownParsingError) in + try body(&span) + } } switch result { case .success(let t): return t @@ -136,7 +138,7 @@ extension ParserSpanProvider { } } -#if canImport(Foundation) +#if !$Embedded && canImport(Foundation) extension Data: ParserSpanProvider { @inlinable public func withParserSpan( diff --git a/Sources/BinaryParsing/Parsers/Data.swift b/Sources/BinaryParsing/Parsers/Data.swift index 85836a1..fabf1dd 100644 --- a/Sources/BinaryParsing/Parsers/Data.swift +++ b/Sources/BinaryParsing/Parsers/Data.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -#if canImport(Foundation) +#if !$Embedded && canImport(Foundation) public import Foundation extension Data { From bc36146e61efde6ebc2f3272f7a97878598ce703 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Wed, 7 Jan 2026 10:53:03 -0600 Subject: [PATCH 02/10] Use typed throws for InlineArray parsers --- .../BinaryParsing/Parsers/InlineArray.swift | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/Sources/BinaryParsing/Parsers/InlineArray.swift b/Sources/BinaryParsing/Parsers/InlineArray.swift index 2fb49bd..66a65a8 100644 --- a/Sources/BinaryParsing/Parsers/InlineArray.swift +++ b/Sources/BinaryParsing/Parsers/InlineArray.swift @@ -18,7 +18,7 @@ extension InlineArray where Element == UInt8 { /// - Throws: A `ParsingError` if `input` does not have at least `count` /// bytes remaining. @inlinable - public init(parsing input: inout ParserSpan) throws { + public init(parsing input: inout ParserSpan) throws(ParsingError) { let slice = try input._divide(atByteOffset: Self.count) self = unsafe slice.withUnsafeBytes { buffer in InlineArray { unsafe buffer[$0] } @@ -28,6 +28,7 @@ extension InlineArray where Element == UInt8 { @available(macOS 26, iOS 26, watchOS 26, tvOS 26, visionOS 26, *) extension InlineArray where Element: ~Copyable { + #if !$Embedded /// Creates a new array by parsing the specified number of elements from the given /// parser span, using the provided closure for parsing. /// @@ -60,4 +61,38 @@ extension InlineArray where Element: ~Copyable { try parser(&input) } } + #endif + + /// Creates a new array by parsing the specified number of elements from the given + /// parser span, using the provided closure for parsing (with a typed error). + /// + /// The provided closure is called `count` times while initializing the inline array. + /// For example, the following code parses a 16-element `InlineArray` of `UInt32` + /// values from a `ParserSpan`. If the `input` parser span doesn't represent enough + /// memory for those 16 values, the call will throw a `ParsingError`. + /// + /// let integers = try InlineArray<16, UInt32>(parsing: &input) { input in + /// try UInt32(parsingBigEndian: &input) + /// } + /// + /// You can also pass a parser initializer to this initializer as a value, if it has + /// the correct shape: + /// + /// let integers = try InlineArray<16, UInt32>( + /// parsing: &input, + /// parser: UInt32.init(parsingBigEndian:)) + /// + /// - Parameters: + /// - input: The `ParserSpan` to consume. + /// - parser: A closure that parses each element from `input`. + /// - Throws: An error if one is thrown from `parser`. + @inlinable + public init( + parsing input: inout ParserSpan, + parser: (inout ParserSpan) throws(ParsingError) -> Element + ) throws(ParsingError) { + self = try InlineArray { _ throws(ParsingError) in + try parser(&input) + } + } } From 50b80a7efe84e2fe6bd07272eee6e62aebb39ea0 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Wed, 7 Jan 2026 10:54:14 -0600 Subject: [PATCH 03/10] Switch array parser to always use `ParsingError` The array parser that is available in non-embedded contexts has an untyped `throws` designation. This change allows for using typed throws with the array parser even on non-embedded platforms. --- Sources/BinaryParsing/Parsers/Array.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/BinaryParsing/Parsers/Array.swift b/Sources/BinaryParsing/Parsers/Array.swift index 678aa22..86d84d0 100644 --- a/Sources/BinaryParsing/Parsers/Array.swift +++ b/Sources/BinaryParsing/Parsers/Array.swift @@ -91,7 +91,7 @@ extension Array { #endif /// Creates a new array by parsing the specified number of elements from the given - /// parser span, using the provided closure for parsing. + /// parser span, using the provided closure for parsing (with a typed error). /// /// The provided closure is called `byteCount` times while initializing the array. /// For example, the following code parses an array of 16 `UInt32` values from a @@ -119,8 +119,8 @@ extension Array { public init( parsing input: inout ParserSpan, count: Int, - parser: (inout ParserSpan) throws(ThrownParsingError) -> Element - ) throws(ThrownParsingError) { + parser: (inout ParserSpan) throws(ParsingError) -> Element + ) throws(ParsingError) { guard count >= 0 else { throw ParsingError(statusOnly: .invalidValue) } From 8465715a6425afb4aa89c3f4b8038b29339f1cd6 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Wed, 7 Jan 2026 11:01:40 -0600 Subject: [PATCH 04/10] Formatting fix --- Sources/BinaryParsing/Parsers/InlineArray.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/BinaryParsing/Parsers/InlineArray.swift b/Sources/BinaryParsing/Parsers/InlineArray.swift index 66a65a8..b87c88a 100644 --- a/Sources/BinaryParsing/Parsers/InlineArray.swift +++ b/Sources/BinaryParsing/Parsers/InlineArray.swift @@ -62,7 +62,7 @@ extension InlineArray where Element: ~Copyable { } } #endif - + /// Creates a new array by parsing the specified number of elements from the given /// parser span, using the provided closure for parsing (with a typed error). /// From 306b5f019280e35c2e0e640a7f512e1fcd06b692 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Wed, 7 Jan 2026 18:17:38 -0600 Subject: [PATCH 05/10] Workaround missing arc4random symbol on Linux --- .../BinaryParsing/Support/LinuxRNGSupport.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 Sources/BinaryParsing/Support/LinuxRNGSupport.swift diff --git a/Sources/BinaryParsing/Support/LinuxRNGSupport.swift b/Sources/BinaryParsing/Support/LinuxRNGSupport.swift new file mode 100644 index 0000000..e9de9a7 --- /dev/null +++ b/Sources/BinaryParsing/Support/LinuxRNGSupport.swift @@ -0,0 +1,16 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Binary Parsing open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +//#if $Embedded && os(Linux) +// Temporary workaround for lack of random support on embedded Linux +@_cdecl("arc4random_buf") +func arc4random_buf(_ buf: UnsafeMutableRawPointer?, _ nbytes: Int) {} +//#endif From 333785849762dac8e747d45d6624c714d60037fb Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Wed, 7 Jan 2026 18:27:30 -0600 Subject: [PATCH 06/10] Skip nightly-main linux --- .github/workflows/pull_request.yml | 3 +-- Sources/BinaryParsing/Support/LinuxRNGSupport.swift | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 3bbff29..58abd79 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -28,8 +28,7 @@ jobs: uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: macos_exclude_xcode_versions: "[{\"xcode_version\": \"16.0\"}, {\"xcode_version\": \"16.1\"}, {\"xcode_version\": \"16.2\"}, {\"xcode_version\": \"16.3\"}]" - linux_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}, {\"swift_version\": \"5.10\"}, {\"swift_version\": \"6.0\"}, {\"swift_version\": \"6.1\"}, {\"swift_version\": \"nightly-main\"}, {\"swift_version\": \"nightly-6.1\"}]" - windows_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}, {\"swift_version\": \"6.0\"}, {\"swift_version\": \"6.1\"}, {\"swift_version\": \"nightly\"}, {\"swift_version\": \"nightly-6.1\"}]" + linux_swift_versions: "[ \"6.2\", \"nightly-6.3\"]" enable_macos_checks: false enable_windows_checks: false linux_build_command: swift test --disable-experimental-prebuilts # prebuilt swift-syntax not building diff --git a/Sources/BinaryParsing/Support/LinuxRNGSupport.swift b/Sources/BinaryParsing/Support/LinuxRNGSupport.swift index e9de9a7..c84abe5 100644 --- a/Sources/BinaryParsing/Support/LinuxRNGSupport.swift +++ b/Sources/BinaryParsing/Support/LinuxRNGSupport.swift @@ -11,6 +11,7 @@ //#if $Embedded && os(Linux) // Temporary workaround for lack of random support on embedded Linux +// swift-format-ignore: AlwaysUseLowerCamelCase @_cdecl("arc4random_buf") -func arc4random_buf(_ buf: UnsafeMutableRawPointer?, _ nbytes: Int) {} +func arc4random_buf(_ buf: UnsafeMutableRawPointer, _ nbytes: Int) {} //#endif From b32bc81013934f540fa4348e7065b3fae4517e55 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Wed, 7 Jan 2026 18:34:53 -0600 Subject: [PATCH 07/10] Only use arc4random hack in embedded --- Sources/BinaryParsing/Support/LinuxRNGSupport.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/BinaryParsing/Support/LinuxRNGSupport.swift b/Sources/BinaryParsing/Support/LinuxRNGSupport.swift index c84abe5..81b58c5 100644 --- a/Sources/BinaryParsing/Support/LinuxRNGSupport.swift +++ b/Sources/BinaryParsing/Support/LinuxRNGSupport.swift @@ -9,9 +9,9 @@ // //===----------------------------------------------------------------------===// -//#if $Embedded && os(Linux) +#if $Embedded && os(Linux) // Temporary workaround for lack of random support on embedded Linux // swift-format-ignore: AlwaysUseLowerCamelCase @_cdecl("arc4random_buf") func arc4random_buf(_ buf: UnsafeMutableRawPointer, _ nbytes: Int) {} -//#endif +#endif From c0af2940c600cfeb2fb830ae1614a78e9e18f705 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Thu, 8 Jan 2026 09:30:52 -0600 Subject: [PATCH 08/10] Don't use xcrun for macOS tests --- .github/workflows/pull_request.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index c6041b7..d6f9a9f 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -27,11 +27,12 @@ jobs: name: Test uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: + enable_macos_checks: true macos_xcode_versions: "[\"26.1\"]" + macos_build_command: swift test linux_swift_versions: "[ \"6.2\", \"nightly-6.3\"]" - enable_macos_checks: true - enable_windows_checks: false linux_build_command: swift test --disable-experimental-prebuilts # prebuilt swift-syntax not building + enable_windows_checks: false soundness: name: Soundness From 92e2d5690abc23e3bb12acef3fe2b26ed900e243 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Thu, 8 Jan 2026 11:48:25 -0600 Subject: [PATCH 09/10] Disable macOS test runner --- .github/workflows/pull_request.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index d6f9a9f..da901de 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -27,9 +27,8 @@ jobs: name: Test uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: - enable_macos_checks: true + enable_macos_checks: false # error: module 'Swift' cannot be imported in embedded Swift mode macos_xcode_versions: "[\"26.1\"]" - macos_build_command: swift test linux_swift_versions: "[ \"6.2\", \"nightly-6.3\"]" linux_build_command: swift test --disable-experimental-prebuilts # prebuilt swift-syntax not building enable_windows_checks: false From b65cec81f67febc0dcffa2d5511b0e9beb13ca7e Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Thu, 8 Jan 2026 12:11:24 -0600 Subject: [PATCH 10/10] Gate embedded targets behind compiler flag --- .github/workflows/pull_request.yml | 20 +++++++++- Package.swift | 61 ++++++++++++++++++++---------- 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index da901de..e18e241 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -27,12 +27,30 @@ jobs: name: Test uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main with: - enable_macos_checks: false # error: module 'Swift' cannot be imported in embedded Swift mode + enable_macos_checks: true macos_xcode_versions: "[\"26.1\"]" linux_swift_versions: "[ \"6.2\", \"nightly-6.3\"]" linux_build_command: swift test --disable-experimental-prebuilts # prebuilt swift-syntax not building enable_windows_checks: false + embedded: + name: Embedded Test (Linux nightly-6.3) + runs-on: ubuntu-latest + container: + image: swiftlang/swift:nightly-6.3-jammy + steps: + - name: Swift version + run: swift --version + - name: Clang version + run: clang --version + - name: Checkout repository + uses: actions/checkout@v4 + - name: Build / Test + run: | + ENABLE_EMBEDDED=1 swift test --disable-experimental-prebuilts + echo "Running embedded Swift executable..." + .build/debug/EmbeddedExample + soundness: name: Soundness uses: swiftlang/github-workflows/.github/workflows/soundness.yml@main diff --git a/Package.swift b/Package.swift index 8f4d3af..ba46203 100644 --- a/Package.swift +++ b/Package.swift @@ -17,11 +17,10 @@ import PackageDescription let package = Package( name: "swift-binary-parsing", platforms: [ - .macOS(.v14), .iOS(.v17), .watchOS(.v10), .tvOS(.v17), .visionOS(.v1), + .macOS(.v13), .iOS(.v16), .watchOS(.v9), .tvOS(.v16), .visionOS(.v1), ], products: [ - .library(name: "BinaryParsing", targets: ["BinaryParsing"]), - .library(name: "BinaryParsingEmbedded", targets: ["BinaryParsingEmbedded"]), + .library(name: "BinaryParsing", targets: ["BinaryParsing"]) ], dependencies: [ .package( @@ -43,14 +42,6 @@ let package = Package( .strictMemorySafety(), ] ), - .target( - name: "BinaryParsingEmbedded", - dependencies: ["BinaryParsingMacros"], - swiftSettings: [ - .enableExperimentalFeature("Embedded"), - .enableExperimentalFeature("Lifetimes"), - ] - ), .macro( name: "BinaryParsingMacros", dependencies: [ @@ -78,14 +69,6 @@ let package = Package( "ParserTest", .product(name: "ArgumentParser", package: "swift-argument-parser"), ]), - .executableTarget( - name: "EmbeddedExample", - dependencies: ["BinaryParsingEmbedded"], - path: "EmbeddedExample", - swiftSettings: [ - .enableExperimentalFeature("Embedded") - ] - ), .testTarget( name: "BinaryParsingTests", @@ -127,7 +110,9 @@ let package = Package( ] ) -if ProcessInfo.processInfo.environment["ENABLE_BENCHMARKING"] != nil { +// MARK: Benchmarking overrides + +if isSet("ENABLE_BENCHMARKING") { package.dependencies += [ .package( url: "https://github.com/ordo-one/package-benchmark", @@ -149,3 +134,39 @@ if ProcessInfo.processInfo.environment["ENABLE_BENCHMARKING"] != nil { ) ] } + +// MARK: Embedded overrides + +if isSet("ENABLE_EMBEDDED") { + package.platforms = [ + .macOS(.v14), .iOS(.v17), .watchOS(.v10), .tvOS(.v17), .visionOS(.v1), + ] + + package.products += [ + .library(name: "BinaryParsingEmbedded", targets: ["BinaryParsingEmbedded"]) + ] + + package.targets += [ + .target( + name: "BinaryParsingEmbedded", + dependencies: ["BinaryParsingMacros"], + swiftSettings: [ + .enableExperimentalFeature("Embedded"), + .enableExperimentalFeature("Lifetimes"), + .strictMemorySafety(), + ] + ), + .executableTarget( + name: "EmbeddedExample", + dependencies: ["BinaryParsingEmbedded"], + path: "EmbeddedExample", + swiftSettings: [ + .enableExperimentalFeature("Embedded") + ] + ), + ] +} + +func isSet(_ key: String) -> Bool { + ProcessInfo.processInfo.environment[key] != nil +}