Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
Expand Down Expand Up @@ -155,7 +155,7 @@
echo "rust=false" >> "$GITHUB_OUTPUT"
fi

if grep -Eq '^(swift/)' <<< "$changed_files"; then
if grep -Eq '^(Package\.swift$|swift/)' <<< "$changed_files"; then
echo "swift=true" >> "$GITHUB_OUTPUT"
else
echo "swift=false" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -481,6 +481,50 @@
cd swift
swift-format lint --configuration .swift-format --recursive --strict Sources Tests Package.swift

swift_linux:
name: Swift Linux CI
needs: changes
if: needs.changes.outputs.swift == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Run Swift unit tests
env:
ENABLE_FORY_DEBUG_OUTPUT: "1"
run: |
swift --version
cd swift
swift test

swift_apple_platforms:
name: Swift ${{ matrix.platform }} Build
needs: changes
if: needs.changes.outputs.swift == 'true'
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
include:
- platform: tvOS
sdk: appletvos
triple: arm64-apple-tvos16.0
- platform: watchOS
sdk: watchos
triple: arm64_32-apple-watchos9.0
- platform: visionOS
sdk: xros
triple: arm64-apple-xros1.0
steps:
- uses: actions/checkout@v5
- name: Build Swift package
run: |
sdk_path="$(xcrun --sdk "${{ matrix.sdk }}" --show-sdk-path)"
swift build \
--package-path . \
--target Fory \
--triple "${{ matrix.triple }}" \
--sdk "$sdk_path"

swift_xlang:
name: Swift Xlang Test
runs-on: macos-latest
Expand Down
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ let package = Package(
name: "fory",
platforms: [
.macOS(.v13),
.iOS(.v16)
.iOS(.v16),
.tvOS(.v16),
.watchOS(.v9),
.visionOS(.v1)
],
products: [
.library(
Expand Down
4 changes: 4 additions & 0 deletions docs/object-serialization/swift/basic-serialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ directly by a type, retroactive conformances, and separate custom serializers.
- `String`
- `Data`

`Int` and `UInt` keep 64-bit wire encodings on every platform. When a decoded
value is outside the native range on a 32-bit target, deserialization throws
`ForyError.invalidData`.

### Date and time

- `Date`
Expand Down
3 changes: 2 additions & 1 deletion docs/start/swift.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ license: |

Fory Swift provides xlang Object Serialization and compiler-generated models.
It is distributed through Swift Package Manager, uses Swift tools 6.0, and
targets macOS 13 or later and iOS 16 or later.
targets macOS 13 or later, iOS and tvOS 16 or later, watchOS 9 or later, and
visionOS 1 or later. Linux requires a Swift 6.0 or later toolchain.

## Verify the Toolchain

Expand Down
5 changes: 4 additions & 1 deletion swift/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ let package = Package(
name: "fory-swift",
platforms: [
.macOS(.v13),
.iOS(.v16)
.iOS(.v16),
.tvOS(.v16),
.watchOS(.v9),
.visionOS(.v1)
],
products: [
.library(
Expand Down
2 changes: 1 addition & 1 deletion swift/Sources/Fory/ByteBuffer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -836,7 +836,7 @@ public final class ByteBuffer {
if isASCII {
return String(decoding: utf8Bytes, as: UTF8.self)
}
if #available(macOS 15.0, iOS 18.0, *) {
if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) {
return String(validating: utf8Bytes, as: UTF8.self)
}
return String(bytes: utf8Bytes, encoding: .utf8)
Expand Down
20 changes: 15 additions & 5 deletions swift/Sources/Fory/Decimal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,18 @@ import Foundation

private let decimalSmallPositiveMax: UInt64 = 0x3fff_ffff_ffff_ffff
private let decimalSmallNegativeAbsMax: UInt64 = 0x4000_0000_0000_0000
private let decimalLengthMask: UInt8 = 0x0f
private let decimalNegativeMask: UInt8 = 0x10
private let decimalCompactMask: UInt8 = 0x20
#if os(Linux)
// Corelibs Foundation reverses the NSDecimal flag bitfield order used by Darwin.
private let decimalLengthMask: UInt8 = 0xf0
private let decimalLengthShift: UInt8 = 4
private let decimalNegativeMask: UInt8 = 0x08
private let decimalCompactMask: UInt8 = 0x04
#else
private let decimalLengthMask: UInt8 = 0x0f
private let decimalLengthShift: UInt8 = 0
private let decimalNegativeMask: UInt8 = 0x10
private let decimalCompactMask: UInt8 = 0x20
#endif
private let decimalHeaderSize = 4
private let decimalMaxMantissaWords = 8
private let decimalMaxMagnitudeBytes = decimalMaxMantissaWords * 2
Expand Down Expand Up @@ -146,7 +155,8 @@ private func foundationDecimalWireState(_ value: Decimal) -> FoundationDecimalWi
return withUnsafeBytes(of: &compact) { raw in
let exponent = Int8(bitPattern: raw[0])
let flags = raw[1]
let length = min(Int(flags & decimalLengthMask), decimalMaxMantissaWords)
let length = min(
Int((flags & decimalLengthMask) >> decimalLengthShift), decimalMaxMantissaWords)
let isNegative = (flags & decimalNegativeMask) != 0

var magnitude: [UInt8] = []
Expand Down Expand Up @@ -178,7 +188,7 @@ private func buildFoundationDecimal(
withUnsafeMutableBytes(of: &value) { raw in
raw.initializeMemory(as: UInt8.self, repeating: 0)
raw[0] = UInt8(bitPattern: exponent)
var flags = UInt8(truncatingIfNeeded: mantissa.length)
var flags = UInt8(truncatingIfNeeded: mantissa.length) << decimalLengthShift
if signum < 0 && !normalized.isEmpty {
flags |= decimalNegativeMask
}
Expand Down
30 changes: 16 additions & 14 deletions swift/Sources/Fory/FieldCodecs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -859,7 +859,7 @@ public enum IntVarintCodec: FieldCodec {
}

public static func readFieldData(_ context: ReadContext) throws -> Int {
Int(try context.buffer.readVarInt64())
try checkedInt64ToInt(context.buffer.readVarInt64())
}
}

Expand All @@ -874,7 +874,7 @@ public enum IntFixedCodec: FieldCodec {
}

public static func readFieldData(_ context: ReadContext) throws -> Int {
Int(try context.buffer.readInt64())
try checkedInt64ToInt(context.buffer.readInt64())
}
}

Expand All @@ -889,7 +889,7 @@ public enum IntTaggedCodec: FieldCodec {
}

public static func readFieldData(_ context: ReadContext) throws -> Int {
Int(try context.buffer.readTaggedInt64())
try checkedInt64ToInt(context.buffer.readTaggedInt64())
}
}

Expand All @@ -904,7 +904,7 @@ public enum UIntVarintCodec: FieldCodec {
}

public static func readFieldData(_ context: ReadContext) throws -> UInt {
UInt(try context.buffer.readVarUInt64())
try checkedUInt64ToUInt(context.buffer.readVarUInt64())
}
}

Expand All @@ -919,7 +919,7 @@ public enum UIntFixedCodec: FieldCodec {
}

public static func readFieldData(_ context: ReadContext) throws -> UInt {
UInt(try context.buffer.readUInt64())
try checkedUInt64ToUInt(context.buffer.readUInt64())
}
}

Expand All @@ -934,7 +934,7 @@ public enum UIntTaggedCodec: FieldCodec {
}

public static func readFieldData(_ context: ReadContext) throws -> UInt {
UInt(try context.buffer.readTaggedUInt64())
try checkedUInt64ToUInt(context.buffer.readTaggedUInt64())
}
}

Expand Down Expand Up @@ -1514,7 +1514,7 @@ private func readIntArrayPayload(
var values: [Int] = []
values.reserveCapacity(count)
for _ in 0..<count {
values.append(Int(try context.buffer.readInt64()))
try values.append(checkedInt64ToInt(context.buffer.readInt64()))
}
return values
}
Expand All @@ -1532,7 +1532,7 @@ private func readUIntArrayPayload(
var values: [UInt] = []
values.reserveCapacity(count)
for _ in 0..<count {
values.append(UInt(try context.buffer.readUInt64()))
try values.append(checkedUInt64ToUInt(context.buffer.readUInt64()))
}
return values
}
Expand Down Expand Up @@ -1702,13 +1702,14 @@ private func readCompatibleElementPayload<ElementCodec: FieldCodec>(
{
switch remoteTypeID {
case .int64:
return uncheckedScalarCast(Int(try context.buffer.readInt64()), to: ElementCodec.Target.self)
return uncheckedScalarCast(
try IntFixedCodec.readFieldData(context), to: ElementCodec.Target.self)
case .varint64:
return uncheckedScalarCast(
Int(try context.buffer.readVarInt64()), to: ElementCodec.Target.self)
try IntVarintCodec.readFieldData(context), to: ElementCodec.Target.self)
case .taggedInt64:
return uncheckedScalarCast(
Int(try context.buffer.readTaggedInt64()), to: ElementCodec.Target.self)
try IntTaggedCodec.readFieldData(context), to: ElementCodec.Target.self)
default:
break
}
Expand Down Expand Up @@ -1747,13 +1748,14 @@ private func readCompatibleElementPayload<ElementCodec: FieldCodec>(
{
switch remoteTypeID {
case .uint64:
return uncheckedScalarCast(UInt(try context.buffer.readUInt64()), to: ElementCodec.Target.self)
return uncheckedScalarCast(
try UIntFixedCodec.readFieldData(context), to: ElementCodec.Target.self)
case .varUInt64:
return uncheckedScalarCast(
UInt(try context.buffer.readVarUInt64()), to: ElementCodec.Target.self)
try UIntVarintCodec.readFieldData(context), to: ElementCodec.Target.self)
case .taggedUInt64:
return uncheckedScalarCast(
UInt(try context.buffer.readTaggedUInt64()), to: ElementCodec.Target.self)
try UIntTaggedCodec.readFieldData(context), to: ElementCodec.Target.self)
default:
break
}
Expand Down
106 changes: 72 additions & 34 deletions swift/Sources/Fory/PrimitiveSerializers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,46 @@

import Foundation

// Int and UInt keep their 64-bit wire encodings on every architecture. Native
// 32-bit targets must reject out-of-range values instead of trapping during conversion.
@usableFromInline
@inline(__always)
internal func checkedInt64ToInt(_ value: Int64) throws -> Int {
#if arch(arm64) || arch(x86_64)
return Int(value)
#else
guard let result = Int(exactly: value) else {
throw int64ToIntOverflow(value)
}
return result
#endif
}

@usableFromInline
@inline(__always)
internal func checkedUInt64ToUInt(_ value: UInt64) throws -> UInt {
#if arch(arm64) || arch(x86_64)
return UInt(value)
#else
guard let result = UInt(exactly: value) else {
throw uint64ToUIntOverflow(value)
}
return result
#endif
}

@usableFromInline
@inline(never)
internal func int64ToIntOverflow(_ value: Int64) -> ForyError {
ForyError.invalidData("int64 value \(value) overflows Int")
}

@usableFromInline
@inline(never)
internal func uint64ToUIntOverflow(_ value: UInt64) -> ForyError {
ForyError.invalidData("uint64 value \(value) overflows UInt")
}

extension Bool: Serializer {
public static var staticTypeId: TypeId { .bool }
public static var readDataAlwaysAdvances: Bool { true }
Expand Down Expand Up @@ -224,53 +264,51 @@ extension UInt64: Serializer {
}
}

#if arch(arm64) || arch(x86_64)
extension Int: Serializer {
public static var staticTypeId: TypeId { .varint64 }
public static var readDataAlwaysAdvances: Bool { true }
extension Int: Serializer {
public static var staticTypeId: TypeId { .varint64 }
public static var readDataAlwaysAdvances: Bool { true }

public static func defaultValue(_ context: ReadContext) throws -> Int { 0 }
public static func defaultValue(_ context: ReadContext) throws -> Int { 0 }

public static func writeTypeInfo(_ context: WriteContext) throws {
context.writeStaticTypeInfo(staticTypeId)
}
public static func writeTypeInfo(_ context: WriteContext) throws {
context.writeStaticTypeInfo(staticTypeId)
}

public static func readTypeInfo(_ context: ReadContext) throws -> TypeInfo? {
try context.readStaticTypeInfo(staticTypeId)
}
public static func readTypeInfo(_ context: ReadContext) throws -> TypeInfo? {
try context.readStaticTypeInfo(staticTypeId)
}

public static func writeData(_ value: Self, _ context: WriteContext) throws {
context.buffer.writeVarInt64(Int64(value))
}
public static func writeData(_ value: Self, _ context: WriteContext) throws {
context.buffer.writeVarInt64(Int64(value))
}

public static func readData(_ context: ReadContext) throws -> Int {
Int(try context.buffer.readVarInt64())
}
public static func readData(_ context: ReadContext) throws -> Int {
try checkedInt64ToInt(context.buffer.readVarInt64())
}
}

extension UInt: Serializer {
public static var staticTypeId: TypeId { .varUInt64 }
public static var readDataAlwaysAdvances: Bool { true }
extension UInt: Serializer {
public static var staticTypeId: TypeId { .varUInt64 }
public static var readDataAlwaysAdvances: Bool { true }

public static func defaultValue(_ context: ReadContext) throws -> UInt { 0 }
public static func defaultValue(_ context: ReadContext) throws -> UInt { 0 }

public static func writeTypeInfo(_ context: WriteContext) throws {
context.writeStaticTypeInfo(staticTypeId)
}
public static func writeTypeInfo(_ context: WriteContext) throws {
context.writeStaticTypeInfo(staticTypeId)
}

public static func readTypeInfo(_ context: ReadContext) throws -> TypeInfo? {
try context.readStaticTypeInfo(staticTypeId)
}
public static func readTypeInfo(_ context: ReadContext) throws -> TypeInfo? {
try context.readStaticTypeInfo(staticTypeId)
}

public static func writeData(_ value: Self, _ context: WriteContext) throws {
context.buffer.writeVarUInt64(UInt64(value))
}
public static func writeData(_ value: Self, _ context: WriteContext) throws {
context.buffer.writeVarUInt64(UInt64(value))
}

public static func readData(_ context: ReadContext) throws -> UInt {
UInt(try context.buffer.readVarUInt64())
}
public static func readData(_ context: ReadContext) throws -> UInt {
try checkedUInt64ToUInt(context.buffer.readVarUInt64())
}
#endif
}

extension Float: Serializer {
public static var staticTypeId: TypeId { .float32 }
Expand Down
Loading
Loading