Skip to content
Get Started

WebAssembly Plugins — Building & Running (Rust)

Rust analogue of the C++ guest SDK — same wire contract, same host (nacowasmplugin), same dmasession/listener model. The crate lives at rust/nanoconda-wasm and its public API is the nanoconda module, matching nanoconda.h field and method names verbatim (order.symbolId, session.newOrder(&mut o), listener.onbook(...)) — porting a strategy between the two SDKs is close to a mechanical find-and-replace. The one deliberate difference is casing on type names themselves (Security, not security), since Rust's compiler lints hard against lowercase type names.

1. Install the Rust wasm32-wasip1 target

rustup target add wasm32-wasip1

No separate SDK download needed here — unlike the C++ side (which needs wasi-sdk), the Rust standard library ships wasm32-wasip1 support directly through rustup.

2. Write a minimal strategy

Same strategy as the C++ example, ported directly:

strategy-example/src/lib.rs
use nanoconda_wasm::nanoconda::{loadState, saveState, Book, DmaSession, Error, Listener, Logger, Order, Security, Trade};
use nanoconda_wasm::plugin_entry;

#[repr(C)]
#[derive(Clone, Copy, Default)]
struct PersistedState {
    fillCount: i64,
    lastFillPrice: i64,
}

impl PersistedState {
    fn as_bytes(&self) -> &[u8] {
        unsafe { core::slice::from_raw_parts((self as *const Self) as *const u8, core::mem::size_of::<Self>()) }
    }
    fn as_bytes_mut(&mut self) -> &mut [u8] {
        unsafe { core::slice::from_raw_parts_mut((self as *mut Self) as *mut u8, core::mem::size_of::<Self>()) }
    }
}

struct MyStrategy {
    session: DmaSession,
    logger: Logger,
    state: PersistedState,
}

impl Listener for MyStrategy {
    fn onsecurity(&mut self, s: &Security) {
        self.logger.info(&format!("security {} tick={}", s.symbol(), s.tickSize));
    }
    fn onbook(&mut self, b: &Book) {
        if b.buys[0].qty != 0 && b.sells[0].qty != 0 {
            self.logger.info(&format!("book symbol={} bid={} ask={}", b.symbolId, b.buys[0].price, b.sells[0].price));
        }
        // react to market data via self.session.newOrder(&mut o), etc.
    }
    fn ontrade(&mut self, t: &Trade) {
        self.logger.info(&format!("trade symbol={} px={} sz={}", t.symbolId, t.lastPrice, t.lastSize));
    }
    fn onorderack(&mut self, o: &Order) { self.logger.info(&format!("order ack id={}", o.orderId)); }
    fn oncancelack(&mut self, _o: &Order) {}
    fn onmodifyack(&mut self, _o: &Order) {}
    fn onorderreject(&mut self, o: &Order) { self.logger.info(&format!("order reject {}", o.code())); }
    fn onmodifyreject(&mut self, _o: &Order) {}
    fn oncancelreject(&mut self, _o: &Order) {}
    fn onfill(&mut self, o: &Order) {
        self.logger.info(&format!("fill id={} qty={}", o.orderId, o.quantityLast));
        self.state.fillCount += 1;
        self.state.lastFillPrice = o.priceLast;
    }
    fn onout(&mut self, _o: &Order) {}
    fn onorderstatus(&mut self, _o: &Order) {}
    fn onerror(&mut self, e: &Error) { self.logger.error(&format!("error {}", e.message())); }
}

