Skip to content
Get Started

WebAssembly Plugins — Building & Running (Rust)

Rust analogue of the C++ guest SDK — same wire contract, same host (nanoconda-cli -a pluginloader), same dmasession/listener model. The crate ships in the rust/ directory of the downloaded package, and its public API is the nanoconda module, matching nanoconda.h field, type, and method names verbatim (order.symbolId, security, session.newOrder(&mut o), listener.onbook(...)) — porting a strategy between the two SDKs is close to a mechanical find-and-replace.

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. If rustup itself isn't installed: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh, then source "$HOME/.cargo/env".

2. Write a minimal strategy

This is a complete, working example — a strategy that logs book/trade/fill events, places a limit buy at the best bid the first time it sees one, tracks fill count and last fill price, and persists that state across restarts:

rust/src/lib.rs
mod nanoconda;
use nanoconda::{book, dmasession, error, listener, loadState, logger, order, orderType, saveState, security, trade};

#[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,
    sentOrder: bool,
}

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));

            // API example: place a limit buy at the best bid, once.
            if !self.sentOrder {
                let mut o = order::default();
                o.symbolId = b.symbolId;
                o.side = b'B';
                o.type_ = orderType::LMT.into();
                o.price = b.buys[0].price;
                o.quantity = 1;
                let r = self.session.newOrder(&mut o);
                self.logger.info(&format!("newOrder result={r}"));
                self.sentOrder = true;
            }
        }
    }
    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 log = logger::default(); // routes to the host's single log file (nanoconda-cli's -l/--log)

        // OPTIONAL: plugin-specific CLI args, e.g. `... -- --threshold 5` on the
        // run command below. `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() { log.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());
        log.info(&format!("loaded state: {n} bytes, fillCount={} lastFillPrice={}", state.fillCount, state.lastFillPrice));

        Box::new(MyStrategy { session, logger: log, state, sentOrder: false })
    },
    |strategy: &mut dyn listener| { let _ = strategy; }
);

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 nanoconda-cli -a pluginloader's -f/--statefile argument, same as the C++ side.

3. Build the guest

From rust/:

make

Output: strategy_example.wasm, right in that directory. (make just runs cargo build --release then copies the result out of Cargo's target-triple build directory; run cargo build --release directly instead if you'd rather — output then lands at target/wasm32-wasip1/release/strategy_example.wasm.) The crate's .cargo/config.toml sets wasm32-wasip1 as the default build target, so plain cargo build is enough — no --target flag needed.

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 crate 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. Run it

No separate host to build — same as the C++ side, nanoconda-cli -a pluginloader already has the wasm loader built in:

nanoconda-cli -a pluginloader -i 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 nanoconda-cli session arguments as the C++ side.

Differences from the C++ SDK

C++ Rust Why
order.type order.type_, or order.kind() for the typed value type is a reserved keyword in Rust.
dmasession::cancelAllOrders(symbolId) / cancelAllOrders(symbolId, userId) (overloaded) cancelAllOrders(symbolId) / cancelAllOrdersForUser(symbolId, userId) (distinct names) Rust doesn't support overloading on arity. Same split for flatten/flattenForUser.
customField::_c() / _c(value) customField::_c() / set_c(value) (same for every _x/set_x) Rust doesn't overload on argument count either.
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.
datainput.symbolId (inherited) datainput.header.symbolId Rust has no struct inheritance; dataheader is a named field instead.
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, enum values, the listener callback set, dmasession's method set, and every type/field name — is a direct, same-case port; a strategy written against one SDK's naming reads almost unchanged against the other. See rust/README.md for the full reference, including the ABI reference shared with the C++ SDK (byte-identical wire structs either way).

Writing a second strategy

Copy the whole rust/ directory to a new location and edit its src/lib.rs. Don't edit src/nanoconda.rs (the SDK) or src/nc_abi_generated.rs (generated, maintained by Nanoconda).