automerge/rust/automerge-c
alexjg 08801ab580
automerge-rs: Introduce ReadDoc and SyncDoc traits and add documentation (#511)
The Rust API has so far grown somewhat organically driven by the needs of the
javascript implementation. This has led to an API which is quite awkward and
unfamiliar to Rust programmers. Additionally there is no documentation to speak
of. This commit is the first movement towards cleaning things up a bit. We touch
a lot of files but the changes are all very mechanical. We introduce a few
traits to abstract over the common operations between `Automerge` and
`AutoCommit`, and add a whole bunch of documentation.

* Add a `ReadDoc` trait to describe methods which read value from a document.
  make `Transactable` extend `ReadDoc`
* Add a `SyncDoc` trait to describe methods necessary for synchronizing
  documents.
* Put the `SyncDoc` implementation for `AutoCommit` behind `AutoCommit::sync` to
  ensure that any open transactions are closed before taking part in the sync
  protocol
* Split `OpObserver` into two traits: `OpObserver` + `BranchableObserver`.
  `BranchableObserver` captures the methods which are only needed for observing
  transactions.
* Add a whole bunch of documentation.

The main changes Rust users will need to make is:

* Import the `ReadDoc` trait wherever you are using the methods which have been
  moved to it. Optionally change concrete paramters on functions to `ReadDoc`
  constraints.
* Likewise import the `SyncDoc` trait wherever you are doing synchronisation
  work
* If you are using the `AutoCommit::*_sync_message` methods you will need to add
  a call to `AutoCommit::sync()` first. E.g. `doc.generate_sync_message` becomes
  `doc.sync().generate_sync_message`
* If you have an implementation of `OpObserver` which you are using in an
  `AutoCommit` then split it into an implementation of `OpObserver` and
  `BranchableObserver`
2023-01-30 19:37:03 +00:00
..
cmake Move rust workspace into ./rust 2022-10-16 19:55:51 +01:00
examples Updated the quickstart example to work with 2022-11-27 23:52:47 -08:00
img Move rust workspace into ./rust 2022-10-16 19:55:51 +01:00
src automerge-rs: Introduce ReadDoc and SyncDoc traits and add documentation (#511) 2023-01-30 19:37:03 +00:00
test wasm: Allow a choice of text representations 2023-01-10 12:52:19 +00:00
.gitignore Text v2. JS Api now uses text by default (#462) 2022-12-09 23:48:07 +00:00
build.rs Update rust toolchain to 1.66 2023-01-10 12:51:56 +00:00
Cargo.toml automerge-cli: remove a bunch of bad dependencies (#478) 2022-12-14 18:06:19 +00:00
cbindgen.toml Move rust workspace into ./rust 2022-10-16 19:55:51 +01:00
CMakeLists.txt Hard-coded automerge-c's initial independent 2022-11-28 00:08:33 -08:00
README.md Improve docs for building automerge-c on a mac (#465) 2022-12-09 13:46:23 +00:00

automerge-c exposes an API to C that can either be used directly or as a basis for other language bindings that have good support for calling into C functions.

Building

See the main README for instructions on getting your environment set up, then you can use ./scripts/ci/cmake-build Release static to build automerge-c.

It will output two files:

  • ./build/Cargo/target/include/automerge-c/automerge.h
  • ./build/Cargo/target/release/libautomerge.a

To use these in your application you must arrange for your C compiler to find these files, either by moving them to the right location on your computer, or by configuring the compiler to reference these directories.

  • export LDFLAGS=-L./build/Cargo/target/release -lautomerge
  • export CFLAGS=-I./build/Cargo/target/include

If you'd like to cross compile the library for different platforms you can do so using cross. For example:

  • cross build --manifest-path rust/automerge-c/Cargo.toml -r --target aarch64-unknown-linux-gnu

This will output a shared library in the directory rust/target/aarch64-unknown-linux-gnu/release/.

You can replace aarch64-unknown-linux-gnu with any cross supported targets. The targets below are known to work, though other targets are expected to work too:

  • x86_64-apple-darwin
  • aarch64-apple-darwin
  • x86_64-unknown-linux-gnu
  • aarch64-unknown-linux-gnu

As a caveat, the header file is currently 32/64-bit dependant. You can re-use it for all 64-bit architectures, but you must generate a specific header for 32-bit targets.

Usage

For full reference, read through automerge.h, or to get started quickly look at the examples.

Almost all operations in automerge-c act on an AMdoc struct which you can get from AMcreate() or AMload(). Operations on a given doc are not thread safe so you must use a mutex or similar to avoid calling more than one function with the same AMdoc pointer concurrently.

As with all functions that either allocate memory, or could fail if given invalid input, AMcreate() returns an AMresult. The AMresult contains the returned doc (or error message), and must be freed with AMfree() after you are done to avoid leaking memory.

#include <automerge-c/automerge.h>
#include <stdio.h>

int main(int argc, char** argv) {
  AMresult *docResult = AMcreate(NULL);

  if (AMresultStatus(docResult) != AM_STATUS_OK) {
    printf("failed to create doc: %s", AMerrorMessage(docResult).src);
    goto cleanup;
  }

  AMdoc *doc = AMresultValue(docResult).doc;

  // useful code goes here!

cleanup:
  AMfree(docResult);
}

If you are writing code in C directly, you can use the AMpush() helper function to reduce the boilerplate of error handling and freeing for you (see examples/quickstart.c).

If you are wrapping automerge-c in another language, particularly one that has a garbage collector, you can call AMfree within a finalizer to ensure that memory is reclaimed when it is no longer needed.

An AMdoc wraps an automerge document which are very similar to JSON documents. Automerge documents consist of a mutable root, which is always a map from string keys to values. Values can have the following types:

  • A number of type double / int64_t / uint64_t
  • An explicit true / false / nul
  • An immutable utf-8 string (AMbyteSpan)
  • An immutable array of arbitrary bytes (AMbyteSpan)
  • A mutable map from string keys to values (AMmap)
  • A mutable list of values (AMlist)
  • A mutable string (AMtext)

If you read from a location in the document with no value a value with .tag == AM_VALUE_VOID will be returned, but you cannot write such a value explicitly.

Under the hood, automerge references mutable objects by the internal object id, and AM_ROOT is always the object id of the root value.

There is a function to put each type of value into either a map or a list, and a function to read the current value from a list. As (in general) collaborators may edit the document at any time, you cannot guarantee that the type of the value at a given part of the document will stay the same. As a result reading from the document will return an AMvalue union that you can inspect to determine its type.

Strings in automerge-c are represented using an AMbyteSpan which contains a pointer and a length. Strings must be valid utf-8 and may contain null bytes. As a convenience you can use AMstr() to get the representation of a null-terminated C string as an AMbyteSpan.

Putting all of that together, to read and write from the root of the document you can do this:

#include <automerge-c/automerge.h>
#include <stdio.h>

int main(int argc, char** argv) {
  // ...previous example...
  AMdoc *doc = AMresultValue(docResult).doc;

  AMresult *putResult = AMmapPutStr(doc, AM_ROOT, AMstr("key"), AMstr("value"));
  if (AMresultStatus(putResult) != AM_STATUS_OK) {
    printf("failed to put: %s", AMerrorMessage(putResult).src);
    goto cleanup;
  }

  AMresult *getResult = AMmapGet(doc, AM_ROOT, AMstr("key"), NULL);
  if (AMresultStatus(getResult) != AM_STATUS_OK) {
    printf("failed to get: %s", AMerrorMessage(getResult).src);
    goto cleanup;
  }

  AMvalue got = AMresultValue(getResult);
  if (got.tag != AM_VALUE_STR) {
    printf("expected to read a string!");
    goto cleanup;
  }

  printf("Got %zu-character string `%s`", got.str.count, got.str.src);

cleanup:
  AMfree(getResult);
  AMfree(putResult);
  AMfree(docResult);
}

Functions that do not return an AMresult (for example AMmapItemValue()) do not allocate memory, but continue to reference memory that was previously allocated. It's thus important to keep the original AMresult alive (in this case the one returned by AMmapRange()) until after you are done with the return values of these functions.

Beyond that, good luck!