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 nanoconda-cli -a pluginloader, the same command that runs a native .so plugin, dispatching to the wasm path automatically based on the file extension.
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 (nanoconda-cli -a pluginloader) |
Links against the real libnanoconda, creates and owns the actual dmasession, and hosts the guest under wasmtime. Same dispatcher nanoconda-cli already uses for native .so plugins (nacoplugin::runPlugin); a .wasm path routes to nacoplugin::runWasmPlugin instead. |
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 might differs (real libnanoconda calls vs. ABI imports). |
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 the session arguments given to nanoconda-cli -a pluginloader (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
nanoconda-cli -a pluginloaderprocess hosts exactly onedmasessionand one guest today. Run multiple processes 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.