WebAssembly Plugins — Building & Running (C++)
This page walks through installing the toolchain, writing a minimal strategy, compiling it to .wasm, building the host loader, and running the two together. See Building & Running (Rust) for the Rust guest SDK instead.
1. Install wasi-sdk
The guest is built against wasi-sdk — no build step of your own, no root required, prebuilt binaries per OS. Check the releases page for a newer version than the one below if you want the latest.
Linux (x86_64):
mkdir -p /opt/wasi-sdk
curl -L https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-33/wasi-sdk-33.0-x86_64-linux.tar.gz \
| tar xz -C /opt/wasi-sdk --strip-components=1
x86_64-linux for arm64-linux.)
macOS:
mkdir -p /opt/wasi-sdk
curl -L https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-33/wasi-sdk-33.0-arm64-macos.tar.gz \
| tar xz -C /opt/wasi-sdk --strip-components=1
arm64-macos for x86_64-macos.)
Windows (PowerShell):
mkdir C:\wasi-sdk
curl.exe -L -o wasi-sdk.tar.gz https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-33/wasi-sdk-33.0-x86_64-windows.tar.gz
tar xzf wasi-sdk.tar.gz -C C:\wasi-sdk --strip-components=1
2. Write a minimal strategy
This is a complete, working example — a strategy that logs book/trade/fill events, tracks fill count and last fill price, and persists that state across restarts:
#include "nanoconda.h"
#include <getopt.h>
#include <stdio.h>
struct PersistedState {
int64_t fillCount = 0;
int64_t lastFillPrice = 0;
};
struct MyStrategy : nanoconda::listener, nanoconda::logger {
nanoconda::dmasession* session = nullptr;
PersistedState state;
MyStrategy() : nanoconda::logger() {} // routes to the host's single log file (nacowasmplugin's -l/--log)
void onsecurity(const nanoconda::security* s) override {
logM("security %s tick=%llu", s->symbol, (unsigned long long)s->tickSize);
}
void onbook(const nanoconda::book* b) override {
if (b->buys[0].qty && b->sells[0].qty) {
logM("book symbol=%llu bid=%lld ask=%lld", (unsigned long long)b->symbolId,
(long long)b->buys[0].price, (long long)b->sells[0].price);
}
// react to market data via session->newOrder(&o), etc.
}
void ontrade(const nanoconda::trade* t) override {
logM("trade symbol=%llu px=%lld sz=%u", (unsigned long long)t->symbolId,
(long long)t->lastPrice, t->lastSize);
}
void onorderack(const nanoconda::order* o) override { logM("order ack id=%llu", (unsigned long long)o->orderId); }
void oncancelack(const nanoconda::order*) override {}
void onmodifyack(const nanoconda::order*) override {}
void onorderreject(const nanoconda::order* o) override { logM("order reject %s", nanoconda::toStr(o->code)); }
void onmodifyreject(const nanoconda::order*) override {}
void oncancelreject(const nanoconda::order*) override {}
void onfill(const nanoconda::order* o) override {
logM("fill id=%llu qty=%u", (unsigned long long)o->orderId, o->quantityLast);
state.fillCount++;
state.lastFillPrice = o->priceLast;
}
void onout(const nanoconda::order*) override {}
void onorderstatus(const nanoconda::order*) override {}
};
static MyStrategy g_strategy;
extern "C" nanoconda::listener* init_nanoconda_plugin(int argc, char** argv, nanoconda::dmasession* session) {
g_strategy.session = session;
// OPTIONAL: plugin-specific CLI args, e.g. `... -- --threshold 5` on the
// nacowasmplugin command line -- argc/argv work exactly like a native
// main()'s (argv[0] is a placeholder), so ordinary getopt_long applies.
static struct option long_options[] = {
{"threshold", required_argument, 0, 't'},
{0, 0, 0, 0}
};
int opt, option_index = 0;
optind = 0; optopt = 0; optarg = nullptr;
while ((opt = getopt_long(argc, argv, "t:", long_options, &option_index)) != -1) {
switch (opt) {
case 't': g_strategy.logM("threshold arg = %s", optarg); break;
default: break;
}
}
// OPTIONAL: restore state from the previous run, if any
int32_t n = nanoconda::loadState(0, &g_strategy.state, sizeof(g_strategy.state));
g_strategy.logM("loaded state: %d bytes, fillCount=%lld lastFillPrice=%lld",
n, (long long)g_strategy.state.fillCount, (long long)g_strategy.state.lastFillPrice);
return &g_strategy;
}
extern "C" void plugin_stop() {
// OPTIONAL: save state to disk
nanoconda::reasoncode r = nanoconda::saveState(0, &g_strategy.state, sizeof(g_strategy.state));
g_strategy.logM("saved state on shutdown: result=%s", nanoconda::toStr(r));
}
nanoconda::saveState/loadState persist to the file given by nacowasmplugin's -f/--statefile argument (see below) — this is how a strategy survives a restart without you managing your own file I/O (which the wasm sandbox wouldn't let you do directly anyway).
3. Build the guest
/opt/wasi-sdk/bin/clang++ -std=c++17 -O2 --target=wasm32-wasip1 \
--sysroot=/opt/wasi-sdk/share/wasi-sysroot -mexec-model=reactor \
-I . example_client.cpp -o example_client.wasm
Or, if you have the provided Makefile:
Use wasi-sdk's own bin/clang++, not your system compiler. -I . is enough — nanoconda.h and nanoconda_wasm_abi.h are siblings in the wasm SDK directory.
-mexec-model=reactor is required — without it you get a _start/exit-style binary instead of a reactor exporting the functions the host calls (starting, nc_cb_*).
4. Build the host loader
g++ -std=c++17 -O2 -I . -I .. nacowasmplugin.cpp \
-lwasmtime -lnanoconda -ldl -lpthread -o nacowasmplugin
This needs a wasmtime C API build with WASI support (the default in official wasmtime releases).
5. Run it
./nacowasmplugin -i example_client.wasm -- wasmplugin -e CME -s ESZ6,NQZ6 -u myuser -p mypass \
-a myaccount -c 0 -l strategy.log -f strategy.state -- --threshold 5
The token right after the first -- (wasmplugin above) is required and otherwise ignored — getopt_long skips argv[0], the same convention native .so plugins already need. A second --, if present, marks the start of arguments meant for the guest; those are passed through to your init_nanoconda_plugin(argc, argv, ...) untouched, and argv[0] there is a placeholder ("client") so ordinary getopt_long works unmodified on the guest side too.
nacowasmplugin CLI arguments
These come right after the required <ignored-token> (wasmplugin in the example above) and are consumed by nacowasmplugin itself — the same admin-sequence arguments nacoplugin::runPlugin takes for a native .so:
| Flag | Meaning |
|---|---|
-i |
Path to the guest .wasm module to load. |
-e |
Exchange. |
-s |
Comma-separated symbols to subscribe. |
-d |
Comma-separated underlyings to subscribe (subscribeUnderlying). |
-u |
Username. |
-p |
Password. |
-a |
Account. |
-c |
CPU to pin the process to. |
-l |
Log file path — shared between host- and guest-side log messages (see Logging in the ABI reference), so both land in one file. |
-f |
State file path, used by nanoconda::saveState/loadState. |
The host builds a real argv for the guest in guest memory — the argument strings themselves, plus a null-terminated array of guest-memory pointers to each one. Since a wasm32 char* is just an i32 offset into linear memory, that array of offsets is a valid char*[] from the guest's point of view, avoiding a separate round trip per string.
Integrating into nanoconda-cli
nacowasmplugin.cpp ships with its own standalone main() for the workflow above. To fold it into nanoconda-cli instead (so it's launched the same way as any other plugin, via -a wasmpluginloader), compile it with -DNC_NO_STANDALONE_MAIN and add this dispatch in nanoconda-cli.cpp:
} else if (!strcmp(application, "wasmpluginloader")) {
optind = 0; optopt = 0; optarg = nullptr;
nacowasmplugin::runPlugin(input, pluginArgc, pluginArgv);
}
Once wired in, the run command becomes: