Ambly Require Reload

October 7, 2015

Ambly remote compilation explained.

How do edits you've made to ClojureScript source make it to your iOS device when using Ambly? What really happens behind the scenes?

It turns out that the answer is—intentionally—not too different from how existing ClojureScript REPLs, like the shipping Node and browser REPLs, work. The approach was glossed over in this post, but here I'd like to pop the hood and go into a little more detail.

Let's say, in src/foo/core.cljs, you add a definition for a new function, foo.core/square

(defn square [x]
  (* x x))

and then in the Ambly REPL, issue

(require 'foo.core :reload)

followed by evaluating

(foo.core/square 3)

to get 9. How does this work?

First, require is a ClojureScript REPL special. It is actually implemented in the base cljs.repl code—nothing special is done by Ambly. Its execution is triggered by simply detecting that a list form has been entered with the symbol 'require as its first element.

Executing require is accomplished by first converting it to an ns form with a :require spec and then evaluating it. For our example, this would look like

(ns cljs.user (:require foo.core))

When cljs.repl sees that it is evaluating an ns form, it loads any dependent namespaces, calling load-dependencies, which calls load-namespace, which causes our namespace to be compiled to JavaScript using the cljs.compiler namespace. (You’ll see all of this in the resulting call stack if you happen to have an error in your source.)

Now normally, the compiler output would be written in out (or to a place configured via :output-dir.) But when an Ambly REPL session is established, it provides an override for the :output-dir setting and “redirects” the ClojureScript compiler to write to a different directory, say /Volumes/Ambly-A8EC7B89.

During app launch, the Objective-C side of Ambly starts up a WebDAV server within the iOS app (which is running on your device or in the simulator) and, when connecting, the Clojure-side causes the operating system to mount the network file system exposed by that WebDAV server (on /Volumes/Ambly-A8EC7B89 for our example).

Effectively, the end result of all of this is that the ClojureScript compiler's output is no longer written to a conventional directory on your computer's disk, but instead to a directory in your iOS app's sandbox, indirectly via the WebDAV mount point.

Getting back to require, this causes some JavaScript representing foo/core.cljs, along with any other compiler output (such as compiler analysis metadata cache and source mapping files) to be written to your app's sandbox.

But, this alone is not enough for require; it also needs to load the resulting JavaScript into the JavaScriptCore instance running within your app. How does this occur?

In this case, cljs.repl delegates back to Ambly, by calling -load, and Ambly satisfies this by sending a goog.require() JavaScript snippet to the app using TCP: In addition to the WebDAV connection, Ambly maintains a regular TCP connection to the iOS app, and it is on this connection that the Clojure-side of the Ambly REPL can cause the Objective-C-side of the REPL to do things, like evaluate JavaScript such as our goog.require() call.

The ClojureScript compiler makes use of the Google Closure dependency management system. Even for a REPL, Closure is involved, even though no minifying optimization are being done, with compilation being done in :none mode.

Evaluating goog.require() within JavaScriptCore in the app ultimately causes CLOSURE_IMPORT_SCRIPT to be executed, and Ambly has set things set up so that this results in the JavaScript file to be read from the iOS sandbox filesystem, followed be having its text being evaluated in JavaScript core. And, of course, evaluating the JavaScript for the namespace essentially causes the namespace to be loaded.

A lot of steps, but: No magic!

The subsequent form (foo.core/square 3) is handled by the Clojure-side of Ambly by regular means: The base cljs.repl compiles it to JavaScript which is then evaluated, which causes Ambly to ferry it across the TCP connection to be executed in JavaScriptCore. Again, no magic.

If you look at the way Ambly works given the description above, you can see that a lot of the description has more to do with cljs.repl than with Ambly. Ambly really is a regular ClojureScript REPL, just like any other. It is not radically different, and is not reinventing any wheels. Instead, Ambly plugs into the existing ClojureScript REPL architecture and adds its value by providing the bits needed to remote things into JavaScriptCore embedded in an iOS app.

David Nolen came up with this beautifully simple design well before Ambly was fleshed out. I like how it leverages existing tooling and support to achieve something quite magical without involving any actual magic.

Tags: Ambly iOS ClojureScript