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
.wasmguest 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
nacowasmpluginprocess hosts exactly onedmasessionand one guest today. Run multiplenacowasmpluginprocesses for multiple concurrent strategies. - No runtime subscription changes. There is no guest-callable
subscribeafter startup — the subscription set is fixed by the host's own-s/-darguments 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.