Swift-erlang-actor-system

I'm excited to share a new actor system we've been building for Swift's distributed actors: swift-erlang-actor-system.
This actor system enables Swift programs to join a distributed Erlang cluster.
Here's an example of a simple chat program using the actor system:
Erlang (and other languages that run on its VM) can connect multiple runtime systems together with distributed Erlang. Each runtime is referred to as a "node". Erlang also supports "C nodes", which allow a program other than the Erlang runtime system to communicate with Erlang nodes and other C nodes.
We've wrapped this C node functionality into an actor system that can be used with Swift's distributed actors. Here's how you can try it out:
- Install Elixir following their instructions. For example, on macOS you can install with Homebrew:
brew install elixir
- Start
epmd
, the "Erlang Port Mapper Daemon". This is how Erlang nodes discover each other by name, instead of IP and port:
epmd
- Start an interactive Elixir node and get the cookie and hostname:
iex --sname elixir_node
iex(elixir_node@YOUR_HOSTNAME)> Node.get_cookie()
:YOUR_COOKIE
- Create a Swift package with a dependency on
otp-interop/swift-erlang-actor-system
, and setup a node and distributed actor:
import ErlangActorSystem
import Distributed
// 1. Declare a distributed actor
@StableNames
distributed actor Counter {
typealias ActorSystem = ErlangActorSystem
private(set) var count: Int = 0
@StableName("increment")
distributed func increment() {
count += 1
print(count)
}
@StableName("decrement")
distributed func decrement() {
count -= 1
print(count)
}
}
// 2. Create a node
let actorSystem = try await ErlangActorSystem(name: "swift_node", cookie: "LJTPNYYQIOIRKYDCWCQH")
// 3. Connect to another node
try await actorSystem.connect(to: "elixir_node@DCKYRD-NMXCKatri")
// 4. Create an instance of a distributed actor using the ErlangActorSystem.
let counter = Counter(actorSystem: actorSystem)
// 5. Give the actor a name so we can find it from another node.
actorSystem.register(counter, name: "counter")
// run loop
while true {}
-
Run the Swift executable.
-
And send messages to our Swift node from Elixir:
iex(elixir_node@YOUR_HOSTNAME)> GenServer.cast({:counter, :"swift_node@YOUR_HOSTNAME"}, :increment)
iex(elixir_node@YOUR_HOSTNAME)> GenServer.cast({:counter, :"swift_node@YOUR_HOSTNAME"}, :decrement)
Swift's actors map nicely to Erlang processes, and Swift's language-level support for distributed actors makes interfacing between the two languages easy.
In the otp-interop
GitHub organization, you'll also find elixir_pack
, a package for bundling Elixir applications to run on iOS and other Apple platforms.
We needed a clean way to communicate between Swift and Elixir on-device—and Swift's distributed actors were a perfect match.
We've also started exploring using distributed Erlang for client-server interaction by filtering messages before accepting them on the server.
swift-erlang-actor-system
uses the erl_interface
C library from Erlang/OTP for networking and serialization. It's included as a C source target in the Swift Package.
You can swap out the Transport
to support custom use cases—such as using WebSockets instead of raw TCP sockets.
Distributed Erlang uses External Term Format to serialize any value in the Erlang VM. erl_interface
provides functions for encoding/decoding terms. We expose this via TermEncoder
and TermDecoder
classes that can convert any Codable
type to this format.
We've also started experimenting with using swift-binary-parsing
to decode terms.
One of the challenges we faced when building this actor system was identifying remote calls across languages.
By default, Swift uses mangled function names to identify remote calls. To call a function on a Swift node from an Erlang node, your Erlang node would need to know about Swift's name mangling.
To get around this, we added the @StableNames
macro. This allows you to decorate the methods of your actor with custom unique names.
This is also important when working with Swift's @Resolvable
macro. @Resolvable
is used on protocols to define actors that are only ever used remotely. We use this to interface with processes that are implemented on the Erlang node. @StableNames
works with @Resolvable
too, you just have to add a conformance to HasStableNames
:
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_cast(:increment, _from, state) do
{:noreply, state + 1}
end
@impl true
def handle_cast(:decrement, _from, state) do
{:noreply, state - 1}
end
end
@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. It will use the stable names provided via the macro to send the correct messages to the Erlang node.
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)
Stable names will likely be necessary in most cross-language actor systems. I'd like to see something like this integrated into Swift in the future.
Looking forward to hearing your thoughts on this actor system, and distributed actors in Swift in general.
What's Your Reaction?






