Sandboxed Extensions
The engine loads and executes extensions as sandboxed WebAssembly components. Each component has isolated memory and accesses host capabilities through interfaces the engine provides. From inside the sandbox, it can call engine statements to read data, edit projects, and control the scene.
Python SDK
Save this workflow as app.py. The engine calls WitWorld.run() when it executes the component:
import spatialengine
from spatialengine.types.std.math import Vec3
class WitWorld:
def run(self):
client = spatialengine.Client(transport=spatialengine.ExtensionTransport())
client.std.scene.shape.spawn_arrow(
start=Vec3(x=0, y=0, z=0),
end=Vec3(x=2, y=1, z=3),
)Use the same generated SDK methods as in a native Python application. ExtensionTransport connects them to the component’s host bindings. The SDK handles statement paths, protobuf encoding, typed results, and engine errors.
Build the Python component
Create a virtual environment for the component. Install the Python SDK wheel without the grpc extra, and install componentize-py in that environment:
python -m pip install componentize-pyDownload the WIT interface as extension.wit beside app.py. In that directory, generate bindings and build the component:
componentize-py -d extension.wit -w extension bindings .
componentize-py -d extension.wit -w extension componentize app -o extension.wasmThe resulting extension.wasm contains your Python workflow. The host bindings become available when the engine instantiates the component.
Run the extension
Enable extensions when you start the native engine:
SpatialEngine --allow-extensionsUse a separate Python environment with the SDK’s grpc extra to connect to the engine. Load the component and run it:
from pathlib import Path
import spatialengine
with spatialengine.connect() as client:
extension = client.std.extension.load(component=Path("extension.wasm").read_bytes())
client.std.extension.run(id=extension)
client.std.extension.unload(id=extension)run() returns when the workflow finishes. Each run starts a fresh sandbox. Use reset(id=extension) to cancel an active run while keeping the component loaded. unload() cancels an active run and removes the component.
For a browser engine, enable extensions during initialization:
import init, { statements } from "@fslabs/spatialengine";
await init({ allowExtensions: true });
const client = await statements();
const component = new Uint8Array(await (await fetch("./extension.wasm")).arrayBuffer());
const extension = await client.std.extension.load({ component });
await client.std.extension.run({ id: extension });
await client.std.extension.unload({ id: extension });Direct ESM imports load the npm package’s adjacent extensions/ assets. For bundled apps, copy that directory to a publicly served path and set extensionAssetBase to its URL during initialization. Browser extensions require a window and cross-origin isolation through COOP and COEP headers. Headless Node.js engines do not host extensions.
Supported languages
Python extensions use componentize-py and the Python SDK through ExtensionTransport.
Both native and browser engines load WebAssembly components. Language support depends on a toolchain that produces a component implementing the engine’s WIT interface. Additional languages can use that interface as their component toolchains become available. A language’s ability to compile a WebAssembly module alone does not provide component support.
Additional languages
Rust is planned for a supported extension workflow. C/C++, Go, and JavaScript/TypeScript are additional candidates. These languages have WebAssembly component toolchains. A supported workflow requires engine bindings and validation on both native and browser hosts.
Other languages
Use a language with WebAssembly component support. The WIT interface and engine protobuf schema provide the contract for a custom implementation.
Generate guest bindings for the WIT extension world with your language’s component toolchain. Implement its exported run() function. The engine calls this function to execute your workflow and supplies the imported engine.execute-statement function.
WIT describes the component boundary. The engine’s protobuf schema describes the statements you can call. Export the schema and generate protobuf message bindings for your language.
To call a statement, pass its full gRPC method path, the serialized protobuf request, and an optional transaction UUID to execute-statement. For example, std.print uses /spatialengine.v1.std.Std/Print. The request bytes contain the protobuf message without gRPC framing.
Decode a successful result as that method’s protobuf response. Decode a failure as spatialengine.v1.std.StatementError. Pass a transaction UUID when a call joins an existing transaction. Calls follow the same transaction rules as SDK calls.
Compile against the interface and SDK from the engine release you target. Desktop and browser hosts have different restrictions. Exercise the component on each target you distribute.
Runtime limits
Extensions are disabled until startup enables them. Treat statement access to an enabled engine as permission to upload and execute components. An extension can invoke the same engine statements as its caller.
Components receive no direct filesystem, environment, network, or standard-input access. Each engine stores at most 32 uploaded components, with a 32 MiB limit per upload. Engines allow four concurrent runs. Runs have a 30-second deadline. Native engines limit each run to 256 MiB of linear memory. Browser workers use the browser’s memory limits.
Reset and unload stop execution. Statements the component already applied remain in the engine. Use a transaction when project edits must succeed or fail together.
Python libraries with native extensions are not automatically available in a WebAssembly component. A Studio app runs in a normal Python environment and has different capabilities.
Choose an integration before moving an existing workflow into a component.