crossbind
GitHub
GUIDE · CONCEPTS · RUST

Rust

Rust binds the way C++ headers do: one import, no proc-macros, no hand-written glue. Bring in a crate straight from crates.io with the cargo: scheme, or write Rust next to your other native sources and import the file.

Setup

A Rust toolchain (cargo plus the platform targets) has to be installed - cargo doubles as the incremental cache, so unchanged code rebuilds as a no-op. The binding layer is a single dev dependency, and bundler plugins already depend on it, so most projects get it transitively:

shell
npm install -D @cpp.js/core-embind-rust
Two limits worth knowing up front
platform: 'wasi' skips Rust entirely - there is no wasm32-wasip3 Rust target yet. And the wasm mt runtime needs nightly, because std is rebuilt with the atomics features: run rustup toolchain install nightly --component rust-src once.

Import a crate directly

Declare the crate in cppjs.config.js, then import it with the cargo: prefix - the prefix names the store, the way node: does. Importing an undeclared crate is a hard error, not a silent miss.

cppjs.config.js
export default {
cargoDependencies: {
uuid: '{ version = "1", features = ["v4"] }',
semver: '1',
},
paths: { config: import.meta.url },
};
src/main.js
import { initNative, Uuid } from 'cargo:uuid';
import { Version, VersionReq } from 'cargo:semver';
 
await initNative();
 
const id = Uuid.newV4().toString();
const ok = new VersionReq('^1.2').matches(new Version('1.4.0'));

The crate's own sources are read - module trees, pub use re-exports, enabled feature gates - and the bridge is generated from what is found there. You write no Rust at all.

Import a local .rs file

Rust can also sit beside your C++ under src/native and be imported like a header. Upstream crates it uses go into the same cargoDependencies map.

src/native/geo_surface.rs
use geo::{ConvexHull, MultiPoint, Point};
 
pub struct Hull { points: Vec<Point<f64>> }
 
impl Hull {
pub fn new() -> Self { Hull { points: Vec::new() } }
pub fn add(&mut self, x: f64, y: f64) { self.points.push(Point::new(x, y)); }
pub fn wkt(&self) -> String { /* ... */ }
}
src/main.js
import { initNative } from './native/native.h';
import { Hull } from './native/geo_surface.rs';

What plain Rust maps to

RustJavaScript
struct + impl methodsclass with methods (Type::new becomes the constructor)
&str / &String parameters, String returnsstrings
i32 / f64 / boolnumber / boolean
i64 / u64BigInt, both directions
Option<T>null / undefinedNone
Result<T, E> returnsthrows an Error on Err
impl DisplaytoString()
free pub fnplain exported function
&OtherClass parameterspass the other class's instance
serde_json::Valuereal JS values, deep-copied at the boundary
Arc<Class>shared ownership across several JS handles
embind_rs::JsValue / JsFunctionlive JS values by identity, and callbacks into JS
JsValue and JsFunction need a synchronous runtime
Native JSI or wasm st on the main thread. Functions cannot cross a worker boundary and identity does not survive structured cloning, so on worker-backed runtimes - the mt default, or useWorker: true - use serde_json::Value instead.

Publish a crate as a package

A whole crate can ship as a package: set export.type: 'cargo' and cargo build --release --target <triple> runs per platform, staging the static library like any other prebuilt. Consumers import the package name exactly as they would a C++ one.

cppjs.config.js
export default {
export: {
type: 'cargo',
crate: '.',
},
paths: { config: import.meta.url },
};

TypeScript

Generated declarations live under .cppjs/, never in your source tree, and @cpp.js/typescript-config wires them. One caveat: when your own tsconfig defines include, it overrides rather than merges - keep .cppjs/rust-crates/types/**/*.d.ts in yours.

tsconfig.json
{ "extends": "@cpp.js/typescript-config" }

The same dts: 'promise' note from C++ bindings applies here.

Type to search every guide page and section.
↑↓ navigate↵ openesc close