How to Fix: Wasmtime 32.0.0 can run CPython.wasm from CLI but not from Rust application

8 min read

Wasmtime 32.0.0 runs CPython.wasm from the CLI but fails in Rust because the CLI wires up WASI differently than your embedded host

If CPython.wasm starts correctly with the wasmtime command but breaks when launched from your Rust code, the problem is usually not the WebAssembly binary itself. The real issue is that the Wasmtime CLI automatically configures a richer WASI host environment than a minimal Rust embedding does. CPython depends on filesystem access, argv, environment setup, preopened directories, and often dynamic loading behavior that the CLI prepares for you. Your Rust application must reproduce that setup explicitly.

Symptoms

This issue typically looks like one of these scenarios:

  • wasmtime CPython.wasm … works from the terminal.
  • The same module fails when instantiated from a Rust host using wasmtime APIs.
  • You see errors related to missing files, imports, startup failures, or module instantiation.
  • Python cannot find its standard library, cannot open expected paths, or exits very early.

The repository linked in the issue description demonstrates exactly this pattern: the binary runs from the CLI but not from an embedded Rust runtime because the host setup is incomplete compared to the CLI.

Understanding the Root Cause

The Wasmtime CLI is not just a thin wrapper around module execution. It constructs a fully configured WASI context and enables the features needed by many real-world WASI applications. A custom Rust host often starts with only the bare minimum, and that mismatch is what breaks CPython.wasm.

There are four technical reasons this happens most often:

1. Missing preopened directories

CPython needs access to its runtime files, especially the Python standard library and supporting resources. When you run from the CLI, you can pass directory mappings and the runtime sees those paths. In Rust, if you do not explicitly preopen the directories that contain Lib, extension artifacts, or working files, CPython cannot resolve them.

2. Missing or different argv/environment

The CLI usually populates stdin/stdout/stderr, command-line arguments, and environment variables in a sensible way. CPython reads those values during startup. If your Rust host does not set the same argv or required environment variables, the interpreter may choose the wrong startup path or fail to locate resources.

3. WASI preview/version mismatch in host wiring

With modern Wasmtime, you must ensure your Rust embedding uses the correct WASI component or core module integration for the module you are loading. If the module expects a specific WASI import surface and your linker/store/context provides another, the CLI may succeed because it detects and wires imports correctly while your Rust code does not.

4. CPython.wasm may rely on the same runtime flags and capabilities the CLI enables

The CLI often enables features such as async support, epoch interruption settings, caching, and module loading options depending on how it is invoked and built. Even when those are not the main cause, a mismatch in Config, Linker, or WasiCtx can change runtime behavior.

In short, the CLI succeeds because it provides a complete host environment. Your Rust program must deliberately mirror that environment.

Step-by-Step Solution

The fix is to make your Rust embedding behave like the CLI. That means:

  1. Create the engine with the right Wasmtime configuration.
  2. Build a proper WASI context.
  3. Preopen every directory CPython needs.
  4. Pass the correct argv and environment variables.
  5. Instantiate the module with the correct linker/import wiring.

1. Add the required dependencies

Make sure your Cargo.toml uses compatible versions of Wasmtime crates.

[dependencies]
anyhow = "1"
wasmtime = "32.0.0"
wasmtime-wasi = "32.0.0"
tokio = { version = "1", features = ["full"] }

2. Build a Wasmtime engine and linker correctly

Start with a configuration close to what the CLI expects.

use anyhow::Result;
use wasmtime::{Config, Engine, Linker, Module, Store};
use wasmtime_wasi::{DirPerms, FilePerms, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};

struct HostState {
    wasi: WasiCtx,
}

impl WasiView for HostState {
    fn ctx(&mut self) -> &mut WasiCtx {
        &mut self.wasi
    }
}

fn build_engine() -> Result<Engine> {
    let mut config = Config::new();
    config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable);
    let engine = Engine::new(&config)?;
    Ok(engine)
}

3. Reproduce the CLI’s WASI setup

This is the most important step. You need to inherit standard streams, pass arguments, pass environment variables, and preopen directories that contain CPython.wasm resources.

use std::path::Path;

fn build_wasi() -> Result<WasiCtx> {
    let mut builder = WasiCtxBuilder::new();

    builder.inherit_stdio();

    builder.arg("python")?;
    builder.arg("-c")?;
    builder.arg("print('hello from CPython.wasm')")?;

    builder.env("PYTHONHOME", "/python")?;
    builder.env("PYTHONPATH", "/python/Lib")?;

    let host_python_dir = Path::new("./python-runtime");
    builder.preopened_dir(
        host_python_dir,
        "/python",
        DirPerms::READ,
        FilePerms::READ,
    )?;

    let host_work_dir = Path::new(".");
    builder.preopened_dir(
        host_work_dir,
        "/work",
        DirPerms::all(),
        FilePerms::all(),
    )?;

    Ok(builder.build())
}

Important: adjust PYTHONHOME, PYTHONPATH, and the preopened host directories to match the actual layout of the repository from the issue. If CPython expects its stdlib in a mounted directory, the guest path and environment variables must align exactly.

4. Add WASI to the linker and instantiate the module

#[tokio::main]
async fn main() -> Result<()> {
    let engine = build_engine()?;
    let module = Module::from_file(&engine, "./CPython.wasm")?;

    let mut linker: Linker<HostState> = Linker::new(&engine);
    wasmtime_wasi::add_to_linker_async(&mut linker)?;

    let wasi = build_wasi()?;
    let mut store = Store::new(&engine, HostState { wasi });

    let instance = linker.instantiate_async(&mut store, &module).await?;

    let start = instance.get_typed_func::<(), ()>(&mut store, "_start")?;
    start.call_async(&mut store, ()).await?;

    Ok(())
}

If your module is not using an exported _start in this exact way, inspect its exports and invoke the correct entrypoint.

5. Mirror the CLI invocation exactly

A very effective debugging strategy is to compare your Rust host to the exact CLI command that works. If the working command uses directory mappings, environment variables, or arguments, port them one by one into Rust.

For example, if this works from CLI:

wasmtime run --dir . --env PYTHONHOME=/python --env PYTHONPATH=/python/Lib CPython.wasm

Then your Rust code must create equivalent:

  • preopened_dir(…, “/python”, …)
  • env(“PYTHONHOME”, “/python”)
  • env(“PYTHONPATH”, “/python/Lib”)

6. Verify the guest-visible filesystem layout

The path visible inside the guest is what matters, not the host path. If the stdlib exists on your host at ./cpython/Lib but CPython looks for /python/Lib in the guest, you must mount the host directory into that guest path.

builder.preopened_dir(
    std::path::Path::new("./cpython"),
    "/python",
    DirPerms::READ,
    FilePerms::READ,
)?;

7. Enable debugging output during startup

If CPython still fails, print and validate all startup assumptions:

  • What argv is being passed?
  • What environment variables exist?
  • Which guest paths are mounted?
  • Does the module export _start?
  • Are all imports satisfied?

You can also compare behavior against the official Wasmtime docs at Wasmtime documentation and the WASI integration guidance in the wasmtime-wasi crate docs.

8. If the module uses additional files, mount them too

CPython distributions for WebAssembly often include more than one file: standard library folders, configuration files, and sometimes extra data. If the repository contains dependencies next to CPython.wasm, do not assume Wasmtime will discover them automatically. Every required path must be visible through a guest mount.

Common Edge Cases

Wrong guest mount point

You mounted the right host directory, but to the wrong guest path. This is one of the most common reasons Python still cannot find its stdlib.

Read-only filesystem when Python expects write access

Some Python startup flows try to write temporary files, cache files, or working outputs. If you only grant READ permissions, startup may fail later than expected.

Using sync APIs for an async linker setup

If you call add_to_linker_async, use the async instantiation and function call path consistently. Mixing sync and async APIs can produce confusing runtime errors.

Not matching the module type

If you are loading a core wasm module but wiring it like a component, or vice versa, the CLI may appear more forgiving because it handles the intended execution path directly.

Environment variables not set inside the guest

Setting variables in the host shell does not automatically guarantee the guest sees the values you expect. Explicitly add them through WasiCtxBuilder.

Relative paths differ between CLI and Rust execution

Your CLI may be running from the repository root, while your Rust application runs from a different working directory. That changes which files actually get mounted.

Permissions too restrictive

If you are troubleshooting, temporarily broaden DirPerms and FilePerms to confirm the issue is permission-related, then tighten them afterward.

CPython package layout assumptions

Some builds expect a very specific filesystem structure. Even if files exist, placing Lib one level too deep or too shallow can break initialization.

FAQ

Why does Wasmtime CLI work if my Rust code uses the same version?

Because version parity alone is not enough. The CLI also creates a complete WASI runtime configuration. Your Rust host must explicitly recreate the same filesystem mounts, arguments, environment, and import wiring.

Do I always need PYTHONHOME and PYTHONPATH for CPython.wasm?

Not always, but for many packaged CPython.wasm builds they are the simplest way to ensure Python finds its standard library inside the guest filesystem. If startup fails, these should be among the first values you verify.

Compare the exact working CLI invocation with your Rust host setup. If the CLI uses directory access or env vars, port those first. Then verify the guest-visible path layout matches what CPython expects. Filesystem mismatches are the most common cause.

To solve this issue reliably, treat the embedded Rust host as a manual reimplementation of the Wasmtime CLI environment. The winning pattern is:

  • Use the correct wasmtime and wasmtime-wasi versions together.
  • Build a proper WasiCtx with inherited stdio.
  • Pass the same argv and env values used by the working CLI command.
  • Preopen the directories containing the Python runtime at the exact guest paths CPython expects.
  • Instantiate the module with the correct WASI linker integration.

Once your Rust application mirrors the CLI host environment closely, CPython.wasm should behave the same way in both places.

Leave a Reply

Your email address will not be published. Required fields are marked *