plugin_entry!(
    |session: DmaSession, args: Vec<String>| -> Box<dyn Listener> {
        let logger = Logger::default(); // routes to the host's single log file (nacowasmplugin's -l/--log)

        // OPTIONAL: plugin-specific CLI args, e.g. `... -- --threshold 5` on the nacowasmplugin
        // command line. `args` is exactly what followed the second `--`, in order.
        let mut iter = args.iter();
        while let Some(arg) = iter.next() {
            if arg == "--threshold" {
                if let Some(v) = iter.next() { logger.info(&format!("threshold arg = {v}")); }
            }
        }

        // OPTIONAL: restore state from the previous run, if any.
        let mut state = PersistedState::default();
        let n = loadState(0, state.as_bytes_mut());
        logger.info(&format!("loaded state: {n} bytes, fillCount={} lastFillPrice={}", state.fillCount, state.lastFillPrice));

        Box::new(MyStrategy { session, logger, state })
    },
    |_strategy: &mut dyn Listener| {}
);

impl Drop for MyStrategy {
    fn drop(&mut self) {
        // OPTIONAL: save state to disk on shutdown.
        let r = saveState(0, self.state.as_bytes());
        self.logger.info(&format!("saved state on shutdown: result={r}"));
    }
}

plugin_entry! is what generates every wasm export the host requires (starting, stopping, nc_abi_version, nc_cb_alloc/nc_cb_free, and all nc_cb_* callback dispatchers) and wires them to the init closure above — it's the Rust equivalent of the C++ SDK handling everything invisibly once you define init_nanoconda_plugin/plugin_stop. nanoconda::saveState/loadState persist to the file given by nacowasmplugin's -f/--statefile argument, same as the C++ side.

3. Build the guest

cd rust/examples/strategy-example
cargo build --release

The workspace's .cargo/config.toml sets wasm32-wasip1 as the default build target, so plain cargo build is enough — no --target flag needed. Output: target/wasm32-wasip1/release/strategy_example.wasm.

The strategy crate is built with crate-type = ["cdylib"], which produces a WASI reactor module (a library-style wasm binary that exports functions the host calls, with no _start/exit entry point) — the same execution model the C++ side gets from -mexec-model=reactor. The workspace also passes -C link-args=--no-entry for this target, so the build stays reactor-shaped even on a toolchain where cdylib alone doesn't already default to it.

4. Build the host loader

Same host as the C++ side — see Building & Running (C++), step 4. Nothing about the host changes for a Rust guest.

5. Run it

./nacowasmplugin -i rust/examples/strategy-example/target/wasm32-wasip1/release/strategy_example.wasm \
    -- wasmplugin -e CME -s ESZ6,NQZ6 -u myuser -p mypass -a myaccount -c 0 -l strategy.log -f strategy.state \
    -- --threshold 5

Same nacowasmplugin CLI arguments as the C++ side.

Differences from the C++ SDK

C++ Rust Why
nanoconda::security, nanoconda::order, ... nanoconda::Security, nanoconda::Order, ... Rust lints hard against lowercase type names; field/method names inside are unchanged.
order.type order.r#type type is a reserved keyword in Rust; r#type is the raw-identifier escape for using a keyword as a plain field name.
dmasession::cancelAllOrders(symbolId) / cancelAllOrders(symbolId, userId) (overloaded) DmaSession::cancelAllOrders(symbolId) / cancelAllOrdersForUser(symbolId, userId) (distinct names) Rust doesn't support overloading on arity. Same split for flatten/flattenForUser.
logM(format, ...) / logL(level, format, ...) (C preprocessor macros, real varargs) logger.logMessage(&format!(...)), or this crate's logM!/logL! macros Rust has no C-style varargs; format!() is the closest equivalent.
Entry point: define init_nanoconda_plugin/plugin_stop, header handles the rest invisibly Entry point: invoke plugin_entry! once at your crate root #[no_mangle]/#[export_name] symbols only survive into the final .wasm when defined in the crate actually compiled as the cdylib — a dependency crate can't inject them the way a C++ header can, so the macro generates them in your crate instead.

Everything else — struct layouts (verified against a real g++ compile of nanoconda_wasm_abi.h), enum values, the Listener callback set, DmaSession's method set — is a direct port. See rust/README.md for the full reference, including the ABI reference shared with the C++ SDK (byte-identical wire structs either way).