This is an automated email from the ASF dual-hosted git repository.
chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git
The following commit(s) were added to refs/heads/main by this push:
new a9d172084 feat(swift): dynamic any serializer for polymorphism (#3368)
a9d172084 is described below
commit a9d172084062dc2fc07efa50eaa280c8463aad0e
Author: Shawn Yang <[email protected]>
AuthorDate: Thu Feb 19 19:27:15 2026 +0800
feat(swift): dynamic any serializer for polymorphism (#3368)
## Why?
Swift xlang polymorphism currently relies on a custom `AnyAnimal` bridge
in the peer, which makes dynamic payload handling narrow and hard to
extend. This PR adds first-class dynamic `Any` serialization so
polymorphic lists/maps can round-trip through the core runtime and
macro-generated models directly.
## What does this PR do?
- Add `AnySerializer.swift` with runtime `Any` codec support, including
dynamic type info read/write, null sentinel handling, and dynamic
payload encode/decode helpers.
- Add dynamic container helpers for `[Any]`, `[String: Any]`, and
`[Int32: Any]` (including map key-type probing for dynamic map decode).
- Expose `WriteContext`/`ReadContext` convenience APIs and new `Fory`
overloads for serializing/deserializing dynamic Any collections in both
buffer and stream-style entry points.
- Extend `TypeResolver` to parse dynamic wire type metadata and decode
dynamic values (primitive/array/list/map/registered types) by id or
name, with registration-mode tracking.
- Add `RefReader.readRefValue(_:)` support used by dynamic Any reference
resolution.
- Extend `@ForyObject` macro generation to support `Any`, `[Any]`,
`[String: Any]`, and `[Int32: Any]` fields with generated read/write
paths and compatibility metadata handling; reject unsupported `Set<Any>`
and unsupported dictionary key types.
- Simplify `ForySwiftXlangPeer` polymorphic handlers by removing the
custom `AnyAnimal` codec and switching to native `[Any]` / `[String:
Any]` round-trips.
## Related issues
Closes #3367
#1017 #3349
## Does this PR introduce any user-facing change?
- [ ] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
---
swift/Sources/ForySwift/AnySerializer.swift | 471 +++++++++++++++++++++
swift/Sources/ForySwift/Context.swift | 108 +++++
swift/Sources/ForySwift/Fory.swift | 417 ++++++++++++++++++
swift/Sources/ForySwift/RefResolver.swift | 8 +
swift/Sources/ForySwift/TypeResolver.swift | 246 +++++++++++
.../Sources/ForySwiftMacros/ForyObjectMacro.swift | 241 ++++++++++-
swift/Sources/ForySwiftXlangPeer/main.swift | 248 +----------
swift/Tests/ForySwiftTests/ForySwiftTests.swift | 165 ++++++++
8 files changed, 1654 insertions(+), 250 deletions(-)
diff --git a/swift/Sources/ForySwift/AnySerializer.swift
b/swift/Sources/ForySwift/AnySerializer.swift
new file mode 100644
index 000000000..c98f5968f
--- /dev/null
+++ b/swift/Sources/ForySwift/AnySerializer.swift
@@ -0,0 +1,471 @@
+// 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
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+import Foundation
+
+private enum DynamicAnyMapHeader {
+ static let keyNull: UInt8 = 0b0000_0010
+ static let declaredKeyType: UInt8 = 0b0000_0100
+ static let valueNull: UInt8 = 0b0001_0000
+}
+
+public struct ForyAnyNullValue: Serializer {
+ public init() {}
+
+ public static func foryDefault() -> ForyAnyNullValue {
+ ForyAnyNullValue()
+ }
+
+ public static var staticTypeId: ForyTypeId {
+ .none
+ }
+
+ public var foryIsNone: Bool {
+ true
+ }
+
+ public func foryWriteData(_ context: WriteContext, hasGenerics: Bool)
throws {
+ _ = context
+ _ = hasGenerics
+ }
+
+ public static func foryReadData(_ context: ReadContext) throws ->
ForyAnyNullValue {
+ _ = context
+ return ForyAnyNullValue()
+ }
+}
+
+private protocol OptionalTypeMarker {
+ static var noneValue: Self { get }
+}
+
+extension Optional: OptionalTypeMarker {
+ static var noneValue: Optional<Wrapped> { nil }
+}
+
+private struct DynamicAnyValue: Serializer {
+ var value: Any = ForyAnyNullValue()
+
+ init(_ value: Any) {
+ self.value = value
+ }
+
+ static func foryDefault() -> DynamicAnyValue {
+ DynamicAnyValue(ForyAnyNullValue())
+ }
+
+ static var staticTypeId: ForyTypeId {
+ .unknown
+ }
+
+ static var isNullableType: Bool {
+ true
+ }
+
+ static var isReferenceTrackableType: Bool {
+ true
+ }
+
+ var foryIsNone: Bool {
+ value is ForyAnyNullValue
+ }
+
+ static func wrapped(_ value: Any?) -> DynamicAnyValue {
+ guard let value else {
+ return .foryDefault()
+ }
+ guard let unwrapped = unwrapOptionalAny(value) else {
+ return .foryDefault()
+ }
+ if unwrapped is NSNull {
+ return .foryDefault()
+ }
+ return DynamicAnyValue(unwrapped)
+ }
+
+ func anyValue() -> Any? {
+ foryIsNone ? nil : value
+ }
+
+ func anyValueForCollection() -> Any {
+ foryIsNone ? NSNull() : value
+ }
+
+ func foryWriteData(_ context: WriteContext, hasGenerics: Bool) throws {
+ if foryIsNone {
+ return
+ }
+ try writeAnyPayload(value, context: context, hasGenerics: hasGenerics)
+ }
+
+ static func foryReadData(_ context: ReadContext) throws -> DynamicAnyValue
{
+ guard let typeInfo = context.dynamicTypeInfo(for: Self.self) else {
+ throw ForyError.invalidData("dynamic Any value requires type info")
+ }
+ context.clearDynamicTypeInfo(for: Self.self)
+ if typeInfo.wireTypeID == .none {
+ return .foryDefault()
+ }
+ return DynamicAnyValue(try
context.typeResolver.readDynamicValue(typeInfo: typeInfo, context: context))
+ }
+
+ static func foryWriteTypeInfo(_ context: WriteContext) throws {
+ _ = context
+ throw ForyError.invalidData("dynamic Any value type info is
runtime-only")
+ }
+
+ func foryWriteTypeInfo(_ context: WriteContext) throws {
+ if foryIsNone {
+ context.writer.writeUInt8(UInt8(truncatingIfNeeded:
ForyTypeId.none.rawValue))
+ return
+ }
+ try writeAnyTypeInfo(value, context: context)
+ }
+
+ static func foryReadTypeInfo(_ context: ReadContext) throws {
+ let typeInfo = try context.typeResolver.readDynamicTypeInfo(context:
context)
+ context.setDynamicTypeInfo(for: Self.self, typeInfo)
+ }
+
+ func foryWrite(
+ _ context: WriteContext,
+ refMode: RefMode,
+ writeTypeInfo: Bool,
+ hasGenerics: Bool
+ ) throws {
+ if refMode != .none {
+ if foryIsNone {
+ context.writer.writeInt8(RefFlag.null.rawValue)
+ return
+ }
+ if refMode == .tracking, anyValueIsReferenceTrackable(value), let
object = value as AnyObject? {
+ if context.refWriter.tryWriteReference(writer: context.writer,
object: object) {
+ return
+ }
+ } else {
+ context.writer.writeInt8(RefFlag.notNullValue.rawValue)
+ }
+ }
+
+ if writeTypeInfo {
+ try foryWriteTypeInfo(context)
+ }
+ try foryWriteData(context, hasGenerics: hasGenerics)
+ }
+
+ static func foryRead(
+ _ context: ReadContext,
+ refMode: RefMode,
+ readTypeInfo: Bool
+ ) throws -> DynamicAnyValue {
+ if refMode != .none {
+ let rawFlag = try context.reader.readInt8()
+ guard let flag = RefFlag(rawValue: rawFlag) else {
+ throw ForyError.refError("invalid ref flag \(rawFlag)")
+ }
+
+ switch flag {
+ case .null:
+ return .foryDefault()
+ case .ref:
+ let refID = try context.reader.readVarUInt32()
+ let referenced = try context.refReader.readRefValue(refID)
+ if let value = referenced as? DynamicAnyValue {
+ return value
+ }
+ if referenced is NSNull {
+ return .foryDefault()
+ }
+ return DynamicAnyValue(referenced)
+ case .refValue:
+ let reservedRefID = context.refReader.reserveRefID()
+ context.pushPendingReference(reservedRefID)
+ if readTypeInfo {
+ try foryReadTypeInfo(context)
+ }
+ let value = try foryReadData(context)
+ context.finishPendingReferenceIfNeeded(value)
+ context.popPendingReference()
+ return value
+ case .notNullValue:
+ break
+ }
+ }
+
+ if readTypeInfo {
+ try foryReadTypeInfo(context)
+ }
+ return try foryReadData(context)
+ }
+}
+
+private func unwrapOptionalAny(_ value: Any) -> Any? {
+ let mirror = Mirror(reflecting: value)
+ guard mirror.displayStyle == .optional else {
+ return value
+ }
+ guard let (_, child) = mirror.children.first else {
+ return nil
+ }
+ return child
+}
+
+private func anyValueIsReferenceTrackable(_ value: Any) -> Bool {
+ guard let serializer = value as? any Serializer else {
+ return false
+ }
+ return type(of: serializer).isReferenceTrackableType
+}
+
+private func writeAnyTypeInfo(_ value: Any, context: WriteContext) throws {
+ if let serializer = value as? any Serializer {
+ try serializer.foryWriteTypeInfo(context)
+ return
+ }
+
+ if value is [Any] {
+ context.writer.writeUInt8(UInt8(truncatingIfNeeded:
ForyTypeId.list.rawValue))
+ return
+ }
+ if value is [String: Any] || value is [Int32: Any] {
+ context.writer.writeUInt8(UInt8(truncatingIfNeeded:
ForyTypeId.map.rawValue))
+ return
+ }
+
+ throw ForyError.invalidData("unsupported dynamic Any runtime type
\(type(of: value))")
+}
+
+private func writeAnyPayload(_ value: Any, context: WriteContext, hasGenerics:
Bool) throws {
+ if let serializer = value as? any Serializer {
+ try serializer.foryWriteData(context, hasGenerics: hasGenerics)
+ return
+ }
+ if let list = value as? [Any] {
+ try writeAnyList(list, context: context, refMode: .none, hasGenerics:
hasGenerics)
+ return
+ }
+ if let map = value as? [String: Any] {
+ // Always include key type info for dynamic map payload.
+ try writeStringAnyMap(map, context: context, refMode: .none,
hasGenerics: false)
+ return
+ }
+ if let map = value as? [Int32: Any] {
+ // Always include key type info for dynamic map payload.
+ try writeInt32AnyMap(map, context: context, refMode: .none,
hasGenerics: false)
+ return
+ }
+ throw ForyError.invalidData("unsupported dynamic Any runtime type
\(type(of: value))")
+}
+
+public func castAnyDynamicValue<T>(_ value: Any?, to type: T.Type) throws -> T
{
+ _ = type
+ if value == nil {
+ if T.self == Any.self {
+ return ForyAnyNullValue() as! T
+ }
+ if T.self == AnyObject.self {
+ return NSNull() as! T
+ }
+ if T.self == (any Serializer).self {
+ return ForyAnyNullValue() as! T
+ }
+ if let optionalType = T.self as? any OptionalTypeMarker.Type {
+ return optionalType.noneValue as! T
+ }
+ }
+
+ guard let typed = value as? T else {
+ throw ForyError.invalidData("cannot cast dynamic Any value to \(type)")
+ }
+ return typed
+}
+
+public func writeAny(
+ _ value: Any?,
+ context: WriteContext,
+ refMode: RefMode,
+ writeTypeInfo: Bool = true,
+ hasGenerics: Bool = false
+) throws {
+ try DynamicAnyValue.wrapped(value).foryWrite(
+ context,
+ refMode: refMode,
+ writeTypeInfo: writeTypeInfo,
+ hasGenerics: hasGenerics
+ )
+}
+
+public func readAny(
+ context: ReadContext,
+ refMode: RefMode,
+ readTypeInfo: Bool = true
+) throws -> Any? {
+ try DynamicAnyValue.foryRead(context, refMode: refMode, readTypeInfo:
readTypeInfo).anyValue()
+}
+
+public func writeAnyList(
+ _ value: [Any]?,
+ context: WriteContext,
+ refMode: RefMode,
+ writeTypeInfo: Bool = false,
+ hasGenerics: Bool = true
+) throws {
+ let wrapped = value?.map { DynamicAnyValue.wrapped($0) }
+ try wrapped.foryWrite(
+ context,
+ refMode: refMode,
+ writeTypeInfo: writeTypeInfo,
+ hasGenerics: hasGenerics
+ )
+}
+
+public func readAnyList(
+ context: ReadContext,
+ refMode: RefMode,
+ readTypeInfo: Bool = false
+) throws -> [Any]? {
+ let wrapped: [DynamicAnyValue]? = try [DynamicAnyValue]?.foryRead(
+ context,
+ refMode: refMode,
+ readTypeInfo: readTypeInfo
+ )
+ return wrapped?.map { $0.anyValueForCollection() }
+}
+
+public func writeStringAnyMap(
+ _ value: [String: Any]?,
+ context: WriteContext,
+ refMode: RefMode,
+ writeTypeInfo: Bool = false,
+ hasGenerics: Bool = true
+) throws {
+ let wrapped = value?.reduce(into: [String: DynamicAnyValue]()) { result,
pair in
+ result[pair.key] = DynamicAnyValue.wrapped(pair.value)
+ }
+ try wrapped.foryWrite(
+ context,
+ refMode: refMode,
+ writeTypeInfo: writeTypeInfo,
+ hasGenerics: hasGenerics
+ )
+}
+
+public func readStringAnyMap(
+ context: ReadContext,
+ refMode: RefMode,
+ readTypeInfo: Bool = false
+) throws -> [String: Any]? {
+ let wrapped: [String: DynamicAnyValue]? = try [String:
DynamicAnyValue]?.foryRead(
+ context,
+ refMode: refMode,
+ readTypeInfo: readTypeInfo
+ )
+ guard let wrapped else {
+ return nil
+ }
+ var map: [String: Any] = [:]
+ map.reserveCapacity(wrapped.count)
+ for pair in wrapped {
+ map[pair.key] = pair.value.anyValueForCollection()
+ }
+ return map
+}
+
+public func writeInt32AnyMap(
+ _ value: [Int32: Any]?,
+ context: WriteContext,
+ refMode: RefMode,
+ writeTypeInfo: Bool = false,
+ hasGenerics: Bool = true
+) throws {
+ let wrapped = value?.reduce(into: [Int32: DynamicAnyValue]()) { result,
pair in
+ result[pair.key] = DynamicAnyValue.wrapped(pair.value)
+ }
+ try wrapped.foryWrite(
+ context,
+ refMode: refMode,
+ writeTypeInfo: writeTypeInfo,
+ hasGenerics: hasGenerics
+ )
+}
+
+public func readInt32AnyMap(
+ context: ReadContext,
+ refMode: RefMode,
+ readTypeInfo: Bool = false
+) throws -> [Int32: Any]? {
+ let wrapped: [Int32: DynamicAnyValue]? = try [Int32:
DynamicAnyValue]?.foryRead(
+ context,
+ refMode: refMode,
+ readTypeInfo: readTypeInfo
+ )
+ guard let wrapped else {
+ return nil
+ }
+ var map: [Int32: Any] = [:]
+ map.reserveCapacity(wrapped.count)
+ for pair in wrapped {
+ map[pair.key] = pair.value.anyValueForCollection()
+ }
+ return map
+}
+
+func readDynamicAnyMapValue(context: ReadContext) throws -> Any {
+ let mapStart = context.reader.getCursor()
+ let keyTypeID = try peekDynamicMapKeyTypeID(context: context)
+ context.reader.setCursor(mapStart)
+
+ switch keyTypeID {
+ case .int32, .varint32:
+ return try readInt32AnyMap(context: context, refMode: .none) ?? [:]
+ case nil, .string:
+ return try readStringAnyMap(context: context, refMode: .none) ?? [:]
+ default:
+ throw ForyError.invalidData("unsupported dynamic map key type
\(String(describing: keyTypeID))")
+ }
+}
+
+private func peekDynamicMapKeyTypeID(context: ReadContext) throws ->
ForyTypeId? {
+ let start = context.reader.getCursor()
+ defer {
+ context.reader.setCursor(start)
+ }
+
+ let length = Int(try context.reader.readVarUInt32())
+ if length == 0 {
+ return nil
+ }
+
+ let header = try context.reader.readUInt8()
+ let keyNull = (header & DynamicAnyMapHeader.keyNull) != 0
+ let keyDeclared = (header & DynamicAnyMapHeader.declaredKeyType) != 0
+ let valueNull = (header & DynamicAnyMapHeader.valueNull) != 0
+
+ if keyDeclared {
+ return nil
+ }
+ if keyNull {
+ return nil
+ }
+
+ if !valueNull {
+ _ = try context.reader.readUInt8()
+ }
+
+ let rawTypeID = try context.reader.readVarUInt32()
+ return ForyTypeId(rawValue: rawTypeID)
+}
diff --git a/swift/Sources/ForySwift/Context.swift
b/swift/Sources/ForySwift/Context.swift
index 8f09fb718..c7aefa8ad 100644
--- a/swift/Sources/ForySwift/Context.swift
+++ b/swift/Sources/ForySwift/Context.swift
@@ -371,3 +371,111 @@ public final class ReadContext {
metaStringReadState.reset()
}
}
+
+public extension WriteContext {
+ func writeAny(
+ _ value: Any?,
+ refMode: RefMode,
+ writeTypeInfo: Bool = true,
+ hasGenerics: Bool = false
+ ) throws {
+ try ForySwift.writeAny(
+ value,
+ context: self,
+ refMode: refMode,
+ writeTypeInfo: writeTypeInfo,
+ hasGenerics: hasGenerics
+ )
+ }
+
+ func writeAnyList(
+ _ value: [Any]?,
+ refMode: RefMode,
+ writeTypeInfo: Bool = false,
+ hasGenerics: Bool = true
+ ) throws {
+ try ForySwift.writeAnyList(
+ value,
+ context: self,
+ refMode: refMode,
+ writeTypeInfo: writeTypeInfo,
+ hasGenerics: hasGenerics
+ )
+ }
+
+ func writeStringAnyMap(
+ _ value: [String: Any]?,
+ refMode: RefMode,
+ writeTypeInfo: Bool = false,
+ hasGenerics: Bool = true
+ ) throws {
+ try ForySwift.writeStringAnyMap(
+ value,
+ context: self,
+ refMode: refMode,
+ writeTypeInfo: writeTypeInfo,
+ hasGenerics: hasGenerics
+ )
+ }
+
+ func writeInt32AnyMap(
+ _ value: [Int32: Any]?,
+ refMode: RefMode,
+ writeTypeInfo: Bool = false,
+ hasGenerics: Bool = true
+ ) throws {
+ try ForySwift.writeInt32AnyMap(
+ value,
+ context: self,
+ refMode: refMode,
+ writeTypeInfo: writeTypeInfo,
+ hasGenerics: hasGenerics
+ )
+ }
+}
+
+public extension ReadContext {
+ func readAny(
+ refMode: RefMode,
+ readTypeInfo: Bool = true
+ ) throws -> Any? {
+ try ForySwift.readAny(
+ context: self,
+ refMode: refMode,
+ readTypeInfo: readTypeInfo
+ )
+ }
+
+ func readAnyList(
+ refMode: RefMode,
+ readTypeInfo: Bool = false
+ ) throws -> [Any]? {
+ try ForySwift.readAnyList(
+ context: self,
+ refMode: refMode,
+ readTypeInfo: readTypeInfo
+ )
+ }
+
+ func readStringAnyMap(
+ refMode: RefMode,
+ readTypeInfo: Bool = false
+ ) throws -> [String: Any]? {
+ try ForySwift.readStringAnyMap(
+ context: self,
+ refMode: refMode,
+ readTypeInfo: readTypeInfo
+ )
+ }
+
+ func readInt32AnyMap(
+ refMode: RefMode,
+ readTypeInfo: Bool = false
+ ) throws -> [Int32: Any]? {
+ try ForySwift.readInt32AnyMap(
+ context: self,
+ refMode: refMode,
+ readTypeInfo: readTypeInfo
+ )
+ }
+}
diff --git a/swift/Sources/ForySwift/Fory.swift
b/swift/Sources/ForySwift/Fory.swift
index d0e97c685..d85c284f5 100644
--- a/swift/Sources/ForySwift/Fory.swift
+++ b/swift/Sources/ForySwift/Fory.swift
@@ -131,6 +131,423 @@ public final class Fory {
return value
}
+ @_disfavoredOverload
+ public func serialize(_ value: Any) throws -> Data {
+ let writer = ByteWriter()
+ writeHead(writer: writer, isNone: false)
+
+ let context = WriteContext(
+ writer: writer,
+ typeResolver: typeResolver,
+ trackRef: config.trackRef,
+ compatible: config.compatible,
+ compatibleTypeDefState: CompatibleTypeDefWriteState(),
+ metaStringWriteState: MetaStringWriteState()
+ )
+ let refMode: RefMode = config.trackRef ? .tracking : .nullOnly
+ try context.writeAny(value, refMode: refMode, writeTypeInfo: true,
hasGenerics: false)
+ context.resetObjectState()
+ return writer.toData()
+ }
+
+ @_disfavoredOverload
+ public func deserialize(_ data: Data, as _: Any.Type = Any.self) throws ->
Any {
+ let reader = ByteReader(data: data)
+ let isNone = try readHead(reader: reader)
+ if isNone {
+ return ForyAnyNullValue()
+ }
+
+ let context = ReadContext(
+ reader: reader,
+ typeResolver: typeResolver,
+ trackRef: config.trackRef,
+ compatible: config.compatible,
+ compatibleTypeDefState: CompatibleTypeDefReadState(),
+ metaStringReadState: MetaStringReadState()
+ )
+ let refMode: RefMode = config.trackRef ? .tracking : .nullOnly
+ let value = try castAnyDynamicValue(
+ context.readAny(refMode: refMode, readTypeInfo: true),
+ to: Any.self
+ )
+ context.resetObjectState()
+ return value
+ }
+
+ @_disfavoredOverload
+ public func serialize(_ value: AnyObject) throws -> Data {
+ let writer = ByteWriter()
+ writeHead(writer: writer, isNone: false)
+
+ let context = WriteContext(
+ writer: writer,
+ typeResolver: typeResolver,
+ trackRef: config.trackRef,
+ compatible: config.compatible,
+ compatibleTypeDefState: CompatibleTypeDefWriteState(),
+ metaStringWriteState: MetaStringWriteState()
+ )
+ let refMode: RefMode = config.trackRef ? .tracking : .nullOnly
+ try context.writeAny(value, refMode: refMode, writeTypeInfo: true,
hasGenerics: false)
+ context.resetObjectState()
+ return writer.toData()
+ }
+
+ @_disfavoredOverload
+ public func deserialize(_ data: Data, as _: AnyObject.Type =
AnyObject.self) throws -> AnyObject {
+ let reader = ByteReader(data: data)
+ let isNone = try readHead(reader: reader)
+ if isNone {
+ return NSNull()
+ }
+
+ let context = ReadContext(
+ reader: reader,
+ typeResolver: typeResolver,
+ trackRef: config.trackRef,
+ compatible: config.compatible,
+ compatibleTypeDefState: CompatibleTypeDefReadState(),
+ metaStringReadState: MetaStringReadState()
+ )
+ let refMode: RefMode = config.trackRef ? .tracking : .nullOnly
+ let value = try castAnyDynamicValue(
+ context.readAny(refMode: refMode, readTypeInfo: true),
+ to: AnyObject.self
+ )
+ context.resetObjectState()
+ return value
+ }
+
+ @_disfavoredOverload
+ public func serialize(_ value: any Serializer) throws -> Data {
+ let writer = ByteWriter()
+ writeHead(writer: writer, isNone: false)
+
+ let context = WriteContext(
+ writer: writer,
+ typeResolver: typeResolver,
+ trackRef: config.trackRef,
+ compatible: config.compatible,
+ compatibleTypeDefState: CompatibleTypeDefWriteState(),
+ metaStringWriteState: MetaStringWriteState()
+ )
+ let refMode: RefMode = config.trackRef ? .tracking : .nullOnly
+ try context.writeAny(value, refMode: refMode, writeTypeInfo: true,
hasGenerics: false)
+ context.resetObjectState()
+ return writer.toData()
+ }
+
+ @_disfavoredOverload
+ public func deserialize(_ data: Data, as _: (any Serializer).Type = (any
Serializer).self) throws -> any Serializer {
+ let reader = ByteReader(data: data)
+ let isNone = try readHead(reader: reader)
+ if isNone {
+ return ForyAnyNullValue()
+ }
+
+ let context = ReadContext(
+ reader: reader,
+ typeResolver: typeResolver,
+ trackRef: config.trackRef,
+ compatible: config.compatible,
+ compatibleTypeDefState: CompatibleTypeDefReadState(),
+ metaStringReadState: MetaStringReadState()
+ )
+ let refMode: RefMode = config.trackRef ? .tracking : .nullOnly
+ let value = try castAnyDynamicValue(
+ context.readAny(refMode: refMode, readTypeInfo: true),
+ to: (any Serializer).self
+ )
+ context.resetObjectState()
+ return value
+ }
+
+ @_disfavoredOverload
+ public func serialize(_ value: [Any]) throws -> Data {
+ let writer = ByteWriter()
+ writeHead(writer: writer, isNone: false)
+
+ let context = WriteContext(
+ writer: writer,
+ typeResolver: typeResolver,
+ trackRef: config.trackRef,
+ compatible: config.compatible,
+ compatibleTypeDefState: CompatibleTypeDefWriteState(),
+ metaStringWriteState: MetaStringWriteState()
+ )
+ let refMode: RefMode = config.trackRef ? .tracking : .nullOnly
+ try context.writeAnyList(value, refMode: refMode, writeTypeInfo: true,
hasGenerics: false)
+ context.resetObjectState()
+ return writer.toData()
+ }
+
+ @_disfavoredOverload
+ public func deserialize(_ data: Data, as _: [Any].Type = [Any].self)
throws -> [Any] {
+ let reader = ByteReader(data: data)
+ let isNone = try readHead(reader: reader)
+ if isNone {
+ return []
+ }
+
+ let context = ReadContext(
+ reader: reader,
+ typeResolver: typeResolver,
+ trackRef: config.trackRef,
+ compatible: config.compatible,
+ compatibleTypeDefState: CompatibleTypeDefReadState(),
+ metaStringReadState: MetaStringReadState()
+ )
+ let refMode: RefMode = config.trackRef ? .tracking : .nullOnly
+ let value = try context.readAnyList(refMode: refMode, readTypeInfo:
true) ?? []
+ context.resetObjectState()
+ return value
+ }
+
+ @_disfavoredOverload
+ public func serialize(_ value: [String: Any]) throws -> Data {
+ let writer = ByteWriter()
+ writeHead(writer: writer, isNone: false)
+
+ let context = WriteContext(
+ writer: writer,
+ typeResolver: typeResolver,
+ trackRef: config.trackRef,
+ compatible: config.compatible,
+ compatibleTypeDefState: CompatibleTypeDefWriteState(),
+ metaStringWriteState: MetaStringWriteState()
+ )
+ let refMode: RefMode = config.trackRef ? .tracking : .nullOnly
+ try context.writeStringAnyMap(value, refMode: refMode, writeTypeInfo:
true, hasGenerics: false)
+ context.resetObjectState()
+ return writer.toData()
+ }
+
+ @_disfavoredOverload
+ public func deserialize(_ data: Data, as _: [String: Any].Type = [String:
Any].self) throws -> [String: Any] {
+ let reader = ByteReader(data: data)
+ let isNone = try readHead(reader: reader)
+ if isNone {
+ return [:]
+ }
+
+ let context = ReadContext(
+ reader: reader,
+ typeResolver: typeResolver,
+ trackRef: config.trackRef,
+ compatible: config.compatible,
+ compatibleTypeDefState: CompatibleTypeDefReadState(),
+ metaStringReadState: MetaStringReadState()
+ )
+ let refMode: RefMode = config.trackRef ? .tracking : .nullOnly
+ let value = try context.readStringAnyMap(refMode: refMode,
readTypeInfo: true) ?? [:]
+ context.resetObjectState()
+ return value
+ }
+
+ @_disfavoredOverload
+ public func serialize(_ value: [Int32: Any]) throws -> Data {
+ let writer = ByteWriter()
+ writeHead(writer: writer, isNone: false)
+
+ let context = WriteContext(
+ writer: writer,
+ typeResolver: typeResolver,
+ trackRef: config.trackRef,
+ compatible: config.compatible,
+ compatibleTypeDefState: CompatibleTypeDefWriteState(),
+ metaStringWriteState: MetaStringWriteState()
+ )
+ let refMode: RefMode = config.trackRef ? .tracking : .nullOnly
+ try context.writeInt32AnyMap(value, refMode: refMode, writeTypeInfo:
true, hasGenerics: false)
+ context.resetObjectState()
+ return writer.toData()
+ }
+
+ @_disfavoredOverload
+ public func deserialize(_ data: Data, as _: [Int32: Any].Type = [Int32:
Any].self) throws -> [Int32: Any] {
+ let reader = ByteReader(data: data)
+ let isNone = try readHead(reader: reader)
+ if isNone {
+ return [:]
+ }
+
+ let context = ReadContext(
+ reader: reader,
+ typeResolver: typeResolver,
+ trackRef: config.trackRef,
+ compatible: config.compatible,
+ compatibleTypeDefState: CompatibleTypeDefReadState(),
+ metaStringReadState: MetaStringReadState()
+ )
+ let refMode: RefMode = config.trackRef ? .tracking : .nullOnly
+ let value = try context.readInt32AnyMap(refMode: refMode,
readTypeInfo: true) ?? [:]
+ context.resetObjectState()
+ return value
+ }
+
+ @_disfavoredOverload
+ public func serializeTo(_ buffer: inout Data, value: [Any]) throws {
+ buffer.append(try serialize(value))
+ }
+
+ @_disfavoredOverload
+ public func serializeTo(_ buffer: inout Data, value: Any) throws {
+ buffer.append(try serialize(value))
+ }
+
+ @_disfavoredOverload
+ public func deserializeFrom(_ reader: ByteReader, as _: Any.Type =
Any.self) throws -> Any {
+ let isNone = try readHead(reader: reader)
+ if isNone {
+ return ForyAnyNullValue()
+ }
+ let context = ReadContext(
+ reader: reader,
+ typeResolver: typeResolver,
+ trackRef: config.trackRef,
+ compatible: config.compatible,
+ compatibleTypeDefState: CompatibleTypeDefReadState(),
+ metaStringReadState: MetaStringReadState()
+ )
+ let refMode: RefMode = config.trackRef ? .tracking : .nullOnly
+ let value = try castAnyDynamicValue(
+ context.readAny(refMode: refMode, readTypeInfo: true),
+ to: Any.self
+ )
+ context.resetObjectState()
+ return value
+ }
+
+ @_disfavoredOverload
+ public func serializeTo(_ buffer: inout Data, value: AnyObject) throws {
+ buffer.append(try serialize(value))
+ }
+
+ @_disfavoredOverload
+ public func deserializeFrom(_ reader: ByteReader, as _: AnyObject.Type =
AnyObject.self) throws -> AnyObject {
+ let isNone = try readHead(reader: reader)
+ if isNone {
+ return NSNull()
+ }
+ let context = ReadContext(
+ reader: reader,
+ typeResolver: typeResolver,
+ trackRef: config.trackRef,
+ compatible: config.compatible,
+ compatibleTypeDefState: CompatibleTypeDefReadState(),
+ metaStringReadState: MetaStringReadState()
+ )
+ let refMode: RefMode = config.trackRef ? .tracking : .nullOnly
+ let value = try castAnyDynamicValue(
+ context.readAny(refMode: refMode, readTypeInfo: true),
+ to: AnyObject.self
+ )
+ context.resetObjectState()
+ return value
+ }
+
+ @_disfavoredOverload
+ public func serializeTo(_ buffer: inout Data, value: any Serializer)
throws {
+ buffer.append(try serialize(value))
+ }
+
+ @_disfavoredOverload
+ public func deserializeFrom(
+ _ reader: ByteReader,
+ as _: (any Serializer).Type = (any Serializer).self
+ ) throws -> any Serializer {
+ let isNone = try readHead(reader: reader)
+ if isNone {
+ return ForyAnyNullValue()
+ }
+ let context = ReadContext(
+ reader: reader,
+ typeResolver: typeResolver,
+ trackRef: config.trackRef,
+ compatible: config.compatible,
+ compatibleTypeDefState: CompatibleTypeDefReadState(),
+ metaStringReadState: MetaStringReadState()
+ )
+ let refMode: RefMode = config.trackRef ? .tracking : .nullOnly
+ let value = try castAnyDynamicValue(
+ context.readAny(refMode: refMode, readTypeInfo: true),
+ to: (any Serializer).self
+ )
+ context.resetObjectState()
+ return value
+ }
+
+ @_disfavoredOverload
+ public func deserializeFrom(_ reader: ByteReader, as _: [Any].Type =
[Any].self) throws -> [Any] {
+ let isNone = try readHead(reader: reader)
+ if isNone {
+ return []
+ }
+ let context = ReadContext(
+ reader: reader,
+ typeResolver: typeResolver,
+ trackRef: config.trackRef,
+ compatible: config.compatible,
+ compatibleTypeDefState: CompatibleTypeDefReadState(),
+ metaStringReadState: MetaStringReadState()
+ )
+ let refMode: RefMode = config.trackRef ? .tracking : .nullOnly
+ let value = try context.readAnyList(refMode: refMode, readTypeInfo:
true) ?? []
+ context.resetObjectState()
+ return value
+ }
+
+ @_disfavoredOverload
+ public func serializeTo(_ buffer: inout Data, value: [String: Any]) throws
{
+ buffer.append(try serialize(value))
+ }
+
+ @_disfavoredOverload
+ public func deserializeFrom(_ reader: ByteReader, as _: [String: Any].Type
= [String: Any].self) throws -> [String: Any] {
+ let isNone = try readHead(reader: reader)
+ if isNone {
+ return [:]
+ }
+ let context = ReadContext(
+ reader: reader,
+ typeResolver: typeResolver,
+ trackRef: config.trackRef,
+ compatible: config.compatible,
+ compatibleTypeDefState: CompatibleTypeDefReadState(),
+ metaStringReadState: MetaStringReadState()
+ )
+ let refMode: RefMode = config.trackRef ? .tracking : .nullOnly
+ let value = try context.readStringAnyMap(refMode: refMode,
readTypeInfo: true) ?? [:]
+ context.resetObjectState()
+ return value
+ }
+
+ @_disfavoredOverload
+ public func serializeTo(_ buffer: inout Data, value: [Int32: Any]) throws {
+ buffer.append(try serialize(value))
+ }
+
+ @_disfavoredOverload
+ public func deserializeFrom(_ reader: ByteReader, as _: [Int32: Any].Type
= [Int32: Any].self) throws -> [Int32: Any] {
+ let isNone = try readHead(reader: reader)
+ if isNone {
+ return [:]
+ }
+ let context = ReadContext(
+ reader: reader,
+ typeResolver: typeResolver,
+ trackRef: config.trackRef,
+ compatible: config.compatible,
+ compatibleTypeDefState: CompatibleTypeDefReadState(),
+ metaStringReadState: MetaStringReadState()
+ )
+ let refMode: RefMode = config.trackRef ? .tracking : .nullOnly
+ let value = try context.readInt32AnyMap(refMode: refMode,
readTypeInfo: true) ?? [:]
+ context.resetObjectState()
+ return value
+ }
+
public func writeHead(writer: ByteWriter, isNone: Bool) {
var bitmap: UInt8 = 0
if config.xlang {
diff --git a/swift/Sources/ForySwift/RefResolver.swift
b/swift/Sources/ForySwift/RefResolver.swift
index 58a96a39f..901706f48 100644
--- a/swift/Sources/ForySwift/RefResolver.swift
+++ b/swift/Sources/ForySwift/RefResolver.swift
@@ -74,6 +74,14 @@ public final class RefReader {
return value
}
+ public func readRefValue(_ refID: UInt32) throws -> Any {
+ let index = Int(refID)
+ guard refs.indices.contains(index) else {
+ throw ForyError.refError("ref_id out of range: \(refID)")
+ }
+ return refs[index]
+ }
+
public func reset() {
refs.removeAll(keepingCapacity: true)
}
diff --git a/swift/Sources/ForySwift/TypeResolver.swift
b/swift/Sources/ForySwift/TypeResolver.swift
index 31711b2cb..2e4a95fa9 100644
--- a/swift/Sources/ForySwift/TypeResolver.swift
+++ b/swift/Sources/ForySwift/TypeResolver.swift
@@ -44,6 +44,12 @@ private struct TypeNameKey: Hashable {
let typeName: String
}
+private enum DynamicRegistrationMode {
+ case idOnly
+ case nameOnly
+ case mixed
+}
+
private struct TypeReader {
let kind: ForyTypeId
let reader: (ReadContext) throws -> Any
@@ -54,6 +60,7 @@ public final class TypeResolver {
private var bySwiftType: [ObjectIdentifier: RegisteredTypeInfo] = [:]
private var byUserTypeID: [UInt32: TypeReader] = [:]
private var byTypeName: [TypeNameKey: TypeReader] = [:]
+ private var registrationModeByKind: [ForyTypeId: DynamicRegistrationMode]
= [:]
public init() {}
@@ -67,6 +74,7 @@ public final class TypeResolver {
typeName: MetaString.empty(specialChar1: "$", specialChar2: "_")
)
bySwiftType[key] = info
+ markRegistrationMode(kind: info.kind, registerByName: false)
byUserTypeID[id] = TypeReader(
kind: T.staticTypeId,
reader: { context in
@@ -97,6 +105,7 @@ public final class TypeResolver {
typeName: typeNameMeta
)
bySwiftType[key] = info
+ markRegistrationMode(kind: info.kind, registerByName: true)
byTypeName[TypeNameKey(namespace: namespace, typeName: typeName)] =
TypeReader(
kind: T.staticTypeId,
reader: { context in
@@ -160,4 +169,241 @@ public final class TypeResolver {
}
return try entry.reader(context)
}
+
+ public func readDynamicTypeInfo(context: ReadContext) throws ->
DynamicTypeInfo {
+ let rawTypeID = try context.reader.readVarUInt32()
+ guard let wireTypeID = ForyTypeId(rawValue: rawTypeID) else {
+ throw ForyError.invalidData("unknown dynamic type id \(rawTypeID)")
+ }
+
+ switch wireTypeID {
+ case .compatibleStruct, .namedCompatibleStruct:
+ let typeMeta = try context.readCompatibleTypeMeta()
+ if typeMeta.registerByName {
+ return DynamicTypeInfo(
+ wireTypeID: wireTypeID,
+ userTypeID: nil,
+ namespace: typeMeta.namespace,
+ typeName: typeMeta.typeName,
+ compatibleTypeMeta: typeMeta
+ )
+ }
+ return DynamicTypeInfo(
+ wireTypeID: wireTypeID,
+ userTypeID: typeMeta.userTypeID,
+ namespace: nil,
+ typeName: nil,
+ compatibleTypeMeta: typeMeta
+ )
+ case .namedStruct, .namedEnum, .namedExt, .namedUnion:
+ let namespace = try Self.readMetaString(
+ reader: context.reader,
+ decoder: .namespace,
+ encodings: namespaceMetaStringEncodings
+ )
+ let typeName = try Self.readMetaString(
+ reader: context.reader,
+ decoder: .typeName,
+ encodings: typeNameMetaStringEncodings
+ )
+ return DynamicTypeInfo(
+ wireTypeID: wireTypeID,
+ userTypeID: nil,
+ namespace: namespace,
+ typeName: typeName,
+ compatibleTypeMeta: nil
+ )
+ case .structType, .enumType, .ext, .typedUnion:
+ switch try dynamicRegistrationMode(for: wireTypeID) {
+ case .idOnly:
+ return DynamicTypeInfo(
+ wireTypeID: wireTypeID,
+ userTypeID: try context.reader.readVarUInt32(),
+ namespace: nil,
+ typeName: nil,
+ compatibleTypeMeta: nil
+ )
+ case .nameOnly:
+ let namespace = try Self.readMetaString(
+ reader: context.reader,
+ decoder: .namespace,
+ encodings: namespaceMetaStringEncodings
+ )
+ let typeName = try Self.readMetaString(
+ reader: context.reader,
+ decoder: .typeName,
+ encodings: typeNameMetaStringEncodings
+ )
+ return DynamicTypeInfo(
+ wireTypeID: wireTypeID,
+ userTypeID: nil,
+ namespace: namespace,
+ typeName: typeName,
+ compatibleTypeMeta: nil
+ )
+ case .mixed:
+ throw ForyError.invalidData("ambiguous dynamic type
registration mode for \(wireTypeID)")
+ }
+ default:
+ return DynamicTypeInfo(
+ wireTypeID: wireTypeID,
+ userTypeID: nil,
+ namespace: nil,
+ typeName: nil,
+ compatibleTypeMeta: nil
+ )
+ }
+ }
+
+ public func readDynamicValue(typeInfo: DynamicTypeInfo, context:
ReadContext) throws -> Any {
+ let value: Any
+ switch typeInfo.wireTypeID {
+ case .bool:
+ value = try Bool.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .int8:
+ value = try Int8.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .int16:
+ value = try Int16.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .int32:
+ value = try ForyInt32Fixed.foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .varint32:
+ value = try Int32.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .int64:
+ value = try ForyInt64Fixed.foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .varint64:
+ value = try Int64.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .taggedInt64:
+ value = try ForyInt64Tagged.foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .uint8:
+ value = try UInt8.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .uint16:
+ value = try UInt16.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .uint32:
+ value = try ForyUInt32Fixed.foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .varUInt32:
+ value = try UInt32.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .uint64:
+ value = try ForyUInt64Fixed.foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .varUInt64:
+ value = try UInt64.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .taggedUInt64:
+ value = try ForyUInt64Tagged.foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .float32:
+ value = try Float.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .float64:
+ value = try Double.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .string:
+ value = try String.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .binary, .uint8Array:
+ value = try Data.foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .boolArray:
+ value = try [Bool].foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .int8Array:
+ value = try [Int8].foryRead(context, refMode: .none, readTypeInfo:
false)
+ case .int16Array:
+ value = try [Int16].foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .int32Array:
+ value = try [Int32].foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .int64Array:
+ value = try [Int64].foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .uint16Array:
+ value = try [UInt16].foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .uint32Array:
+ value = try [UInt32].foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .uint64Array:
+ value = try [UInt64].foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .float32Array:
+ value = try [Float].foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .float64Array:
+ value = try [Double].foryRead(context, refMode: .none,
readTypeInfo: false)
+ case .list:
+ value = try context.readAnyList(refMode: .none) ?? []
+ case .map:
+ value = try readDynamicAnyMapValue(context: context)
+ case .structType, .enumType, .ext, .typedUnion:
+ if let userTypeID = typeInfo.userTypeID {
+ value = try readByUserTypeID(userTypeID, context: context)
+ } else if let namespace = typeInfo.namespace, let typeName =
typeInfo.typeName {
+ value = try readByTypeName(
+ namespace: namespace.value,
+ typeName: typeName.value,
+ context: context
+ )
+ } else {
+ throw ForyError.invalidData("missing dynamic registration info
for \(typeInfo.wireTypeID)")
+ }
+ case .namedStruct, .namedEnum, .namedExt, .namedUnion:
+ guard let namespace = typeInfo.namespace, let typeName =
typeInfo.typeName else {
+ throw ForyError.invalidData("missing dynamic type name for
\(typeInfo.wireTypeID)")
+ }
+ value = try readByTypeName(
+ namespace: namespace.value,
+ typeName: typeName.value,
+ context: context
+ )
+ case .compatibleStruct, .namedCompatibleStruct:
+ guard let compatibleTypeMeta = typeInfo.compatibleTypeMeta else {
+ throw ForyError.invalidData("missing compatible type meta for
\(typeInfo.wireTypeID)")
+ }
+ if compatibleTypeMeta.registerByName {
+ value = try readByTypeName(
+ namespace: compatibleTypeMeta.namespace.value,
+ typeName: compatibleTypeMeta.typeName.value,
+ context: context,
+ compatibleTypeMeta: compatibleTypeMeta
+ )
+ } else {
+ guard let userTypeID = compatibleTypeMeta.userTypeID else {
+ throw ForyError.invalidData("missing user type id in
compatible dynamic type meta")
+ }
+ value = try readByUserTypeID(
+ userTypeID,
+ context: context,
+ compatibleTypeMeta: compatibleTypeMeta
+ )
+ }
+ case .none:
+ value = ForyAnyNullValue()
+ default:
+ throw ForyError.invalidData("unsupported dynamic type id
\(typeInfo.wireTypeID)")
+ }
+ return value
+ }
+
+ private func markRegistrationMode(kind: ForyTypeId, registerByName: Bool) {
+ let mode: DynamicRegistrationMode = registerByName ? .nameOnly :
.idOnly
+ guard let existing = registrationModeByKind[kind] else {
+ registrationModeByKind[kind] = mode
+ return
+ }
+ if existing != mode {
+ registrationModeByKind[kind] = .mixed
+ }
+ }
+
+ private func dynamicRegistrationMode(for kind: ForyTypeId) throws ->
DynamicRegistrationMode {
+ guard let mode = registrationModeByKind[kind] else {
+ throw ForyError.typeNotRegistered("no dynamic registration mode
for kind \(kind)")
+ }
+ return mode
+ }
+
+ private static func readMetaString(
+ reader: ByteReader,
+ decoder: MetaStringDecoder,
+ encodings: [MetaStringEncoding]
+ ) throws -> MetaString {
+ let header = try reader.readUInt8()
+ let encodingIndex = Int(header & 0b11)
+ guard encodingIndex < encodings.count else {
+ throw ForyError.invalidData("invalid meta string encoding index")
+ }
+
+ var length = Int(header >> 2)
+ if length >= 0b11_1111 {
+ length = 0b11_1111 + Int(try reader.readVarUInt32())
+ }
+ let bytes = try reader.readBytes(count: length)
+ return try decoder.decode(bytes: bytes, encoding:
encodings[encodingIndex])
+ }
}
diff --git a/swift/Sources/ForySwiftMacros/ForyObjectMacro.swift
b/swift/Sources/ForySwiftMacros/ForyObjectMacro.swift
index bb475efa9..b0a5bc5f3 100644
--- a/swift/Sources/ForySwiftMacros/ForyObjectMacro.swift
+++ b/swift/Sources/ForySwiftMacros/ForyObjectMacro.swift
@@ -100,6 +100,13 @@ private enum FieldEncoding: String {
case tagged
}
+private enum DynamicAnyCodecKind {
+ case anyValue
+ case anyList
+ case stringAnyMap
+ case int32AnyMap
+}
+
private struct ParsedField {
let name: String
let typeText: String
@@ -114,6 +121,7 @@ private struct ParsedField {
let isCompressedNumeric: Bool
let primitiveSize: Int
let customCodecType: String?
+ let dynamicAnyCodec: DynamicAnyCodecKind?
}
private struct ParsedDecl {
@@ -170,6 +178,7 @@ private func parseFields(_ declaration: some
DeclGroupSyntax) throws -> ParsedDe
concreteType: concreteType,
fieldEncoding: fieldEncoding
)
+ let dynamicAnyCodec = try resolveDynamicAnyCodec(rawType: rawType)
let classification = typeResolution.classification
let group: Int
if classification.isPrimitive {
@@ -196,7 +205,8 @@ private func parseFields(_ declaration: some
DeclGroupSyntax) throws -> ParsedDe
typeID: classification.typeID,
isCompressedNumeric: classification.isCompressedNumeric,
primitiveSize: classification.primitiveSize,
- customCodecType: typeResolution.customCodecType
+ customCodecType: typeResolution.customCodecType,
+ dynamicAnyCodec: dynamicAnyCodec
)
)
originalIndex += 1
@@ -383,6 +393,62 @@ private func resolveFieldType(
}
}
+private func resolveDynamicAnyCodec(rawType: String) throws ->
DynamicAnyCodecKind? {
+ let optional = unwrapOptional(rawType)
+ let concreteType = trimType(optional.type)
+
+ if isDynamicAnyConcreteType(concreteType) {
+ return .anyValue
+ }
+
+ if let elementType = parseArrayElement(concreteType),
containsDynamicAny(typeText: elementType) {
+ return .anyList
+ }
+
+ if let elementType = parseSetElement(concreteType),
containsDynamicAny(typeText: elementType) {
+ throw MacroExpansionErrorMessage("Set<...> with Any elements is not
supported by @ForyObject yet")
+ }
+
+ if let (keyType, valueType) = parseDictionary(concreteType),
+ containsDynamicAny(typeText: keyType) || containsDynamicAny(typeText:
valueType) {
+ let normalizedKeyType = trimType(unwrapOptional(keyType).type)
+ if normalizedKeyType == "String" {
+ return .stringAnyMap
+ }
+ if normalizedKeyType == "Int32" {
+ return .int32AnyMap
+ }
+ throw MacroExpansionErrorMessage(
+ "Dictionary<\(keyType), ...> with Any values is only supported for
String or Int32 keys"
+ )
+ }
+
+ return nil
+}
+
+private func containsDynamicAny(typeText: String) -> Bool {
+ let optional = unwrapOptional(typeText)
+ let concreteType = trimType(optional.type)
+
+ if isDynamicAnyConcreteType(concreteType) {
+ return true
+ }
+
+ if let elementType = parseArrayElement(concreteType) {
+ return containsDynamicAny(typeText: elementType)
+ }
+
+ if let elementType = parseSetElement(concreteType) {
+ return containsDynamicAny(typeText: elementType)
+ }
+
+ if let (keyType, valueType) = parseDictionary(concreteType) {
+ return containsDynamicAny(typeText: keyType) ||
containsDynamicAny(typeText: valueType)
+ }
+
+ return false
+}
+
private func sortFields(_ fields: [ParsedField]) -> [ParsedField] {
fields.sorted { lhs, rhs in
if lhs.group != rhs.group {
@@ -459,7 +525,17 @@ private func buildSchemaFingerprint(fields: [ParsedField])
-> String {
.map { field -> String in
let typeID = fingerprintTypeID(for: field)
let nullable = field.isOptional ? "1" : "0"
- let trackRefExpr = "(trackRef &&
\(field.typeText).isReferenceTrackableType) ? 1 : 0"
+ let trackRefExpr: String
+ if let dynamicAnyCodec = field.dynamicAnyCodec {
+ switch dynamicAnyCodec {
+ case .anyValue:
+ trackRefExpr = "trackRef ? 1 : 0"
+ case .anyList, .stringAnyMap, .int32AnyMap:
+ trackRefExpr = "0"
+ }
+ } else {
+ trackRefExpr = "(trackRef &&
\(field.typeText).isReferenceTrackableType) ? 1 : 0"
+ }
return
"\"\(field.fieldIdentifier),\(typeID),\\(\(trackRefExpr)),\(nullable);\""
}
if entries.isEmpty {
@@ -496,7 +572,12 @@ private func buildDefaultDecl(isClass: Bool, fields:
[ParsedField]) -> String {
let args = fields
.sorted(by: { $0.originalIndex < $1.originalIndex })
- .map { "\($0.name): \($0.typeText).foryDefault()" }
+ .map { field in
+ if field.dynamicAnyCodec != nil {
+ return "\(field.name): \(dynamicAnyDefaultExpr(typeText:
field.typeText))"
+ }
+ return "\(field.name): \(field.typeText).foryDefault()"
+ }
.joined(separator: ",\n ")
return """
@@ -538,6 +619,13 @@ private func buildWriteDataDecl(sortedFields:
[ParsedField]) -> String {
private func schemaWriteLine(for field: ParsedField) -> String {
let refMode = fieldRefModeExpression(field)
+ if let dynamicAnyCodec = field.dynamicAnyCodec {
+ return dynamicAnyWriteLine(
+ field: field,
+ dynamicAnyCodec: dynamicAnyCodec,
+ refModeExpr: refMode
+ )
+ }
let hasGenerics = field.isCollection ? "true" : "false"
if let codecType = field.customCodecType {
if field.isOptional {
@@ -550,6 +638,13 @@ private func schemaWriteLine(for field: ParsedField) ->
String {
private func compatibleWriteLine(for field: ParsedField) -> String {
let refMode = fieldRefModeExpression(field)
+ if let dynamicAnyCodec = field.dynamicAnyCodec {
+ return dynamicAnyWriteLine(
+ field: field,
+ dynamicAnyCodec: dynamicAnyCodec,
+ refModeExpr: refMode
+ )
+ }
let hasGenerics = field.isCollection ? "true" : "false"
if let codecType = field.customCodecType {
if field.isOptional {
@@ -648,7 +743,12 @@ private func buildReadDataDecl(isClass: Bool, fields:
[ParsedField], sortedField
.joined(separator: ",\n ")
let compatibleDefaults = fields
.sorted(by: { $0.originalIndex < $1.originalIndex })
- .map { "var __\($0.name) = \($0.typeText).foryDefault()" }
+ .map { field in
+ if field.dynamicAnyCodec != nil {
+ return "var __\(field.name): \(field.typeText) =
\(dynamicAnyDefaultExpr(typeText: field.typeText))"
+ }
+ return "var __\(field.name) = \(field.typeText).foryDefault()"
+ }
.joined(separator: "\n ")
let compatibleCases = sortedFields.map { field -> String in
let valueExpr = readFieldExpr(
@@ -693,6 +793,14 @@ private func readFieldExpr(
refModeExpr: String,
readTypeInfoExpr: String
) -> String {
+ if let dynamicAnyCodec = field.dynamicAnyCodec {
+ return dynamicAnyReadExpr(
+ field: field,
+ dynamicAnyCodec: dynamicAnyCodec,
+ refModeExpr: refModeExpr,
+ readTypeInfoExpr: readTypeInfoExpr
+ )
+ }
if let codecType = field.customCodecType {
if field.isOptional {
return "try \(codecType)?.foryRead(context, refMode:
\(refModeExpr), readTypeInfo: false)?.rawValue"
@@ -702,8 +810,61 @@ private func readFieldExpr(
return "try \(field.typeText).foryRead(context, refMode: \(refModeExpr),
readTypeInfo: \(readTypeInfoExpr))"
}
+private func dynamicAnyWriteLine(
+ field: ParsedField,
+ dynamicAnyCodec: DynamicAnyCodecKind,
+ refModeExpr: String
+) -> String {
+ switch dynamicAnyCodec {
+ case .anyValue:
+ return "try context.writeAny(self.\(field.name), refMode:
\(refModeExpr), writeTypeInfo: true, hasGenerics: false)"
+ case .anyList:
+ if field.isOptional {
+ return "try context.writeAnyList(self.\(field.name) as [Any]?,
refMode: \(refModeExpr), hasGenerics: true)"
+ }
+ return "try context.writeAnyList(self.\(field.name) as [Any], refMode:
\(refModeExpr), hasGenerics: true)"
+ case .stringAnyMap:
+ if field.isOptional {
+ return "try context.writeStringAnyMap(self.\(field.name) as
[String: Any]?, refMode: \(refModeExpr), hasGenerics: true)"
+ }
+ return "try context.writeStringAnyMap(self.\(field.name) as [String:
Any], refMode: \(refModeExpr), hasGenerics: true)"
+ case .int32AnyMap:
+ if field.isOptional {
+ return "try context.writeInt32AnyMap(self.\(field.name) as [Int32:
Any]?, refMode: \(refModeExpr), hasGenerics: true)"
+ }
+ return "try context.writeInt32AnyMap(self.\(field.name) as [Int32:
Any], refMode: \(refModeExpr), hasGenerics: true)"
+ }
+}
+
+private func dynamicAnyReadExpr(
+ field: ParsedField,
+ dynamicAnyCodec: DynamicAnyCodecKind,
+ refModeExpr: String,
+ readTypeInfoExpr _: String
+) -> String {
+ let metatypeExpr = "(\(field.typeText)).self"
+ switch dynamicAnyCodec {
+ case .anyValue:
+ return "try castAnyDynamicValue(context.readAny(refMode:
\(refModeExpr), readTypeInfo: true), to: \(metatypeExpr))"
+ case .anyList:
+ return "try castAnyDynamicValue(context.readAnyList(refMode:
\(refModeExpr)), to: \(metatypeExpr))"
+ case .stringAnyMap:
+ return "try castAnyDynamicValue(context.readStringAnyMap(refMode:
\(refModeExpr)), to: \(metatypeExpr))"
+ case .int32AnyMap:
+ return "try castAnyDynamicValue(context.readInt32AnyMap(refMode:
\(refModeExpr)), to: \(metatypeExpr))"
+ }
+}
+
private func fieldRefModeExpression(_ field: ParsedField) -> String {
let nullable = field.isOptional ? "true" : "false"
+ if let dynamicAnyCodec = field.dynamicAnyCodec {
+ switch dynamicAnyCodec {
+ case .anyValue:
+ return "RefMode.from(nullable: \(nullable), trackRef:
context.trackRef)"
+ case .anyList, .stringAnyMap, .int32AnyMap:
+ return "RefMode.from(nullable: \(nullable), trackRef: false)"
+ }
+ }
return "RefMode.from(nullable: \(nullable), trackRef: context.trackRef &&
\(field.typeText).isReferenceTrackableType)"
}
@@ -711,10 +872,22 @@ private func compatibleTypeMetaFieldExpression(
_ field: ParsedField,
trackRefExpression: String
) -> String {
- buildCompatibleTypeMetaFieldTypeExpression(
+ let fieldTrackRefExpression: String
+ if let dynamicAnyCodec = field.dynamicAnyCodec {
+ switch dynamicAnyCodec {
+ case .anyValue:
+ fieldTrackRefExpression = trackRefExpression
+ case .anyList, .stringAnyMap, .int32AnyMap:
+ fieldTrackRefExpression = "false"
+ }
+ } else {
+ fieldTrackRefExpression = "\(trackRefExpression) &&
\(field.typeText).isReferenceTrackableType"
+ }
+
+ return buildCompatibleTypeMetaFieldTypeExpression(
typeText: field.typeText,
nullableExpression: field.isOptional ? "true" : "false",
- trackRefExpression: "\(trackRefExpression) &&
\(field.typeText).isReferenceTrackableType",
+ trackRefExpression: fieldTrackRefExpression,
explicitTypeID: field.customCodecType == nil ? nil : field.typeID
)
}
@@ -790,6 +963,8 @@ TypeMetaFieldType(
let typeIDExpr: String
if let explicitTypeID {
typeIDExpr = "\(explicitTypeID)"
+ } else if isDynamicAnyConcreteType(concreteType) {
+ typeIDExpr = "UInt32(ForyTypeId.unknown.rawValue)"
} else {
typeIDExpr = "UInt32(\(concreteType).staticTypeId.rawValue)"
}
@@ -840,6 +1015,9 @@ private struct TypeClassification {
private func classifyType(_ typeText: String) -> TypeClassification {
let normalized = trimType(typeText)
+ if isDynamicAnyConcreteType(normalized) {
+ return .init(typeID: 0, isPrimitive: false, isBuiltIn: true,
isCollection: false, isMap: false, isCompressedNumeric: false, primitiveSize: 0)
+ }
switch normalized {
case "Bool":
@@ -916,6 +1094,57 @@ private func parseArrayElement(_ type: String) -> String?
{
return nil
}
+private func dynamicAnyDefaultExpr(typeText: String) -> String {
+ let optional = unwrapOptional(typeText)
+ if optional.isOptional {
+ return "nil"
+ }
+
+ let concreteType = normalizeTypeForDynamicAny(optional.type)
+ if concreteType == "AnyObject" {
+ return "NSNull()"
+ }
+ if concreteType == "Any" || isAnySerializerExistentialType(concreteType) {
+ return "ForyAnyNullValue()"
+ }
+ if parseArrayElement(concreteType) != nil {
+ return "[]"
+ }
+ if parseDictionary(concreteType) != nil {
+ return "[:]"
+ }
+ return "\(typeText)()"
+}
+
+private func isDynamicAnyConcreteType(_ typeText: String) -> Bool {
+ let normalized = normalizeTypeForDynamicAny(typeText)
+ if normalized == "Any" || normalized == "AnyObject" {
+ return true
+ }
+ return isAnySerializerExistentialType(normalized)
+}
+
+private func isAnySerializerExistentialType(_ normalizedType: String) -> Bool {
+ let normalized = normalizeTypeForDynamicAny(normalizedType)
+ guard normalized.hasPrefix("any") else {
+ return false
+ }
+
+ let protocolType = String(normalized.dropFirst(3))
+ if protocolType == "Serializer" {
+ return true
+ }
+ return protocolType.hasSuffix(".Serializer")
+}
+
+private func normalizeTypeForDynamicAny(_ typeText: String) -> String {
+ var normalized = trimType(typeText)
+ while normalized.hasPrefix("("), normalized.hasSuffix(")"),
normalized.count > 1 {
+ normalized = String(normalized.dropFirst().dropLast())
+ }
+ return normalized
+}
+
private func parseSetElement(_ type: String) -> String? {
if type.hasPrefix("Set<") && type.hasSuffix(">") {
let start = type.index(type.startIndex, offsetBy: "Set<".count)
diff --git a/swift/Sources/ForySwiftXlangPeer/main.swift
b/swift/Sources/ForySwiftXlangPeer/main.swift
index c63316ccc..d29404f8a 100644
--- a/swift/Sources/ForySwiftXlangPeer/main.swift
+++ b/swift/Sources/ForySwiftXlangPeer/main.swift
@@ -339,234 +339,14 @@ private struct Cat {
var lives: Int32 = 0
}
-private enum AnyAnimal: Serializer {
- case dog(Dog)
- case cat(Cat)
-
- static func foryDefault() -> AnyAnimal {
- .dog(.foryDefault())
- }
-
- static var staticTypeId: ForyTypeId {
- .unknown
- }
-
- func foryWriteData(_ context: WriteContext, hasGenerics: Bool) throws {
- switch self {
- case .dog(let value):
- try value.foryWriteData(context, hasGenerics: hasGenerics)
- case .cat(let value):
- try value.foryWriteData(context, hasGenerics: hasGenerics)
- }
- }
-
- static func foryReadData(_ context: ReadContext) throws -> AnyAnimal {
- guard let typeInfo = context.dynamicTypeInfo(for: Self.self) else {
- throw ForyError.invalidData("AnyAnimal requires pre-read type info
during decode")
- }
- return try decode(context, typeInfo: typeInfo)
- }
-
- static func foryWriteTypeInfo(_ context: WriteContext) throws {
- _ = context
- throw ForyError.invalidData("AnyAnimal type info is dynamic")
- }
-
- func foryWriteTypeInfo(_ context: WriteContext) throws {
- switch self {
- case .dog:
- try Dog.foryWriteTypeInfo(context)
- case .cat:
- try Cat.foryWriteTypeInfo(context)
- }
- }
-
- static func foryReadTypeInfo(_ context: ReadContext) throws {
- let typeInfo = try parseDynamicTypeInfo(context)
- context.setDynamicTypeInfo(for: Self.self, typeInfo)
- }
-
- func foryWrite(
- _ context: WriteContext,
- refMode: RefMode,
- writeTypeInfo: Bool,
- hasGenerics: Bool
- ) throws {
- if refMode != .none {
- context.writer.writeInt8(RefFlag.notNullValue.rawValue)
- }
- switch self {
- case .dog(let value):
- try value.foryWrite(
- context,
- refMode: .none,
- writeTypeInfo: writeTypeInfo,
- hasGenerics: hasGenerics
- )
- case .cat(let value):
- try value.foryWrite(
- context,
- refMode: .none,
- writeTypeInfo: writeTypeInfo,
- hasGenerics: hasGenerics
- )
- }
- }
-
- static func foryRead(
- _ context: ReadContext,
- refMode: RefMode,
- readTypeInfo: Bool
- ) throws -> AnyAnimal {
- if refMode != .none {
- let rawFlag = try context.reader.readInt8()
- guard let flag = RefFlag(rawValue: rawFlag) else {
- throw ForyError.refError("invalid ref flag \(rawFlag)")
- }
- switch flag {
- case .null:
- return .foryDefault()
- case .notNullValue, .refValue:
- break
- case .ref:
- throw ForyError.refError("AnyAnimal does not support
reference-only payloads")
- }
- }
-
- let typeInfo: DynamicTypeInfo
- if readTypeInfo {
- typeInfo = try parseDynamicTypeInfo(context)
- } else if let pendingTypeInfo = context.dynamicTypeInfo(for:
Self.self) {
- typeInfo = pendingTypeInfo
- } else {
- throw ForyError.invalidData("AnyAnimal requires type info")
- }
- return try decode(context, typeInfo: typeInfo)
- }
-
- private static func parseDynamicTypeInfo(_ context: ReadContext) throws ->
DynamicTypeInfo {
- let rawTypeID = try context.reader.readVarUInt32()
- guard let typeID = ForyTypeId(rawValue: rawTypeID) else {
- throw ForyError.invalidData("unknown dynamic animal type id
\(rawTypeID)")
- }
- switch typeID {
- case .structType:
- let userTypeID = try context.reader.readVarUInt32()
- return DynamicTypeInfo(
- wireTypeID: .structType,
- userTypeID: userTypeID,
- namespace: nil,
- typeName: nil,
- compatibleTypeMeta: nil
- )
- case .compatibleStruct:
- let typeMeta = try context.readCompatibleTypeMeta()
- guard let userTypeID = typeMeta.userTypeID else {
- throw ForyError.invalidData("missing user type id for dynamic
compatible animal")
- }
- return DynamicTypeInfo(
- wireTypeID: .compatibleStruct,
- userTypeID: userTypeID,
- namespace: nil,
- typeName: nil,
- compatibleTypeMeta: typeMeta
- )
- case .namedStruct:
- let namespace = try readPeerMetaString(
- context.reader,
- decoder: .namespace,
- encodings: namespaceMetaStringEncodings
- )
- let typeName = try readPeerMetaString(
- context.reader,
- decoder: .typeName,
- encodings: typeNameMetaStringEncodings
- )
- return DynamicTypeInfo(
- wireTypeID: .namedStruct,
- userTypeID: nil,
- namespace: namespace,
- typeName: typeName,
- compatibleTypeMeta: nil
- )
- case .namedCompatibleStruct:
- let typeMeta = try context.readCompatibleTypeMeta()
- return DynamicTypeInfo(
- wireTypeID: .namedCompatibleStruct,
- userTypeID: nil,
- namespace: typeMeta.namespace,
- typeName: typeMeta.typeName,
- compatibleTypeMeta: typeMeta
- )
- default:
- throw ForyError.invalidData("unsupported dynamic animal wire type
\(typeID)")
- }
- }
-
- private static func decode(_ context: ReadContext, typeInfo:
DynamicTypeInfo) throws -> AnyAnimal {
- let value: Any
- switch typeInfo.wireTypeID {
- case .structType:
- guard let userTypeID = typeInfo.userTypeID else {
- throw ForyError.invalidData("missing user type id for dynamic
animal")
- }
- value = try context.typeResolver.readByUserTypeID(userTypeID,
context: context)
- case .compatibleStruct:
- guard let userTypeID = typeInfo.userTypeID else {
- throw ForyError.invalidData("missing user type id for dynamic
compatible animal")
- }
- guard let compatibleTypeMeta = typeInfo.compatibleTypeMeta else {
- throw ForyError.invalidData("missing compatible type meta for
dynamic animal")
- }
- value = try context.typeResolver.readByUserTypeID(
- userTypeID,
- context: context,
- compatibleTypeMeta: compatibleTypeMeta
- )
- case .namedStruct:
- guard let namespace = typeInfo.namespace, let typeName =
typeInfo.typeName else {
- throw ForyError.invalidData("missing dynamic type name for
animal")
- }
- value = try context.typeResolver.readByTypeName(
- namespace: namespace.value,
- typeName: typeName.value,
- context: context
- )
- case .namedCompatibleStruct:
- guard let namespace = typeInfo.namespace, let typeName =
typeInfo.typeName else {
- throw ForyError.invalidData("missing dynamic compatible type
name for animal")
- }
- guard let compatibleTypeMeta = typeInfo.compatibleTypeMeta else {
- throw ForyError.invalidData("missing compatible type meta for
dynamic named animal")
- }
- value = try context.typeResolver.readByTypeName(
- namespace: namespace.value,
- typeName: typeName.value,
- context: context,
- compatibleTypeMeta: compatibleTypeMeta
- )
- default:
- throw ForyError.invalidData("unsupported dynamic animal wire type
\(typeInfo.wireTypeID)")
- }
-
- if let dog = value as? Dog {
- return .dog(dog)
- }
- if let cat = value as? Cat {
- return .cat(cat)
- }
- throw ForyError.invalidData("unsupported dynamic animal payload type")
- }
-}
-
@ForyObject
private struct AnimalListHolder {
- var animals: [AnyAnimal] = []
+ var animals: [Any] = []
}
@ForyObject
private struct AnimalMapHolder {
- var animalMap: [String: AnyAnimal] = [:]
+ var animalMap: [String: Any] = [:]
}
private enum StringOrLong: Serializer, Equatable {
@@ -711,26 +491,6 @@ private func debugLog(_ message: String) {
}
}
-private func readPeerMetaString(
- _ reader: ByteReader,
- decoder: MetaStringDecoder,
- encodings: [MetaStringEncoding]
-) throws -> MetaString {
- let header = try reader.readUInt8()
- let encodingIndex = Int(header & 0b11)
- guard encodingIndex < encodings.count else {
- throw ForyError.invalidData("invalid meta string encoding index")
- }
-
- var length = Int(header >> 2)
- let bigNameThreshold = 0b11_1111
- if length >= bigNameThreshold {
- length = bigNameThreshold + Int(try reader.readVarUInt32())
- }
- let bytes = try reader.readBytes(count: length)
- return try decoder.decode(bytes: bytes, encoding: encodings[encodingIndex])
-}
-
private func verifyBufferCase(_ caseName: String, _ payload: [UInt8]) throws
-> [UInt8] {
let reader = ByteReader(bytes: payload)
let writer = ByteWriter(capacity: payload.count)
@@ -1050,7 +810,7 @@ private func handlePolymorphicList(_ bytes: [UInt8])
throws -> [UInt8] {
let fory = Fory(config: .init(xlang: true, trackRef: false, compatible:
true))
registerPolymorphicTypes(fory)
return try roundTripStream(bytes) { reader, out in
- let animals: [AnyAnimal] = try fory.deserializeFrom(reader)
+ let animals: [Any] = try fory.deserializeFrom(reader)
let holder: AnimalListHolder = try fory.deserializeFrom(reader)
try fory.serializeTo(&out, value: animals)
try fory.serializeTo(&out, value: holder)
@@ -1061,7 +821,7 @@ private func handlePolymorphicMap(_ bytes: [UInt8]) throws
-> [UInt8] {
let fory = Fory(config: .init(xlang: true, trackRef: false, compatible:
true))
registerPolymorphicTypes(fory)
return try roundTripStream(bytes) { reader, out in
- let animalMap: [String: AnyAnimal] = try fory.deserializeFrom(reader)
+ let animalMap: [String: Any] = try fory.deserializeFrom(reader)
let holder: AnimalMapHolder = try fory.deserializeFrom(reader)
try fory.serializeTo(&out, value: animalMap)
try fory.serializeTo(&out, value: holder)
diff --git a/swift/Tests/ForySwiftTests/ForySwiftTests.swift
b/swift/Tests/ForySwiftTests/ForySwiftTests.swift
index 78fe17d4a..7f85a7233 100644
--- a/swift/Tests/ForySwiftTests/ForySwiftTests.swift
+++ b/swift/Tests/ForySwiftTests/ForySwiftTests.swift
@@ -66,6 +66,29 @@ final class Node {
}
}
+@ForyObject
+struct AnyObjectHolder {
+ var value: AnyObject
+ var optionalValue: AnyObject?
+ var items: [AnyObject]
+}
+
+@ForyObject
+struct AnySerializerHolder {
+ var value: any Serializer
+ var items: [any Serializer]
+ var map: [String: any Serializer]
+}
+
+@ForyObject
+struct AnyFieldHolder {
+ var value: Any
+ var optionalValue: Any?
+ var list: [Any]
+ var stringMap: [String: Any]
+ var int32Map: [Int32: Any]
+}
+
@Test
func primitiveRoundTrip() throws {
let fory = Fory()
@@ -209,6 +232,148 @@ func macroClassReferenceTracking() throws {
#expect(decoded.next === decoded)
}
+@Test
+func topLevelAnyRoundTrip() throws {
+ let fory = Fory()
+ fory.register(Address.self, id: 209)
+
+ let value: Any = Address(street: "AnyTop", zip: 8080)
+ let data = try fory.serialize(value)
+ let decoded: Any = try fory.deserialize(data)
+ #expect(decoded as? Address == Address(street: "AnyTop", zip: 8080))
+
+ var buffer = Data()
+ try fory.serializeTo(&buffer, value: value)
+ let decodedFrom: Any = try fory.deserializeFrom(ByteReader(data: buffer))
+ #expect(decodedFrom as? Address == Address(street: "AnyTop", zip: 8080))
+
+ let nullAny: Any = Optional<Int32>.none as Any
+ let nullData = try fory.serialize(nullAny)
+ let nullDecoded: Any = try fory.deserialize(nullData)
+ #expect(nullDecoded is ForyAnyNullValue)
+}
+
+@Test
+func topLevelAnyObjectRoundTrip() throws {
+ let fory = Fory(config: .init(xlang: true, trackRef: true))
+ fory.register(Node.self, id: 210)
+
+ let value: AnyObject = Node(value: 123)
+ let data = try fory.serialize(value)
+ let decoded: AnyObject = try fory.deserialize(data)
+
+ let node = decoded as? Node
+ #expect(node != nil)
+ #expect(node?.value == 123)
+
+ var buffer = Data()
+ try fory.serializeTo(&buffer, value: value)
+ let decodedFrom: AnyObject = try fory.deserializeFrom(ByteReader(data:
buffer))
+ #expect((decodedFrom as? Node)?.value == 123)
+}
+
+@Test
+func topLevelAnySerializerRoundTrip() throws {
+ let fory = Fory()
+ fory.register(Address.self, id: 211)
+
+ let value: any Serializer = Address(street: "AnyStreet", zip: 9090)
+ let data = try fory.serialize(value)
+ let decoded: any Serializer = try fory.deserialize(data)
+
+ let address = decoded as? Address
+ #expect(address == Address(street: "AnyStreet", zip: 9090))
+
+ var buffer = Data()
+ try fory.serializeTo(&buffer, value: value)
+ let decodedFrom: any Serializer = try
fory.deserializeFrom(ByteReader(data: buffer))
+ #expect(decodedFrom as? Address == Address(street: "AnyStreet", zip: 9090))
+}
+
+@Test
+func macroDynamicAnyObjectAndAnySerializerFieldsRoundTrip() throws {
+ let fory = Fory(config: .init(xlang: true, trackRef: true))
+ fory.register(Node.self, id: 220)
+ fory.register(Address.self, id: 221)
+ fory.register(AnyObjectHolder.self, id: 222)
+ fory.register(AnySerializerHolder.self, id: 223)
+
+ let sharedNode = Node(value: 77)
+ let objectHolder = AnyObjectHolder(
+ value: sharedNode,
+ optionalValue: nil,
+ items: [sharedNode, NSNull()]
+ )
+ let objectData = try fory.serialize(objectHolder)
+ let objectDecoded: AnyObjectHolder = try fory.deserialize(objectData)
+ #expect((objectDecoded.value as? Node)?.value == 77)
+ #expect(objectDecoded.optionalValue == nil)
+ #expect(objectDecoded.items.count == 2)
+ #expect((objectDecoded.items[0] as? Node)?.value == 77)
+ #expect(objectDecoded.items[1] is NSNull)
+
+ let serializerHolder = AnySerializerHolder(
+ value: Address(street: "Root", zip: 10001),
+ items: [Int32(11), Address(street: "Nested", zip: 10002)],
+ map: [
+ "age": Int64(19),
+ "address": Address(street: "Mapped", zip: 10003),
+ ]
+ )
+ let serializerData = try fory.serialize(serializerHolder)
+ let serializerDecoded: AnySerializerHolder = try
fory.deserialize(serializerData)
+
+ #expect(serializerDecoded.value as? Address == Address(street: "Root",
zip: 10001))
+ #expect(serializerDecoded.items.count == 2)
+ #expect(serializerDecoded.items[0] as? Int32 == 11)
+ #expect(serializerDecoded.items[1] as? Address == Address(street:
"Nested", zip: 10002))
+ #expect(serializerDecoded.map["age"] as? Int64 == 19)
+ #expect(serializerDecoded.map["address"] as? Address == Address(street:
"Mapped", zip: 10003))
+}
+
+@Test
+func macroAnyFieldsRoundTrip() throws {
+ let fory = Fory()
+ fory.register(Address.self, id: 224)
+ fory.register(AnyFieldHolder.self, id: 225)
+
+ let value = AnyFieldHolder(
+ value: Address(street: "AnyRoot", zip: 11001),
+ optionalValue: nil,
+ list: [Int32(7), "hello", Address(street: "AnyList", zip: 11002),
NSNull()],
+ stringMap: [
+ "count": Int64(3),
+ "name": "map",
+ "address": Address(street: "AnyMap", zip: 11003),
+ "empty": NSNull(),
+ ],
+ int32Map: [
+ 1: Int32(-9),
+ 2: "v2",
+ 3: Address(street: "AnyIntMap", zip: 11004),
+ 4: NSNull(),
+ ]
+ )
+ let data = try fory.serialize(value)
+ let decoded: AnyFieldHolder = try fory.deserialize(data)
+
+ #expect(decoded.value as? Address == Address(street: "AnyRoot", zip:
11001))
+ #expect(decoded.optionalValue == nil)
+ #expect(decoded.list.count == 4)
+ #expect(decoded.list[0] as? Int32 == 7)
+ #expect(decoded.list[1] as? String == "hello")
+ #expect(decoded.list[2] as? Address == Address(street: "AnyList", zip:
11002))
+ #expect(decoded.list[3] is NSNull)
+ #expect(decoded.stringMap["count"] as? Int64 == 3)
+ #expect(decoded.stringMap["name"] as? String == "map")
+ #expect(decoded.stringMap["address"] as? Address == Address(street:
"AnyMap", zip: 11003))
+ #expect(decoded.stringMap["empty"] is NSNull)
+ #expect(decoded.int32Map[1] as? Int32 == -9)
+ #expect(decoded.int32Map[2] as? String == "v2")
+ #expect(decoded.int32Map[3] as? Address == Address(street: "AnyIntMap",
zip: 11004))
+ #expect(decoded.int32Map[4] is NSNull)
+}
+
@Test
func collectionAndMapReferenceTracking() throws {
let fory = Fory(config: .init(xlang: true, trackRef: true))
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]