An Automerge implementation for swift
Automerge-swifter
An Automerge implementation for swift.
This is a reasonably low-level library with relatively few concessions to ergonomics, nicer APIs should be built on top of this work.
This is also a first draft I (@alexjg) am not particularly familiar with Swift so I expect many things are weird or wrong. Please tell me what those things are!
Docs available here
A demo app here
Quickstart
Add a dependency in Package.swift
(note the use of the .product(..)
dependency for the target, this is because our repository name does not match the product name, or something like that, see here)
let package = Package(
...
dependencies: [
...
.package(url: "git@github.com:automerge/automerge-swifter.git", from: "0.0.1")
],
targets: [
.executableTarget(
...
dependencies: [.product(name: "Automerge", package: "automerge-swifter")],
...)
]
)
Now you can create a document and do all sorts of Automerge things with it
let doc = Document()
let list = try! doc.putObject(obj: ObjId.ROOT, key: "colours", ty: .List)
try! doc.insert(obj: list, index: 0, .String("blue"))
try! doc.insert(obj: list, index: 1, .String("red"))
let doc2 = doc.fork()
try! doc2.insert(obj: list, index: 0, .String("green"))
try! doc.delete(obj: list, index: 0)
try! doc.merge(doc2) // `doc` now contains {"colours": ["green", "red"]}
Building and developing
This package is implemented by wrapping the Rust library. There are two problems to solve to make this possible:
- Writing and/or generating a bunch of code to cross the FFI bridge from Rust to swift
- Distributing the compiled Rust in a way that swift understands
We use the Uniffi framework from Mozilla. Uniffi takes in an IDL file describing the FFI interface and some rust source code which implements the Rust side of the interface. Given this IDL Uniffi generates a swift package providing the swift side of the interface. However, the generated code is not very idiomatic swift, so we wrap it in a handwritten swift side wrapper of our own. Finally, we have to actually distribute the rust code as a binary XCFramework.
The moving parts here then are:
- The
rust/src/automerge.udl
file which describes the FFI interface - The
rust/build.rs
build script, which uses Uniffi to generate the boilerplate parts of the rust side of the interface - The
rust/src/*
files which implement the Automerge specific parts of the rust binding - The
rust/uniffi-bindgen.rs
script, which uses Uniffi to output a Swift wrapper around the interface - The source files in
./Sources
and./Tests
which implement the handwritten swift wrappers - The
./scripts/build-xcframework.sh
script, which builds the rust project and packages it into an XCFramework
Actually, the build-xcframework.sh
script does a bit more than this. It builds the rust framework, then generates the swift package and copies it into ./AutomergeUniffi
, then also generates the XCFramework and places it in automergeFFI.xcframework.zip
.
What this means is that the typical development cycle usually looks like this:
- Write a failing test in
Tests/*.swift
- Modify the
rust/src/automerge.udl
file to expose the additional methods or data you need from the rust side - In the rust project write rust code to implement the IDL. The build script generates the new code Uniffi needs and will produce compile errors until you implement the required parts. This means you just run
cargo build
in./rust
and modify code until cargo is happy - Run
./scripts/build-xcframework.sh
to generate the new xcframework based on the new bindings you’ve implemented - Wire up the swift side of the wrappers in
./Sources/*
- Run tests on the swift side with
swift test
Benchmarking
The repository has two-dimensional benchmarking as a seperate project in the directory CollectionBenchmarks
.
It uses the library swift-collections-benchmarks to run benchmarks that are relevant over the size of the collection.
The benchmark baselines were built on an Apple M1 MacBook Pro.
Building the docs
The script ./scripts/preview-docs.sh
will run a web server previewing the docs. This won’t pick up all source code changes so you may need to restart it occasionally (I have not figured out which changes it does and does not pick up on).
Releasing
Swift package manager requires that the built artifacts for the binaryTarget
we are distributing are part of the repository. It’s kind of awkward to have build artifacts in the repository though. To avoid this we only put the build artifacts (specifically, the AutomergeFFI.xcframework.zip
file and the AutomergeUniffi
folder which are produced by scripts/build-xcframework.sh
) in the repository for tagged releases and otherwise we .gitignore
them. This is a bit of a hack, feel free to suggest better alternatives.
Creating a release then requires doing the following things:
- Checkout a new branch
- Run
scripts/build-xcframework.sh
- Add a commit containing
AutomergeFFI.xcframework.zip
andAutomergeUniffi
to the index - Tag the branch with a version – e.g.
git tag 0.0.1
- Push the tag to the remote