Skip to main content

Using Rust

Cpp.js binds plain Rust the same way it binds C++ headers: one import line, no proc-macros, no hand-written glue. The same JavaScript works on web, iOS and Android. (platform: 'wasi' skips Rust — there is no wasm32-wasip3 Rust target yet.)

Add the binding layer once (bundler plugins already depend on it, so most projects get it transitively):

npm install -D @cpp.js/core-embind-rust

A Rust toolchain (cargo plus the platform targets) must be installed; cargo itself acts as the incremental cache, so unchanged code rebuilds as a no-op.

Import a crate directly (cargo: scheme)

Use a crates.io crate without writing any local Rust. Declare it in cppjs.config.js, then import it with the cargo: prefix — the prefix names the store, like node: does:

cppjs.config.js
export default {
cargoDependencies: {
uuid: '{ version = "1", features = ["v4"] }',
semver: '1',
},
paths: { config: import.meta.url },
};
JavaScript
import { initCppJs, Uuid } from 'cargo:uuid';
import { Version, VersionReq } from 'cargo:semver';

await initCppJs();
const id = Uuid.newV4().toString();
const ok = new VersionReq('^1.2').matches(new Version('1.4.0'));

Cpp.js reads the crate's own sources — following module trees, pub use re-exports and enabled feature gates — and generates the bridge from what it finds. Importing an undeclared crate is a hard error.

Import an app-local .rs file

Write Rust next to your other native sources and import it like a header. Upstream crates it uses go into the same cargoDependencies:

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 { /* … */ }
}
JavaScript
import { initCppJs, Hull } from './native/geo_surface.rs';

Publish a Rust package

A whole crate can ship as a Cpp.js package: set export.type: 'cargo' (see Export). Cpp.js runs cargo build --release --target <triple> per platform and stages the static library like any prebuilt — consumers import the package name exactly like a C++ package.

What plain Rust maps to

RustJavaScript
struct + impl methodsclass with methods (Type::new → constructor)
&str / &String parameters, String returnsstrings
i32 / f64 / boolnumber / boolean
i64 / u64BigInt (both directions)
Option<T> parameters and returnsnull/undefinedNone
Result<T, E> returnsthrows an Error on Err
impl DisplaytoString()
free pub fnplain exported function
&OtherClass parameterspass the other class's instance

TypeScript

Generated declarations never live in your source tree — everything sits under .cppjs/, and the shared @cpp.js/typescript-config package wires all of it. Add it as a devDependency and extend it once (TS 5.5+; array form when you already extend another config):

tsconfig.json
{ "extends": "@cpp.js/typescript-config" }
tsconfig.json (React Native)
{ "extends": ["@react-native/typescript-config", "@cpp.js/typescript-config"] }

One caveat: include is overridden (not merged) when your tsconfig defines its own — keep .cppjs/rust-crates/types/**/*.d.ts in yours in that case.

Running with initCppJs({ useWorker: true })? Set dts: 'promise' in cppjs.config.js so every generated signature returns Promise<...> to match the async runtime (write await new X(...) for construction).