maned 0.4.1

Call .mnd scripts in-process, through the mnd_* C ABI
Documentation

the maned mark, geared

maned-rs

Call .mnd scripts from Rust, in-process, via link-time FFI against libmaned_c.

let mut scoring = maned::load("scoring.mnd")?;
let result = scoring.run(&serde_json::json!({ "x": [[2, 0], [0, 2]] }))?;
let y = result.tensor("y")?;        // Tensor { shape: [2, 2], data: [...] }

One entry point in the engine — mnd_request — answers the same request→response contract as maned-serve's REST API, with no socket and no HTTP framing. A response here is byte-identical to what the REST API returns for the same script and body, and the test suite walks the shared conformance corpus to prove it.

Quickstart

Step by step, from nothing to a running program:

  1. Install the engine library — download maned-capi-0.4.0-<target>.tar.gz from https://github.com/FabianoDicheti/maned/releases, unpack it, and either copy lib/ + include/ into a standard prefix (/usr/local) or remember its path for step 4.

  2. Create a project and add the crate (published on crates.io):

cargo new scoring && cd scoring
cargo add maned serde_json
  1. Save the script as scoring.mnd and put this in src/main.rs:
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut script = maned::load("scoring.mnd")?;
    let result = script.run(&serde_json::json!({"x": [[2, 0], [0, 2]]}))?;
    let y = result.tensor("y")?;
    println!("y = shape {:?}, data {:?}", y.shape, y.data);
    Ok(())
}
  1. Build and run. With the tarball in a standard prefix, plain cargo run works; with a custom location, name it:
$ MANED_LIB_DIR=/path/to/capi/lib MANED_INCLUDE_DIR=/path/to/capi/include cargo run
y = shape [2, 2], data [2, 4, 0, 2]

(If the binary dies at startup with Library not loaded: @rpath/..., read Downstream binaries need the rpath too.)

The same program lives in this repo as maned/examples/scoring.rs — cargo run --example scoring. Three deep dives sit beside it: --example files (file routes), --example decimals_tokio --features tokio (exact decimals + async), --example routed (the local Bark VM).

Read this first: linking and licensing

  • Dynamic-link only. Static linking (libmaned_c.a) is not licensed — the maned LICENSE §3 grants redistribution of the shared library in unmodified form, and a static archive would also defeat the symbol gate. build.rs refuses a static link request by design, at length.
  • The library must exist on the build machine. A .crate cannot ship per-platform binaries, so install the maned-capi-$VERSION-$target.tar.gz tarball first, then cargo add maned. The opt-in vendored feature (not implemented yet) would download it at build time — and would make the built artifact mixed-licence, while the crate itself stays pure Apache/MIT.
  • Licence-audit tooling cannot see the native library. cargo-deny/cargo-about walk the crate graph; libmaned_c is not in it. A compliance scan will report a clean Apache/MIT tree while a proprietary-but-redistributable binary sits in the final executable. Nothing dishonest is happening, but only this README says so.

Building and testing

maned-sys/build.rs resolves the library in this order, always keyed on the target and never the host:

  1. MANED_LIB_DIR / MANED_INCLUDE_DIR — explicit, wins over everything.
  2. pkg-config --libs maned_c, honouring PKG_CONFIG_PATH and PKG_CONFIG_SYSROOT_DIR. Cross builds need PKG_CONFIG_ALLOW_CROSS=1.
  3. /usr/local/lib, /opt/homebrew/lib, $HOME/.local/lib.
  4. Otherwise the build fails with a message naming every path tried, the tarball to install and the MANED_LIB_DIR override.

This checkout points at the capi artifact beside it through .cargo/config.toml, so the whole suite runs with no arguments:

cargo test                  # default features
cargo test --features tokio # + the async surface

Anything else, point it at an unpacked tarball:

MANED_LIB_DIR=/path/to/maned-capi/lib \
MANED_CONFORMANCE_DIR=/path/to/maned-capi/conformance \
cargo test

MSRV is 1.77, not the 1.65 the plan proposed. The layout gate uses core::mem::offset_of!, stable in 1.77, and asserting the mnd_response offsets from maned_c_abi.json is worth more than twelve versions of compiler compatibility. Everything else in the crate would build on 1.65.

Downstream binaries need the rpath too

cargo:rustc-link-search and -lmaned_c propagate through the dependency graph; cargo:rustc-link-arg does not — it only ever applies to the targets of the package that emitted it. libmaned_c's install name is @rpath/libmaned_c.dylib, so a binary that links it without an LC_RPATH builds fine and dies at startup. maned-sys covers its own tests, maned's build.rs covers maned's, and a downstream binary crate wants one of:

  • a build.rs of its own with println!("cargo:rustc-link-arg=-Wl,-rpath,{}", std::env::var("MANED_LIB_DIR")?);
  • or the library installed somewhere the loader already looks.

The docsrs-stub feature is mutually exclusive with linking

It exists so docs.rs — sandboxed, no network, no library — can render the API: build.rs short-circuits to no link directives. cargo test --all-features therefore does not work; use --features tokio.

The file routes

run() goes to /run.json, which renders an out:: image as a nested integer array and a csv as a JSON tensor. For the engine's actual file bytes there is the multipart route, and it takes file bytes on the way in too — a csv() / image() / parquet() in:: never has to be decoded into JSON first.

Call Route Returns
script.run_files(&[("h0", bytes)]) POST /run multipart BTreeMap<String, Vec<u8>>, one entry per out::
script.frame(&payload) POST /frame Response — the frame document is the body
script.frame_csv(&payload) POST /frame.csv Vec<u8>, undecoded
script.inputs() / .outputs() / .routes() / .health() GET /health the contract, fetched once and cached

maned::multipart::{encode, decode, boundary_for} are public for anyone driving request() by hand. The encoding is byte-identical to manedpy's and maned-kotlin's — tests/io.rs asserts it against a frozen body, so a drift is red rather than merely different.

SharedScript has the same four calls plus run_files_async / frame_async under the tokio feature.

a maned wolf with a crab

Non-goals (v1)

  • Windows — the serve and net layers are POSIX throughout. build.rs refuses the target explicitly rather than failing obscurely.
  • Cancellation — calls are uninterruptible; tokio::time::timeout around run_async resolves while the work continues, and dropping the runtime waits for it. There is deliberately no timeout parameter, because it could not honour its name.
  • static linking — see above.

Crates

  • maned-sys — raw FFI (links = "maned_c"); src/bindings.rs is destined to be bindgen output, checked in, never hand-edited. Its unit test asserts size_of::<mnd_response>() and every field offset against the tarball's maned_c_abi.json.
  • maned — the safe API. Script (&mut self, Send, !Sync) and SharedScript (Sync, only constructible for non-routed scripts) encode the thread-safety table in the type system; tests/compile_fail/ asserts the programs that must not compile.

Threads, in one table

Safe? How the crate says so
Concurrent calls, one handle, non-routed yes SharedScript: Sync
Concurrent calls, one handle, routed no &mut self on Script::run
Two handles, same routed device no Script: !Sync ⇒ Arc<Script>: !Send

into_shared() is the only door from the second row to the first, and it checks mnd_script_is_routed() as it opens. On a routed script it hands the Script back inside the error, so refusing to share it does not destroy it.

Licensed Apache-2.0 OR MIT.