Example app (Rust)¶
Warning
The Rust interface is experimental and is not covered by the API stability commitments in Release and compatibility policy. Its Rust API, C ABI and build integration may change incompatibly in any release, so use the SDK, CCF libraries and documentation from the same CCF revision.
The Rust SDK, the ccf-app crate, exposes a small subset of the native application API: read-only and read-write endpoints, user-certificate or no authentication, request and response access, OData errors, and raw-byte KV get, has, put and remove.
API schemas, custom authentication, caller identity, historical queries, indexing, logging, crypto helpers and commit callbacks are not exposed.
See the API reference for details.
Build¶
Install the build dependencies and Rust 1.90, then build the sample from the repository root:
cmake -S . -B build -GNinja -DCMAKE_BUILD_TYPE=Debug
cmake --build build --target basic_rust
This produces the executable build/samples/apps/basic_rust/basic_rust.
The application is a staticlib crate that depends on the SDK:
[package]
name = "ccf-basic-rust"
version = "0.1.0"
edition = "2024"
[lib]
name = "ccf_basic_rust_app"
crate-type = ["staticlib"]
[dependencies]
ccf-app = { path = "../../../src/rust/ccf-app" }
[profile.release]
lto = true
codegen-units = 1
add_ccf_rust_app builds the crate and links it with CCF’s C++ bridge and launcher:
cmake_minimum_required(VERSION 3.21)
project(basic_rust LANGUAGES C CXX)
set(CCF_PROJECT "ccf")
if(NOT TARGET "ccf")
find_package(${CCF_PROJECT} REQUIRED)
endif()
add_ccf_rust_app(
basic_rust
MANIFEST_PATH ${CMAKE_CURRENT_LIST_DIR}/Cargo.toml
PACKAGE ccf-basic-rust
LIB_NAME ccf_basic_rust_app
)
Cargo runs on every build with --locked, so commit Cargo.lock.
CMake Debug builds use Cargo’s dev profile, and other build types use release.
LIB_NAME must match the crate’s [lib] name, and defaults to PACKAGE with dashes replaced by underscores.
Add other Rust code as Cargo dependencies, not as separate staticlib archives, which may export duplicate runtime symbols.
For a quick type check without linking, run cargo check --locked --lib in the crate directory.
Write the application¶
samples/apps/basic_rust/src/lib.rs defines fn register(registry: &mut Registry) -> Result<(), BridgeError>, which installs handlers on the Registry, and exports it with export_app!:
ccf_app::export_app!(register);
This read-write handler stores the request body under the key path parameter:
registry.read_write(
"/records/{key}",
"PUT",
Auth::UserCert,
|context| -> EndpointResult {
let body = context.body()?.to_vec();
let key = required_key(context.path_param("key"))?;
context.map(RECORDS).put(key.as_bytes(), &body)?;
context.set_status(204)?;
Ok(())
},
)?;
Paths exclude the /app prefix, and Auth::UserCert accepts only certificates registered as CCF users.
The body is borrowed from the context, so it is copied before path_param, which takes &mut self.
? turns any BridgeError into HTTP 500, so client errors are returned explicitly as an EndpointError, as the sample’s required_key helper does for a missing key.
This read-only handler returns the stored value, or HTTP 404 if there is none:
registry.read_only(
"/records/{key}",
"GET",
Auth::UserCert,
|context| -> EndpointResult {
let key = required_key(context.path_param("key"))?;
match context.map(RECORDS).get(key.as_bytes())? {
Some(value) => {
context.set_status(200)?;
context.set_header("content-type", "application/octet-stream")?;
context.set_body(&value)?;
Ok(())
}
None => Err(EndpointError::new(404, "ResourceNotFound", "No such key")),
}
},
)?;
The sample’s other endpoints exist only for CCF’s end-to-end tests, and several are unauthenticated, so remove them from any copy.
Run the sample¶
Start a sandbox network:
cd build
../tests/sandbox/sandbox.sh --package samples/apps/basic_rust/basic_rust
From another terminal in the same directory, write and read a record as user0:
export CCF_URL=https://127.0.0.1:8000 # As printed by the sandbox
export CCF_COMMON=workspace/sandbox_common
curl --cacert "$CCF_COMMON/service_cert.pem" \
--cert "$CCF_COMMON/user0_cert.pem" --key "$CCF_COMMON/user0_privk.pem" \
-i -X PUT "$CCF_URL/app/records/example" \
-H "content-type: application/octet-stream" --data-binary 'hello Rust'
curl --cacert "$CCF_COMMON/service_cert.pem" \
--cert "$CCF_COMMON/user0_cert.pem" --key "$CCF_COMMON/user0_privk.pem" \
-i "$CCF_URL/app/records/example"
The PUT returns HTTP 204, and the GET returns HTTP 200 with the body hello Rust.
A missing key returns HTTP 404, and a request without the user certificate returns HTTP 401.
A successful response does not mean that the write is committed; see Verifying Transactions.
These requests are also covered by the e2e_basic_rust test in tests/basic_rust.py.
Use an installed SDK¶
To build outside the CCF source tree, install CCF, copy the sample project without its test-only endpoints, and point its ccf-app dependency at the installed SDK:
ccf-app = { path = "/opt/ccf/share/ccf/src/rust/ccf-app" }
Then build and run it with the same installation:
cmake -S . -B build -GNinja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_PREFIX_PATH=/opt/ccf
cmake --build build --target basic_rust
/opt/ccf/bin/sandbox.sh --package "$PWD/build/basic_rust"
Handler rules¶
CCF may run handlers concurrently, and re-execute them after a transaction conflict, so any side effects outside the KV must be safe to repeat. Handlers must implement the
RetrySafeHandlermarker trait, which everySend + Synctype does, and be'static.Contexts and map handles are only valid during one handler call. Values returned by
getare owned copies.KV writes are applied only if the response status is 2xx.
An
EndpointErrorwhose status is not a known HTTP error status is sent as HTTP 500, with its code and message.Panics are caught and sent as HTTP 500, which requires
panic = "unwind".export_app!keeps panic messages from application code out of node output, which is visible to the host, and custom panic hooks must not print confidential data either.Keys and values are raw bytes; maps never apply a
Codecimplicitly, and the chosen encoding is part of the application’s ledger format.Maps whose names start with
public:are stored in the ledger in plaintext, and all other maps are encrypted. Like C++ applications, Rust applications are trusted code and are not prevented from accessing governance or internal maps.