This library provides a runtime for Swift Distributed Actors backed by erl_interface
from Erlang/OTP.
Create an instance of the ErlangActorSystem
with a name
and cookie
used to verify the connection.
let actorSystem = try ErlangActorSystem(name: "node1", cookie: "cookie")
You can connect multiple actor systems (nodes) together with
ErlangActorSystem/connect(to:)
.
try await actorSystem.connect(to: "node2@localhost")
You can connect to any node visible to EPMD (Erlang Port Mapper Daemon) by its node name and hostname. You can also connect directly by IP address and port if you're not using EPMD.
Create distributed actors that use the ErlangActorSystem
.
distributed actor Counter {
typealias ActorSystem = ErlangActorSystem
private(set) var count: Int = 0
distributed func increment() {
count += 1
}
distributed func decrement() {
count -= 1
}
}
Create an instance of a distributed actor by passing your ErlangActorSystem
instance.
let counter = Counter(actorSystem: actorSystem)
try await counter.increment()
#expect(await counter.count == 1)
Resolve remote actors by their ID. The ID can be a PID or a registered name.
An actor can register a name on a remote node with ErlangActorSystem/register(_:name:)
.
let remoteCounter = try Counter.resolve(id: .name("counter", node: "node2@localhost"))
try await remoteCounter.increment()
Codable values are encoded to Erlang's External Term Format.
Calls are sent to remote actors using the GenServer
message format.
You can use this actor system purely from Swift. However, since it uses Erlang's C node API, it can interface directly with Erlang/Elixir nodes.
To test this, you can start IEx (Interactive Elixir) as a distributed node:
iex --sname iex
Get the cookie from IEx:
iex(iex@hostname)1> Node.get_cookie()
From Swift, create an ErlangActorSystem
with the same cookie value (without the leading colon :
)
and connect it to the IEx node.
let actorSystem = try ErlangActorSystem(name: "swift", cookie: "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
try await actorSystem.connect(to: "iex@hostname")
You can confirm that the Swift node has connected to IEx with Node.list/1
:
iex(iex@hostname)2> Node.list(:hidden)
[:"swift@hostname"]
Now create an distributed actor and register a name for it.
@StableNames
distributed actor PingPong {
typealias ActorSystem = ErlangActorSystem
@StableName("ping")
distributed func ping() -> String {
return "pong"
}
}
let pingPong = PingPong(actorSystem: actorSystem)
try await actorSystem.register(pingPong, name: "ping_pong")
In IEx, use GenServer.call/3
from Elixir to make distributed calls on your Swift actor.
iex(iex@hostname)3> GenServer.call({:ping_pong, :"swift@hostname"}, :ping)
"pong"
By default, Swift uses mangled function names to identify remote calls. To interface with other languages, we need to establish stable names for distributed functions.
The StableName
macro lets you declare a custom name for a distributed function.
Add the StableName
macro to each distributed func
/distributed var
and the @StableNames
macro to the actor.
This will generate a mapping between your stable names and the mangled function names used by the Swift runtime.
@StableNames // <- generates the mapping between stable and mangled names
distributed actor Counter {
typealias ActorSystem = ErlangActorSystem
private var _count = 0
@StableName("count") // <- declares the name to use for this member
distributed var count: Int { _count }
@StableName("increment") // <- can be used on `var` or `func`
distributed func increment() {
_count += 1
}
@StableName("decrement") // <- all stable names must be unique
distributed func decrement() {
_count -= 1
}
}
You may have some GenServers that are only declared in Erlang/Elixir:
defmodule Counter do
use GenServer
@impl true
def init(count), do: {:ok, count}
@impl true
def handle_call(:count, _from, state) do
{:reply, state, state}
end
@impl true
def handle_call(:increment, _from, state) do
{:reply, :ok, state + 1}
end
@impl true
def handle_call(:decrement, _from, state) do
{:reply, :ok, state - 1}
end
end
To interface with these GenServers, create a protocol in Swift.
Add the Resolvable
and StableName
macros. You must declare a conformance to HasStableNames
manually.
@Resolvable
@StableNames
protocol Counter: DistributedActor, HasStableNames where ActorSystem == ErlangActorSystem {
@StableName("count")
distributed var count: Int { get }
@StableName("increment")
distributed func increment()
@StableName("decrement")
distributed func decrement()
}
A concrete actor implementing this protocol called $Counter
will be created.
Use this implementation to resolve the remote actor.
let counter: some Counter = try $Counter.resolve(
id: .name("counter", node: "iex@hostname"),
using: actorSystem
)
try await counter.increment()
#expect(try await counter.count == 1)