maned-rs
Call .mnd scripts from Rust, in-process, via link-time FFI against libmaned_c.
let mut scoring = load?;
let result = scoring.run?;
let y = result.tensor?; // 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.
Read this first: linking and licensing
- Dynamic-link only. Static linking (
libmaned_c.a) is not licensed — the manedLICENSE§3 grants redistribution of the shared library in unmodified form, and a static archive would also defeat the symbol gate.build.rsrefuses astaticlink request by design, at length. - The library must exist on the build machine. A
.cratecannot ship per-platform binaries, so install themaned-capi-$VERSION-$target.tar.gztarball first, thencargo add maned. The opt-invendoredfeature (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-aboutwalk the crate graph;libmaned_cis 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:
MANED_LIB_DIR/MANED_INCLUDE_DIR— explicit, wins over everything.pkg-config --libs maned_c, honouringPKG_CONFIG_PATHandPKG_CONFIG_SYSROOT_DIR. Cross builds needPKG_CONFIG_ALLOW_CROSS=1./usr/local/lib,/opt/homebrew/lib,$HOME/.local/lib.- Otherwise the build fails with a message naming every path tried, the
tarball to install and the
MANED_LIB_DIRoverride.
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.rsof its own withprintln!("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.
Non-goals (v1)
- Windows — the serve and net layers are POSIX throughout.
build.rsrefuses the target explicitly rather than failing obscurely. - Cancellation — calls are uninterruptible;
tokio::time::timeoutaroundrun_asyncresolves while the work continues, and dropping the runtime waits for it. There is deliberately notimeoutparameter, because it could not honour its name. staticlinking — see above.
Crates
maned-sys— raw FFI (links = "maned_c");src/bindings.rsis destined to be bindgen output, checked in, never hand-edited. Its unit test assertssize_of::<mnd_response>()and every field offset against the tarball'smaned_c_abi.json.maned— the safe API.Script(&mut self,Send,!Sync) andSharedScript(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.