diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 8519ac5e..3039687d 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -2,10 +2,10 @@ name: CI
on:
push:
branches:
- - main
+ - main
pull_request:
branches:
- - main
+ - main
jobs:
fmt:
runs-on: ubuntu-latest
@@ -14,7 +14,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
- toolchain: 1.67.0
+ toolchain: 1.60.0
default: true
components: rustfmt
- uses: Swatinem/rust-cache@v1
@@ -28,7 +28,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
- toolchain: 1.67.0
+ toolchain: 1.60.0
default: true
components: clippy
- uses: Swatinem/rust-cache@v1
@@ -42,14 +42,10 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
- toolchain: 1.67.0
+ toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- - name: Build rust docs
- run: ./scripts/ci/rust-docs
- shell: bash
- - name: Install doxygen
- run: sudo apt-get install -y doxygen
+ - run: ./scripts/ci/docs
shell: bash
cargo-deny:
@@ -64,50 +60,23 @@ jobs:
- uses: actions/checkout@v2
- uses: EmbarkStudios/cargo-deny-action@v1
with:
- arguments: '--manifest-path ./rust/Cargo.toml'
command: check ${{ matrix.checks }}
wasm_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- - name: Install wasm-bindgen-cli
- run: cargo install wasm-bindgen-cli wasm-opt
- - name: Install wasm32 target
- run: rustup target add wasm32-unknown-unknown
+ - name: Install wasm-pack
+ run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: run tests
run: ./scripts/ci/wasm_tests
- deno_tests:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - uses: denoland/setup-deno@v1
- with:
- deno-version: v1.x
- - name: Install wasm-bindgen-cli
- run: cargo install wasm-bindgen-cli wasm-opt
- - name: Install wasm32 target
- run: rustup target add wasm32-unknown-unknown
- - name: run tests
- run: ./scripts/ci/deno_tests
-
- js_fmt:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v2
- - name: install
- run: yarn global add prettier
- - name: format
- run: prettier -c javascript/.prettierrc javascript
js_tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- - name: Install wasm-bindgen-cli
- run: cargo install wasm-bindgen-cli wasm-opt
- - name: Install wasm32 target
- run: rustup target add wasm32-unknown-unknown
+ - name: Install wasm-pack
+ run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: run tests
run: ./scripts/ci/js_tests
@@ -118,7 +87,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
- toolchain: nightly-2023-01-26
+ toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- name: Install CMocka
@@ -127,8 +96,6 @@ jobs:
uses: jwlawson/actions-setup-cmake@v1.12
with:
cmake-version: latest
- - name: Install rust-src
- run: rustup component add rust-src
- name: Build and test C bindings
run: ./scripts/ci/cmake-build Release Static
shell: bash
@@ -138,7 +105,9 @@ jobs:
strategy:
matrix:
toolchain:
- - 1.67.0
+ - 1.60.0
+ - nightly
+ continue-on-error: ${{ matrix.toolchain == 'nightly' }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
@@ -157,7 +126,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
- toolchain: 1.67.0
+ toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- run: ./scripts/ci/build-test
@@ -170,8 +139,9 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
- toolchain: 1.67.0
+ toolchain: 1.60.0
default: true
- uses: Swatinem/rust-cache@v1
- run: ./scripts/ci/build-test
shell: bash
+
diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
index b501d526..3474dd47 100644
--- a/.github/workflows/docs.yaml
+++ b/.github/workflows/docs.yaml
@@ -30,16 +30,32 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: clean
- args: --manifest-path ./rust/Cargo.toml --doc
+ args: --doc
- name: Build Rust docs
uses: actions-rs/cargo@v1
with:
command: doc
- args: --manifest-path ./rust/Cargo.toml --workspace --all-features --no-deps
+ args: --workspace --all-features --no-deps
- name: Move Rust docs
- run: mkdir -p docs && mv rust/target/doc/* docs/.
+ run: mkdir -p docs && mv target/doc/* docs/.
+ shell: bash
+
+ - name: Install doxygen
+ run: sudo apt-get install -y doxygen
+ shell: bash
+
+ - name: Install cmocka
+ run: sudo apt-get install -y libcmocka-dev
+ shell: bash
+
+ - name: Build C docs
+ run: ./scripts/ci/cmake-docs
+ shell: bash
+
+ - name: Move C docs
+ run: mkdir -p docs/automerge-c && mv automerge-c/build/src/html/* docs/automerge-c/.
shell: bash
- name: Configure root page
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
deleted file mode 100644
index 762671ff..00000000
--- a/.github/workflows/release.yaml
+++ /dev/null
@@ -1,214 +0,0 @@
-name: Release
-on:
- push:
- branches:
- - main
-
-jobs:
- check_if_wasm_version_upgraded:
- name: Check if WASM version has been upgraded
- runs-on: ubuntu-latest
- outputs:
- wasm_version: ${{ steps.version-updated.outputs.current-package-version }}
- wasm_has_updated: ${{ steps.version-updated.outputs.has-updated }}
- steps:
- - uses: JiPaix/package-json-updated-action@v1.0.5
- id: version-updated
- with:
- path: rust/automerge-wasm/package.json
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- publish-wasm:
- name: Publish WASM package
- runs-on: ubuntu-latest
- needs:
- - check_if_wasm_version_upgraded
- # We create release only if the version in the package.json has been upgraded
- if: needs.check_if_wasm_version_upgraded.outputs.wasm_has_updated == 'true'
- steps:
- - uses: actions/setup-node@v3
- with:
- node-version: '16.x'
- registry-url: 'https://registry.npmjs.org'
- - uses: denoland/setup-deno@v1
- - uses: actions/checkout@v3
- with:
- fetch-depth: 0
- ref: ${{ github.ref }}
- - name: Get rid of local github workflows
- run: rm -r .github/workflows
- - name: Remove tmp_branch if it exists
- run: git push origin :tmp_branch || true
- - run: git checkout -b tmp_branch
- - name: Install wasm-bindgen-cli
- run: cargo install wasm-bindgen-cli wasm-opt
- - name: Install wasm32 target
- run: rustup target add wasm32-unknown-unknown
- - name: run wasm js tests
- id: wasm_js_tests
- run: ./scripts/ci/wasm_tests
- - name: run wasm deno tests
- id: wasm_deno_tests
- run: ./scripts/ci/deno_tests
- - name: build release
- id: build_release
- run: |
- npm --prefix $GITHUB_WORKSPACE/rust/automerge-wasm run release
- - name: Collate deno release files
- if: steps.wasm_js_tests.outcome == 'success' && steps.wasm_deno_tests.outcome == 'success'
- run: |
- mkdir $GITHUB_WORKSPACE/deno_wasm_dist
- cp $GITHUB_WORKSPACE/rust/automerge-wasm/deno/* $GITHUB_WORKSPACE/deno_wasm_dist
- cp $GITHUB_WORKSPACE/rust/automerge-wasm/index.d.ts $GITHUB_WORKSPACE/deno_wasm_dist
- cp $GITHUB_WORKSPACE/rust/automerge-wasm/README.md $GITHUB_WORKSPACE/deno_wasm_dist
- cp $GITHUB_WORKSPACE/rust/automerge-wasm/LICENSE $GITHUB_WORKSPACE/deno_wasm_dist
- sed -i '1i /// ' $GITHUB_WORKSPACE/deno_wasm_dist/automerge_wasm.js
- - name: Create npm release
- if: steps.wasm_js_tests.outcome == 'success' && steps.wasm_deno_tests.outcome == 'success'
- run: |
- if [ "$(npm --prefix $GITHUB_WORKSPACE/rust/automerge-wasm show . version)" = "$VERSION" ]; then
- echo "This version is already published"
- exit 0
- fi
- EXTRA_ARGS="--access public"
- if [[ $VERSION == *"alpha."* ]] || [[ $VERSION == *"beta."* ]] || [[ $VERSION == *"rc."* ]]; then
- echo "Is pre-release version"
- EXTRA_ARGS="$EXTRA_ARGS --tag next"
- fi
- if [ "$NODE_AUTH_TOKEN" = "" ]; then
- echo "Can't publish on NPM, You need a NPM_TOKEN secret."
- false
- fi
- npm publish $GITHUB_WORKSPACE/rust/automerge-wasm $EXTRA_ARGS
- env:
- NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
- VERSION: ${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
- - name: Commit wasm deno release files
- run: |
- git config --global user.name "actions"
- git config --global user.email actions@github.com
- git add $GITHUB_WORKSPACE/deno_wasm_dist
- git commit -am "Add deno release files"
- git push origin tmp_branch
- - name: Tag wasm release
- if: steps.wasm_js_tests.outcome == 'success' && steps.wasm_deno_tests.outcome == 'success'
- uses: softprops/action-gh-release@v1
- with:
- name: Automerge Wasm v${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
- tag_name: js/automerge-wasm-${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
- target_commitish: tmp_branch
- generate_release_notes: false
- draft: false
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - name: Remove tmp_branch
- run: git push origin :tmp_branch
- check_if_js_version_upgraded:
- name: Check if JS version has been upgraded
- runs-on: ubuntu-latest
- outputs:
- js_version: ${{ steps.version-updated.outputs.current-package-version }}
- js_has_updated: ${{ steps.version-updated.outputs.has-updated }}
- steps:
- - uses: JiPaix/package-json-updated-action@v1.0.5
- id: version-updated
- with:
- path: javascript/package.json
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- publish-js:
- name: Publish JS package
- runs-on: ubuntu-latest
- needs:
- - check_if_js_version_upgraded
- - check_if_wasm_version_upgraded
- - publish-wasm
- # We create release only if the version in the package.json has been upgraded and after the WASM release
- if: |
- (always() && ! cancelled()) &&
- (needs.publish-wasm.result == 'success' || needs.publish-wasm.result == 'skipped') &&
- needs.check_if_js_version_upgraded.outputs.js_has_updated == 'true'
- steps:
- - uses: actions/setup-node@v3
- with:
- node-version: '16.x'
- registry-url: 'https://registry.npmjs.org'
- - uses: denoland/setup-deno@v1
- - uses: actions/checkout@v3
- with:
- fetch-depth: 0
- ref: ${{ github.ref }}
- - name: Get rid of local github workflows
- run: rm -r .github/workflows
- - name: Remove js_tmp_branch if it exists
- run: git push origin :js_tmp_branch || true
- - run: git checkout -b js_tmp_branch
- - name: check js formatting
- run: |
- yarn global add prettier
- prettier -c javascript/.prettierrc javascript
- - name: run js tests
- id: js_tests
- run: |
- cargo install wasm-bindgen-cli wasm-opt
- rustup target add wasm32-unknown-unknown
- ./scripts/ci/js_tests
- - name: build js release
- id: build_release
- run: |
- npm --prefix $GITHUB_WORKSPACE/javascript run build
- - name: build js deno release
- id: build_deno_release
- run: |
- VERSION=$WASM_VERSION npm --prefix $GITHUB_WORKSPACE/javascript run deno:build
- env:
- WASM_VERSION: ${{ needs.check_if_wasm_version_upgraded.outputs.wasm_version }}
- - name: run deno tests
- id: deno_tests
- run: |
- npm --prefix $GITHUB_WORKSPACE/javascript run deno:test
- - name: Collate deno release files
- if: steps.js_tests.outcome == 'success' && steps.deno_tests.outcome == 'success'
- run: |
- mkdir $GITHUB_WORKSPACE/deno_js_dist
- cp $GITHUB_WORKSPACE/javascript/deno_dist/* $GITHUB_WORKSPACE/deno_js_dist
- - name: Create npm release
- if: steps.js_tests.outcome == 'success' && steps.deno_tests.outcome == 'success'
- run: |
- if [ "$(npm --prefix $GITHUB_WORKSPACE/javascript show . version)" = "$VERSION" ]; then
- echo "This version is already published"
- exit 0
- fi
- EXTRA_ARGS="--access public"
- if [[ $VERSION == *"alpha."* ]] || [[ $VERSION == *"beta."* ]] || [[ $VERSION == *"rc."* ]]; then
- echo "Is pre-release version"
- EXTRA_ARGS="$EXTRA_ARGS --tag next"
- fi
- if [ "$NODE_AUTH_TOKEN" = "" ]; then
- echo "Can't publish on NPM, You need a NPM_TOKEN secret."
- false
- fi
- npm publish $GITHUB_WORKSPACE/javascript $EXTRA_ARGS
- env:
- NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
- VERSION: ${{ needs.check_if_js_version_upgraded.outputs.js_version }}
- - name: Commit js deno release files
- run: |
- git config --global user.name "actions"
- git config --global user.email actions@github.com
- git add $GITHUB_WORKSPACE/deno_js_dist
- git commit -am "Add deno js release files"
- git push origin js_tmp_branch
- - name: Tag JS release
- if: steps.js_tests.outcome == 'success' && steps.deno_tests.outcome == 'success'
- uses: softprops/action-gh-release@v1
- with:
- name: Automerge v${{ needs.check_if_js_version_upgraded.outputs.js_version }}
- tag_name: js/automerge-${{ needs.check_if_js_version_upgraded.outputs.js_version }}
- target_commitish: js_tmp_branch
- generate_release_notes: false
- draft: false
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- - name: Remove js_tmp_branch
- run: git push origin :js_tmp_branch
diff --git a/.gitignore b/.gitignore
index f77865d0..eca9df3f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,5 @@
+/target
/.direnv
perf.*
/Cargo.lock
build/
-.vim/*
-/target
diff --git a/rust/Cargo.toml b/Cargo.toml
similarity index 71%
rename from rust/Cargo.toml
rename to Cargo.toml
index 5d29fc9f..7eb899e8 100644
--- a/rust/Cargo.toml
+++ b/Cargo.toml
@@ -3,15 +3,14 @@ members = [
"automerge",
"automerge-c",
"automerge-cli",
- "automerge-test",
"automerge-wasm",
"edit-trace",
]
-resolver = "2"
[profile.release]
+debug = true
lto = true
-codegen-units = 1
+opt-level = 3
[profile.bench]
-debug = true
\ No newline at end of file
+debug = true
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..9f8db2d1
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,13 @@
+rust:
+ cd automerge && cargo test
+
+wasm:
+ cd automerge-wasm && yarn
+ cd automerge-wasm && yarn build
+ cd automerge-wasm && yarn test
+ cd automerge-wasm && yarn link
+
+js: wasm
+ cd automerge-js && yarn
+ cd automerge-js && yarn link "automerge-wasm"
+ cd automerge-js && yarn test
diff --git a/README.md b/README.md
index ad174da4..4c58e8d1 100644
--- a/README.md
+++ b/README.md
@@ -1,147 +1,110 @@
-# Automerge
+# Automerge RS
[](https://automerge.org/)
[](https://automerge.org/automerge-rs/automerge/)
[](https://github.com/automerge/automerge-rs/actions/workflows/ci.yaml)
-[](https://github.com/automerge/automerge-rs/actions/workflows/docs.yaml)
-Automerge is a library which provides fast implementations of several different
-CRDTs, a compact compression format for these CRDTs, and a sync protocol for
-efficiently transmitting those changes over the network. The objective of the
-project is to support [local-first](https://www.inkandswitch.com/local-first/) applications in the same way that relational
-databases support server applications - by providing mechanisms for persistence
-which allow application developers to avoid thinking about hard distributed
-computing problems. Automerge aims to be PostgreSQL for your local-first app.
+This is a rust implementation of the [Automerge](https://github.com/automerge/automerge) file format and network protocol.
-If you're looking for documentation on the JavaScript implementation take a look
-at https://automerge.org/docs/hello/. There are other implementations in both
-Rust and C, but they are earlier and don't have documentation yet. You can find
-them in `rust/automerge` and `rust/automerge-c` if you are comfortable
-reading the code and tests to figure out how to use them.
-
-If you're familiar with CRDTs and interested in the design of Automerge in
-particular take a look at https://automerge.org/docs/how-it-works/backend/
-
-Finally, if you want to talk to us about this project please [join the
-Slack](https://join.slack.com/t/automerge/shared_invite/zt-e4p3760n-kKh7r3KRH1YwwNfiZM8ktw)
+If you are looking for the origional `automerge-rs` project that can be used as a wasm backend to the javascript implementation, it can be found [here](https://github.com/automerge/automerge-rs/tree/automerge-1.0).
## Status
-This project is formed of a core Rust implementation which is exposed via FFI in
-javascript+WASM, C, and soon other languages. Alex
-([@alexjg](https://github.com/alexjg/)]) is working full time on maintaining
-automerge, other members of Ink and Switch are also contributing time and there
-are several other maintainers. The focus is currently on shipping the new JS
-package. We expect to be iterating the API and adding new features over the next
-six months so there will likely be several major version bumps in all packages
-in that time.
+This project has 4 components:
-In general we try and respect semver.
+1. _automerge_ - a rust implementation of the library. This project is the most mature and being used in a handful of small applications.
+2. _automerge-wasm_ - a js/wasm interface to the underlying rust library. This api is generally mature and in use in a handful of projects as well.
+3. _automerge-js_ - this is a javascript library using the wasm interface to export the same public api of the primary automerge project. Currently this project passes all of automerge's tests but has not been used in any real project or packaged as an NPM. Alpha testers welcome.
+4. _automerge-c_ - this is a c library intended to be an ffi integration point for all other languages. It is currently a work in progress and not yet ready for any testing.
-### JavaScript
+## How?
-A stable release of the javascript package is currently available as
-`@automerge/automerge@2.0.0` where. pre-release verisions of the `2.0.1` are
-available as `2.0.1-alpha.n`. `2.0.1*` packages are also available for Deno at
-https://deno.land/x/automerge
+The current iteration of automerge-rs is complicated to work with because it
+adopts the frontend/backend split architecture of the JS implementation. This
+architecture was necessary due to basic operations on the automerge opset being
+too slow to perform on the UI thread. Recently @orionz has been able to improve
+the performance to the point where the split is no longer necessary. This means
+we can adopt a much simpler mutable API.
-### Rust
+The architecture is now built around the `OpTree`. This is a data structure
+which supports efficiently inserting new operations and realising values of
+existing operations. Most interactions with the `OpTree` are in the form of
+implementations of `TreeQuery` - a trait which can be used to traverse the
+optree and producing state of some kind. User facing operations are exposed on
+an `Automerge` object, under the covers these operations typically instantiate
+some `TreeQuery` and run it over the `OpTree`.
-The rust codebase is currently oriented around producing a performant backend
-for the Javascript wrapper and as such the API for Rust code is low level and
-not well documented. We will be returning to this over the next few months but
-for now you will need to be comfortable reading the tests and asking questions
-to figure out how to use it. If you are looking to build rust applications which
-use automerge you may want to look into
-[autosurgeon](https://github.com/alexjg/autosurgeon)
+## Development
-## Repository Organisation
+Please feel free to open issues and pull requests.
-- `./rust` - the rust rust implementation and also the Rust components of
- platform specific wrappers (e.g. `automerge-wasm` for the WASM API or
- `automerge-c` for the C FFI bindings)
-- `./javascript` - The javascript library which uses `automerge-wasm`
- internally but presents a more idiomatic javascript interface
-- `./scripts` - scripts which are useful to maintenance of the repository.
- This includes the scripts which are run in CI.
-- `./img` - static assets for use in `.md` files
+### Running CI
-## Building
+The steps CI will run are all defined in `./scripts/ci`. Obviously CI will run
+everything when you submit a PR, but if you want to run everything locally
+before you push you can run `./scripts/ci/run` to run everything.
-To build this codebase you will need:
+### Running the JS tests
-- `rust`
-- `node`
-- `yarn`
-- `cmake`
-- `cmocka`
+You will need to have [node](https://nodejs.org/en/), [yarn](https://yarnpkg.com/getting-started/install), [rust](https://rustup.rs/) and [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/) installed.
-You will also need to install the following with `cargo install`
+To build and test the rust library:
-- `wasm-bindgen-cli`
-- `wasm-opt`
-- `cargo-deny`
-
-And ensure you have added the `wasm32-unknown-unknown` target for rust cross-compilation.
-
-The various subprojects (the rust code, the wrapper projects) have their own
-build instructions, but to run the tests that will be run in CI you can run
-`./scripts/ci/run`.
-
-### For macOS
-
-These instructions worked to build locally on macOS 13.1 (arm64) as of
-Nov 29th 2022.
-
-```bash
-# clone the repo
-git clone https://github.com/automerge/automerge-rs
-cd automerge-rs
-
-# install rustup
-curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
-
-# install homebrew
-/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
-
-# install cmake, node, cmocka
-brew install cmake node cmocka
-
-# install yarn
-npm install --global yarn
-
-# install javascript dependencies
-yarn --cwd ./javascript
-
-# install rust dependencies
-cargo install wasm-bindgen-cli wasm-opt cargo-deny
-
-# get nightly rust to produce optimized automerge-c builds
-rustup toolchain install nightly
-rustup component add rust-src --toolchain nightly
-
-# add wasm target in addition to current architecture
-rustup target add wasm32-unknown-unknown
-
-# Run ci script
-./scripts/ci/run
+```shell
+ $ cd automerge
+ $ cargo test
```
-If your build fails to find `cmocka.h` you may need to teach it about homebrew's
-installation location:
+To build and test the wasm library:
-```
-export CPATH=/opt/homebrew/include
-export LIBRARY_PATH=/opt/homebrew/lib
-./scripts/ci/run
+```shell
+ ## setup
+ $ cd automerge-wasm
+ $ yarn
+
+ ## building or testing
+ $ yarn build
+ $ yarn test
+
+ ## without this the js library wont automatically use changes
+ $ yarn link
+
+ ## cutting a release or doing benchmarking
+ $ yarn release
```
-## Contributing
+To test the js library. This is where most of the tests reside.
-Please try and split your changes up into relatively independent commits which
-change one subsystem at a time and add good commit messages which describe what
-the change is and why you're making it (err on the side of longer commit
-messages). `git blame` should give future maintainers a good idea of why
-something is the way it is.
+```shell
+ ## setup
+ $ cd automerge-js
+ $ yarn
+ $ yarn link "automerge-wasm"
+
+ ## testing
+ $ yarn test
+```
+
+And finally, to build and test the C bindings with CMake:
+
+```shell
+## setup
+$ cd automerge-c
+$ mkdir -p build
+$ cd build
+$ cmake -S .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF
+## building and testing
+$ cmake --build .
+```
+To add debugging symbols, replace `Release` with `Debug`.
+To build a shared library instead of a static one, replace `OFF` with `ON`.
+
+The C bindings can be built and tested on any platform for which CMake is
+available but the steps for doing so vary across platforms and are too numerous
+to list here.
+
+## Benchmarking
+
+The `edit-trace` folder has the main code for running the edit trace benchmarking.
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 00000000..646c0c20
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,32 @@
+### next steps:
+ 1. C API
+ 2. port rust command line tool
+ 3. fast load
+
+### ergonomics:
+ 1. value() -> () or something that into's a value
+
+### automerge:
+ 1. single pass (fast) load
+ 2. micro-patches / bare bones observation API / fully hydrated documents
+
+### future:
+ 1. handle columns with unknown data in and out
+ 2. branches with different indexes
+
+### Peritext
+ 1. add mark / remove mark -- type, start/end elemid (inclusive,exclusive)
+ 2. track any formatting ops that start or end on a character
+ 3. ops right before the character, ops right after that character
+ 4. query a single character - character, plus marks that start or end on that character
+ what is its current formatting,
+ what are the ops that include that in their span,
+ None = same as last time, Set( bold, italic ),
+ keep these on index
+ 5. op probably belongs with the start character - possible packed at the beginning or end of the list
+
+### maybe:
+ 1. tables
+
+### no:
+ 1. cursors
diff --git a/automerge-c/.gitignore b/automerge-c/.gitignore
new file mode 100644
index 00000000..cb544af0
--- /dev/null
+++ b/automerge-c/.gitignore
@@ -0,0 +1,3 @@
+automerge
+automerge.h
+automerge.o
diff --git a/automerge-c/CMakeLists.txt b/automerge-c/CMakeLists.txt
new file mode 100644
index 00000000..4ffca094
--- /dev/null
+++ b/automerge-c/CMakeLists.txt
@@ -0,0 +1,137 @@
+cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
+
+set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
+
+# Parse the library name, project name and project version out of Cargo's TOML file.
+set(CARGO_LIB_SECTION OFF)
+
+set(LIBRARY_NAME "")
+
+set(CARGO_PKG_SECTION OFF)
+
+set(CARGO_PKG_NAME "")
+
+set(CARGO_PKG_VERSION "")
+
+file(READ Cargo.toml TOML_STRING)
+
+string(REPLACE ";" "\\\\;" TOML_STRING "${TOML_STRING}")
+
+string(REPLACE "\n" ";" TOML_LINES "${TOML_STRING}")
+
+foreach(TOML_LINE IN ITEMS ${TOML_LINES})
+ string(REGEX MATCH "^\\[(lib|package)\\]$" _ ${TOML_LINE})
+
+ if(CMAKE_MATCH_1 STREQUAL "lib")
+ set(CARGO_LIB_SECTION ON)
+
+ set(CARGO_PKG_SECTION OFF)
+ elseif(CMAKE_MATCH_1 STREQUAL "package")
+ set(CARGO_LIB_SECTION OFF)
+
+ set(CARGO_PKG_SECTION ON)
+ endif()
+
+ string(REGEX MATCH "^name += +\"([^\"]+)\"$" _ ${TOML_LINE})
+
+ if(CMAKE_MATCH_1 AND (CARGO_LIB_SECTION AND NOT CARGO_PKG_SECTION))
+ set(LIBRARY_NAME "${CMAKE_MATCH_1}")
+ elseif(CMAKE_MATCH_1 AND (NOT CARGO_LIB_SECTION AND CARGO_PKG_SECTION))
+ set(CARGO_PKG_NAME "${CMAKE_MATCH_1}")
+ endif()
+
+ string(REGEX MATCH "^version += +\"([^\"]+)\"$" _ ${TOML_LINE})
+
+ if(CMAKE_MATCH_1 AND CARGO_PKG_SECTION)
+ set(CARGO_PKG_VERSION "${CMAKE_MATCH_1}")
+ endif()
+
+ if(LIBRARY_NAME AND (CARGO_PKG_NAME AND CARGO_PKG_VERSION))
+ break()
+ endif()
+endforeach()
+
+project(${CARGO_PKG_NAME} VERSION ${CARGO_PKG_VERSION} LANGUAGES C DESCRIPTION "C bindings for the Automerge Rust backend.")
+
+include(CTest)
+
+option(BUILD_SHARED_LIBS "Enable the choice of a shared or static library.")
+
+include(CMakePackageConfigHelpers)
+
+include(GNUInstallDirs)
+
+string(MAKE_C_IDENTIFIER ${PROJECT_NAME} SYMBOL_PREFIX)
+
+string(TOUPPER ${SYMBOL_PREFIX} SYMBOL_PREFIX)
+
+set(CARGO_TARGET_DIR "${CMAKE_CURRENT_BINARY_DIR}/Cargo/target")
+
+add_subdirectory(src)
+
+# Generate and install the configuration header.
+math(EXPR INTEGER_PROJECT_VERSION_MAJOR "${PROJECT_VERSION_MAJOR} * 100000")
+
+math(EXPR INTEGER_PROJECT_VERSION_MINOR "${PROJECT_VERSION_MINOR} * 100")
+
+math(EXPR INTEGER_PROJECT_VERSION_PATCH "${PROJECT_VERSION_PATCH}")
+
+math(EXPR INTEGER_PROJECT_VERSION "${INTEGER_PROJECT_VERSION_MAJOR} + ${INTEGER_PROJECT_VERSION_MINOR} + ${INTEGER_PROJECT_VERSION_PATCH}")
+
+configure_file(
+ ${CMAKE_MODULE_PATH}/config.h.in
+ config.h
+ @ONLY
+ NEWLINE_STYLE LF
+)
+
+install(
+ FILES ${CMAKE_BINARY_DIR}/config.h
+ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
+)
+
+if(BUILD_TESTING)
+ add_subdirectory(test)
+
+ enable_testing()
+endif()
+
+add_subdirectory(examples EXCLUDE_FROM_ALL)
+
+# Generate and install .cmake files
+set(PROJECT_CONFIG_NAME "${PROJECT_NAME}-config")
+
+set(PROJECT_CONFIG_VERSION_NAME "${PROJECT_CONFIG_NAME}-version")
+
+write_basic_package_version_file(
+ ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_VERSION_NAME}.cmake
+ VERSION ${PROJECT_VERSION}
+ COMPATIBILITY ExactVersion
+)
+
+# The namespace label starts with the title-cased library name.
+string(SUBSTRING ${LIBRARY_NAME} 0 1 NS_FIRST)
+
+string(SUBSTRING ${LIBRARY_NAME} 1 -1 NS_REST)
+
+string(TOUPPER ${NS_FIRST} NS_FIRST)
+
+string(TOLOWER ${NS_REST} NS_REST)
+
+string(CONCAT NAMESPACE ${NS_FIRST} ${NS_REST} "::")
+
+# \note CMake doesn't automate the exporting of an imported library's targets
+# so the package configuration script must do it.
+configure_package_config_file(
+ ${CMAKE_MODULE_PATH}/${PROJECT_CONFIG_NAME}.cmake.in
+ ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_NAME}.cmake
+ INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
+)
+
+install(
+ FILES
+ ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_NAME}.cmake
+ ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_CONFIG_VERSION_NAME}.cmake
+ DESTINATION
+ ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}
+)
diff --git a/rust/automerge-c/Cargo.toml b/automerge-c/Cargo.toml
similarity index 82%
rename from rust/automerge-c/Cargo.toml
rename to automerge-c/Cargo.toml
index 95a3a29c..851a3470 100644
--- a/rust/automerge-c/Cargo.toml
+++ b/automerge-c/Cargo.toml
@@ -7,8 +7,8 @@ license = "MIT"
rust-version = "1.57.0"
[lib]
-name = "automerge_core"
-crate-type = ["staticlib"]
+name = "automerge"
+crate-type = ["cdylib", "staticlib"]
bench = false
doc = false
@@ -19,4 +19,4 @@ libc = "^0.2"
smol_str = "^0.1.21"
[build-dependencies]
-cbindgen = "^0.24"
+cbindgen = "^0.20"
diff --git a/automerge-c/Makefile b/automerge-c/Makefile
new file mode 100644
index 00000000..a5ab353b
--- /dev/null
+++ b/automerge-c/Makefile
@@ -0,0 +1,30 @@
+
+CC=gcc
+CFLAGS=-I.
+DEPS=automerge.h
+LIBS=-lpthread -ldl -lm
+LDIR=../target/release
+LIB=../target/release/libautomerge.a
+DEBUG_LIB=../target/debug/libautomerge.a
+
+all: $(DEBUG_LIB) automerge
+
+debug: LDIR=../target/debug
+debug: automerge $(DEBUG_LIB)
+
+automerge: automerge.o $(LDIR)/libautomerge.a
+ $(CC) -o $@ automerge.o $(LDIR)/libautomerge.a $(LIBS) -L$(LDIR)
+
+$(DEBUG_LIB): src/*.rs
+ cargo build
+
+$(LIB): src/*.rs
+ cargo build --release
+
+%.o: %.c $(DEPS)
+ $(CC) -c -o $@ $< $(CFLAGS)
+
+.PHONY: clean
+
+clean:
+ rm -f *.o automerge $(LIB) $(DEBUG_LIB)
diff --git a/automerge-c/README.md b/automerge-c/README.md
new file mode 100644
index 00000000..d500f330
--- /dev/null
+++ b/automerge-c/README.md
@@ -0,0 +1,95 @@
+
+## Methods we need to support
+
+### Basic management
+
+ 1. `AMcreate()`
+ 1. `AMclone(doc)`
+ 1. `AMfree(doc)`
+ 1. `AMconfig(doc, key, val)` // set actor
+ 1. `actor = get_actor(doc)`
+
+### Transactions
+
+ 1. `AMpendingOps(doc)`
+ 1. `AMcommit(doc, message, time)`
+ 1. `AMrollback(doc)`
+
+### Write
+
+ 1. `AMset{Map|List}(doc, obj, prop, value)`
+ 1. `AMinsert(doc, obj, index, value)`
+ 1. `AMpush(doc, obj, value)`
+ 1. `AMdel{Map|List}(doc, obj, prop)`
+ 1. `AMinc{Map|List}(doc, obj, prop, value)`
+ 1. `AMspliceText(doc, obj, start, num_del, text)`
+
+### Read
+
+ 1. `AMkeys(doc, obj, heads)`
+ 1. `AMlength(doc, obj, heads)`
+ 1. `AMvalues(doc, obj, heads)`
+ 1. `AMtext(doc, obj, heads)`
+
+### Sync
+
+ 1. `AMgenerateSyncMessage(doc, state)`
+ 1. `AMreceiveSyncMessage(doc, state, message)`
+ 1. `AMinitSyncState()`
+
+### Save / Load
+
+ 1. `AMload(data)`
+ 1. `AMloadIncremental(doc, data)`
+ 1. `AMsave(doc)`
+ 1. `AMsaveIncremental(doc)`
+
+### Low Level Access
+
+ 1. `AMapplyChanges(doc, changes)`
+ 1. `AMgetChanges(doc, deps)`
+ 1. `AMgetChangesAdded(doc1, doc2)`
+ 1. `AMgetHeads(doc)`
+ 1. `AMgetLastLocalChange(doc)`
+ 1. `AMgetMissingDeps(doc, heads)`
+
+### Encode/Decode
+
+ 1. `AMencodeChange(change)`
+ 1. `AMdecodeChange(change)`
+ 1. `AMencodeSyncMessage(change)`
+ 1. `AMdecodeSyncMessage(change)`
+ 1. `AMencodeSyncState(change)`
+ 1. `AMdecodeSyncState(change)`
+
+## Open Question - Memory management
+
+Most of these calls return one or more items of arbitrary length. Doing memory management in C is tricky. This is my proposed solution...
+
+###
+
+ ```
+ // returns 1 or zero opids
+ n = automerge_set(doc, "_root", "hello", datatype, value);
+ if (n) {
+ automerge_pop(doc, &obj, len);
+ }
+
+ // returns n values
+ n = automerge_values(doc, "_root", "hello");
+ for (i = 0; i
+#include
+#include
+#include
+#include "automerge.h"
+
+#define MAX_BUFF_SIZE 4096
+
+int main() {
+ int n = 0;
+ int data_type = 0;
+ char buff[MAX_BUFF_SIZE];
+ char obj[MAX_BUFF_SIZE];
+ AMresult* res = NULL;
+
+ printf("begin\n");
+
+ AMdoc* doc = AMcreate();
+
+ printf("AMconfig()...");
+ AMconfig(doc, "actor", "aabbcc");
+ printf("pass!\n");
+
+ printf("AMmapSetStr()...\n");
+ res = AMmapSetStr(doc, NULL, "string", "hello world");
+ if (AMresultStatus(res) != AM_STATUS_COMMAND_OK)
+ {
+ printf("AMmapSet() failed: %s\n", AMerrorMessage(res));
+ return 1;
+ }
+ AMclear(res);
+ printf("pass!\n");
+
+ AMdestroy(doc);
+ printf("end\n");
+}
diff --git a/automerge-c/build.rs b/automerge-c/build.rs
new file mode 100644
index 00000000..e953527f
--- /dev/null
+++ b/automerge-c/build.rs
@@ -0,0 +1,25 @@
+extern crate cbindgen;
+
+use std::{env, path::PathBuf};
+
+fn main() {
+ let crate_dir = PathBuf::from(
+ env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR env var is not defined"),
+ );
+
+ let config = cbindgen::Config::from_file("cbindgen.toml")
+ .expect("Unable to find cbindgen.toml configuration file");
+
+ // let mut config: cbindgen::Config = Default::default();
+ // config.language = cbindgen::Language::C;
+
+ if let Ok(writer) = cbindgen::generate_with_config(&crate_dir, config) {
+ writer.write_to_file(crate_dir.join("automerge.h"));
+
+ // Also write the generated header into the target directory when
+ // specified (necessary for an out-of-source build a la CMake).
+ if let Ok(target_dir) = env::var("CARGO_TARGET_DIR") {
+ writer.write_to_file(PathBuf::from(target_dir).join("automerge.h"));
+ }
+ }
+}
diff --git a/rust/automerge-c/cbindgen.toml b/automerge-c/cbindgen.toml
similarity index 54%
rename from rust/automerge-c/cbindgen.toml
rename to automerge-c/cbindgen.toml
index 21eaaadd..20b7a41b 100644
--- a/rust/automerge-c/cbindgen.toml
+++ b/automerge-c/cbindgen.toml
@@ -1,7 +1,7 @@
after_includes = """\n
/**
* \\defgroup enumerations Public Enumerations
- * Symbolic names for integer constants.
+ Symbolic names for integer constants.
*/
/**
@@ -10,25 +10,16 @@ after_includes = """\n
* \\brief The root object of a document.
*/
#define AM_ROOT NULL
-
-/**
- * \\memberof AMdoc
- * \\def AM_CHANGE_HASH_SIZE
- * \\brief The count of bytes in a change hash.
- */
-#define AM_CHANGE_HASH_SIZE 32
-"""
-autogen_warning = """
-/**
- * \\file
- * \\brief All constants, functions and types in the core Automerge C API.
- *
- * \\warning This file is auto-generated by cbindgen.
- */
"""
+autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
documentation = true
documentation_style = "doxy"
-include_guard = "AUTOMERGE_C_H"
+header = """
+/** \\file
+ * All constants, functions and types in the Automerge library's C API.
+ */
+ """
+include_guard = "automerge_h"
includes = []
language = "C"
line_length = 140
@@ -45,4 +36,4 @@ prefix_with_name = true
rename_variants = "ScreamingSnakeCase"
[export]
-item_types = ["constants", "enums", "functions", "opaque", "structs", "typedefs"]
+item_types = ["enums", "structs", "opaque", "constants", "functions"]
diff --git a/rust/automerge-c/cmake/automerge-c-config.cmake.in b/automerge-c/cmake/automerge-c-config.cmake.in
similarity index 100%
rename from rust/automerge-c/cmake/automerge-c-config.cmake.in
rename to automerge-c/cmake/automerge-c-config.cmake.in
diff --git a/automerge-c/cmake/config.h.in b/automerge-c/cmake/config.h.in
new file mode 100644
index 00000000..08643fc5
--- /dev/null
+++ b/automerge-c/cmake/config.h.in
@@ -0,0 +1,14 @@
+#ifndef @SYMBOL_PREFIX@_CONFIG_INCLUDED
+#define @SYMBOL_PREFIX@_CONFIG_INCLUDED
+
+/* This header is auto-generated by CMake. */
+
+#define @SYMBOL_PREFIX@_VERSION @INTEGER_PROJECT_VERSION@
+
+#define @SYMBOL_PREFIX@_MAJOR_VERSION (@SYMBOL_PREFIX@_VERSION / 100000)
+
+#define @SYMBOL_PREFIX@_MINOR_VERSION ((@SYMBOL_PREFIX@_VERSION / 100) % 1000)
+
+#define @SYMBOL_PREFIX@_PATCH_VERSION (@SYMBOL_PREFIX@_VERSION % 100)
+
+#endif /* @SYMBOL_PREFIX@_CONFIG_INCLUDED */
diff --git a/rust/automerge-c/cmake/file-regex-replace.cmake b/automerge-c/cmake/file_regex_replace.cmake
similarity index 87%
rename from rust/automerge-c/cmake/file-regex-replace.cmake
rename to automerge-c/cmake/file_regex_replace.cmake
index 09005bc2..27306458 100644
--- a/rust/automerge-c/cmake/file-regex-replace.cmake
+++ b/automerge-c/cmake/file_regex_replace.cmake
@@ -1,6 +1,4 @@
-# This CMake script is used to perform string substitutions within a generated
-# file.
-cmake_minimum_required(VERSION 3.23 FATAL_ERROR)
+cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
if(NOT DEFINED MATCH_REGEX)
message(FATAL_ERROR "Variable \"MATCH_REGEX\" is not defined.")
diff --git a/rust/automerge-c/cmake/file-touch.cmake b/automerge-c/cmake/file_touch.cmake
similarity index 82%
rename from rust/automerge-c/cmake/file-touch.cmake
rename to automerge-c/cmake/file_touch.cmake
index 2c196755..087d59b6 100644
--- a/rust/automerge-c/cmake/file-touch.cmake
+++ b/automerge-c/cmake/file_touch.cmake
@@ -1,6 +1,4 @@
-# This CMake script is used to force Cargo to regenerate the header file for the
-# core bindings after the out-of-source build directory has been cleaned.
-cmake_minimum_required(VERSION 3.23 FATAL_ERROR)
+cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
if(NOT DEFINED CONDITION)
message(FATAL_ERROR "Variable \"CONDITION\" is not defined.")
diff --git a/rust/automerge-c/examples/CMakeLists.txt b/automerge-c/examples/CMakeLists.txt
similarity index 61%
rename from rust/automerge-c/examples/CMakeLists.txt
rename to automerge-c/examples/CMakeLists.txt
index f080237b..09ddeb70 100644
--- a/rust/automerge-c/examples/CMakeLists.txt
+++ b/automerge-c/examples/CMakeLists.txt
@@ -1,39 +1,41 @@
+cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
+
add_executable(
- ${LIBRARY_NAME}_quickstart
+ example_quickstart
quickstart.c
)
-set_target_properties(${LIBRARY_NAME}_quickstart PROPERTIES LINKER_LANGUAGE C)
+set_target_properties(example_quickstart PROPERTIES LINKER_LANGUAGE C)
# \note An imported library's INTERFACE_INCLUDE_DIRECTORIES property can't
# contain a non-existent path so its build-time include directory
# must be specified for all of its dependent targets instead.
target_include_directories(
- ${LIBRARY_NAME}_quickstart
- PRIVATE "$"
+ example_quickstart
+ PRIVATE "$"
)
-target_link_libraries(${LIBRARY_NAME}_quickstart PRIVATE ${LIBRARY_NAME})
+target_link_libraries(example_quickstart PRIVATE ${LIBRARY_NAME})
-add_dependencies(${LIBRARY_NAME}_quickstart ${BINDINGS_NAME}_artifacts)
+add_dependencies(example_quickstart ${LIBRARY_NAME}_artifacts)
if(BUILD_SHARED_LIBS AND WIN32)
add_custom_command(
- TARGET ${LIBRARY_NAME}_quickstart
+ TARGET example_quickstart
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_SHARED_LIBRARY_SUFFIX}
- ${CMAKE_BINARY_DIR}
+ ${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Copying the DLL built by Cargo into the examples directory..."
VERBATIM
)
endif()
add_custom_command(
- TARGET ${LIBRARY_NAME}_quickstart
+ TARGET example_quickstart
POST_BUILD
COMMAND
- ${LIBRARY_NAME}_quickstart
+ example_quickstart
COMMENT
"Running the example quickstart..."
VERBATIM
diff --git a/rust/automerge-c/examples/README.md b/automerge-c/examples/README.md
similarity index 68%
rename from rust/automerge-c/examples/README.md
rename to automerge-c/examples/README.md
index 17e69412..17aa2227 100644
--- a/rust/automerge-c/examples/README.md
+++ b/automerge-c/examples/README.md
@@ -5,5 +5,5 @@
```shell
cmake -E make_directory automerge-c/build
cmake -S automerge-c -B automerge-c/build
-cmake --build automerge-c/build --target automerge_quickstart
+cmake --build automerge-c/build --target example_quickstart
```
diff --git a/automerge-c/examples/quickstart.c b/automerge-c/examples/quickstart.c
new file mode 100644
index 00000000..5b90fdcd
--- /dev/null
+++ b/automerge-c/examples/quickstart.c
@@ -0,0 +1,157 @@
+#include
+#include
+
+#include
+
+AMvalue test(AMresult*, AMvalueVariant const);
+
+/*
+ * Based on https://automerge.github.io/docs/quickstart
+ */
+int main(int argc, char** argv) {
+ AMresult* const doc1_result = AMcreate();
+ AMdoc* const doc1 = AMresultValue(doc1_result).doc;
+ if (doc1 == NULL) {
+ fprintf(stderr, "`AMcreate()` failure.");
+ exit(EXIT_FAILURE);
+ }
+ AMresult* const cards_result = AMmapPutObject(doc1, AM_ROOT, "cards", AM_OBJ_TYPE_LIST);
+ AMvalue value = test(cards_result, AM_VALUE_OBJ_ID);
+ AMobjId const* const cards = value.obj_id;
+ AMresult* const card1_result = AMlistPutObject(doc1, cards, 0, true, AM_OBJ_TYPE_MAP);
+ value = test(card1_result, AM_VALUE_OBJ_ID);
+ AMobjId const* const card1 = value.obj_id;
+ AMresult* result = AMmapPutStr(doc1, card1, "title", "Rewrite everything in Clojure");
+ test(result, AM_VALUE_VOID);
+ AMfree(result);
+ result = AMmapPutBool(doc1, card1, "done", false);
+ test(result, AM_VALUE_VOID);
+ AMfree(result);
+ AMresult* const card2_result = AMlistPutObject(doc1, cards, 0, true, AM_OBJ_TYPE_MAP);
+ value = test(card2_result, AM_VALUE_OBJ_ID);
+ AMobjId const* const card2 = value.obj_id;
+ result = AMmapPutStr(doc1, card2, "title", "Rewrite everything in Haskell");
+ test(result, AM_VALUE_VOID);
+ AMfree(result);
+ result = AMmapPutBool(doc1, card2, "done", false);
+ test(result, AM_VALUE_VOID);
+ AMfree(result);
+ AMfree(card2_result);
+ result = AMcommit(doc1, "Add card", NULL);
+ test(result, AM_VALUE_CHANGE_HASHES);
+ AMfree(result);
+
+ AMresult* doc2_result = AMcreate();
+ AMdoc* doc2 = AMresultValue(doc2_result).doc;
+ if (doc2 == NULL) {
+ fprintf(stderr, "`AMcreate()` failure.");
+ AMfree(card1_result);
+ AMfree(cards_result);
+ AMfree(doc1_result);
+ exit(EXIT_FAILURE);
+ }
+ result = AMmerge(doc2, doc1);
+ test(result, AM_VALUE_CHANGE_HASHES);
+ AMfree(result);
+ AMfree(doc2_result);
+
+ AMresult* const save_result = AMsave(doc1);
+ value = test(save_result, AM_VALUE_BYTES);
+ AMbyteSpan binary = value.bytes;
+ doc2_result = AMload(binary.src, binary.count);
+ doc2 = AMresultValue(doc2_result).doc;
+ AMfree(save_result);
+ if (doc2 == NULL) {
+ fprintf(stderr, "`AMload()` failure.");
+ AMfree(card1_result);
+ AMfree(cards_result);
+ AMfree(doc1_result);
+ exit(EXIT_FAILURE);
+ }
+
+ result = AMmapPutBool(doc1, card1, "done", true);
+ test(result, AM_VALUE_VOID);
+ AMfree(result);
+ result = AMcommit(doc1, "Mark card as done", NULL);
+ test(result, AM_VALUE_CHANGE_HASHES);
+ AMfree(result);
+ AMfree(card1_result);
+
+ result = AMlistDelete(doc2, cards, 0);
+ test(result, AM_VALUE_VOID);
+ AMfree(result);
+ result = AMcommit(doc2, "Delete card", NULL);
+ test(result, AM_VALUE_CHANGE_HASHES);
+ AMfree(result);
+
+ result = AMmerge(doc1, doc2);
+ test(result, AM_VALUE_CHANGE_HASHES);
+ AMfree(result);
+ AMfree(doc2_result);
+
+ result = AMgetChanges(doc1, NULL);
+ value = test(result, AM_VALUE_CHANGES);
+ AMchange const* change = NULL;
+ while ((change = AMchangesNext(&value.changes, 1)) != NULL) {
+ size_t const size = AMobjSizeAt(doc1, cards, change);
+ printf("%s %ld\n", AMchangeMessage(change), size);
+ }
+ AMfree(result);
+ AMfree(cards_result);
+ AMfree(doc1_result);
+}
+
+/**
+ * \brief Extracts a value with the given discriminant from the given result
+ * or writes a message to `stderr`, frees the given result and
+ * terminates the program.
+ *
+.* \param[in] result A pointer to an `AMresult` struct.
+ * \param[in] discriminant An `AMvalueVariant` enum tag.
+ * \return An `AMvalue` struct.
+ * \pre \p result must be a valid address.
+ */
+AMvalue test(AMresult* result, AMvalueVariant const discriminant) {
+ static char prelude[64];
+
+ if (result == NULL) {
+ fprintf(stderr, "NULL `AMresult` struct pointer.");
+ exit(EXIT_FAILURE);
+ }
+ AMstatus const status = AMresultStatus(result);
+ if (status != AM_STATUS_OK) {
+ switch (status) {
+ case AM_STATUS_ERROR: sprintf(prelude, "Error"); break;
+ case AM_STATUS_INVALID_RESULT: sprintf(prelude, "Invalid result"); break;
+ default: sprintf(prelude, "Unknown `AMstatus` tag %d", status);
+ }
+ fprintf(stderr, "%s; %s.", prelude, AMerrorMessage(result));
+ AMfree(result);
+ exit(EXIT_FAILURE);
+ }
+ AMvalue const value = AMresultValue(result);
+ if (value.tag != discriminant) {
+ char const* label = NULL;
+ switch (value.tag) {
+ case AM_VALUE_ACTOR_ID: label = "AM_VALUE_ACTOR_ID"; break;
+ case AM_VALUE_BOOLEAN: label = "AM_VALUE_BOOLEAN"; break;
+ case AM_VALUE_BYTES: label = "AM_VALUE_BYTES"; break;
+ case AM_VALUE_CHANGE_HASHES: label = "AM_VALUE_CHANGE_HASHES"; break;
+ case AM_VALUE_CHANGES: label = "AM_VALUE_CHANGES"; break;
+ case AM_VALUE_COUNTER: label = "AM_VALUE_COUNTER"; break;
+ case AM_VALUE_F64: label = "AM_VALUE_F64"; break;
+ case AM_VALUE_INT: label = "AM_VALUE_INT"; break;
+ case AM_VALUE_VOID: label = "AM_VALUE_VOID"; break;
+ case AM_VALUE_NULL: label = "AM_VALUE_NULL"; break;
+ case AM_VALUE_OBJ_ID: label = "AM_VALUE_OBJ_ID"; break;
+ case AM_VALUE_STR: label = "AM_VALUE_STR"; break;
+ case AM_VALUE_TIMESTAMP: label = "AM_VALUE_TIMESTAMP"; break;
+ case AM_VALUE_UINT: label = "AM_VALUE_UINT"; break;
+ default: label = "";
+ }
+ fprintf(stderr, "Unexpected `AMvalueVariant` tag `%s` (%d).", label, value.tag);
+ AMfree(result);
+ exit(EXIT_FAILURE);
+ }
+ return value;
+}
diff --git a/rust/automerge-c/docs/img/brandmark.png b/automerge-c/img/brandmark.png
similarity index 100%
rename from rust/automerge-c/docs/img/brandmark.png
rename to automerge-c/img/brandmark.png
diff --git a/automerge-c/src/CMakeLists.txt b/automerge-c/src/CMakeLists.txt
new file mode 100644
index 00000000..2e6a5658
--- /dev/null
+++ b/automerge-c/src/CMakeLists.txt
@@ -0,0 +1,237 @@
+cmake_minimum_required(VERSION 3.18 FATAL_ERROR)
+
+find_program (
+ CARGO_CMD
+ "cargo"
+ PATHS "$ENV{CARGO_HOME}/bin"
+ DOC "The Cargo command"
+)
+
+if(NOT CARGO_CMD)
+ message(FATAL_ERROR "Cargo (Rust package manager) not found! Install it and/or set the CARGO_HOME environment variable.")
+endif()
+
+string(TOLOWER "${CMAKE_BUILD_TYPE}" BUILD_TYPE_LOWER)
+
+if(BUILD_TYPE_LOWER STREQUAL debug)
+ set(CARGO_BUILD_TYPE "debug")
+
+ set(CARGO_FLAG "")
+else()
+ set(CARGO_BUILD_TYPE "release")
+
+ set(CARGO_FLAG "--release")
+endif()
+
+set(CARGO_CURRENT_BINARY_DIR "${CARGO_TARGET_DIR}/${CARGO_BUILD_TYPE}")
+
+set(
+ CARGO_OUTPUT
+ ${CARGO_TARGET_DIR}/${LIBRARY_NAME}.h
+ ${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}
+ ${CARGO_CURRENT_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}
+)
+
+if(WIN32)
+ # \note The basename of an import library output by Cargo is the filename
+ # of its corresponding shared library.
+ list(APPEND CARGO_OUTPUT ${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}${CMAKE_STATIC_LIBRARY_SUFFIX})
+endif()
+
+add_custom_command(
+ OUTPUT
+ ${CARGO_OUTPUT}
+ COMMAND
+ # \note cbindgen won't regenerate its output header file after it's
+ # been removed but it will after its configuration file has been
+ # updated.
+ ${CMAKE_COMMAND} -DCONDITION=NOT_EXISTS -P ${CMAKE_SOURCE_DIR}/cmake/file_touch.cmake -- ${CARGO_TARGET_DIR}/${LIBRARY_NAME}.h ${CMAKE_SOURCE_DIR}/cbindgen.toml
+ COMMAND
+ ${CMAKE_COMMAND} -E env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} ${CARGO_CMD} build ${CARGO_FLAG}
+ MAIN_DEPENDENCY
+ lib.rs
+ DEPENDS
+ byte_span.rs
+ change_hashes.rs
+ change.rs
+ changes.rs
+ doc.rs
+ doc/list.rs
+ doc/map.rs
+ doc/utils.rs
+ obj.rs
+ result.rs
+ sync.rs
+ sync/have.rs
+ sync/haves.rs
+ sync/message.rs
+ sync/state.rs
+ ${CMAKE_SOURCE_DIR}/build.rs
+ ${CMAKE_SOURCE_DIR}/Cargo.toml
+ ${CMAKE_SOURCE_DIR}/cbindgen.toml
+ WORKING_DIRECTORY
+ ${CMAKE_SOURCE_DIR}
+ COMMENT
+ "Producing the library artifacts with Cargo..."
+ VERBATIM
+)
+
+add_custom_target(
+ ${LIBRARY_NAME}_artifacts
+ DEPENDS ${CARGO_OUTPUT}
+)
+
+# \note cbindgen's naming behavior isn't fully configurable and it ignores
+# `const fn` calls (https://github.com/eqrion/cbindgen/issues/252).
+add_custom_command(
+ TARGET ${LIBRARY_NAME}_artifacts
+ POST_BUILD
+ COMMAND
+ # Compensate for cbindgen's variant struct naming.
+ ${CMAKE_COMMAND} -DMATCH_REGEX=AM\([^_]+_[^_]+\)_Body -DREPLACE_EXPR=AM\\1 -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CARGO_TARGET_DIR}/${LIBRARY_NAME}.h
+ COMMAND
+ # Compensate for cbindgen's union tag enum type naming.
+ ${CMAKE_COMMAND} -DMATCH_REGEX=AM\([^_]+\)_Tag -DREPLACE_EXPR=AM\\1Variant -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CARGO_TARGET_DIR}/${LIBRARY_NAME}.h
+ COMMAND
+ # Compensate for cbindgen's translation of consecutive uppercase letters to "ScreamingSnakeCase".
+ ${CMAKE_COMMAND} -DMATCH_REGEX=A_M\([^_]+\)_ -DREPLACE_EXPR=AM_\\1_ -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CARGO_TARGET_DIR}/${LIBRARY_NAME}.h
+ COMMAND
+ # Compensate for cbindgen ignoring `std:mem::size_of()` calls.
+ ${CMAKE_COMMAND} -DMATCH_REGEX=USIZE_ -DREPLACE_EXPR=\+${CMAKE_SIZEOF_VOID_P} -P ${CMAKE_SOURCE_DIR}/cmake/file_regex_replace.cmake -- ${CARGO_TARGET_DIR}/${LIBRARY_NAME}.h
+ WORKING_DIRECTORY
+ ${CMAKE_SOURCE_DIR}
+ COMMENT
+ "Compensating for cbindgen deficits..."
+ VERBATIM
+)
+
+if(BUILD_SHARED_LIBS)
+ if(WIN32)
+ set(LIBRARY_DESTINATION "${CMAKE_INSTALL_BINDIR}")
+ else()
+ set(LIBRARY_DESTINATION "${CMAKE_INSTALL_LIBDIR}")
+ endif()
+
+ set(LIBRARY_DEFINE_SYMBOL "${SYMBOL_PREFIX}_EXPORTS")
+
+ # \note The basename of an import library output by Cargo is the filename
+ # of its corresponding shared library.
+ set(LIBRARY_IMPLIB "${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}${CMAKE_STATIC_LIBRARY_SUFFIX}")
+
+ set(LIBRARY_LOCATION "${CARGO_CURRENT_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_SHARED_LIBRARY_SUFFIX}")
+
+ set(LIBRARY_NO_SONAME "${WIN32}")
+
+ set(LIBRARY_SONAME "${CMAKE_SHARED_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_SHARED_LIBRARY_SUFFIX}")
+
+ set(LIBRARY_TYPE "SHARED")
+else()
+ set(LIBRARY_DEFINE_SYMBOL "")
+
+ set(LIBRARY_DESTINATION "${CMAKE_INSTALL_LIBDIR}")
+
+ set(LIBRARY_IMPLIB "")
+
+ set(LIBRARY_LOCATION "${CARGO_CURRENT_BINARY_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}")
+
+ set(LIBRARY_NO_SONAME "TRUE")
+
+ set(LIBRARY_SONAME "")
+
+ set(LIBRARY_TYPE "STATIC")
+endif()
+
+add_library(${LIBRARY_NAME} ${LIBRARY_TYPE} IMPORTED GLOBAL)
+
+set_target_properties(
+ ${LIBRARY_NAME}
+ PROPERTIES
+ # \note Cargo writes a debug build into a nested directory instead of
+ # decorating its name.
+ DEBUG_POSTFIX ""
+ DEFINE_SYMBOL "${LIBRARY_DEFINE_SYMBOL}"
+ IMPORTED_IMPLIB "${LIBRARY_IMPLIB}"
+ IMPORTED_LOCATION "${LIBRARY_LOCATION}"
+ IMPORTED_NO_SONAME "${LIBRARY_NO_SONAME}"
+ IMPORTED_SONAME "${LIBRARY_SONAME}"
+ LINKER_LANGUAGE C
+ PUBLIC_HEADER "${CARGO_TARGET_DIR}/${LIBRARY_NAME}.h"
+ SOVERSION "${PROJECT_VERSION_MAJOR}"
+ VERSION "${PROJECT_VERSION}"
+ # \note Cargo exports all of the symbols automatically.
+ WINDOWS_EXPORT_ALL_SYMBOLS "TRUE"
+)
+
+target_compile_definitions(${LIBRARY_NAME} INTERFACE $)
+
+target_include_directories(
+ ${LIBRARY_NAME}
+ INTERFACE
+ "$"
+)
+
+set(CMAKE_THREAD_PREFER_PTHREAD TRUE)
+
+set(THREADS_PREFER_PTHREAD_FLAG TRUE)
+
+find_package(Threads REQUIRED)
+
+set(LIBRARY_DEPENDENCIES Threads::Threads ${CMAKE_DL_LIBS})
+
+if(WIN32)
+ list(APPEND LIBRARY_DEPENDENCIES Bcrypt userenv ws2_32)
+else()
+ list(APPEND LIBRARY_DEPENDENCIES m)
+endif()
+
+target_link_libraries(${LIBRARY_NAME} INTERFACE ${LIBRARY_DEPENDENCIES})
+
+install(
+ FILES $
+ TYPE LIB
+ # \note The basename of an import library output by Cargo is the filename
+ # of its corresponding shared library.
+ RENAME "${CMAKE_STATIC_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_STATIC_LIBRARY_SUFFIX}"
+ OPTIONAL
+)
+
+set(LIBRARY_FILE_NAME "${CMAKE_${LIBRARY_TYPE}_LIBRARY_PREFIX}${LIBRARY_NAME}${CMAKE_${CMAKE_BUILD_TYPE}_POSTFIX}${CMAKE_${LIBRARY_TYPE}_LIBRARY_SUFFIX}")
+
+install(
+ FILES $
+ RENAME "${LIBRARY_FILE_NAME}"
+ DESTINATION ${LIBRARY_DESTINATION}
+)
+
+install(
+ FILES $
+ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}
+)
+
+find_package(Doxygen OPTIONAL_COMPONENTS dot)
+
+if(DOXYGEN_FOUND)
+ set(DOXYGEN_GENERATE_LATEX YES)
+
+ set(DOXYGEN_PDF_HYPERLINKS YES)
+
+ set(DOXYGEN_PROJECT_LOGO "${CMAKE_SOURCE_DIR}/img/brandmark.png")
+
+ set(DOXYGEN_SORT_BRIEF_DOCS YES)
+
+ set(DOXYGEN_USE_MDFILE_AS_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md")
+
+ doxygen_add_docs(
+ ${LIBRARY_NAME}_docs
+ "${CARGO_TARGET_DIR}/${LIBRARY_NAME}.h"
+ "${CMAKE_SOURCE_DIR}/README.md"
+ USE_STAMP_FILE
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+ COMMENT "Producing documentation with Doxygen..."
+ )
+
+ # \note A Doxygen input file isn't a file-level dependency so the Doxygen
+ # command must instead depend upon a target that outputs the file or
+ # it will just output an error message when it can't be found.
+ add_dependencies(${LIBRARY_NAME}_docs ${LIBRARY_NAME}_artifacts)
+endif()
diff --git a/automerge-c/src/byte_span.rs b/automerge-c/src/byte_span.rs
new file mode 100644
index 00000000..4ed7198a
--- /dev/null
+++ b/automerge-c/src/byte_span.rs
@@ -0,0 +1,62 @@
+use automerge as am;
+
+/// \struct AMbyteSpan
+/// \brief A contiguous sequence of bytes.
+///
+#[repr(C)]
+pub struct AMbyteSpan {
+ /// A pointer to an array of bytes.
+ /// \warning \p src is only valid until the `AMfree()` function is
+ /// called on the `AMresult` struct hosting the array of bytes to
+ /// which it points.
+ src: *const u8,
+ /// The number of bytes in the array.
+ count: usize,
+}
+
+impl Default for AMbyteSpan {
+ fn default() -> Self {
+ Self {
+ src: std::ptr::null(),
+ count: 0,
+ }
+ }
+}
+
+impl From<&am::ActorId> for AMbyteSpan {
+ fn from(actor: &am::ActorId) -> Self {
+ let slice = actor.to_bytes();
+ Self {
+ src: slice.as_ptr(),
+ count: slice.len(),
+ }
+ }
+}
+
+impl From<&mut am::ActorId> for AMbyteSpan {
+ fn from(actor: &mut am::ActorId) -> Self {
+ let slice = actor.to_bytes();
+ Self {
+ src: slice.as_ptr(),
+ count: slice.len(),
+ }
+ }
+}
+
+impl From<&am::ChangeHash> for AMbyteSpan {
+ fn from(change_hash: &am::ChangeHash) -> Self {
+ Self {
+ src: change_hash.0.as_ptr(),
+ count: change_hash.0.len(),
+ }
+ }
+}
+
+impl From<&[u8]> for AMbyteSpan {
+ fn from(slice: &[u8]) -> Self {
+ Self {
+ src: slice.as_ptr(),
+ count: slice.len(),
+ }
+ }
+}
diff --git a/rust/automerge-c/src/change.rs b/automerge-c/src/change.rs
similarity index 53%
rename from rust/automerge-c/src/change.rs
rename to automerge-c/src/change.rs
index 8529ed94..2ebd7469 100644
--- a/rust/automerge-c/src/change.rs
+++ b/automerge-c/src/change.rs
@@ -1,55 +1,32 @@
use automerge as am;
-use std::cell::RefCell;
+use std::ffi::CString;
+use std::os::raw::c_char;
use crate::byte_span::AMbyteSpan;
+use crate::change_hashes::AMchangeHashes;
use crate::result::{to_result, AMresult};
-macro_rules! to_change {
- ($handle:expr) => {{
- let handle = $handle.as_ref();
- match handle {
- Some(b) => b,
- None => return AMresult::error("Invalid `AMchange*`").into(),
- }
- }};
-}
-
/// \struct AMchange
-/// \installed_headerfile
/// \brief A group of operations performed by an actor.
-#[derive(Eq, PartialEq)]
pub struct AMchange {
body: *mut am::Change,
- change_hash: RefCell