Files
logos-tutorial/EXTISM_MODULES.md
2026-04-06 13:29:23 -04:00

895 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 | ~100KB1MB+ | ~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_<module_name>`)
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<QString, WasmFunc>`.
### 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_<name>`
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 <module_name> --path <wasm_file>`
2. **Receives an auth token** by creating a `QLocalServer` on socket `logos_token_<name>` 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<QString, WasmFunc> 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<std::string>()));
}
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: `<modules-dir>/<module_name>/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