Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Graceful error handling for host function calls #73

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ pub fn host_fn(

#vis unsafe fn #name #generics (#original_inputs) -> Result<#output, extism_pdk::Error> {
let res = extism_pdk::Memory::from(#impl_name(#(#into_inputs),*));
extism_pdk::get_host_func_error()?;
<#output as extism_pdk::FromBytes>::from_bytes(&res.to_vec())
}
};
Expand All @@ -341,6 +342,7 @@ pub fn host_fn(

#vis unsafe fn #name #generics (#original_inputs) -> Result<#output, extism_pdk::Error> {
let res = #impl_name(#(#into_inputs),*);
extism_pdk::get_host_func_error()?;
Ok(res)
}
};
Expand Down
62 changes: 62 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use crate::*;
use base64::prelude::*;
use std::sync::LazyLock;


/// If the host is appending a sentinel prefix to the host function errors
/// it will also pass that prefix to the guest, so the guest knows what to
/// trim off the front of the raw bytes. This is stored in a lazy lock because
/// it is sent once and never changed. Subsequent calls to get host function
/// errors will not need to recompute this value.
static HOST_FUNC_ERROR_PREFIX: LazyLock<Option<Vec<u8>>> = LazyLock::new(|| {
std::env::var("EXTISM_HOST_FUNC_ERROR_PREFIX")
.ok()
.and_then(|p| BASE64_STANDARD.decode(p.as_bytes()).ok())
});

/// Checks to see if the most recent host function call returned an error.
/// Retrieves any error set by the host function and stripes any sentinel bytes from it.
///
/// This function interacts with the Extism kernel's `error_get` mechanism to fetch any
/// error currently stored in WASM memory. If an error exists, it determines whether the
/// error was created by a host function (using sentinel bytes in the prefix).
///
/// ### Behavior:
/// - Calls `extism::error_get` to retrieve the current error's memory offset.
/// - If no error is present (offset is `0`), returns `Ok(())`.
/// - Attempts to locate the memory at the given offset and process the error bytes:
/// - If a known sentinel prefix is found, the function strips the prefix and treats
/// the remaining bytes as the error message from the host function.
/// - If no prefix is present, the error is returned as-is.
/// - Converts the error bytes into a UTF-8 string and wraps it in an `Error::msg`.
///
/// ### Notes:
/// - Errors originating from host functions are marked with a unique sentinel prefix
/// (defined by `HOST_FUNC_ERROR_PREFIX`). This prefix is appended during serialization
/// by the host and removed during deserialization here, enabling reliable error
/// identification and processing.
pub fn get_host_func_error() -> Result<(), Error> {
let offset = unsafe { extism::error_get() };
if offset == 0 {
return Ok(());
}

// We need to reset the error now that we've read it
unsafe { extism::error_set(0) };

if let Some(mem) = Memory::find(offset).map(|mem| mem.to_vec()) {
let message = if let Some(prefix) = HOST_FUNC_ERROR_PREFIX.as_ref() {
// Attempt to strip the sentinel prefix for host function errors
mem.strip_prefix(prefix.as_slice())
.map(|stripped| String::from_utf8_lossy(stripped))
.unwrap_or_else(|| String::from_utf8_lossy(&mem))
} else {
// Handle the memory as a UTF-8 string if no prefix is available
String::from_utf8_lossy(&mem)
};

return Err(Error::msg(message.to_string()));
}

Ok(())
}
1 change: 1 addition & 0 deletions src/extism.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ extern "C" {
pub fn free(offs: u64);
pub fn output_set(offs: u64, length: u64);
pub fn error_set(offs: u64);
pub fn error_get() -> u64;
pub fn store_u8(offs: u64, data: u8);
pub fn load_u8(offs: u64) -> u8;
pub fn store_u64(offs: u64, data: u64);
Expand Down
4 changes: 4 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ pub mod var;
/// Types and functions for making HTTP requests
pub mod http;

/// Functions and utilities for working with host function errors
mod error;
pub use error::get_host_func_error;

pub use anyhow::Error;
pub use extism_convert::*;
pub use extism_convert::{FromBytes, FromBytesOwned, ToBytes};
Expand Down