Skip to content

otp-interop/swift-erlang-actor-system

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Swift Erlang Actor System

This library provides a runtime for Swift Distributed Actors backed by erl_interface from Erlang/OTP.

Getting Started

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.

Connecting to Elixir

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"

Stable Names

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
    }
}

External Actors

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)

About

Erlang/Elixir integration for Swift Distributed Actors

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages