A production-grade WASM/Wasmtime Component Model plugin engine built in Rust. BEX enables sandboxed, deterministic plugin execution using WebAssembly components with WIT interface definitions. Designed for media streaming, metadata, and content provider applications with a Pure C ABI integration layer — no cxx dependency.
What's New in v6
This version incorporates all fixes from the QuickJS Integration Plan v3 review:
Critical Bug Fixes
eval-js now accepts input parameter — user data is safely injected as a global variable instead of being concatenated into JS source code, eliminating code injection vulnerabilities
call-js-fn now accepts fn-source parameter — functions are registered and auto-re-registered when source changes, eliminating the broken track_functions text parser
TextEncoder/TextDecoder are now spec-correct UTF-8 — properly handles CJK characters, emoji, and all Unicode code points
crypto.getRandomValues uses Rust-backed CSPRNG — rand::thread_rng() instead of Math.random()
crypto.subtle is now fully implemented — SHA-1/256/384/512, AES-CBC encrypt/decrypt, HMAC-SHA256/512, PBKDF2, all in pure JS with immediate-resolved Promises
args_json is passed as a string, not eval'd — eliminates JS injection attack vector
Pool dispatch is non-blocking — uses try_send instead of blocking send, returns PoolBusy instead of hanging Wasmtime threads
Pool shutdown uses SeqCst ordering — fixes race condition on ARM/Apple Silicon
Missing Features Now Implemented
console.log routes to Rust tracing — no longer silently dropped
setTimeout/setInterval call callbacks synchronously — no longer silently skipped
clear-js-fn WIT function — allows unregistering JS functions when cipher rotates
JsPoolConfig wired into EngineConfig — JS pool settings are configurable from engine config
Design Improvements
Idle context eviction throttled to 30-second intervals — reduces overhead from checking on every loop iteration
apply_fn helper removed — func.call((args_json,)) used directly
max_stack_bytes configurable via JsPoolConfig — default 512KB
All compiler warnings fixed — unused imports, dead code, unused variables
atob/btoa are Rust-backed — correct Latin-1 handling via base64 crate
WASM-Only: All plugins run as WebAssembly components via Wasmtime — no native plugins, no dual-mode engine
Component Model: Uses Wasmtime's Component Model with WIT interface definitions for type-safe host-guest communication
Callback-Driven: C++ backend submits requests via bex_submit_*(), receives results via a C function pointer callback (BexResultCallback) invoked from a background Tokio thread. No polling, no event queue, no drain pattern.
Self-Describing IDs: The engine treats IDs as opaque strings — it does not know or care what they mean. Each plugin defines its own ID format and parses IDs internally. For example, get_servers takes a single id parameter; the plugin parses slug$ep=1$sub=1$dub=0 because it knows its own encoding scheme. There is no episode_id parameter anywhere in the engine.
Lane-Based Scheduling: Three concurrency lanes — Control (1), User (4), Background (2) — with semaphores, plus global WASM (4) and HTTP (8) permit limits
Cancellation: Each request gets a CancellationToken; cancel via bex_cancel_request()
Pure C ABI: The Rust engine exports extern "C" functions matching bex_engine.h. No cxx, no bridge codegen, no special build steps. Link the Rust static/shared library natively.
The Rust engine exposes a Pure C ABI through bex_engine.h. No cxx, no code generation, no bridge crate. The Rust library compiles as both cdylib and staticlib, and CMake links it natively.
How It Works
C++ calls bex_submit_search(engine, plugin_id, query, callback, user_data)
Rust spawns a Tokio task that does the work
On completion, Rust invokes callback(user_data, request_id, success, payload, len) from the Tokio background thread
C++ receives the result in the callback and can parse/copy the payload before the callback returns
C++ Integration Example
cpp
1#include"bex_engine.h"23// 1. Define a callback handler4extern"C"voidon_result(void* user_data,uint64_t req_id,5bool success,constuint8_t* payload, size_t len){6auto* promise =static_cast<std::promise<std::string>*>(user_data);7if(success){8 promise->set_value(std::string(reinterpret_cast<constchar*>(payload), len));9}else{10 promise->set_exception(std::make_exception_ptr(11 std::runtime_error(std::string(reinterpret_cast<constchar*>(payload), len))));12}13}1415// 2. Create engine16BexEngine* engine =bex_engine_new("/path/to/data");1718// 3. Submit async requests — returns request_id immediately19std::promise<std::string> promise;20uint64_t req1 =bex_submit_home(engine,"bex.gogoanime", on_result,&promise);21uint64_t req2 =bex_submit_search(engine,"bex.gogoanime","one piece", on_result,&promise);22uint64_t req3 =bex_submit_info(engine,"bex.gogoanime","one-piece", on_result,&promise);23uint64_t req4 =bex_submit_servers(engine,"bex.gogoanime","one-piece$ep=1$sub=1$dub=0",24 on_result,&promise);2526// 4. Wait for results (or use the callback in your event loop)27std::string result = promise.get_future().get();2829// 5. Cancel a request30bex_cancel_request(engine, req2);3132// 6. Plugin management (synchronous)33bex_engine_install(engine,"/path/to/plugin.bex");34bex_engine_uninstall(engine,"bex.gogoanime");35BexPluginInfoList plugins =bex_engine_list_plugins(engine);36bex_plugin_info_list_free(plugins);3738// 7. API key management (synchronous)39bex_engine_secret_set(engine,"bex.imdb","api-key","your-key");4041// 8. Shutdown42bex_engine_free(engine);
1# Build the Rust engine and CLI2cargo build --release
34# Build and pack all plugins (compile → component convert → pack)5bash build-plugins.sh
67# Build the C++ CLI with CMake8cd cpp-cli &&mkdir build &&cd build
9cmake .. -DCMAKE_BUILD_TYPE=Release
10make -j$(nproc)
Build a Single Plugin (Manual)
Plugins are built in three steps: compile to WASM, convert to a component, then pack.
bash
1# Step 1: Compile to wasm32-wasip12cargo build -p bex-gogoanime --target wasm32-wasip1 --release
34# Step 2: Convert to a WASM Component (requires wasm-tools + WASI adapter)5wasm-tools component new \6 target/wasm32-wasip1/release/bex_gogoanime.wasm \7 -o target/components/bex-gogoanime.component.wasm \8 --adapt ~/.cargo/registry/src/index.crates.io-*/wasi-preview1-component-adapter-provider-*/artefacts/wasi_snapshot_preview1.reactor.wasm
910# Step 3: Pack into a .bex package11cargo run -p bex-cli --release -- pack \12 dist/bex-gogoanime.yaml \13 target/components/bex-gogoanime.component.wasm \14 dist/bex-gogoanime.bex
Install and Use
bash
1# Install a plugin2cargo run -p bex-cli --release -- install dist/bex-gogoanime.bex
34# List installed plugins5cargo run -p bex-cli --release -- list
67# Get detailed plugin info8cargo run -p bex-cli --release -- plugin-info bex.gogoanime
Plugin Management
Rust CLI
bash
1# Install / uninstall2bex install dist/bex-gogoanime.bex
3bex uninstall bex.gogoanime
45# List installed plugins6bex list
78# Show detailed plugin info (capabilities, enabled state, etc.)9bex plugin-info bex.gogoanime
1011# Enable / disable12bex enable bex.gogoanime
13bex disable bex.gogoanime
1415# Inspect a .bex package without installing16bex inspect dist/bex-gogoanime.bex
1718# Engine stats19bex stats
C++ CLI
bash
1# Install / uninstall2./bexcli install dist/bex-gogoanime.bex
3./bexcli uninstall bex.gogoanime
45# List plugins (with capabilities column)6./bexcli list
78# Detailed plugin info (includes API keys list)9./bexcli info-plugin bex.gogoanime
1011# Enable / disable12./bexcli enable bex.gogoanime
13./bexcli disable bex.gogoanime
1415# Engine stats16./bexcli stats
API Key / Secret Management
The engine provides per-plugin secret storage backed by Redb. Secrets are scoped to a plugin ID and are accessible to the plugin at runtime via the secrets WIT interface. This is the mechanism for storing API keys, tokens, and other credentials that plugins need.
Rust CLI
bash
1# Set an API key2bex set-key bex.imdb api-key "your-api-key-here"34# Get an API key value5bex get-key bex.imdb api-key
67# Delete an API key8bex delete-key bex.imdb api-key
910# List all keys for a plugin11bex list-keys bex.imdb
C++ CLI
bash
1# Set an API key2./bexcli set-key bex.imdb api-key "your-api-key-here"34# Get an API key value5./bexcli get-key bex.imdb api-key
67# Delete an API key8./bexcli delete-key bex.imdb api-key
910# List all keys for a plugin11./bexcli list-keys bex.imdb
C API
c
1// Store a secret2bex_engine_secret_set(engine,"bex.imdb","api-key","your-key");34// Retrieve a secret5char buf[4096];6size_t buf_len =sizeof(buf);7bex_engine_secret_get(engine,"bex.imdb","api-key", buf,&buf_len);89// Delete a secret10bex_engine_secret_delete(engine,"bex.imdb","api-key");1112// List all secret key names (comma-separated, caller frees with bex_string_free)13char* keys =bex_engine_secret_keys(engine,"bex.imdb");14bex_string_free(keys);
Plugin Access (WIT)
Inside a plugin, secrets are accessed read-only through the secrets host interface:
Secrets that a plugin expects are declared in the manifest:
yaml
1secrets:2- api-key
3- tmdb-token
Self-Describing IDs
Self-describing IDs are the core design pattern for how the BEX engine handles typed identifiers. The engine itself treats all IDs as opaque strings — it does not parse, validate, or interpret them. Only the plugin knows what its IDs mean and how to decode them.
This means:
get_servers takes a single id parameter — there is no separate episode_id parameter
The engine never parses IDs — it passes them straight through to the plugin
Each plugin defines its own ID encoding — different plugins can use entirely different schemes
IDs are portable — they can be stored, serialized, and passed between systems without the engine needing to understand them
Example: GogoAnime Episode IDs
The GogoAnime plugin encodes episode context directly in the ID:
{slug}$ep={episode_number}$sub={0|1}$dub={0|1}
ID
Meaning
one-piece$ep=1$sub=1$dub=0
One Piece episode 1, subbed
jujutsu-kaisen-tv$ep=24$sub=0$dub=1
Jujutsu Kaisen episode 24, dubbed
When get_servers is called with this ID, the GogoAnime plugin splits on $ and parses each key-value pair to determine the slug, episode number, and sub/dub flags. The engine never does this parsing — it just passes the string through.
Example: IMDb Media IDs
The IMDb plugin might use a different scheme entirely (e.g., tt1234567), and the engine works equally well because it does not interpret the ID.
Usage
bash
1# The ID is self-describing — pass it directly2bex servers bex.gogoanime 'one-piece$ep=1$sub=1$dub=0'34# C++ CLI works the same way5./bexcli servers bex.gogoanime 'one-piece$ep=1$sub=1$dub=0'
WIT Interface Definitions
Host-Provided APIs (imports — plugins call these)
Interface
Functions
Description
http
send-request
HTTP client with caching, redirect control, size limits
kv
set, get, remove, keys
Scoped key-value storage
secrets
get
Read-only secret/API key access
log
write
Structured logging through host
clock
now-ms, monotonic
Time access
rng
bytes
Secure random bytes
js
eval-js, eval-js-opts, call-js-fn, clear-js-fn
QuickJS sandbox — safe JS eval, function call, and cleanup
Copy wit/plugin.wit from the engine repository into your plugin's wit/ directory.
3. Generate bindings
Run cargo build --target wasm32-wasip1 --release once to generate src/bindings.rs, then implement the Guest trait:
rust
1#[allow(warnings)]2modbindings;34usebindings::bex::plugin::common::*;5usebindings::bex::plugin::http;6usebindings::exports::api::Guest;78structComponent;910implGuestforComponent{11fnget_home(_ctx:RequestContext)->Result<Vec<HomeSection>,PluginError>{12Ok(vec![HomeSection{13 id:"home".to_string(),14 title:"My Plugin".to_string(),15 subtitle:None,16 items:vec![],17 next_page:None,18 layout:CardLayout::Grid,19 show_rank:false,20 categories:vec![],21 extra:vec![],22}])23}2425fnsearch(_ctx:RequestContext, query:String, _filters:SearchFilters)->Result<PagedResult,PluginError>{26let response =http::send_request(&http::Request{27 method:http::Method::Get,28 url:format!("https://api.example.com/search?q={}", query),29 headers:vec![],30 body:None,31 timeout_ms:Some(10000),32 follow_redirects:true,33 cache_mode:http::CacheMode::Normal,34 max_bytes:Some(1024*1024),35}).map_err(|e|PluginError::Network(format!("{:?}", e)))?;3637Ok(PagedResult{ items:vec![], categories:vec![], next_page:None})38}3940fnget_servers(_ctx:RequestContext, id:String)->Result<Vec<Server>,PluginError>{41// The ID is self-describing — parse it however your plugin needs42// The engine does not interpret the ID, only your plugin does43let parts:Vec<&str>= id.split('$').collect();44// ... parse and fetch servers ...45Ok(vec![])46}4748// ... implement other methods ...49}5051bindings::export!(Component with_types_in bindings);
4. Build, convert, and pack
bash
1# Step 1: Compile to WASM2cargo build --target wasm32-wasip1 --release
34# Step 2: Convert to a WASM Component5wasm-tools component new \6 target/wasm32-wasip1/release/my_plugin.wasm \7 -o target/components/my-plugin.component.wasm \8 --adapt /path/to/wasi_snapshot_preview1.reactor.wasm
910# Step 3: Pack into a .bex package11bex pack manifest.yaml target/components/my-plugin.component.wasm my-plugin.bex
1213# Step 4: Install14bex install my-plugin.bex
Plugin Manifest
yaml
1schema:12id: bex.my-plugin
3name: My Plugin
4version: 1.0.0
5authors:6- Your Name
7abi:">=1.0.0,<2.0.0"8provides:9home:true10search:true11info:true12servers:true13stream:true14network:15hosts:16-"api.example.com"17concurrent:418storage:true19secrets:20- api-key
21display:22description: My awesome plugin
23tags:24- streaming
25priority:100
Capability Bits
Bit
Name
Methods
0
HOME
get_home
1
CATEGORY
get_category
2
SEARCH
search
3
INFO
get_info
4
SERVERS
get_servers
5
STREAM
resolve_stream
6
SUBTITLES
search_subtitles, download_subtitle
7
ARTICLES
get_articles, search_articles
CMake Integration
The C++ CLI's CMakeLists.txt is designed to be self-contained and reusable. You can integrate the BEX engine into any C++ project with minimal setup. The only requirement is the bex_engine.h header and the Rust static/shared library — no cxx bridge, no code generation, no special include paths.
Quick Integration
Build the Rust library:
cargo build -p bex-runtime --release
Copy the cpp-cli/ directory into your project (or reference it via BEX_ENGINE_ROOT).
In your CMakeLists.txt:
cmake
1cmake_minimum_required(VERSION3.16)2project(myapp LANGUAGES C CXX)34set(CMAKE_CXX_STANDARD17)56# Point to the bex-engine root (required if not inside the repo)7set(BEX_ENGINE_ROOT"/path/to/bex-engine")89# Add the bex engine subdirectory10add_subdirectory(${BEX_ENGINE_ROOT}/cpp-cli bex_engine_build)1112# Link against the imported bex::engine target13add_executable(myapp main.cpp)14target_link_libraries(myapp PRIVATEbex::engine)
BEX_ENGINE_ROOT Variable
The CMake build uses BEX_ENGINE_ROOT to locate the Rust library and the C header:
Default: If not set, it walks up from CMAKE_SOURCE_DIR to find a directory containing Cargo.toml
Override: Set -DBEX_ENGINE_ROOT=/path/to/bex-engine when running cmake
Imported Target
The CMakeLists.txt creates an INTERFACE library target bex::engine with all include paths and link libraries configured:
target_link_libraries(myapp PRIVATE bex::engine)
Helper Target
A rustlib custom target is provided to build the Rust library from CMake:
make rustlib
Required Files
File
Purpose
cpp-cli/bex_engine.h
Pure C ABI header (the FFI boundary)
target/release/libbex_runtime.a
Rust static library (Pure C ABI exports)
That's it. No generated headers, no bridge codegen, no extra include paths.