Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DistDemo — an iPhone as a distributed-actor host

A real Xcode project. A real distributed actor. Apple's own Distributed runtime executing the call. The caller is a web page.

  Safari / Chrome                        iPhone Simulator
  ─────────────────                      ────────────────────────────
  miniswift.run/studio     ──HTTP──▶     DistDemo.app
  try await phone.hello()                executeDistributedTarget(...)
  (compiled to WebAssembly               (real Swift, real Apple runtime)
   in the browser)        ◀──bytes──

Run it

  1. Open DistDemo.xcodeproj, pick any iPhone simulator, press ▶. The app shows Listening · 127.0.0.1:8891.
  2. Open https://miniswift.run/studio/?demo=simulator and press ▶ Run.

You should see the phone answer, and each call appear live on the phone screen.

Hello world — from the iPhone Simulator, running real Swift
iPhone 17 Pro · iOS 27.0
42

If the browser blocks it

An HTTPS page calling a local address is gated by Chrome's Local Network Access permission. Allow it when Chrome asks, or use Safari or Firefox, which do not apply this restriction. This is a browser policy — CORS headers do not change it. (The server already sends permissive CORS and answers the OPTIONS preflight, which application/octet-stream requires.)

What this demonstrates

Apple ships no network transport for distributed actors. The only concrete system in the SDK is LocalTestingDistributedActorSystem, and it is in-process. But DistributedActorSystem is a public protocol and

Distributed.swiftmodule is available for iphonesimulator, so you can write your own. ActorSystem.swift is one.

The call itself is not interpreted by this code. It is handed to executeDistributedTarget(on:target:invocationDecoder:handler:) — Apple's own machinery — which is why this counts as real interop rather than a lookalike.

deviceName() exists to settle the obvious objection: the browser asks the phone what it is, and the phone answers. Nothing on the JavaScript side can invent that.

Layout

file
DistDemo/Wire.swift the byte format, nothing else
DistDemo/ActorSystem.swift DistributedActorSystem conformance + dispatch
DistDemo/Greeter.swift the actor whose bodies actually run
DistDemo/Server.swift NWListener HTTP server, loopback only
DistDemo/MangledTargets.swift generated — see below
Tools/sync-targets.sh regenerates that table from the built binary

Notes for anyone changing this

The source has no comments, so the traps that cost real time live here.

Hold the listener. Server keeps its NWListener in a property. When it was a local let inside start(), the listener was released on return: the kernel socket stayed open so lsof showed LISTEN and TCP connected — but newConnectionHandler was dead, so requests were never read and the client hung with no reply. Two earlier hypotheses (IPv6, iOS Local Network permission) were both wrong.

Bind loopback only. NWListener(using: .tcp, on: port) listens on every interface, which on iOS falls under Local Network privacy: the listener reports ready, the kernel accepts TCP, and newConnectionHandler never fires. requiredLocalEndpoint pins it to 127.0.0.1, outside that scope.

Keep the actor and the server alive. Both are globals. As locals in init they are released when it returns, which reproduces the "connects, no reply" symptom above.

The decoder is inout. executeDistributedTarget consumes it.

Text("…:\(port)") localizes the number. Text takes a LocalizedStringKey, so the port rendered as 8.891. Build the string with String(port).

pending must be @Published. It backs the "N waiting" label through a computed property. As a plain var the label froze: the browser collected the message and the count never moved, so it looked undelivered when it was not.

The wire is not symmetric. Bool is 1 byte, Int is 8. Reads are done byte by byte because the wire is packed and load(as:) needs alignment.

If you rename a method on Greeter

MiniSwift names a target Greeter.hello; Swift names the same method $s8DistDemo7GreeterC5helloyS2SYaKFTE. The mangled form depends on the module name and the signature, so it cannot be written by hand — it is read out of the compiled binary.

Renaming a method still builds clean. What breaks is runtime: the server no longer finds the target and returns 404, with no clue anywhere. After any change to Greeter, build once and run:

Tools/sync-targets.sh /path/to/Build/Products/Debug-iphonesimulator/DistDemo.app

then build again.

Why the browser never hosts

A web page cannot open a listening socket, so the direction is fixed: the browser calls, the phone answers. For browser-to-browser there is a separate transport that uses no network at all — https://miniswift.run/studio/?demo=distributed runs a distributed actor across two tabs.

About

An iPhone as a distributed actor host, called from Swift running in the browser.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages