Skip to content
Get Started

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
(arm64: swap 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
(Intel Mac: swap 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:

example_client.cpp
#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:

make WASI_SDK=/opt/wasi-sdk

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:

nanoconda-cli -a wasmpluginloader -i strategy.wasm -- wasmplugin -e CME -s ESZ6,NQZ6 -u myuser -p mypass -a myaccount -c 0 -l strategy.log