# WebAssembly Modules in Logos This document describes how WebAssembly (WASM) modules work in the Logos platform — how to build one, how the runtime loads and executes it, and what changes were made across the codebase to enable WASM support. ## Table of Contents - [Overview](#overview) - [How It Works (Architecture)](#how-it-works-architecture) - [Process Model](#process-model) - [IPC: Transparent to Callers](#ipc-transparent-to-callers) - [Type System](#type-system) - [Export Discovery](#export-discovery) - [Authentication](#authentication) - [Tutorial: Building a WASM Calculator Module](#tutorial-building-a-wasm-calculator-module) - [Prerequisites](#prerequisites) - [Step 1: Create the Project Structure](#step-1-create-the-project-structure) - [Step 2: Write the Rust Guest Code](#step-2-write-the-rust-guest-code) - [Step 3: Configure the Module (metadata.json)](#step-3-configure-the-module-metadatajson) - [Step 4: Create the Package Manifest (manifest.json)](#step-4-create-the-package-manifest-manifestjson) - [Step 5: Write the Nix Build (flake.nix)](#step-5-write-the-nix-build-flakenix) - [Step 6: Build](#step-6-build) - [Step 7: Test with logoscore](#step-7-test-with-logoscore) - [Step 8: Package with lgx](#step-8-package-with-lgx) - [Calling a WASM Module from Another Module](#calling-a-wasm-module-from-another-module) - [Cross-Repo Changes That Enable WASM Support](#cross-repo-changes-that-enable-wasm-support) - [logos-liblogos](#logos-liblogos) - [logos-cpp-sdk](#logos-cpp-sdk) - [logos-package-manager](#logos-package-manager) - [logos-logoscore-cli](#logos-logoscore-cli) - [Reference](#reference) - [Type Mappings](#type-mappings) - [Function Signature Requirements](#function-signature-requirements) - [Filtered Exports](#filtered-exports) - [Platform Variant Names](#platform-variant-names) - [Limitations and Future Work](#limitations-and-future-work) - [Troubleshooting](#troubleshooting) --- ## Overview A traditional Logos module is a C++/Qt plugin — a `.so` or `.dylib` compiled with CMake, linked against Qt 6, and loaded via `QPluginLoader`. This works well but requires C++ and Qt knowledge. **WASM modules are a simpler alternative.** You write pure computation in Rust (or any language that compiles to `wasm32-unknown-unknown`), export plain C functions, and the Logos runtime loads the `.wasm` binary directly via [Wasmtime](https://wasmtime.dev/). No C++, no Qt, no CMake, no code generation. > **Note on naming:** Despite the document title, Logos does **not** use [Extism](https://extism.org/). The runtime uses Wasmtime directly with raw C ABI exports (`extern "C"` + `#[no_mangle]`). Extism's PDK and host SDK are not involved. Functions take and return scalar numeric types — not Extism-style input/output buffers. **What you write:** | File | Purpose | |------|---------| | `wasm-guest/src/lib.rs` | Your module logic in Rust | | `wasm-guest/Cargo.toml` | Rust build config | | `metadata.json` | Module name, version, type | | `manifest.json` | Package manifest for module discovery | | `flake.nix` | Nix build orchestration | **What you don't write:** No C++ source, no Qt headers, no CMakeLists.txt, no C header, no generated code. **Comparison with native Qt modules:** | | Native Qt Module | WASM Module | |---|---|---| | Language | C++ (or any language via C FFI + code generator) | Rust (or any language targeting wasm32) | | Build system | CMake + Nix + logos-module-builder | Cargo + Nix (no CMake) | | Output | `.so` / `.dylib` Qt plugin | `.wasm` binary | | Host process | `logos_host` | `logos_host_wasm` | | Runtime | QPluginLoader + Qt MOC | Wasmtime | | IPC | Qt Remote Objects | Qt Remote Objects (same) | | Types | Full Qt type system (QString, QVariantMap, etc.) | Scalars only (i32, i64, f32, f64) | | Inter-module calls | Yes, via LogosAPI | Yes, via LogosAPI (same) | | File size | ~100KB–1MB+ | ~15KB for a calculator | --- ## How It Works (Architecture) ### Process Model Every module — native or WASM — runs in its own process for isolation. The core process (`logoscore`) spawns a host process for each module: ``` logoscore (main process) ├── logos_host "capability_module" "/path/to/capability_module_plugin.so" ├── logos_host "chat_module" "/path/to/chat_module_plugin.so" └── logos_host_wasm "wasm_calc_module" "/path/to/wasm_calc_module.wasm" ↑ ↑ --name arg --path arg ``` When `logoscore` detects a `.wasm` file (by extension), it spawns `logos_host_wasm` instead of `logos_host`. The WASM host process: 1. **Receives an auth token** via a local Unix socket (`logos_token_`) 2. **Loads the `.wasm` binary** using the Wasmtime C API — compiles it to native code, creates an engine/store/instance 3. **Discovers exported functions** by iterating module exports and caching their type signatures 4. **Registers on the IPC bus** via `LogosAPI::getProvider()->registerObject()` — making it callable by any other module 5. **Enters the Qt event loop** to serve remote method calls ### IPC: Transparent to Callers From the caller's perspective, WASM modules are **indistinguishable from native modules**. The same Qt Remote Objects transport layer carries method calls between processes. A native C++ module calling a WASM module looks exactly like calling any other module: ```cpp // This works identically whether "wasm_calc_module" is a .so or a .wasm LogosAPIClient* client = logosAPI->getClient("wasm_calc_module"); QVariant result = client->invokeRemoteMethod("wasm_calc_module", "add", 3, 5); // result == QVariant(8) ``` The call path is: ``` Caller module (any process) → Qt Remote Objects (IPC) → logos_host_wasm process → ModuleProxy::callRemoteMethod() → WasmProviderObject::callMethod() → QVariant args → wasmtime_val_t conversion → wasmtime_func_call() → WASM function executes → wasmtime_val_t result → QVariant conversion ← returns QVariant ← returns to ModuleProxy ← serialized back over IPC ← received by caller ← QVariant result ``` ### Type System WASM modules support four scalar types. Each maps bidirectionally between WASM, Rust, Qt, and `logoscore -c` argument auto-detection: | WASM Type | Rust Type | Qt/QVariant Type | logoscore -c auto-detect | |-----------|-----------|------------------|--------------------------| | `i32` | `i32` | `int` | Integer literals (small) | | `i64` | `i64` | `qlonglong` | Integer literals | | `f32` | `f32` | `float` (stored as `double`) | Decimal literals | | `f64` | `f64` | `double` | Decimal literals | | (void) | `()` | `QVariant(true)` | — | **Conversion rules in `WasmProviderObject::callMethod()`:** - Incoming `QVariant` args are converted using `toInt()`, `toLongLong()`, `toFloat()`, or `toDouble()` based on the WASM function's declared parameter types - Return values are converted back: `i32` → `QVariant(int)`, `i64` → `QVariant(qlonglong)`, `f32`/`f64` → `QVariant(double)` - Void functions (no return value) return `QVariant(true)` ### Export Discovery When a `.wasm` module is loaded, `WasmProviderObject::discoverExports()` iterates all module exports and registers every exported function — except internal/WASI bookkeeping symbols: **Skipped exports:** - `memory`, `_start`, `_initialize` - `__data_end`, `__heap_base`, `__indirect_function_table` - Any name starting with `__` (double underscore) Everything else becomes a callable method. The parameter count and types are read from the function's Wasmtime type signature and cached in a `QHash`. ### Authentication WASM modules use the same token-based authentication as native modules: 1. `logoscore` spawns `logos_host_wasm` with `--name` and `--path` args 2. `logos_host_wasm` creates a `QLocalServer` on socket `logos_token_` 3. `logoscore` connects and sends a UUID auth token 4. The WASM host saves the token for `"core"` and `"capability_module"` 5. All subsequent IPC calls include the token, validated by `ModuleProxy` --- ## Tutorial: Building a WASM Calculator Module This walks through building the `logos-wasm-calc-module` — a calculator with `add`, `subtract`, `multiply`, `divide`, `factorial`, and `fibonacci` functions. ### Prerequisites - A working [logos-workspace](https://github.com/logos-co/logos-workspace) checkout with Nix installed - The `ws` CLI on your PATH: `export PATH="/path/to/workspace/scripts:$PATH"` - Rust and Cargo (provided automatically by Nix during build) ### Step 1: Create the Project Structure ```bash mkdir -p logos-wasm-calc-module/wasm-guest/src cd logos-wasm-calc-module ``` Your final directory structure: ``` logos-wasm-calc-module/ ├── metadata.json # Module metadata ├── manifest.json # Package manifest for discovery ├── flake.nix # Nix build ├── flake.lock # (auto-generated by nix) ├── .gitignore └── wasm-guest/ # Rust source ├── Cargo.toml ├── Cargo.lock └── src/ └── lib.rs # Module logic ``` Create `.gitignore`: ``` result result-* wasm-guest/target/ ``` ### Step 2: Write the Rust Guest Code #### 2.1 Configure Cargo Create `wasm-guest/Cargo.toml`: ```toml [package] name = "wasm_calc" version = "1.0.0" edition = "2021" [lib] crate-type = ["cdylib"] [profile.release] opt-level = "s" lto = true ``` **Key settings:** - **`crate-type = ["cdylib"]`** — produces a C-compatible dynamic library. For `wasm32-unknown-unknown`, this means a `.wasm` binary with exported functions. - **`opt-level = "s"`** — optimize for size. WASM binaries are small; this keeps them minimal (~16KB for a calculator). - **`lto = true`** — link-time optimization strips unused code, critical for WASM where every byte counts. > **No external dependencies.** This example has zero crate dependencies, so `cargo build --offline` works in the Nix sandbox. If you need crates, you'll need to use `rustPlatform.buildRustPackage` instead of calling `cargo` directly. #### 2.2 Write the Module Logic Create `wasm-guest/src/lib.rs`: ```rust // Each exported function becomes a callable method in the Logos module. // Functions use the C ABI and are exported with #[no_mangle] so Wasmtime // can discover them by name. #[no_mangle] pub extern "C" fn add(a: i64, b: i64) -> i64 { a + b } #[no_mangle] pub extern "C" fn subtract(a: i64, b: i64) -> i64 { a - b } #[no_mangle] pub extern "C" fn multiply(a: i64, b: i64) -> i64 { a * b } #[no_mangle] pub extern "C" fn divide(a: i64, b: i64) -> i64 { if b == 0 { -1 } else { a / b } } #[no_mangle] pub extern "C" fn factorial(n: i64) -> i64 { if n < 0 { return -1; } let mut result: i64 = 1; for i in 1..=n { result = result.checked_mul(i).unwrap_or(-1); if result < 0 { return -1; } } result } #[no_mangle] pub extern "C" fn fibonacci(n: i64) -> i64 { if n < 0 { return -1; } if n <= 1 { return n; } let (mut a, mut b) = (0i64, 1i64); for _ in 2..=n { let next = a.checked_add(b).unwrap_or(-1); if next < 0 { return -1; } a = b; b = next; } b } ``` **Rules for exported functions:** 1. **`#[no_mangle]`** — prevents Rust name mangling so Wasmtime finds the function by its plain name 2. **`pub extern "C"`** — uses the C calling convention (standard for WASM exports) 3. **Scalar types only** — parameters and return values must be `i32`, `i64`, `f32`, or `f64` 4. **No panics across FFI** — use `checked_*` operations and return error values instead of panicking. A panic in WASM becomes a trap, which the host logs as an error and returns `QVariant()` (empty). ### Step 3: Configure the Module (metadata.json) Create `metadata.json`: ```json { "name": "wasm_calc_module", "version": "1.0.0", "description": "Calculator module implemented as WebAssembly", "author": "Logos Core Team", "type": "core", "category": "general", "main": "wasm_calc_module_plugin", "dependencies": [] } ``` This file follows the same schema as native modules. The `type` must be `"core"` for the package manager to discover it as a module (vs. a UI plugin). ### Step 4: Create the Package Manifest (manifest.json) Create `manifest.json`: ```json { "name": "wasm_calc_module", "version": "1.0.0", "type": "core", "category": "general", "description": "Calculator module implemented as WebAssembly", "author": "Logos Core Team", "main": "wasm_calc_module.wasm", "manifestVersion": "0.1.0", "dependencies": [] } ``` **The `main` field** tells the package manager which file to load. For WASM modules, this is the `.wasm` filename. It can be either: - A **string** (simplest) — the `.wasm` filename, used on all platforms - An **object** mapping platform variants to filenames (useful if you have different builds per platform, though WASM is typically architecture-independent): ```json "main": { "linux-x86_64": "wasm_calc_module.wasm", "linux-arm64": "wasm_calc_module.wasm", "darwin-arm64": "wasm_calc_module.wasm", "darwin-x86_64": "wasm_calc_module.wasm" } ``` The string form is preferred for WASM since the same binary runs everywhere. ### Step 5: Write the Nix Build (flake.nix) Create `flake.nix`: ```nix { description = "Calculator module as WebAssembly — loadable by logoscore via logos_host_wasm"; inputs = { logos-nix.url = "github:logos-co/logos-nix"; nixpkgs.follows = "logos-nix/nixpkgs"; nix-bundle-lgx.url = "github:logos-co/nix-bundle-lgx"; logos-package.url = "github:logos-co/logos-package"; nix-bundle-dir.url = "github:logos-co/nix-bundle-dir"; }; outputs = { self, nixpkgs, logos-nix, nix-bundle-lgx, logos-package, nix-bundle-dir }: let systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system); in { packages = forAllSystems (system: let pkgs = import nixpkgs { inherit system; }; # Stage 1: Compile Rust to .wasm wasmLib = pkgs.stdenv.mkDerivation { pname = "wasm-calc"; version = "1.0.0"; src = ./wasm-guest; nativeBuildInputs = [ pkgs.cargo pkgs.rustc pkgs.lld ]; buildPhase = '' runHook preBuild export HOME=$TMPDIR export CARGO_HOME=$TMPDIR/cargo mkdir -p $CARGO_HOME cargo build --target wasm32-unknown-unknown --release --offline 2>&1 runHook postBuild ''; installPhase = '' runHook preInstall mkdir -p $out/lib cp target/wasm32-unknown-unknown/release/wasm_calc.wasm $out/lib/ runHook postInstall ''; }; # Stage 2: Create module directory with canonical naming moduleLib = (pkgs.runCommand "logos-wasm_calc_module-module-lib-1.0.0" {} '' mkdir -p $out/lib cp ${wasmLib}/lib/wasm_calc.wasm $out/lib/wasm_calc_module.wasm '') // { src = ./.; version = "1.0.0"; }; bundleLgx = nix-bundle-lgx.bundlers.${system}.default; in { default = moduleLib; lib = moduleLib; wasm = wasmLib; lgx = bundleLgx moduleLib; } ); }; } ``` **Build pipeline:** ``` ┌──────────────────────────────────────────────────────────────────────┐ │ │ │ Stage 1: Compile Rust → WASM │ │ ──────────────────────────── │ │ wasm-guest/src/lib.rs ──→ cargo build --target wasm32-unknown- │ │ unknown --release │ │ ──→ wasm_calc.wasm (~16KB) │ │ │ │ Stage 2: Package as module │ │ ─────────────────────────── │ │ wasm_calc.wasm ──→ renamed to wasm_calc_module.wasm │ │ ──→ placed in lib/ directory │ │ │ │ Stage 3 (optional): Bundle as .lgx │ │ ────────────────────────────────── │ │ module lib/ ──→ nix-bundle-lgx ──→ wasm_calc_module.lgx │ │ │ └──────────────────────────────────────────────────────────────────────┘ ``` **Key points:** - The Rust target `wasm32-unknown-unknown` ships with every Rust compiler — no additional sysroot or WASI SDK needed - `--offline` works because there are no external crate dependencies - `lld` is included as a linker (Cargo uses it for WASM targets) - The module name in the output (`wasm_calc_module.wasm`) must match the `main` field in `manifest.json` ### Step 6: Build ```bash # Initialize git (nix flakes require tracked files) git init git add -A # Build nix build # Check the output ls -la result/lib/ # -r-xr-xr-x 1 ... 15945 ... wasm_calc_module.wasm file result/lib/wasm_calc_module.wasm # WebAssembly (wasm) binary module version 0x1 (MVP) ``` ### Step 7: Test with logoscore To test, you need two things: 1. A `logoscore` build that includes `logos_host_wasm` (the WASM host binary) 2. The module installed in the directory layout the package manager expects #### 7.1 Build logoscore with WASM support The `logos_host_wasm` binary is built by `logos-liblogos` when Wasmtime is available (it's a build dependency). Build `logoscore` with the local `logos-liblogos`: ```bash nix build path:./repos/logos-logoscore-cli#cli \ --override-input logos-liblogos path:./repos/logos-liblogos \ -o /tmp/logoscore-result ``` Verify `logos_host_wasm` is in the closure: ```bash WASM_HOST=$(nix-store -qR /tmp/logoscore-result \ | xargs -I{} sh -c 'test -f {}/bin/logos_host_wasm && echo {}/bin/logos_host_wasm' 2>/dev/null \ | head -1) echo "WASM host: $WASM_HOST" ``` #### 7.2 Set up the module directory The package manager expects each module in a subdirectory with a `manifest.json`: ```bash mkdir -p /tmp/test-modules/wasm_calc_module cp result/lib/wasm_calc_module.wasm /tmp/test-modules/wasm_calc_module/ cp manifest.json /tmp/test-modules/wasm_calc_module/ ``` Resulting structure: ``` /tmp/test-modules/ └── wasm_calc_module/ ├── manifest.json └── wasm_calc_module.wasm ``` #### 7.3 Run with logoscore ```bash LOGOS_HOST_WASM_PATH="$WASM_HOST" /tmp/logoscore-result/bin/logoscore \ -m /tmp/test-modules \ -l wasm_calc_module \ -c "wasm_calc_module.add(3, 5)" \ -c "wasm_calc_module.multiply(6, 7)" \ -c "wasm_calc_module.factorial(10)" \ -c "wasm_calc_module.fibonacci(20)" \ --quit-on-finish ``` Expected output: ``` Method call successful. Result: 8 Method call successful. Result: 42 Method call successful. Result: 3628800 Method call successful. Result: 6765 ``` > **Why `LOGOS_HOST_WASM_PATH`?** The logoscore-cli flake.nix currently sets `LOGOS_HOST_PATH` for native modules but does not yet set `LOGOS_HOST_WASM_PATH`. The plugin launcher looks for the WASM host binary in three places: (1) the `LOGOS_HOST_WASM_PATH` env var, (2) next to the `logoscore` binary, (3) `../bin/` relative to the modules directory. Until the flake is fixed, the env var is the simplest workaround. Use `-v` for verbose output to debug loading issues: ```bash LOGOS_HOST_WASM_PATH="$WASM_HOST" /tmp/logoscore-result/bin/logoscore -v \ -m /tmp/test-modules -l wasm_calc_module \ -c "wasm_calc_module.add(3, 5)" --quit-on-finish ``` ### Step 8: Package with lgx ```bash lgx create wasm_calc_module lgx add wasm_calc_module.lgx -v linux-x86_64 -f result/lib/wasm_calc_module.wasm lgx add wasm_calc_module.lgx -v darwin-arm64 -f result/lib/wasm_calc_module.wasm lgx verify wasm_calc_module.lgx ``` Or use the Nix-based bundler (builds the `.lgx` automatically): ```bash nix build .#lgx ``` --- ## Calling a WASM Module from Another Module WASM modules participate in the same IPC system as native modules. From any Qt module with a `LogosAPI*` pointer: ```cpp // Raw call (works from any module — native or WASM) LogosAPIClient* client = logosAPI->getClient("wasm_calc_module"); QVariant result = client->invokeRemoteMethod("wasm_calc_module", "add", 10, 20); qDebug() << result; // QVariant(qlonglong, 30) // Check for errors if (!result.isValid()) { qWarning() << "Call failed (unknown method, timeout, or WASM trap)"; } ``` From `logoscore -c` (command line): ```bash logoscore -m /path/to/modules \ -l wasm_calc_module,my_other_module \ -c "wasm_calc_module.add(10, 20)" ``` The caller does not need to know whether the target module is a `.so` plugin or a `.wasm` binary. The IPC transport and authentication are identical. --- ## Cross-Repo Changes That Enable WASM Support WASM module support required changes across five repositories. This section describes each change in detail. ### logos-liblogos The bulk of the implementation. Three new files and two modified files. #### New: `src/logos_host_wasm/logos_host_wasm.cpp` The WASM host process entry point — analogous to `logos_host` (which loads native Qt plugins). The `main()` function: 1. **Parses arguments** via CLI11: `--name --path ` 2. **Receives an auth token** by creating a `QLocalServer` on socket `logos_token_` and waiting up to 10 seconds for the core process to connect and send a UUID 3. **Instantiates `WasmProviderObject`** with the `.wasm` path 4. **Registers on IPC** via `LogosAPI::getProvider()->registerObject(name, wasmProvider)`, which wraps the provider in a `ModuleProxy` and publishes it on the Qt Remote Objects bus 5. **Saves auth tokens** for `"core"` and `"capability_module"` in `TokenManager` 6. **Enters the Qt event loop** to serve incoming remote method calls indefinitely #### New: `src/logos_host_wasm/wasm_provider_object.h` Declares `WasmProviderObject`, which inherits `LogosProviderObject` (from logos-cpp-sdk). Key members: ```cpp class WasmProviderObject : public LogosProviderObject { // Each discovered WASM function struct WasmFunc { wasmtime_func_t func; // function handle wasm_functype_t* type; // parameter/return type info int paramCount; int resultCount; }; wasm_engine_t* m_engine; // Wasmtime engine (manages compilation) wasmtime_store_t* m_store; // Wasmtime store (execution state) wasmtime_module_t* m_module; // compiled module wasmtime_instance_t m_instance; // instantiated module QHash m_functions; // exported functions cache }; ``` #### New: `src/logos_host_wasm/wasm_provider_object.cpp` The implementation, with four key sections: **`loadModule()`** — Reads the `.wasm` binary, creates a Wasmtime engine, compiles the module to native code, creates a store (no WASI — pure computation), and instantiates the module via a linker. Error handling covers both compilation errors and instantiation traps. **`discoverExports()`** — Iterates all module exports via `wasmtime_instance_export_nth()`. For each exported function (skipping internal symbols like `memory`, `_start`, `__heap_base`, etc.), it reads the function's type signature, counts parameters and results, and caches the handle in `m_functions`. **`callMethod()`** — Looks up the function by name, converts `QVariant` arguments to `wasmtime_val_t` based on declared parameter types (i32/i64/f32/f64), calls the function via `wasmtime_func_call()`, and converts the result back to `QVariant`. Handles both errors and runtime traps gracefully. **`getMethods()`** — Returns a `QJsonArray` describing all exported functions with their names, parameter types, return types, and signatures. This is what `lm` and other introspection tools use (though `lm` currently only works with Qt plugins, not `.wasm` files). #### Modified: `src/logos_core/plugin_launcher.cpp` Added WASM detection and host binary resolution: ```cpp // In PluginLauncher::launch(): bool isWasm = pluginPath.endsWith(".wasm", Qt::CaseInsensitive); QString hostPath = isWasm ? resolveLogosHostWasmPath(pluginsDirs) : resolveLogosHostPath(pluginsDirs); ``` Added `resolveLogosHostWasmPath()` which uses `resolveHostBinary("logos_host_wasm", "LOGOS_HOST_WASM_PATH", pluginsDirs)` — checking the env var, then the application directory, then `../bin/` relative to the plugins directory. #### Modified: `src/logos_core/plugin_registry.cpp` Added special handling for `.wasm` files during module discovery. When the package manager returns a module whose `mainFilePath` ends in `.wasm`, the registry skips `QPluginLoader` introspection (which would fail on a WASM binary) and registers it directly from manifest metadata: ```cpp if (mainFilePath.endsWith(".wasm", Qt::CaseInsensitive)) { PluginInfo info; info.path = mainFilePath; // Parse dependencies from manifest JSON if (mod.contains("dependencies") && mod["dependencies"].is_array()) { for (const auto& dep : mod["dependencies"]) info.dependencies.append(QString::fromStdString(dep.get())); } m_plugins.insert(name, info); continue; // skip Qt plugin processing } ``` #### Modified: `src/CMakeLists.txt` Conditional build of `logos_host_wasm` when Wasmtime is available: ```cmake find_library(WASMTIME_LIBRARY NAMES wasmtime) find_path(WASMTIME_INCLUDE_DIR NAMES wasmtime.h) if(WASMTIME_LIBRARY AND WASMTIME_INCLUDE_DIR) add_executable(logos_host_wasm logos_host_wasm/logos_host_wasm.cpp logos_host_wasm/wasm_provider_object.h logos_host_wasm/wasm_provider_object.cpp ) target_link_libraries(logos_host_wasm PRIVATE Qt6::Core Qt6::RemoteObjects Qt6::Network logos_sdk CLI11::CLI11 ${WASMTIME_LIBRARY} ) endif() ``` The `logos_host_wasm` target is also conditionally added to the install targets in the top-level `CMakeLists.txt`. #### Modified: `nix/default.nix` Added `pkgs.wasmtime` to `buildInputs` so the Wasmtime C library and headers are available during CMake configuration. ### logos-cpp-sdk #### `cpp/logos_provider_object.h` This file defines `LogosProviderObject` — the abstract base class that `WasmProviderObject` extends. It was not modified for WASM support; the existing interface was sufficient: ```cpp class LogosProviderObject { public: virtual QVariant callMethod(const QString& methodName, const QVariantList& args) = 0; virtual bool informModuleToken(const QString& moduleName, const QString& token) = 0; virtual QJsonArray getMethods() = 0; virtual void setEventListener(EventCallback callback) = 0; virtual void init(void* apiInstance) = 0; virtual QString providerName() const = 0; virtual QString providerVersion() const = 0; }; ``` The key design decision was that `LogosProviderObject` is **not Qt-specific** — it uses `QVariant` for data but doesn't require `Q_OBJECT`, MOC, or `QPluginLoader`. This made it possible to implement a Wasmtime-backed provider without any changes to the SDK. ### logos-package-manager #### `src/package_manager_lib.cpp` No WASM-specific changes were needed. The `scanInstalledByTypes()` function already works generically: 1. Scans module directories for subdirectories containing `manifest.json` 2. Reads the `main` field and resolves it to a file path (trying platform variants) 3. Returns the resolved `mainFilePath` — the registry then checks the extension The `.wasm` extension handling happens entirely in `logos-liblogos` (plugin_registry.cpp). The package manager is extension-agnostic. ### logos-logoscore-cli #### `flake.nix` The logoscore-cli flake wraps the binary with environment variables via `qtWrapperArgs`: ```nix qtWrapperArgs = [ "--set LOGOS_HOST_PATH ${liblogos}/bin/logos_host" ]; ``` **Missing:** A corresponding `--set LOGOS_HOST_WASM_PATH ${liblogos}/bin/logos_host_wasm` entry. This is why the `LOGOS_HOST_WASM_PATH` environment variable must be set manually when testing. This is a known gap that should be fixed. --- ## Reference ### Type Mappings | WASM | Rust | C | Qt (QVariant) | getMethods() string | |------|------|---|---------------|---------------------| | `i32` | `i32` | `int32_t` | `int` | `"int"` | | `i64` | `i64` | `int64_t` | `qlonglong` | `"qlonglong"` | | `f32` | `f32` | `float` | `double` (via `float`) | `"float"` | | `f64` | `f64` | `double` | `double` | `"double"` | | (none) | `()` | `void` | `QVariant(true)` | `"void"` | ### Function Signature Requirements For a function to be discoverable by the Logos WASM host: | Requirement | Example | |-------------|---------| | `#[no_mangle]` attribute | Prevents Rust name mangling | | `pub extern "C"` | C calling convention | | Scalar parameter types | `i32`, `i64`, `f32`, `f64` only | | Scalar or void return type | Same four types, or no return | | No panics across FFI | Use `checked_*` or return error values | **Valid:** ```rust #[no_mangle] pub extern "C" fn add(a: i64, b: i64) -> i64 { a + b } #[no_mangle] pub extern "C" fn pi() -> f64 { std::f64::consts::PI } #[no_mangle] pub extern "C" fn noop() { } ``` **Invalid (will not work):** ```rust // String parameters — not supported pub extern "C" fn greet(name: *const c_char) -> *const c_char { ... } // Struct return — not a scalar type pub extern "C" fn get_point() -> Point { ... } ``` ### Filtered Exports These export names are automatically skipped during discovery: | Export | Reason | |--------|--------| | `memory` | WASM linear memory (not a function) | | `_start` | WASI entry point | | `_initialize` | WASI initialization | | `__data_end` | WASM data segment boundary | | `__heap_base` | WASM heap start | | `__indirect_function_table` | WASM function table | | `__*` (any double-underscore prefix) | Internal/compiler-generated symbols | ### Platform Variant Names The package manager tries these variant names in order when resolving `manifest.json` `main` entries: | Platform | Primary | Alias | Dev variant | |----------|---------|-------|-------------| | Linux x86_64 | `linux-x86_64` | `linux-amd64` | `linux-x86_64-dev` | | Linux ARM64 | `linux-aarch64` | `linux-arm64` | `linux-aarch64-dev` | | macOS ARM64 | `darwin-arm64` | `aarch64-darwin` | `darwin-arm64-dev` | | macOS x86_64 | `darwin-x86_64` | `x86_64-darwin` | `darwin-x86_64-dev` | For WASM modules, the binary is architecture-independent, so a simple string `main` field is preferred over per-variant entries. --- ## Limitations and Future Work | Limitation | Details | |------------|---------| | **Scalars only** | No string, map, list, or complex type support. Functions can only take and return `i32`, `i64`, `f32`, `f64`. | | **No WASI** | Modules target `wasm32-unknown-unknown` (pure computation). No filesystem, network, or clock access from WASM. | | **No `lm` support** | The `lm` module inspector uses `QPluginLoader` and cannot introspect `.wasm` files. Use `logoscore -v` to see discovered methods. | | **No module-builder template** | `logos-module-builder` has templates for native C++ modules but no WASM template yet. WASM modules must be structured manually. | | **`LOGOS_HOST_WASM_PATH` workaround** | The `logoscore-cli` flake.nix doesn't set this env var yet. Must be set manually or the binary placed next to `logoscore`. | | **No events** | `WasmProviderObject` has an `EventCallback` but WASM functions are synchronous and cannot emit events. | | **Single return value** | WASM functions can return at most one value. Multi-return WASM is not yet supported. | --- ## Troubleshooting ### "Module not found in known plugins: wasm_calc_module" The package manager didn't discover the module. Check: 1. The module directory structure is correct: `//manifest.json` + `.wasm` file 2. `manifest.json` has `"type": "core"` (not `"ui"` or other types) 3. The `main` field in `manifest.json` matches the actual `.wasm` filename 4. The `.wasm` file actually exists at the resolved path ### "logos_host_wasm not found" The WASM host binary isn't where the plugin launcher expects it. Fix: ```bash # Find it in the nix store WASM_HOST=$(nix-store -qR /path/to/logoscore-result \ | xargs -I{} sh -c 'test -f {}/bin/logos_host_wasm && echo {}/bin/logos_host_wasm' 2>/dev/null \ | head -1) # Set the env var export LOGOS_HOST_WASM_PATH="$WASM_HOST" ``` Or build `logos-liblogos` directly and point to it: ```bash cd repos/logos-liblogos && nix build .#logos-liblogos export LOGOS_HOST_WASM_PATH="$(pwd)/result/bin/logos_host_wasm" ``` ### "WasmProviderObject: compile error" The `.wasm` binary is invalid or uses unsupported features. Check: - Built with `--target wasm32-unknown-unknown` (not `wasm32-wasi`) - The `crate-type` is `["cdylib"]`, not `["rlib"]` or `["lib"]` - No unsupported WASM proposals are used (the default Wasmtime config is used) ### "WasmProviderObject: call trap" A runtime trap during function execution. Common causes: - **Integer overflow** — use `checked_mul()`, `checked_add()`, etc. - **Division by zero** — WASM traps on `i32.div_s` / `i64.div_s` with zero divisor - **Unreachable** — Rust `panic!()` compiles to `unreachable` in WASM, which traps - **Stack overflow** — deep recursion in WASM hits the stack limit ### "Method call returned invalid result" The method call failed — the module isn't loaded, the method doesn't exist, or IPC timed out. Run with `-v` to see the full debug log. ### Build fails with "cargo build --offline" error Your Rust code has external crate dependencies. Either: 1. Remove the dependencies (if possible) 2. Use `rustPlatform.buildRustPackage` in your `flake.nix` with a `cargoHash` instead of calling `cargo` directly