LIBCMA is a lightweight Rust binding for Cartesi Machine Application (CMA) tooling: parse rollup inputs, build on-chain voucher payloads, and manage application assets (deposits, withdrawals, transfers, balances) for ETH, ERC-20, ERC-721, and ERC-1155.
Headers come from git submodules under third_party/:
third_party/machine-asset-tools—libcma(parser, types, ledger)third_party/machine-guest-tools—libcmtheaders used bylibcma
git clone --recurse-submodules https://github.com/Mugen-Builders/libcma_binding_rust
# or, if already cloned:
git submodule update --init --recursiveA plain cargo build works out of the box — build.rs takes care of the fiddly parts:
- Submodules auto-init. If
third_party/is empty (you cloned without--recurse-submodules),build.rsrunsgit submodule update --init --recursivefor you. - bindgen clang headers auto-fallback.
bindgenneeds the compiler's builtin headers (stdbool.h, …). If yourlibclangships without them (e.g. you havelibclang1but notlibclang-common-*-dev),build.rsfalls back to GCC's builtin header dir automatically — noBINDGEN_EXTRA_CLANG_ARGSneeded. Set that env var yourself to override.
So the only hard host requirement for the default build is:
- A Rust toolchain +
libclang(forbindgen) andgit(for the submodule fetch).
For the riscv64 build (linking the real C++ libcma), build.rs also cross-compiles the static archive from source if it isn't already present. That path additionally needs:
- GNU
make,wget, and network access - The RISC-V GCC 14 cross toolchain —
g++-14-riscv64-linux-gnu/gcc-14-riscv64-linux-gnu(libcma's C++ source requires GCC ≥ 14). Override the compiler names withCMA_RISCV64_CXX/CMA_RISCV64_CC.
The Cartesi SDK / app Docker image used to build the machine already provides all of these.
The crate has three mutually exclusive backends. Exactly one must be enabled;
enabling zero or more than one is a hard compile_error! (enforced at the top of
src/lib.rs).
| Feature | Default | Real libcma? | When to use |
|---|---|---|---|
mock |
yes | No — in-memory stub | Host development and cargo test: compiles the thread-local stubs in src/mocks.rs so the crate builds and the plumbing/parser tests run with no C++ toolchain, no network, and no RISC-V archive. Never use in production — it is not a real ledger. |
host-real |
no | Yes (host, x86_64) | Running the real C++ libcma off-chain on the host, e.g. a sequencer predicting the Cartesi machine's ledger state. build.rs builds and links the real static archive for the host. |
riscv64 |
no | Yes (Cartesi machine) | Running the real C++ libcma inside the Cartesi machine. build.rs cross-compiles build/riscv64/libcma.a from the submodule source if it isn't already present (needs the RISC-V GCC 14 cross toolchain). |
The backends are mutually exclusive and mock is a default feature, so the
link gate in build.rs keys off mock. To build against the real libcma you
MUST also turn default features off — otherwise the default mock stays enabled
and you silently link the stub instead of the real ledger:
# real libcma on the host (off-chain, e.g. sequencer prediction)
cargo build --no-default-features --features host-real
# real libcma cross-compiled for the Cartesi machine
cargo build --no-default-features --features riscv64In Cargo.toml:
libcma_binding_rust = { version = "...", default-features = false, features = ["host-real"] } # or "riscv64"If you forget default-features = false, enabling host-real or riscv64
alongside the default mock trips the mutual-exclusivity compile_error! — read
its message; the fix is to disable default features.
libcma.a contains only libcma's own objects. Its parser references libcmt's C
ABI helpers (cmt_abi_*, cmt_buf_*), so build.rs emits -lcmt for the
riscv64 backend and libcmt.a must be on the linker's search path in the
stage that cross-links your binary. Upstream deliberately does not build libcmt
for riscv64 — its real io backend needs the Cartesi Linux kernel headers, which
exist only inside the guest — so it comes from the released machine-guest-tools
package. (host-real needs nothing extra: build.rs builds and statically links
libcmt.a itself, from the vendored sources with the mock io backend.)
Installing machine-guest-tools in your application's runtime stage is not enough — by then the binary is already linked. Stage it into the cross sysroot of the build stage, using the same guest-tools version your runtime stage installs, so you link and run against one libcmt ABI:
# in the cross-build stage, before `cargo build`
ARG MACHINE_GUEST_TOOLS_VERSION
ADD https://github.com/cartesi/machine-guest-tools/releases/download/v${MACHINE_GUEST_TOOLS_VERSION}/machine-guest-tools_riscv64.deb /tmp/gt.deb
RUN dpkg-deb -x /tmp/gt.deb /tmp/gt && \
cp -a /tmp/gt/usr/lib/libcmt.a /usr/riscv64-linux-gnu/lib/ && \
cp -a /tmp/gt/usr/include/libcmt /usr/riscv64-linux-gnu/include/
# libcma is C++20/23, so it needs GCC >= 14; the stock template installs 13.
ENV CMA_RISCV64_CXX=riscv64-linux-gnu-g++-14 \
CMA_RISCV64_CC=riscv64-linux-gnu-gcc-14Symptom if this is missing: the C++ compile succeeds and the failure appears only
at the final link, as undefined symbol: cmt_abi_get_uint, cmt_buf_split, and
friends. It surfaces only once something calls the C parser bindings, because
static archive members are pulled lazily.
The Cartesi Rust template builds with -C target-feature=+crt-static, and that
flag is honoured for riscv64gc-unknown-linux-gnu — glibc is linked in
statically. A pure-Rust application therefore produces a fully static binary with
no PT_INTERP at all, which is what the machine's rootfs expects.
Pulling in a C++ library changes that. If libstdc++ were linked dynamically, the
binary would keep a single NEEDED libstdc++.so.6 and, with it, an interpreter
of /lib/ld.so.1 — a path that does not exist in the machine rootfs (Ubuntu
riscv64 installs the loader as /lib/ld-linux-riscv64-lp64d.so.1). The machine
then cannot exec the application and reports only:
WARN rollup_http_server::dapp_process] throwing exception because dapp failed to
start with No such file or directory (os error 2)
which names neither the loader nor libstdc++.
So when +crt-static is set, build.rs binds libstdc++ and libcmt statically as
well, locating each archive through <compiler> -print-file-name= so the paths
come from the toolchain rather than being guessed. Nothing is required of the
application beyond providing a C++ toolchain that ships libstdc++.a — the
g++-14-riscv64-linux-gnu package does. Verify with:
$ riscv64-linux-gnu-readelf -l dapp | grep -i interpreter # expect no output
$ riscv64-linux-gnu-readelf -d dapp | grep NEEDED # expect no outputhost-real and riscv64 compile the C++ libcma with SIMD-free / generic flags
(-DBOOST_UNORDERED_DISABLE_SSE2, -DBOOST_UNORDERED_DISABLE_NEON,
-DBOOST_INTERPROCESS_FORCE_GENERIC_EMULATION). This makes the on-disk 32-byte
account records (single-asset drive format v2: balance uint96 little-endian
[low u64 | high u32] | owner 20 bytes, no padding) byte-identical across
x86_64 and riscv64. That invariant is what
makes off-chain prediction with host-real sound: the host reproduces, byte for
byte, exactly what the machine computes on-chain.
Ledger wraps a self-referential C++ object (Boost.Interprocess) held on the
heap for relocation safety, and is therefore !Send / !Sync. Do not move
or share a Ledger across threads without external synchronization. Downstream
code that needs Send typically wraps the Ledger in a mutex together with its
own unsafe impl Send.
Ledger wraps cma_ledger_* with helpers for file/buffer initialization, asset/account retrieval, deposit/withdraw/transfer, balance, and total supply.
retrieve_ether_assets()usesAssetType::BaseAssetTypealso supportsTokenAddress,TokenAddressId, andTokenAddressIdAmountRetrieveOperation::FindAndRemoveis supported
Pure-Rust parser aligned with machine-asset-tools / Cartesi Rollups v2.0:
- Portal deposit decoding (packed + ABI tails for ERC-721/1155)
- Auto-decode withdrawals/transfers by function selector
- Inspect decoding for
ledger_getBalanceandledger_getTotalSupply - Voucher encoding for Ether, ERC-20, ERC-721 (
safeTransferFrom), ERC-1155 single/batch (safeTransferFrom/safeBatchTransferFrom)
cma_decode_advance(req_type, input) -> Result<CmaParserInput, CmaParserError>cma_decode_inspect(input) -> Result<CmaParserInput, CmaParserError>cma_encode_voucher(req_type, app_address, voucher_request) -> Result<CmaVoucher, CmaParserError>
CmaVoucher fields: destination, value (wei for ether vouchers), payload.
Inspect params are flat JSON strings, e.g.:
{"method":"ledger_getBalance","params":["0x...account...","0x...token...","0x1"]}cargo testtests/parser_tests.rs— integration tests against the pure-Rust parsertests/parser_vectors.rs— vectors ported fromthird_party/machine-asset-tools/tests/parser.ctests/ledger_tests.rs— ledger tests via themockbackend
CI runs the mock-backend tests on every push/PR and attempts an riscv64 link check when libcma can be built.
MIT (see Cargo.toml).
