Skip to content
Get Started

WebAssembly Plugins — Overview

What it is

Nanoconda strategies are normally written as native C++, compiled to a .so, and loaded by nanoconda-cli via dlopen() (see Nanoconda CLI). WebAssembly Plugins are a second way to run a strategy: compile it to a sandboxed .wasm guest module instead, and run it under wasmtime via a small host loader, nacowasmplugin — the wasm equivalent of nanoconda-cli's own plugin loader.

The same dmasession/listener model you already know from native strategy development carries over directly: onbook, ontrade, onfill, session->newOrder(...), and the rest of the Order Entry API and Algo Control API work exactly the same way inside a .wasm guest as they do in a native .so.

Why you'd use this instead of native:

  • Sandboxing. A .wasm guest runs inside wasmtime's sandbox — it can't touch host memory, the filesystem, or the network except through the ABI calls Nanoconda explicitly exposes.
  • Portability. The wire contract (nanoconda_wasm_abi.h) is a plain C header — any language with a wasm32 compile target can implement it, not just C++. The C++ guest SDK shipped today is the first of what can be several language bindings against the same ABI.
  • Aimed at minimal hot-path overhead. Order submission and market data calls are designed to avoid copying or serializing data across the host/guest boundary, so the sandboxing isn't meant to add meaningful cost there.

If you don't need sandboxing or multi-language support, native .so strategies remain the lower-friction path and have no wasmtime dependency. WebAssembly Plugins are an additional option, not a replacement.

Architecture

Side Role
Host (nacowasmplugin) A small process that links against the real libnanoconda, creates and owns the actual dmasession, and hosts the guest under wasmtime. Equivalent to nacoplugin::runPlugin in nanoconda-cli.cpp.
Guest (your strategy.wasm) Your strategy code, compiled to wasm32. Implements nanoconda::listener and holds a nanoconda::dmasession* exactly like a native strategy.
Wire contract (nanoconda_wasm_abi.h) Plain C: struct mirrors and every function that crosses the host/guest boundary. This is the ABI a non-C++ language would target to write a guest in another language.
Guest SDK (nanoconda.h, wasm directory) The C++ convenience layer over the ABI — a minimal-diff derivative of the real, native nanoconda.h. Every enum, POD struct, and the listener interface are unconditional, identical source to native; only dmasession, logger, and a few free functions have small #if defined(__wasm__) sections where the implementation genuinely differs (real libnanoconda calls vs. ABI imports). Diff it against the native header (diff nanoconda.h wasm/nanoconda.h) to see exactly what's wasm-specific.

Admin sequence is host-driven

registerApplication, dmasession::init, subscribe/subscribeUnderlying, start, stop, and replayTrace are all handled by the host before your guest even runs, from nacowasmplugin's own CLI arguments (see Building & Running). These methods are still declared on the guest-side dmasession, matching native, but calling them from guest code is a no-op — subscriptions and startup are already done by the time init_nanoconda_plugin runs. listSymbols is the one exception: it's callable at runtime, for entitlements discovered after the initial call.

plugin_stop() is the shutdown counterpart of init_nanoconda_plugin — same name and signature as a native .so's exit point, no arguments. The host calls it once on shutdown (SIGINT/SIGTERM), so you can saveState(...) or do other cleanup there. Call nanoconda::requestShutdown() when your own strategy logic decides it's done (not on a crash — that's what this is for, instead of abort()/a trap); it still runs plugin_stop() and normal teardown, just triggered by the guest instead of a signal.

Entry point

Identical signature to a native .so plugin's init_nanoconda_plugin — existing native client code that only needs this entry point doesn't have to change at all to also build for wasm:

struct MyStrategy : nanoconda::listener, nanoconda::logger {
   nanoconda::dmasession* session = nullptr;
   MyStrategy() : nanoconda::logger() {}
   void onbook(const nanoconda::book* b) override { logM("..."); /* ...session->newOrder(...)... */ }
   // ...
};
static MyStrategy g_strategy;

extern "C" nanoconda::listener* init_nanoconda_plugin(int argc, char** argv, nanoconda::dmasession* session) {
   g_strategy.session = session; // ready to use directly, no dmasession::fromHandle() needed

   // argc/argv work exactly like a native main()'s -- argv[0] is a placeholder
   // ("client"), so ordinary getopt_long(argc, argv, ...) works unmodified.

   return &g_strategy;
}

extern "C" void plugin_stop() {
   // g_strategy.session is already whatever init_nanoconda_plugin stored
}

init_nanoconda_plugin is declared extern "C" purely because it has to match the exact language linkage of the native .so entry point of the same name — that's the one place in the whole ABI that has this requirement. Nothing else needs extern "C" linkage in guest code.

What's different from native

Everything not listed below — struct layouts, enum values, dmasession/listener method signatures — is identical to native nanoconda.h; a strategy written against one compiles against the other unchanged.

Native Wasm behavior
registerApplication/subscribe/subscribeUnderlying/start/stop/replayTrace No-op — the host already did this before your guest ran (see Admin sequence).
dmasession::init() Dropped — init_nanoconda_plugin hands you a ready dmasession* directly.
dmasession::getRiskLimits() (no-arg) Present, backed by a static buffer.
nacoPx Dropped (not part of dmasession/listener).
logM/logL/logger::log* printf-style varargs, sent to the host's real logger — see ABI Reference — Logging.

Everything else — getSymbolName/getSecurity/getEpoch_ns, order/bookorder/customField methods, risk — works exactly like native, just proxied to the host; see the ABI Reference if you need the full list.

Current constraints

These are open items in the current design, not permanent limitations — flag any of them to your account representative if they block your use case:

  • One guest per process. One nacowasmplugin process hosts exactly one dmasession and one guest today. Run multiple nacowasmplugin processes for multiple concurrent strategies.
  • No runtime subscription changes. There is no guest-callable subscribe after startup — the subscription set is fixed by the host's own -s/-d arguments at launch (see Building & Running).
  • No execution budget. There is no fuel/epoch limit imposed on guest calls today.
  • Session lifetime. The session is never torn down mid-run — it runs until SIGINT/SIGTERM.
  • Single-threaded callback delivery. One thread is assumed for the host's run() loop and callback delivery into the guest.

See Building & Running (C++) or Building & Running (Rust) for the practical steps to compile and run a guest, and ABI Reference for the file-by-file breakdown of the contract itself.