I've been trying to figure this out for ages. Tim Bart responded to my call for help on Hacker News with this extremely useful code example showing how to run Python code in WebAssembly inside Python, using wasmtime-py and the new Python WASM build released by VMware Wasm Labs.
First step is to download the VMWare build:
cd /tmp
mkdir wasm
cd wasm
wget https://github.com/vmware-labs/webassembly-language-runtimes/releases/download/python%2F3.11.1%2B20230118-f23f3f3/python-aio-3.11.1.zip
unzip python-aio-3.11.1.zip
Create a virtual environment and install wasmtime
:
python3 -m venv venv
source venv/bin/activate
pip install wasmtime
Here's my version of Tim's code, slightly modified to provide a run_python_code()
function:
from wasmtime import Config, Engine, Linker, Module, Store, WasiConfig
import os
import tempfile
class Result:
def __init__(self, result, mem_size, data_len, consumed):
self.result = result
self.mem_size = mem_size
self.data_len = data_len
self.consumed = consumed
def __str__(self):
return f"""\
result:
{self.result}
mem size pages of 64kb: {self.mem_size}
data length: {self.data_len}
fuel consumed: {self.consumed}
"""
def run_python_code(code, fuel=400_000_000):
engine_cfg = Config()
engine_cfg.consume_fuel = True
engine_cfg.cache = True
linker = Linker(Engine(engine_cfg))
linker.define_wasi()
python_module = Module.from_file(linker.engine, "bin/python-3.11.1.wasm")
config = WasiConfig()
config.argv = ("python", "-c", code)
config.preopen_dir(".", "/")
with tempfile.TemporaryDirectory() as chroot:
out_log = os.path.join(chroot, "out.log")
err_log = os.path.join(chroot, "err.log")
config.stdout_file = out_log
config.stderr_file = err_log
store = Store(linker.engine)
# Limits how many instructions can be executed:
store.add_fuel(fuel)
store.set_wasi(config)
instance = linker.instantiate(store, python_module)
# _start is the default wasi main function
start = instance.exports(store)["_start"]
mem = instance.exports(store)["memory"]
try:
start(store)
except Exception as e:
print(e)
raise
with open(out_log) as f:
result = f.read()
return Result(
result, mem.size(store), mem.data_len(store), store.fuel_consumed()
)
if __name__ == "__main__":
for code in (
"print('hello world')",
"for i in range(10000): print('hello world')",
"print('hello world')",
"for i in range(100000): print('hello world')",
"import sqlite3; print(sqlite3.connect(':memory:').execute('select sqlite_version()').fetchone()[0])"
):
try:
print(code)
print("====")
print(run_python_code(code))
except Exception as e:
print(e)
Running this produces the following output (truncated), which illustrates what happens to things that run out of "fuel" (which I set to default to 400,000,000 units):
wasmtime % pipenv run python demo.py
print('hello world')
====
result:
hello world
mem size pages of 64kb: 160
data length: 10485760
fuel consumed: 230790953
for i in range(10000): print('hello world')
====
error while executing at wasm backtrace:
0: 0xb02e6 - <unknown>!<wasm function 1505>
1: 0xb2967 - <unknown>!<wasm function 1536>
2: 0x1b9221 - <unknown>!<wasm function 3563>
3: 0x1ae61a - <unknown>!<wasm function 3558>
4: 0x49be2a - <unknown>!<wasm function 10123>
note: using the `WASMTIME_BACKTRACE_DETAILS=1` environment variable may show more debugging information
Caused by:
wasm trap: all fuel consumed by WebAssembly
error while executing at wasm backtrace:
0: 0xb02e6 - <unknown>!<wasm function 1505>
1: 0xb2967 - <unknown>!<wasm function 1536>
2: 0x1b9221 - <unknown>!<wasm function 3563>
3: 0x1ae61a - <unknown>!<wasm function 3558>
4: 0x49be2a - <unknown>!<wasm function 10123>
note: using the `WASMTIME_BACKTRACE_DETAILS=1` environment variable may show more debugging information
Caused by:
wasm trap: all fuel consumed by WebAssembly
print('hello world')
====
result:
hello world
mem size pages of 64kb: 160
data length: 10485760
fuel consumed: 230794521
for i in range(100000): print('hello world')
====
error while executing at wasm backtrace:
0: 0x7d7f - <unknown>!<wasm function 72>
...
17: 0x1c52e9 - <unknown>!<wasm function 3618>
18: 0x49b7c9 - <unknown>!<wasm function 10123>
note: using the `WASMTIME_BACKTRACE_DETAILS=1` environment variable may show more debugging information
Caused by:
wasm trap: all fuel consumed by WebAssembly
error while executing at wasm backtrace:
0: 0x7d7f - <unknown>!<wasm function 72>
...
16: 0x1c53c2 - <unknown>!<wasm function 3619>
17: 0x1c52e9 - <unknown>!<wasm function 3618>
18: 0x49b7c9 - <unknown>!<wasm function 10123>
note: using the `WASMTIME_BACKTRACE_DETAILS=1` environment variable may show more debugging information
Caused by:
wasm trap: all fuel consumed by WebAssembly
import sqlite3; print(sqlite3.connect(':memory:').execute('select sqlite_version()').fetchone()[0])
====
result:
3.39.2
mem size pages of 64kb: 160
data length: 10485760
fuel consumed: 381954995
That last code line - import sqlite3; print(sqlite3.connect(':memory:').execute('select sqlite_version()').fetchone()[0])
- produces the following output:
3.39.2
This confirms that there's a WebAssembly build of SQLite bundled in with that Python WASM package, and it's a relatively recent version.
Created 2023-02-02T13:37:03-08:00, updated 2023-02-02T13:45:34-08:00 · History · Edit