use std::path::{Path, PathBuf};
use futures::StreamExt;
use tokio::runtime::Handle;
use wasmtime::{Caller, Engine, Linker, Module, Store};
use wasmtime_wasi::p1::WasiP1Ctx;
use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
use wasmtime_wasi::{I32Exit, WasiCtxBuilder};
use crate::context::Context;
use crate::error::Error;
struct PyHostState {
wasi: WasiP1Ctx,
ctx: Context,
handle: Handle,
result: Vec<u8>,
}
const ERR_BIT: u32 = 1 << 31;
const MAX_OUTPUT_BYTES: usize = 256 * 1024 * 1024;
#[derive(serde::Deserialize)]
struct HarnessOutput {
eval: serde_json::Value,
stdout: String,
}
pub struct Python {
engine: Engine,
module: Module,
}
impl Python {
pub async fn initialize(bin_dir: PathBuf) -> Result<Self, Error> {
let engine = Engine::new(&wasmtime::Config::new()).map_err(wasm_err)?;
let cache_dir = bin_dir.join("cache");
let artifact = cache_dir.join(format!(
"rustpython-{}.cwasm",
crate::python_wasm::RUSTPYTHON_WASM_HASH
));
if let Some(module) = try_load(&engine, &artifact).await {
return Ok(Self { engine, module });
}
let claim = objectiveai_sdk::lockfile::wait_acquire(
&bin_dir.join("locks"),
crate::python_wasm::RUSTPYTHON_WASM_HASH,
&format!("pid {}", std::process::id()),
)
.await
.map_err(|e| Error::PythonWasm(format!("bin lock: {e}")))?;
let result = create_under_lock(&engine, &cache_dir, &artifact).await;
claim
.release()
.map_err(|e| Error::PythonWasm(format!("bin lock release: {e}")))?;
Ok(Self {
engine,
module: result?,
})
}
pub async fn exec_code<I, T>(
&self,
ctx: &Context,
code: &str,
input: Option<I>,
) -> Result<Option<T>, Error>
where
I: serde::Serialize,
T: serde::de::DeserializeOwned,
{
let input = match input {
Some(i) => serde_json::to_value(i).map_err(Error::InlineJson)?,
None => serde_json::Value::Null,
};
let wrapped = wrap_code(code, &input);
let engine = self.engine.clone();
let module = self.module.clone();
let ctx = ctx.clone();
let handle = Handle::current();
let raw = tokio::task::spawn_blocking(move || {
run_blocking(&engine, &module, &wrapped, ctx, handle)
})
.await
.map_err(|e| Error::PythonWasm(format!("python task join: {e}")))??;
parse_envelope(&raw)
}
pub async fn exec_file<I, T>(
&self,
ctx: &Context,
path: &Path,
input: Option<I>,
) -> Result<Option<T>, Error>
where
I: serde::Serialize,
T: serde::de::DeserializeOwned,
{
let code = tokio::fs::read_to_string(path)
.await
.map_err(|e| Error::PythonFileRead(path.to_path_buf(), e))?;
self.exec_code(ctx, &code, input).await
}
}
async fn try_load(engine: &Engine, artifact: &Path) -> Option<Module> {
if !tokio::fs::try_exists(artifact).await.unwrap_or(false) {
return None;
}
let engine = engine.clone();
let path = artifact.to_path_buf();
tokio::task::spawn_blocking(move || {
unsafe { Module::deserialize_file(&engine, &path) }.ok()
})
.await
.ok()
.flatten()
}
async fn create_under_lock(
engine: &Engine,
cache_dir: &Path,
artifact: &Path,
) -> Result<Module, Error> {
if let Some(module) = try_load(engine, artifact).await {
return Ok(module);
}
let (module, serialized) = {
let engine = engine.clone();
tokio::task::spawn_blocking(move || -> Result<(Module, Vec<u8>), Error> {
let wasm = zstd::decode_all(crate::python_wasm::RUSTPYTHON_WASM_ZSTD)
.map_err(|e| Error::PythonWasm(format!("decompress embedded blob: {e}")))?;
let module = Module::new(&engine, &wasm).map_err(wasm_err)?;
let serialized = module.serialize().map_err(wasm_err)?;
Ok((module, serialized))
})
.await
.map_err(|e| Error::PythonWasm(format!("python jit task join: {e}")))??
};
let publish = async {
tokio::fs::create_dir_all(cache_dir).await?;
let tmp = artifact.with_extension(format!("cwasm.tmp-{}", std::process::id()));
tokio::fs::write(&tmp, &serialized).await?;
tokio::fs::rename(&tmp, artifact).await
};
if publish.await.is_ok() {
prune_stale(cache_dir, artifact).await;
}
Ok(module)
}
async fn prune_stale(cache_dir: &Path, current: &Path) {
let Ok(mut entries) = tokio::fs::read_dir(cache_dir).await else {
return;
};
while let Ok(Some(entry)) = entries.next_entry().await {
let path = entry.path();
if path == current {
continue;
}
let name = entry.file_name();
let name = name.to_string_lossy();
if name.starts_with("rustpython-") && (name.ends_with(".cwasm") || name.contains(".cwasm.tmp-")) {
let _ = tokio::fs::remove_file(&path).await;
}
}
}
fn run_blocking(
engine: &Engine,
module: &Module,
code: &str,
ctx: Context,
handle: Handle,
) -> Result<String, Error> {
let mut linker: Linker<PyHostState> = Linker::new(engine);
wasmtime_wasi::p1::add_to_linker_sync(&mut linker, |s: &mut PyHostState| &mut s.wasi)
.map_err(wasm_err)?;
add_objectiveai_host(&mut linker)?;
let stdout = MemoryOutputPipe::new(MAX_OUTPUT_BYTES);
let stderr = MemoryOutputPipe::new(MAX_OUTPUT_BYTES);
let wasi = WasiCtxBuilder::new()
.args(&["rustpython", "-c", code])
.stdout(stdout.clone())
.stderr(stderr.clone())
.build_p1();
let mut store = Store::new(
engine,
PyHostState {
wasi,
ctx,
handle,
result: Vec::new(),
},
);
let outcome = linker
.instantiate(&mut store, module)
.and_then(|instance| instance.get_typed_func::<(), ()>(&mut store, "_start"))
.and_then(|start| start.call(&mut store, ()));
drop(store);
let exited_clean = match outcome {
Ok(()) => true,
Err(e) => match e.downcast_ref::<I32Exit>() {
Some(exit) if exit.0 == 0 => true,
Some(_) => {
let stderr = String::from_utf8_lossy(&stderr.contents()).into_owned();
return Err(Error::PythonException(stderr));
}
None => {
let stderr = String::from_utf8_lossy(&stderr.contents()).into_owned();
return Err(Error::PythonWasm(format!("{e:#}; stderr:\n{stderr}")));
}
},
};
debug_assert!(exited_clean);
Ok(String::from_utf8_lossy(&stdout.contents()).into_owned())
}
fn add_objectiveai_host(linker: &mut Linker<PyHostState>) -> Result<(), Error> {
linker
.func_wrap(
"objectiveai",
"host_execute",
|mut caller: Caller<'_, PyHostState>, argv_ptr: u32, argv_len: u32| -> u32 {
if caller.data().ctx.no_objectiveai {
return stash_error(
&mut caller,
"objectiveai.execute is disabled (--no-objectiveai)",
);
}
let Some(memory) = caller.get_export("memory").and_then(|e| e.into_memory()) else {
return stash_error(&mut caller, "objectiveai.execute: guest exports no memory");
};
let req = {
let data = memory.data(&caller);
let start = argv_ptr as usize;
let end = start.saturating_add(argv_len as usize);
match data.get(start..end) {
Some(bytes) => bytes.to_vec(),
None => {
return stash_error(
&mut caller,
"objectiveai.execute: argv pointer/length out of bounds",
);
}
}
};
let ctx = caller.data().ctx.clone();
let handle = caller.data().handle.clone();
match run_request(ctx, handle, &req) {
Ok(bytes) => {
let len = bytes.len() as u32;
caller.data_mut().result = bytes;
len
}
Err(msg) => stash_error(&mut caller, &msg),
}
},
)
.map_err(wasm_err)?;
linker
.func_wrap(
"objectiveai",
"host_result",
|mut caller: Caller<'_, PyHostState>, out_ptr: u32| {
let result = std::mem::take(&mut caller.data_mut().result);
if let Some(memory) = caller.get_export("memory").and_then(|e| e.into_memory()) {
let _ = memory.write(&mut caller, out_ptr as usize, &result);
}
},
)
.map_err(wasm_err)?;
Ok(())
}
fn stash_error(caller: &mut Caller<'_, PyHostState>, msg: &str) -> u32 {
let bytes = msg.as_bytes().to_vec();
let len = bytes.len() as u32;
caller.data_mut().result = bytes;
len | ERR_BIT
}
fn run_request(ctx: Context, handle: Handle, req: &[u8]) -> Result<Vec<u8>, String> {
let req = req.to_vec();
let (tx, rx) = std::sync::mpsc::sync_channel(1);
handle.spawn(async move {
let _ = tx.send(run_request_async(ctx, req).await);
});
rx.recv()
.map_err(|e| format!("objectiveai.execute: dispatch worker dropped: {e}"))?
}
async fn run_request_async(ctx: Context, req: Vec<u8>) -> Result<Vec<u8>, String> {
let arr = match serde_json::from_slice::<serde_json::Value>(&req) {
Ok(serde_json::Value::Array(arr)) => arr,
Ok(_) => {
return Err(
"objectiveai.execute: expected a list[str] (argv) or list[list[str]] (batch)"
.to_string(),
);
}
Err(e) => return Err(format!("objectiveai.execute: invalid request: {e}")),
};
let is_batch = arr.first().map_or(true, serde_json::Value::is_array);
if is_batch {
let argvs: Vec<Vec<String>> = serde_json::from_value(serde_json::Value::Array(arr))
.map_err(|e| {
format!("objectiveai.execute: invalid batch (expected list[list[str]]): {e}")
})?;
let tasks: Vec<_> = argvs
.into_iter()
.map(|argv| {
let ctx = ctx.clone();
tokio::spawn(async move { run_one_argv(&ctx, argv).await })
})
.collect();
let results = futures::future::join_all(tasks).await;
let mut out: Vec<Vec<objectiveai_sdk::cli::command::ResponseItem>> =
Vec::with_capacity(results.len());
for result in results {
out.push(result.map_err(|e| format!("objectiveai.execute: task panicked: {e}"))??);
}
serde_json::to_vec(&out).map_err(|e| format!("objectiveai.execute: serialize: {e}"))
} else {
let argv: Vec<String> = serde_json::from_value(serde_json::Value::Array(arr))
.map_err(|e| format!("objectiveai.execute: invalid argv (expected list[str]): {e}"))?;
let items = run_one_argv(&ctx, argv).await?;
serde_json::to_vec(&items).map_err(|e| format!("objectiveai.execute: serialize: {e}"))
}
}
async fn run_one_argv(
ctx: &Context,
argv: Vec<String>,
) -> Result<Vec<objectiveai_sdk::cli::command::ResponseItem>, String> {
let request = objectiveai_sdk::cli::command::parse_request(&argv).map_err(|e| match e {
objectiveai_sdk::cli::command::ParseError::Clap(e) => format!("objectiveai.execute: {e}"),
objectiveai_sdk::cli::command::ParseError::FromArgs(e) => {
format!("objectiveai.execute: {e:?}")
}
})?;
let mut stream = crate::command::command::execute(ctx, request)
.await
.map_err(|e| format!("objectiveai.execute: {e}"))?;
let mut items = Vec::new();
while let Some(item) = stream.next().await {
items.push(item.map_err(|e| format!("objectiveai.execute: {e}"))?);
}
Ok(items)
}
fn wasm_err(e: wasmtime::Error) -> Error {
Error::PythonWasm(format!("{e:#}"))
}
fn parse_envelope<T: serde::de::DeserializeOwned>(raw: &str) -> Result<Option<T>, Error> {
let envelope: HarnessOutput = serde_json::from_str(raw)
.map_err(|e| Error::PythonHarnessBroken(e.to_string()))?;
let eval_err = if !envelope.eval.is_null() {
let eval_str = envelope.eval.to_string();
let mut de = serde_json::Deserializer::from_str(&eval_str);
match serde_path_to_error::deserialize(&mut de) {
Ok(result) => return Ok(Some(result)),
Err(e) => Some(e),
}
} else {
None
};
if envelope.stdout.trim().is_empty() {
return match eval_err {
Some(e) => Err(Error::PythonDeserialize(e)),
None => Ok(None),
};
}
let mut de = serde_json::Deserializer::from_str(&envelope.stdout);
match serde_path_to_error::deserialize(&mut de) {
Ok(result) => Ok(Some(result)),
Err(stdout_err) => Err(Error::PythonDeserialize(eval_err.unwrap_or(stdout_err))),
}
}
fn wrap_code(code: &str, input: &serde_json::Value) -> String {
use base64::Engine as _;
let encoded = base64::engine::general_purpose::STANDARD.encode(code);
let input_json = serde_json::to_string(input).unwrap_or_else(|_| "null".to_string());
let encoded_input = base64::engine::general_purpose::STANDARD.encode(input_json);
format!(
r#"
import ast as __oai_ast, json as __oai_json, sys as __oai_sys, io as __oai_io, base64 as __oai_b64, os as __oai_os
import objectiveai as __oai_objectiveai
__oai_real_stdout = __oai_sys.stdout
__oai_tree = __oai_ast.parse(__oai_b64.b64decode("{encoded}").decode())
__oai_input = __oai_json.loads(__oai_b64.b64decode("{encoded_input}").decode())
__oai_capture = __oai_io.StringIO()
__oai_sys.stdout = __oai_capture
__oai_eval = None
__oai_globals = {{"__name__": "__main__", "__builtins__": __builtins__, "input": __oai_input, "objectiveai": __oai_objectiveai}}
if __oai_tree.body and isinstance(__oai_tree.body[-1], __oai_ast.Expr):
__oai_last = __oai_tree.body.pop()
exec(compile(__oai_tree, "<inline>", "exec"), __oai_globals)
__oai_eval = eval(compile(__oai_ast.Expression(__oai_last.value), "<inline>", "eval"), __oai_globals)
else:
exec(compile(__oai_tree, "<inline>", "exec"), __oai_globals)
# Restore the REAL captured stdout (not sys.__stdout__, which is None in
# this WASI build — restoring it would leave a None stream that the
# interpreter's shutdown flush trips over: "Exception ignored in: None:
# 'NoneType' object has no attribute 'flush'", a non-zero exit).
__oai_sys.stdout = __oai_real_stdout
print(__oai_json.dumps({{"eval": __oai_eval, "stdout": __oai_capture.getvalue()}}))
# Flush and hard-exit before finalization runs — skips the buggy
# shutdown flush entirely while preserving the emitted envelope.
__oai_sys.stdout.flush()
__oai_os._exit(0)
"#
)
}