mirror of
https://github.com/apple/swift.git
synced 2026-11-06 00:14:32 +01:00
350 lines
13 KiB
Swift
350 lines
13 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 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
|
|
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import _Concurrency
|
|
import Distributed
|
|
|
|
// ==== ----------------------------------------------------------------------
|
|
// MARK: The serialization requirement
|
|
//
|
|
// We cannot use Codable in Embedded Swift so actor systems most likely "bring their own favorite coding mechanism".
|
|
// We simulate one here which might be a typical choice, a requested size and then encoding into a preallocated buffer
|
|
// offered as an OutputSpan. This is just a toy encoder/decoder, real impls could do something else.
|
|
|
|
public protocol EmbeddedSerializationRequirement {
|
|
// The exact number of bytes `encode(into:)` will append.
|
|
var serializedByteCount: Int { get }
|
|
|
|
// Append exactly `serializedByteCount` bytes describing this value
|
|
func encode(into output: inout OutputSpan<UInt8>)
|
|
|
|
// Reconstruct a value by consuming bytes off the front of `input`,
|
|
// advancing it past what was read
|
|
static func decode(from input: inout Span<UInt8>) throws -> Self
|
|
}
|
|
|
|
// ==== ----------------------------------------------------------------------
|
|
// MARK: A tiny length-prefixed wire format
|
|
//
|
|
// This is a trivial lenght-prefixed format: "<decimal-byte-count>|<payload>"
|
|
|
|
private let sep = UInt8(ascii: "|")
|
|
|
|
// Render an `Int` as its decimal ASCII bytes, without going through `String`.
|
|
// Public so other modules can serialize integer fields of their own types
|
|
public func asciiDigits(_ value: Int) -> [UInt8] {
|
|
if value == 0 { return [UInt8(ascii: "0")] }
|
|
var v = value
|
|
let negative = v < 0
|
|
var digits: [UInt8] = []
|
|
// Build least-significant digit first, then reverse
|
|
while v != 0 {
|
|
let d = v % 10
|
|
// Works for negative v too: -(d) is a single digit 0...9
|
|
digits.append(UInt8(ascii: "0") + UInt8(d < 0 ? -d : d))
|
|
v /= 10
|
|
}
|
|
if negative { digits.append(UInt8(ascii: "-")) }
|
|
return Array(digits.reversed())
|
|
}
|
|
|
|
// Parse decimal ASCII bytes back into an `Int`, without going through `String`.
|
|
// Public so other modules can deserialize integer fields of their own types
|
|
public func parseInt(_ bytes: ArraySlice<UInt8>) -> Int? {
|
|
if bytes.isEmpty { return nil }
|
|
var result = 0
|
|
var negative = false
|
|
var idx = bytes.startIndex
|
|
if bytes[idx] == UInt8(ascii: "-") {
|
|
negative = true
|
|
idx = bytes.index(after: idx)
|
|
if idx == bytes.endIndex { return nil }
|
|
}
|
|
while idx != bytes.endIndex {
|
|
let b = bytes[idx]
|
|
guard b >= UInt8(ascii: "0"), b <= UInt8(ascii: "9") else { return nil }
|
|
result = result * 10 + Int(b - UInt8(ascii: "0"))
|
|
idx = bytes.index(after: idx)
|
|
}
|
|
return negative ? -result : result
|
|
}
|
|
|
|
// Copy all remaining bytes of a borrowed `Span` into an owned array and advance
|
|
// the span past them. Public so other modules can implement `decode(from:)` for
|
|
// their own types without touching `Span`'s element API directly
|
|
public func drain(_ input: inout Span<UInt8>) -> [UInt8] {
|
|
let n = input.count
|
|
var out: [UInt8] = []
|
|
out.reserveCapacity(n)
|
|
var i = 0
|
|
while i < n {
|
|
out.append(input[i])
|
|
i += 1
|
|
}
|
|
input = input.extracting(droppingFirst: n)
|
|
return out
|
|
}
|
|
|
|
// ==== ----------------------------------------------------------------------
|
|
// MARK: Serialization conformances for the built-in payload types
|
|
|
|
extension String: EmbeddedSerializationRequirement {
|
|
public var serializedByteCount: Int { utf8.count }
|
|
public func encode(into output: inout OutputSpan<UInt8>) {
|
|
for byte in utf8 { output.append(byte) }
|
|
}
|
|
public static func decode(from input: inout Span<UInt8>) throws -> String {
|
|
String(decoding: drain(&input), as: UTF8.self)
|
|
}
|
|
}
|
|
|
|
extension Int: EmbeddedSerializationRequirement {
|
|
public var serializedByteCount: Int { asciiDigits(self).count }
|
|
public func encode(into output: inout OutputSpan<UInt8>) {
|
|
for byte in asciiDigits(self) { output.append(byte) }
|
|
}
|
|
public static func decode(from input: inout Span<UInt8>) throws -> Int {
|
|
let bytes = drain(&input)
|
|
guard let n = parseInt(bytes[...]) else { throw WireError.badValue }
|
|
return n
|
|
}
|
|
}
|
|
|
|
final class CallBuffer {
|
|
var argBytes: [UInt8] = []
|
|
init() {}
|
|
}
|
|
|
|
final class ResultBuffer {
|
|
var returnBytes: [UInt8] = []
|
|
init() {}
|
|
}
|
|
|
|
public enum WireError: Error {
|
|
case underflow
|
|
case malformed
|
|
case badValue
|
|
}
|
|
|
|
// ==== ----------------------------------------------------------------------
|
|
// MARK: Encoder / Decoder / ResultHandler
|
|
|
|
public struct EmbeddedFakeInvocationEncoder: DistributedTargetInvocationEncoder {
|
|
let buffer: CallBuffer
|
|
init(buffer: CallBuffer) { self.buffer = buffer }
|
|
public mutating func doneRecording() throws {}
|
|
}
|
|
extension EmbeddedFakeInvocationEncoder {
|
|
// The single generic record method. The synthesized distributed thunk emits a
|
|
// specialized call to it for each argument; there are no per-type overloads
|
|
public mutating func recordArgument<Value: EmbeddedSerializationRequirement>(
|
|
_ argument: RemoteCallArgument<Value>) throws {
|
|
appendField(argument.value)
|
|
}
|
|
|
|
// Append one length-prefixed field to the request wire: the payload's byte
|
|
// count as decimal ASCII, a "|" separator, then exactly that many payload
|
|
// bytes written straight into freshly reserved array capacity through an
|
|
// `OutputSpan`. Generic over the serialization protocol, so any conforming
|
|
// type serializes the same way
|
|
public func appendField<Value: EmbeddedSerializationRequirement>(_ value: Value) {
|
|
let n = value.serializedByteCount
|
|
buffer.argBytes.append(contentsOf: asciiDigits(n))
|
|
buffer.argBytes.append(sep)
|
|
buffer.argBytes.append(addingCapacity: n) { output in
|
|
value.encode(into: &output)
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct EmbeddedFakeInvocationDecoder: DistributedTargetInvocationDecoder {
|
|
let buffer: CallBuffer
|
|
// Read cursor into `buffer.argBytes`; each `decodeNextArgument` advances it
|
|
var offset: Int = 0
|
|
init(buffer: CallBuffer) { self.buffer = buffer }
|
|
}
|
|
|
|
extension EmbeddedFakeInvocationDecoder {
|
|
public mutating func decodeNextArgument<Argument: EmbeddedSerializationRequirement>() throws -> Argument {
|
|
let payload = try takeField()
|
|
let owned = Array(payload)
|
|
var span = owned.span
|
|
return try Argument.decode(from: &span)
|
|
}
|
|
|
|
// Peel one length-prefixed field off the front of the incoming wire,
|
|
// returning its payload bytes and advancing the read cursor past them. Public
|
|
// so other modules' `decode(from:)` conformances can reuse the framing
|
|
public mutating func takeField() throws -> ArraySlice<UInt8> {
|
|
let bytes = buffer.argBytes
|
|
guard offset < bytes.count else { throw WireError.underflow }
|
|
var i = offset
|
|
while i < bytes.count, bytes[i] != sep { i += 1 }
|
|
guard i < bytes.count else { throw WireError.malformed }
|
|
guard let n = parseInt(bytes[offset..<i]), n >= 0 else { throw WireError.malformed }
|
|
let start = i + 1
|
|
let end = start + n
|
|
guard end <= bytes.count else { throw WireError.underflow }
|
|
offset = end
|
|
return bytes[start..<end]
|
|
}
|
|
}
|
|
|
|
public struct EmbeddedFakeResultHandler: DistributedTargetInvocationResultHandler {
|
|
let buffer: ResultBuffer
|
|
init(buffer: ResultBuffer) { self.buffer = buffer }
|
|
public func onReturnVoid() async throws { buffer.returnBytes = [] }
|
|
public func onThrow(error: any Error) async throws {
|
|
fatalError("threw in handler")
|
|
}
|
|
|
|
// Serialize the single response field, length-prefixed like the request
|
|
// fields. Symmetric with the encoder's `appendField`
|
|
public func writeResponse<Value: EmbeddedSerializationRequirement>(_ value: Value) {
|
|
let n = value.serializedByteCount
|
|
var out: [UInt8] = []
|
|
out.append(contentsOf: asciiDigits(n))
|
|
out.append(sep)
|
|
out.append(addingCapacity: n) { output in
|
|
value.encode(into: &output)
|
|
}
|
|
buffer.returnBytes = out
|
|
}
|
|
}
|
|
extension EmbeddedFakeResultHandler {
|
|
// The single generic result method. The synthesized receive-dispatch emits a
|
|
// specialized call to it with the distributed func's return value
|
|
public func onReturn<Success: EmbeddedSerializationRequirement>(value: Success) async throws {
|
|
writeResponse(value)
|
|
}
|
|
}
|
|
|
|
// ==== ----------------------------------------------------------------------
|
|
// MARK: The actor system
|
|
|
|
public struct EmbeddedFakeActorID: Sendable, Hashable {
|
|
public let id: UInt64
|
|
public init(id: UInt64) { self.id = id }
|
|
}
|
|
|
|
public final class EmbeddedFakeRoundtripActorSystem: DistributedActorSystem, @unchecked Sendable {
|
|
public typealias ActorID = EmbeddedFakeActorID
|
|
public typealias SerializationRequirement = EmbeddedSerializationRequirement
|
|
public typealias InvocationEncoder = EmbeddedFakeInvocationEncoder
|
|
public typealias InvocationDecoder = EmbeddedFakeInvocationDecoder
|
|
public typealias ResultHandler = EmbeddedFakeResultHandler
|
|
|
|
// Each hosted actor's monomorphized receive entrypoint.
|
|
public typealias LocalDispatch =
|
|
(borrowing RemoteCallTarget, inout InvocationDecoder, ResultHandler) async throws -> Void
|
|
|
|
var active: [ActorID: LocalDispatch] = [:]
|
|
var nextID: UInt64 = 1
|
|
|
|
public init() {}
|
|
|
|
public func resolve<Act>(id: ActorID, as actorType: Act.Type) throws -> Act?
|
|
where Act: DistributedActor, Act.ActorSystem == EmbeddedFakeRoundtripActorSystem {
|
|
return nil // always remote: calls route through `remoteCall`
|
|
}
|
|
public func assignID<Act>(_ actorType: Act.Type) -> ActorID
|
|
where Act: DistributedActor, Act.ActorSystem == EmbeddedFakeRoundtripActorSystem {
|
|
defer { nextID += 1 }
|
|
return ActorID(id: nextID)
|
|
}
|
|
|
|
public func actorReady<Act>(_ actor: Act)
|
|
where Act: DistributedActor, Act.ActorSystem == EmbeddedFakeRoundtripActorSystem {
|
|
// This specific pattern is necessary so that the transparent executeDistributedTarget
|
|
// gets inlined here and works on a concrete Act then, otherwise it would not work in Embedded.
|
|
active[actor.id] = { [self] target, decoder, handler in
|
|
try await self.executeDistributedTarget(
|
|
on: actor, target: target, invocationDecoder: &decoder, handler: handler)
|
|
}
|
|
}
|
|
public func resignID(_ id: ActorID) {
|
|
active.removeValue(forKey: id)
|
|
}
|
|
|
|
public func makeInvocationEncoder() -> InvocationEncoder { .init(buffer: CallBuffer()) }
|
|
|
|
public func remoteCall<Act, Err, Res>(
|
|
on actor: Act,
|
|
target: RemoteCallTarget,
|
|
invocation: inout InvocationEncoder,
|
|
throwing: Err.Type,
|
|
returning: Res.Type
|
|
) async throws -> Res
|
|
where Act: DistributedActor,
|
|
Act.ID == ActorID,
|
|
Err: Error,
|
|
Res: EmbeddedSerializationRequirement {
|
|
print("[swift] remoteCall reached")
|
|
guard let dispatch = active[actor.id] else {
|
|
fatalError("no local actor hosted for the target id")
|
|
}
|
|
|
|
// The caller's encoder holds the fully serialized arguments. Read them out
|
|
// as the request payload and hand a copy to the callee - the array value
|
|
// copy is the "network": the callee gets its own buffer, not a shared ref
|
|
let requestWire = invocation.buffer.argBytes
|
|
|
|
// ==================== NETWORK: request -> callee =====================
|
|
let requestBuffer = CallBuffer()
|
|
requestBuffer.argBytes = requestWire
|
|
var decoder = InvocationDecoder(buffer: requestBuffer)
|
|
|
|
// The callee decodes its arguments from `decoder`, runs the target, and
|
|
// serializes its return value into this response buffer
|
|
let resultBuffer = ResultBuffer()
|
|
let handler = ResultHandler(buffer: resultBuffer)
|
|
try await dispatch(target, &decoder, handler)
|
|
|
|
// Take the serialized response back across the wire and decode it into `Res`
|
|
let responseWire = resultBuffer.returnBytes
|
|
|
|
// ==================== NETWORK: response -> caller ====================
|
|
let responseBuffer = CallBuffer()
|
|
responseBuffer.argBytes = responseWire
|
|
var responseDecoder = InvocationDecoder(buffer: responseBuffer)
|
|
return try responseDecoder.decodeNextArgument()
|
|
}
|
|
|
|
public func remoteCallVoid<Act, Err>(
|
|
on actor: Act,
|
|
target: RemoteCallTarget,
|
|
invocation: inout InvocationEncoder,
|
|
throwing: Err.Type
|
|
) async throws
|
|
where Act: DistributedActor, Act.ID == ActorID, Err: Error {
|
|
print("[swift] remoteCallVoid reached")
|
|
guard let dispatch = active[actor.id] else {
|
|
fatalError("no local actor hosted for the target id")
|
|
}
|
|
|
|
// Same request crossing as `remoteCall`: hand the callee a copy of the
|
|
// serialized arguments over the wire
|
|
let requestWire = invocation.buffer.argBytes
|
|
// ==================== NETWORK: request -> callee =====================
|
|
let requestBuffer = CallBuffer()
|
|
requestBuffer.argBytes = requestWire
|
|
var decoder = InvocationDecoder(buffer: requestBuffer)
|
|
|
|
// The callee decodes its arguments and runs the target. A void target
|
|
// resolves through the handler's `onReturnVoid`, which writes an empty
|
|
// response - there is nothing to read back
|
|
let resultBuffer = ResultBuffer()
|
|
let handler = ResultHandler(buffer: resultBuffer)
|
|
try await dispatch(target, &decoder, handler)
|
|
}
|
|
}
|