feat: Implement parts of the extism runtime in WebAssembly (#384)

This PR adds the `kernel` directory which contains a port of the Extism
memory allocator compiled to WebAssembly and removes
`runtime/src/memory.rs` completely.

Being able to re-use memory functions as a WASM module allows us to
begin to experiment with porting Extism to new runtimes!

This is in a draft state while I'm verifying some of these changes.
This commit is contained in:
zach
2023-07-27 11:31:23 -07:00
committed by GitHub
parent 618c132194
commit 3da526286e
28 changed files with 1084 additions and 901 deletions

View File

@@ -5,3 +5,4 @@ members = [
"rust",
"libextism",
]
exclude = ["kernel"]

View File

@@ -21,6 +21,10 @@ endif
build:
cargo build --release $(FEATURE_FLAGS) --manifest-path libextism/Cargo.toml
.PHONY: kernel
kernel:
cd kernel && bash build.sh
lint:
cargo clippy --release --no-deps --manifest-path runtime/Cargo.toml

2
kernel/.cargo/config Normal file
View File

@@ -0,0 +1,2 @@
[build]
target = "wasm32-unknown-unknown"

11
kernel/Cargo.toml Normal file
View File

@@ -0,0 +1,11 @@
[package]
name = "extism-runtime-kernel"
version = "0.1.0"
edition = "2021"
[dependencies]
[workspace]
members = [
"."
]

20
kernel/README.md Normal file
View File

@@ -0,0 +1,20 @@
# Extism kernel
The Extism kernel implements core parts of the Extism runtime in Rust compiled to WebAssembly. This code is a conceptual
re-write of [memory.rs][] with the goal of making core parts of the Extism implementation more portable across WebAssembly
runtimes.
See [lib.rs][] for more details about the implementation itself.
## Building
Because this crate is built using the `wasm32-unknown-unknown` target, it is a separate build process from the `extism-runtime` crate.
To build `extism-runtime.wasm`, strip it and copy it to the proper location in the `extism-runtime` tree you can run:
```shell
$ sh build.sh
```
[memory.rs]: https://github.com/extism/extism/blob/f4aa139eced4a74eb4a103f78222ba503e146109/runtime/src/memory.rs
[lib.rs]: ./src/lib.rs

7
kernel/build.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
cargo build --release --target wasm32-unknown-unknown --package extism-runtime-kernel --bin extism-runtime
cp target/wasm32-unknown-unknown/release/extism-runtime.wasm .
wasm-strip extism-runtime.wasm || :
mv extism-runtime.wasm ../runtime/src/extism-runtime.wasm

View File

@@ -0,0 +1,10 @@
#![no_main]
#![no_std]
pub use extism_runtime_kernel::*;
#[cfg(target_arch = "wasm32")]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
core::arch::wasm32::unreachable()
}

446
kernel/src/lib.rs Normal file
View File

@@ -0,0 +1,446 @@
//! # Extism kernel
//!
//! - Isolated memory from both host and plugin
//! - An allocator for managing that memory
//! - Input/output handling
//! - Error message handling
//! - Backward compatible `extism_*` functions
//!
//! ## Allocator
//!
//! The Extism allocator is a bump allocator that tracks the `length` of the total number of bytes
//! available to the allocator and `position` to track how much of the data has been used. Things like memory
//! have not really been optimized at all. When a new allocation that is larger than the remaning size is made,
//! the allocator attempts to call `memory.grow` if that fails a `0` offset is returned, which should be interpreted
//! as a failed allocation.
//!
//! ## Input/Output
//!
//! Input and output are just allocated blocks of memory that are marked as either input or output using
//! the `extism_input_set` or `extism_output_set` functions. The global variables `INPUT_OFFSET` contains
//! the offset in memory to the input data and `INPUT_LENGTH` contains the size of the input data. `OUTPUT_OFFSET`
//! and `OUTPUT_LENGTH` are used for the output data.
//!
//! ## Error handling
//!
//! The `ERROR` global is used to track the current error message. If it is set to `0` then there is no error.
//! The length of the error message can be retreived using `extism_length`.
//!
//! ## Memory offsets
//! An offset of `0` is similar to a `NULL` pointer in C - it implies an allocation failure or memory error
//! of some kind
//!
//! ## Extism functions
//!
//! These functions are backward compatible with the pre-kernel runtime, but a few new functions are added to
//! give runtimes more access to the internals necesarry to load data in and out of a plugin.
#![no_std]
#![allow(clippy::missing_safety_doc)]
use core::sync::atomic::*;
pub type Pointer = u64;
pub type Length = u64;
/// WebAssembly page size
const PAGE_SIZE: usize = 65536;
/// Determines the amount of bytes that can be wasted by re-using a block. If more than this number is wasted by re-using
/// a block then it will be split into two smaller blocks.
const BLOCK_SPLIT_SIZE: usize = 128;
/// Offset to the input data
static mut INPUT_OFFSET: Pointer = 0;
/// Length of the input data
static mut INPUT_LENGTH: Length = 0;
/// Offset to the output data
static mut OUTPUT_OFFSET: Pointer = 0;
/// Offset to the input data
static mut OUTPUT_LENGTH: Length = 0;
/// Current error message
static mut ERROR: AtomicU64 = AtomicU64::new(0);
/// Determines if the kernel has been initialized already
static mut INITIALIZED: AtomicBool = AtomicBool::new(false);
/// A pointer to the first page that will be managed by Extism, this is set during initialization
static mut START_PAGE: usize = 0;
/// Provides information about the usage status of a `MemoryBlock`
#[repr(u8)]
#[derive(PartialEq)]
pub enum MemoryStatus {
/// Unused memory that is available b
Unused = 0,
/// In-use memory
Active = 1,
/// Free memory that is available for re-use
Free = 2,
}
/// A single `MemoryRoot` exists at the start of the memory to track information about the total
/// size of the allocated memory and the position of the bump allocator.
///
/// The overall layout of the Extism-manged memory is organized like this:
/// |------|-------|---------|-------|--------------|
/// | Root | Block | Data | Block | Data | ...
/// |------|-------|---------|-------|--------------|
///
/// Where `Root` and `Block` are fixed to the size of the `MemoryRoot` and `MemoryBlock` structs. But
/// the size of `Data` is dependent on the allocation size.
#[repr(C)]
pub struct MemoryRoot {
/// Position of the bump allocator, relative to `START_PAGE`
pub position: AtomicU64,
/// The total size of all data allocated using this allocator
pub length: AtomicU64,
/// A pointer to where the blocks begin
pub blocks: [MemoryBlock; 0],
}
/// A `MemoryBlock` contains some metadata about a single allocation
#[repr(C)]
pub struct MemoryBlock {
/// The usage status of the block, `Unused` or `Free` blocks can be re-used.
pub status: AtomicU8,
/// The total size of the allocation
pub size: usize,
/// The number of bytes currently being used. If this block is a fresh allocation then `size` and `used` will
/// always be the same. If a block is re-used then these numbers may differ.
pub used: usize,
/// A pointer to the block data
pub data: [u8; 0],
}
/// Returns the number of pages needed for the given number of bytes
pub fn num_pages(nbytes: u64) -> usize {
let nbytes = nbytes as f64;
let page = PAGE_SIZE as f64;
((nbytes / page) + 0.5) as usize
}
// Get the `MemoryRoot` at the correct offset in memory
#[inline]
unsafe fn memory_root() -> &'static mut MemoryRoot {
&mut *((START_PAGE * PAGE_SIZE) as *mut MemoryRoot)
}
impl MemoryRoot {
/// Initialize or load the `MemoryRoot` from the correct position in memory
pub unsafe fn new() -> &'static mut MemoryRoot {
// If this fails then `INITIALIZED` is already `true` and we can just return the
// already initialized `MemoryRoot`
if INITIALIZED
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_err()
{
return memory_root();
}
// Ensure that at least one page is allocated to store the `MemoryRoot` data
START_PAGE = core::arch::wasm32::memory_grow(0, 1);
if START_PAGE == usize::MAX {
panic!("Out of memory");
}
// Initialize the `MemoryRoot` length, position and data
let root = memory_root();
root.length.store(
PAGE_SIZE as u64 - core::mem::size_of::<MemoryRoot>() as u64,
Ordering::Release,
);
root.position.store(0, Ordering::Release);
// Ensure the first block is marked as `Unused`
#[allow(clippy::size_of_in_element_count)]
core::ptr::write_bytes(
root.blocks.as_mut_ptr() as *mut _,
MemoryStatus::Unused as u8,
core::mem::size_of::<MemoryBlock>(),
);
root
}
/// Resets the position of the allocator and zeroes out all allocations
pub unsafe fn reset(&mut self) {
core::ptr::write_bytes(
self.blocks.as_mut_ptr() as *mut u8,
0,
self.length.load(Ordering::Acquire) as usize,
);
self.position.store(0, Ordering::Release);
}
// Find a block that is free to use, this can be a new block or an existing freed block. The `self_position` argument
// is used to avoid loading the allocators position more than once when performing an allocation.
unsafe fn find_free_block(
&mut self,
length: Length,
self_position: u64,
) -> Option<&'static mut MemoryBlock> {
// Get the first block
let mut block = self.blocks.as_mut_ptr();
// Only loop while the block pointer is less then the current position
while (block as u64) < self.blocks.as_ptr() as u64 + self_position {
let b = &mut *block;
let status = b.status.load(Ordering::Acquire);
// An unused block is safe to use
if status == MemoryStatus::Unused as u8 {
return Some(b);
}
// Re-use freed blocks when they're large enough
if status == MemoryStatus::Free as u8 && b.size >= length as usize {
// Split block if there is too much excess
if b.size - length as usize >= BLOCK_SPLIT_SIZE {
b.size -= length as usize;
b.used = 0;
let block1 = b.data.as_mut_ptr().add(b.size) as *mut MemoryBlock;
let b1 = &mut *block1;
b1.size = length as usize;
b1.used = 0;
b1.status.store(MemoryStatus::Free as u8, Ordering::Release);
return Some(b1);
}
// Otherwise return the whole block
return Some(b);
}
// Get the next block
block = b.next_ptr();
}
None
}
/// Create a new `MemoryBlock`, when `Some(block)` is returned, `block` will contain at least enough room for `length` bytes
/// but may be as large as `length` + `BLOCK_SPLIT_SIZE` bytes. When `None` is returned the allocation has failed.
pub unsafe fn alloc(&mut self, length: Length) -> Option<&'static mut MemoryBlock> {
let self_position = self.position.load(Ordering::Acquire);
let self_length = self.length.load(Ordering::Acquire);
let b = self.find_free_block(length, self_position);
// If there's a free block then re-use it
if let Some(b) = b {
b.used = length as usize;
b.status
.store(MemoryStatus::Active as u8, Ordering::Release);
return Some(b);
}
// Get the current index for a new block
let curr = self.blocks.as_ptr() as u64 + self_position;
// Get the number of bytes available
let mem_left = self_length - self_position;
// When the allocation is larger than the number of bytes available
// we will need to try to grow the memory
if length >= mem_left {
// Calculate the number of pages needed to cover the remaining bytes
let npages = num_pages(length);
let x = core::arch::wasm32::memory_grow(0, npages);
if x == usize::MAX {
return None;
}
self.length
.fetch_add(npages as u64 * PAGE_SIZE as u64, Ordering::SeqCst);
}
// Bump the position by the size of the actual data + the size of the MemoryBlock structure
self.position.fetch_add(
length + core::mem::size_of::<MemoryBlock>() as u64,
Ordering::SeqCst,
);
// Initialize a new block at the current position
let ptr = curr as *mut MemoryBlock;
let block = &mut *ptr;
block
.status
.store(MemoryStatus::Active as u8, Ordering::Release);
block.size = length as usize;
block.used = length as usize;
Some(block)
}
/// Finds the block at an offset in memory
pub unsafe fn find_block(&mut self, offs: Pointer) -> Option<&mut MemoryBlock> {
if offs >= self.blocks.as_ptr() as Pointer + self.length.load(Ordering::Acquire) as Pointer
{
return None;
}
let ptr = offs - core::mem::size_of::<MemoryBlock>() as u64;
let ptr = ptr as *mut MemoryBlock;
Some(&mut *ptr)
}
}
impl MemoryBlock {
/// Get a pointer to the next block
///
/// NOTE: This does no checking to ensure the resulting pointer is valid, the offset
/// is calculated based on metadata provided by the current block
#[inline]
pub unsafe fn next_ptr(&mut self) -> *mut MemoryBlock {
self.data
.as_mut_ptr()
.add(self.size + core::mem::size_of::<MemoryBlock>()) as *mut MemoryBlock
}
/// Mark a block as free
pub fn free(&mut self) {
self.status
.store(MemoryStatus::Free as u8, Ordering::Release);
}
}
// Extism functions - these functions should be
/// Allocate a block of memory and return the offset
#[no_mangle]
pub unsafe fn extism_alloc(n: Length) -> Pointer {
let region = MemoryRoot::new();
let block = region.alloc(n);
match block {
Some(block) => block.data.as_mut_ptr() as Pointer,
None => 0,
}
}
/// Free allocated memory
#[no_mangle]
pub unsafe fn extism_free(p: Pointer) {
let block = MemoryRoot::new().find_block(p);
if let Some(block) = block {
block.free();
}
}
/// Get the length of an allocated memory block
#[no_mangle]
pub unsafe fn extism_length(p: Pointer) -> Length {
if p == 0 {
return 0;
}
if let Some(block) = MemoryRoot::new().find_block(p) {
block.used as Length
} else {
0
}
}
/// Load a byte from Extism-managed memory
#[no_mangle]
pub unsafe fn extism_load_u8(p: Pointer) -> u8 {
*(p as *mut u8)
}
/// Load a u64 from Extism-managed memory
#[no_mangle]
pub unsafe fn extism_load_u64(p: Pointer) -> u64 {
*(p as *mut u64)
}
/// Load a byte from the input data
#[no_mangle]
pub unsafe fn extism_input_load_u8(p: Pointer) -> u8 {
*((INPUT_OFFSET + p) as *mut u8)
}
/// Load a u64 from the input data
#[no_mangle]
pub unsafe fn extism_input_load_u64(p: Pointer) -> u64 {
*((INPUT_OFFSET + p) as *mut u64)
}
/// Write a byte in Extism-managed memory
#[no_mangle]
pub unsafe fn extism_store_u8(p: Pointer, x: u8) {
*(p as *mut u8) = x;
}
/// Write a u64 in Extism-managed memory
#[no_mangle]
pub unsafe fn extism_store_u64(p: Pointer, x: u64) {
unsafe {
*(p as *mut u64) = x;
}
}
/// Set the range of the input data in memory
#[no_mangle]
pub fn extism_input_set(p: Pointer, len: Length) {
unsafe {
INPUT_OFFSET = p;
INPUT_LENGTH = len;
}
}
/// Set the range of the output data in memory
#[no_mangle]
pub fn extism_output_set(p: Pointer, len: Length) {
unsafe {
OUTPUT_OFFSET = p;
OUTPUT_LENGTH = len;
}
}
/// Get the input length
#[no_mangle]
pub fn extism_input_length() -> Length {
unsafe { INPUT_LENGTH }
}
/// Get the input offset in Exitsm-managed memory
#[no_mangle]
pub fn extism_input_offset() -> Length {
unsafe { INPUT_OFFSET }
}
/// Get the output length
#[no_mangle]
pub fn extism_output_length() -> Length {
unsafe { OUTPUT_LENGTH }
}
/// Get the output offset in Extism-managed memory
#[no_mangle]
pub fn extism_output_offset() -> Length {
unsafe { OUTPUT_OFFSET }
}
/// Reset the allocator
#[no_mangle]
pub unsafe fn extism_reset() {
ERROR.store(0, Ordering::SeqCst);
MemoryRoot::new().reset()
}
/// Set the error message offset
#[no_mangle]
pub unsafe fn extism_error_set(ptr: Pointer) {
ERROR.store(ptr, Ordering::SeqCst);
}
/// Get the error message offset, if it's `0` then no error has been set
#[no_mangle]
pub unsafe fn extism_error_get() -> Pointer {
ERROR.load(Ordering::SeqCst)
}
/// Get the position of the allocator, this can be used as an indication of how many bytes are currently in-use
#[no_mangle]
pub unsafe fn extism_memory_bytes() -> Length {
MemoryRoot::new().position.load(Ordering::Acquire)
}

View File

@@ -1,6 +1,6 @@
[package]
name = "libextism"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
authors = ["The Extism Authors", "oss@extism.org"]
license = "BSD-3-Clause"

View File

@@ -1,6 +1,6 @@
[package]
name = "extism-manifest"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
authors = ["The Extism Authors", "oss@extism.org"]
license = "BSD-3-Clause"

View File

@@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
#[deprecated]
pub type ManifestMemory = MemoryOptions;
#[derive(Default, serde::Serialize, serde::Deserialize)]
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct MemoryOptions {
@@ -12,7 +12,7 @@ pub struct MemoryOptions {
pub max_pages: Option<u32>,
}
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct HttpRequest {
@@ -43,7 +43,7 @@ impl HttpRequest {
}
}
#[derive(Default, serde::Serialize, serde::Deserialize)]
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct WasmMetadata {
@@ -81,7 +81,7 @@ impl From<Vec<u8>> for Wasm {
#[deprecated]
pub type ManifestWasm = Wasm;
#[derive(serde::Serialize, serde::Deserialize)]
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
#[serde(untagged)]
#[serde(deny_unknown_fields)]
@@ -153,7 +153,7 @@ fn base64_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::
schema.into()
}
#[derive(Default, serde::Serialize, serde::Deserialize)]
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Manifest {

View File

@@ -5,7 +5,9 @@ import hashlib
import pathlib
sys.path.append(".")
from extism import Function, host_fn, ValType, Plugin
from extism import Function, host_fn, ValType, Plugin, set_log_file
set_log_file("stderr", "trace")
@host_fn
@@ -26,7 +28,7 @@ def main(args):
if len(args) > 1:
data = args[1].encode()
else:
data = b"some data from python!"
data = b"a" * 1024
wasm_file_path = (
pathlib.Path(__file__).parent.parent / "wasm" / "code-functions.wasm"
@@ -46,11 +48,13 @@ def main(args):
]
plugin = Plugin(manifest, wasi=True, functions=functions)
# Call `count_vowels`
wasm_vowel_count = json.loads(plugin.call("count_vowels", data))
wasm_vowel_count = plugin.call("count_vowels", data)
print(wasm_vowel_count)
j = json.loads(wasm_vowel_count)
print("Number of vowels:", wasm_vowel_count["count"])
print("Number of vowels:", j["count"])
assert wasm_vowel_count["count"] == count_vowels(data)
assert j["count"] == count_vowels(data)
if __name__ == "__main__":

View File

@@ -22,8 +22,7 @@ log4rs = "1.1"
url = "2"
glob = "0.3"
ureq = {version = "2.5", optional=true}
extism-manifest = { version = "0.3.0", path = "../manifest" }
pretty-hex = { version = "0.3" }
extism-manifest = { version = "0.4.0", path = "../manifest" }
uuid = { version = "1", features = ["v4"] }
libc = "0.2"

View File

@@ -1,4 +1,16 @@
fn main() {
println!("cargo:rerun-if-changed=src/extism-runtime.wasm");
let dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
// Attempt to build the kernel, this is only done as a convenience when developing the
// kernel an should not be relied on. When changes are made to the kernel run
// `sh build.sh` in the `kernel/` directory to ensure it run successfully.
let _ = std::process::Command::new("bash")
.args(&["build.sh"])
.current_dir(dir.join("../kernel"))
.status()
.unwrap();
let fn_macro = "
#define EXTISM_FUNCTION(N) extern void N(ExtismCurrentPlugin*, const ExtismVal*, ExtismSize, ExtismVal*, ExtismSize, void*)
#define EXTISM_GO_FUNCTION(N) extern void N(void*, ExtismVal*, ExtismSize, ExtismVal*, ExtismSize, uintptr_t)

BIN
runtime/src/extism-runtime.wasm Executable file

Binary file not shown.

View File

@@ -2,60 +2,6 @@ use std::collections::BTreeMap;
use crate::*;
/// Internal stores data that is available to the caller in PDK functions
pub struct Internal {
/// Call input length
pub input_length: usize,
/// Pointer to call input
pub input: *const u8,
/// Memory offset that points to the output
pub output_offset: usize,
/// Length of output in memory
pub output_length: usize,
/// WASI context
pub wasi: Option<Wasi>,
/// Keep track of the status from the last HTTP request
pub http_status: u16,
/// Store plugin-specific error messages
pub last_error: std::cell::RefCell<Option<std::ffi::CString>>,
/// Plugin variables
pub vars: BTreeMap<String, Vec<u8>>,
/// A pointer to the plugin memory, this should mostly be used from the PDK
pub memory: *mut PluginMemory,
pub(crate) available_pages: Option<u32>,
}
/// InternalExt provides a unified way of acessing `memory`, `store` and `internal` values
pub trait InternalExt {
fn memory(&self) -> &PluginMemory;
fn memory_mut(&mut self) -> &mut PluginMemory;
fn store(&self) -> &Store<Internal> {
self.memory().store()
}
fn store_mut(&mut self) -> &mut Store<Internal> {
self.memory_mut().store_mut()
}
fn internal(&self) -> &Internal {
self.store().data()
}
fn internal_mut(&mut self) -> &mut Internal {
self.store_mut().data_mut()
}
}
/// WASI context
pub struct Wasi {
/// wasi
@@ -64,13 +10,211 @@ pub struct Wasi {
/// wasi-nn
#[cfg(feature = "nn")]
pub nn: wasmtime_wasi_nn::WasiNnCtx,
#[cfg(not(feature = "nn"))]
pub nn: (),
}
/// Internal stores data that is available to the caller in PDK functions
pub struct Internal {
/// Store
pub store: *mut Store<Internal>,
/// Linker
pub linker: *mut wasmtime::Linker<Internal>,
/// WASI context
pub wasi: Option<Wasi>,
/// Keep track of the status from the last HTTP request
pub http_status: u16,
/// Plugin variables
pub vars: BTreeMap<String, Vec<u8>>,
pub manifest: Manifest,
pub available_pages: Option<u32>,
pub(crate) memory_limiter: Option<MemoryLimiter>,
}
/// InternalExt provides a unified way of acessing `memory`, `store` and `internal` values
pub trait InternalExt {
fn store(&self) -> &Store<Internal>;
fn store_mut(&mut self) -> &mut Store<Internal>;
fn linker(&self) -> &Linker<Internal>;
fn linker_mut(&mut self) -> &mut Linker<Internal>;
fn linker_and_store(&mut self) -> (&mut Linker<Internal>, &mut Store<Internal>);
fn internal(&self) -> &Internal {
self.store().data()
}
fn internal_mut(&mut self) -> &mut Internal {
self.store_mut().data_mut()
}
fn memory_ptr(&mut self) -> *mut u8 {
let (linker, mut store) = self.linker_and_store();
if let Some(mem) = linker.get(&mut store, "env", "memory") {
if let Some(mem) = mem.into_memory() {
return mem.data_ptr(&mut store);
}
}
std::ptr::null_mut()
}
fn memory(&mut self) -> &mut [u8] {
let (linker, mut store) = self.linker_and_store();
let mem = linker
.get(&mut store, "env", "memory")
.unwrap()
.into_memory()
.unwrap();
let ptr = mem.data_ptr(&store);
if ptr.is_null() {
return &mut [];
}
let size = mem.data_size(&store);
unsafe { std::slice::from_raw_parts_mut(ptr, size) }
}
fn memory_read(&mut self, offs: u64, len: Size) -> &[u8] {
let offs = offs as usize;
let len = len as usize;
let mem = self.memory();
&mem[offs..offs + len]
}
fn memory_read_str(&mut self, offs: u64) -> Result<&str, std::str::Utf8Error> {
let len = self.memory_length(offs);
std::str::from_utf8(self.memory_read(offs, len))
}
fn memory_write(&mut self, offs: u64, bytes: impl AsRef<[u8]>) {
let b = bytes.as_ref();
let offs = offs as usize;
let len = b.len();
self.memory()[offs..offs + len].copy_from_slice(bytes.as_ref());
}
fn memory_alloc(&mut self, n: Size) -> Result<u64, Error> {
let (linker, mut store) = self.linker_and_store();
let output = &mut [Val::I64(0)];
linker
.get(&mut store, "env", "extism_alloc")
.unwrap()
.into_func()
.unwrap()
.call(&mut store, &[Val::I64(n as i64)], output)?;
let offs = output[0].unwrap_i64() as u64;
if offs == 0 {
anyhow::bail!("out of memory")
}
Ok(offs)
}
fn memory_alloc_bytes(&mut self, bytes: impl AsRef<[u8]>) -> Result<u64, Error> {
let b = bytes.as_ref();
let offs = self.memory_alloc(b.len() as Size)?;
self.memory_write(offs, b);
Ok(offs)
}
fn memory_free(&mut self, offs: u64) {
let (linker, mut store) = self.linker_and_store();
linker
.get(&mut store, "env", "extism_free")
.unwrap()
.into_func()
.unwrap()
.call(&mut store, &[Val::I64(offs as i64)], &mut [])
.unwrap();
}
fn memory_length(&mut self, offs: u64) -> u64 {
let (linker, mut store) = self.linker_and_store();
let output = &mut [Val::I64(0)];
linker
.get(&mut store, "env", "extism_length")
.unwrap()
.into_func()
.unwrap()
.call(&mut store, &[Val::I64(offs as i64)], output)
.unwrap();
output[0].unwrap_i64() as u64
}
// A convenience method to set the plugin error and return a value
fn error<E>(&mut self, e: impl std::fmt::Debug, x: E) -> E {
let s = format!("{e:?}");
debug!("Set error: {:?}", s);
if let Ok(offs) = self.memory_alloc_bytes(&s) {
let (linker, mut store) = self.linker_and_store();
if let Some(f) = linker.get(&mut store, "env", "extism_error_set") {
f.into_func()
.unwrap()
.call(&mut store, &[Val::I64(offs as i64)], &mut [])
.unwrap();
}
}
x
}
fn clear_error(&mut self) {
let (linker, mut store) = self.linker_and_store();
if let Some(f) = linker.get(&mut store, "env", "extism_error_set") {
f.into_func()
.unwrap()
.call(&mut store, &[Val::I64(0)], &mut [])
.unwrap();
}
}
fn has_error(&mut self) -> bool {
let (linker, mut store) = self.linker_and_store();
let output = &mut [Val::I64(0)];
linker
.get(&mut store, "env", "extism_error_get")
.unwrap()
.into_func()
.unwrap()
.call(&mut store, &[], output)
.unwrap();
output[0].unwrap_i64() != 0
}
fn get_error(&mut self) -> Option<&str> {
let (linker, mut store) = self.linker_and_store();
let output = &mut [Val::I64(0)];
linker
.get(&mut store, "env", "extism_error_get")
.unwrap()
.into_func()
.unwrap()
.call(&mut store, &[], output)
.unwrap();
let offs = output[0].unwrap_i64() as u64;
if offs == 0 {
return None;
}
let length = self.memory_length(offs);
let data = self.memory_read(offs, length);
let s = std::str::from_utf8(data);
match s {
Ok(s) => Some(s),
Err(_) => None,
}
}
}
impl Internal {
pub(crate) fn new(
manifest: &Manifest,
manifest: Manifest,
wasi: bool,
available_pages: Option<u32>,
) -> Result<Self, Error> {
@@ -91,49 +235,106 @@ impl Internal {
#[cfg(feature = "nn")]
let nn = wasmtime_wasi_nn::WasiNnCtx::new()?;
#[cfg(not(feature = "nn"))]
#[allow(clippy::let_unit_value)]
let nn = ();
Some(Wasi {
ctx: ctx.build(),
#[cfg(feature = "nn")]
nn,
})
} else {
None
};
let memory_limiter = if let Some(pgs) = available_pages {
let n = pgs as usize * 65536;
Some(MemoryLimiter {
max_bytes: n,
bytes_left: n,
})
} else {
None
};
Ok(Internal {
input_length: 0,
output_offset: 0,
output_length: 0,
input: std::ptr::null(),
wasi,
memory: std::ptr::null_mut(),
manifest,
http_status: 0,
last_error: std::cell::RefCell::new(None),
vars: BTreeMap::new(),
linker: std::ptr::null_mut(),
store: std::ptr::null_mut(),
available_pages,
memory_limiter,
})
}
pub fn set_error(&self, e: impl std::fmt::Debug) {
debug!("Set error: {:?}", e);
*self.last_error.borrow_mut() = Some(error_string(e));
pub fn linker(&self) -> &wasmtime::Linker<Internal> {
unsafe { &*self.linker }
}
/// Unset `last_error` field
pub fn clear_error(&self) {
*self.last_error.borrow_mut() = None;
pub fn linker_mut(&mut self) -> &mut wasmtime::Linker<Internal> {
unsafe { &mut *self.linker }
}
}
impl InternalExt for Internal {
fn memory(&self) -> &PluginMemory {
unsafe { &*self.memory }
fn store(&self) -> &Store<Internal> {
unsafe { &*self.store }
}
fn memory_mut(&mut self) -> &mut PluginMemory {
unsafe { &mut *self.memory }
fn store_mut(&mut self) -> &mut Store<Internal> {
unsafe { &mut *self.store }
}
fn linker(&self) -> &Linker<Internal> {
unsafe { &*self.linker }
}
fn linker_mut(&mut self) -> &mut Linker<Internal> {
unsafe { &mut *self.linker }
}
fn linker_and_store(&mut self) -> (&mut Linker<Internal>, &mut Store<Internal>) {
unsafe { (&mut *self.linker, &mut *self.store) }
}
}
pub(crate) struct MemoryLimiter {
bytes_left: usize,
max_bytes: usize,
}
impl MemoryLimiter {
pub(crate) fn reset(&mut self) {
self.bytes_left = self.max_bytes;
}
}
impl wasmtime::ResourceLimiter for MemoryLimiter {
fn memory_growing(
&mut self,
current: usize,
desired: usize,
maximum: Option<usize>,
) -> Result<bool> {
if let Some(max) = maximum {
if desired > max {
return Ok(false);
}
}
let d = desired - current;
if d > self.bytes_left {
return Ok(false);
}
self.bytes_left -= d;
Ok(true)
}
fn table_growing(&mut self, _current: u32, desired: u32, maximum: Option<u32>) -> Result<bool> {
if let Some(max) = maximum {
return Ok(desired <= max);
}
Ok(true)
}
}

View File

@@ -5,7 +5,6 @@ mod context;
mod function;
mod internal;
pub mod manifest;
mod memory;
pub(crate) mod pdk;
mod plugin;
mod plugin_ref;
@@ -16,7 +15,6 @@ pub use context::Context;
pub use function::{Function, UserData, Val, ValType};
pub use internal::{Internal, InternalExt, Wasi};
pub use manifest::Manifest;
pub use memory::{MemoryBlock, PluginMemory, ToMemoryBlock};
pub use plugin::Plugin;
pub use plugin_ref::PluginRef;
pub(crate) use timer::{Timer, TimerAction};

View File

@@ -7,7 +7,7 @@ use sha2::Digest;
use crate::*;
/// Manifest wraps the manifest exported by `extism_manifest`
#[derive(Default, serde::Serialize, serde::Deserialize)]
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct Manifest(extism_manifest::Manifest);
@@ -60,6 +60,8 @@ fn check_hash(hash: &Option<String>, data: &[u8]) -> Result<(), Error> {
}
}
const WASM: &[u8] = include_bytes!("extism-runtime.wasm");
/// Convert from manifest to a wasmtime Module
fn to_module(engine: &Engine, wasm: &extism_manifest::Wasm) -> Result<(String, Module), Error> {
match wasm {
@@ -167,6 +169,7 @@ const WASM_MAGIC: [u8; 4] = [0x00, 0x61, 0x73, 0x6d];
impl Manifest {
/// Create a new Manifest, returns the manifest and a map of modules
pub fn new(engine: &Engine, data: &[u8]) -> Result<(Self, BTreeMap<String, Module>), Error> {
let extism_module = Module::new(engine, WASM)?;
let has_magic = data.len() >= 4 && data[0..4] == WASM_MAGIC;
let is_wast = data.starts_with(b"(module") || data.starts_with(b";;");
if !has_magic && !is_wast {
@@ -178,12 +181,14 @@ impl Manifest {
}
let t = serde_json::from_slice::<Self>(data)?;
let m = t.modules(engine)?;
let mut m = t.modules(engine)?;
m.insert("env".to_string(), extism_module);
return Ok((t, m));
}
let m = Module::new(engine, data)?;
let mut modules = BTreeMap::new();
modules.insert("env".to_string(), extism_module);
modules.insert("main".to_string(), m);
Ok((Manifest::default(), modules))
}

View File

@@ -1,376 +0,0 @@
use std::collections::BTreeMap;
use crate::*;
use pretty_hex::PrettyHex;
/// Handles memory for plugins
pub struct PluginMemory {
/// wasmtime Store
pub store: Option<Store<Internal>>,
/// WASM memory
pub memory: Memory,
/// Tracks allocated blocks
pub live_blocks: BTreeMap<usize, usize>,
/// Tracks free blocks
pub free: Vec<MemoryBlock>,
/// Tracks current offset in memory
pub position: usize,
/// Extism manifest
pub manifest: Manifest,
}
/// `ToMemoryBlock` is used to convert from Rust values to blocks of WASM memory
pub trait ToMemoryBlock {
fn to_memory_block(&self, mem: &PluginMemory) -> Result<MemoryBlock, Error>;
}
impl ToMemoryBlock for MemoryBlock {
fn to_memory_block(&self, _mem: &PluginMemory) -> Result<MemoryBlock, Error> {
Ok(*self)
}
}
impl ToMemoryBlock for (usize, usize) {
fn to_memory_block(&self, _mem: &PluginMemory) -> Result<MemoryBlock, Error> {
Ok(MemoryBlock {
offset: self.0,
length: self.1,
})
}
}
impl ToMemoryBlock for usize {
fn to_memory_block(&self, mem: &PluginMemory) -> Result<MemoryBlock, Error> {
match mem.at_offset(*self) {
Some(x) => Ok(x),
None => Err(Error::msg(format!("Invalid memory offset: {}", self))),
}
}
}
const PAGE_SIZE: u32 = 65536;
// BLOCK_SIZE_THRESHOLD exists to ensure that free blocks are never split up any
// smaller than this value
const BLOCK_SIZE_THRESHOLD: usize = 32;
impl PluginMemory {
/// Create memory for a plugin
pub fn new(store: Store<Internal>, memory: Memory, manifest: Manifest) -> Self {
PluginMemory {
free: Vec::new(),
live_blocks: BTreeMap::new(),
store: Some(store),
memory,
position: 1,
manifest,
}
}
pub fn store(&self) -> &Store<Internal> {
self.store.as_ref().unwrap()
}
pub fn store_mut(&mut self) -> &mut Store<Internal> {
self.store.as_mut().unwrap()
}
/// Moves module to a new store
pub fn reinstantiate(&mut self) -> Result<(), Error> {
if let Some(store) = self.store.take() {
let engine = store.engine().clone();
let internal = store.into_data();
let pages = internal.available_pages;
let mut store = Store::new(&engine, internal);
store.epoch_deadline_callback(|_internal| Err(Error::msg("timeout")));
self.memory = Memory::new(&mut store, MemoryType::new(2, pages))?;
self.store = Some(store);
}
self.reset();
Ok(())
}
/// Write byte to memory
pub(crate) fn store_u8(&mut self, offs: usize, data: u8) -> Result<(), MemoryAccessError> {
trace!("store_u8: offset={offs} data={data:#04x}");
if offs >= self.size() {
// This should raise MemoryAccessError
let buf = &mut [0];
self.memory.read(&self.store.as_ref().unwrap(), offs, buf)?;
return Ok(());
}
self.memory.data_mut(&mut self.store.as_mut().unwrap())[offs] = data;
Ok(())
}
/// Read byte from memory
pub(crate) fn load_u8(&self, offs: usize) -> Result<u8, MemoryAccessError> {
trace!("load_u8: offset={offs}");
if offs >= self.size() {
// This should raise MemoryAccessError
let buf = &mut [0];
self.memory.read(&self.store.as_ref().unwrap(), offs, buf)?;
return Ok(0);
}
Ok(self.memory.data(&self.store.as_ref().unwrap())[offs])
}
/// Write u64 to memory
pub(crate) fn store_u64(&mut self, offs: usize, data: u64) -> Result<(), Error> {
trace!("store_u64: offset={offs} data={data:#18x}");
let handle = MemoryBlock {
offset: offs,
length: 8,
};
self.write(handle, data.to_ne_bytes())?;
Ok(())
}
/// Read u64 from memory
pub(crate) fn load_u64(&self, offs: usize) -> Result<u64, Error> {
trace!("load_u64: offset={offs}");
let mut buf = [0; 8];
let handle = MemoryBlock {
offset: offs,
length: 8,
};
self.read(handle, &mut buf)?;
Ok(u64::from_ne_bytes(buf))
}
/// Write slice to memory
pub fn write(&mut self, pos: impl ToMemoryBlock, data: impl AsRef<[u8]>) -> Result<(), Error> {
let pos = pos.to_memory_block(self)?;
assert!(data.as_ref().len() <= pos.length);
self.memory
.write(&mut self.store.as_mut().unwrap(), pos.offset, data.as_ref())?;
Ok(())
}
/// Read slice from memory
pub fn read(&self, pos: impl ToMemoryBlock, mut data: impl AsMut<[u8]>) -> Result<(), Error> {
let pos = pos.to_memory_block(self)?;
assert!(data.as_mut().len() <= pos.length);
self.memory
.read(&self.store.as_ref().unwrap(), pos.offset, data.as_mut())?;
Ok(())
}
/// Size of memory in bytes
pub fn size(&self) -> usize {
self.memory.data_size(&self.store.as_ref().unwrap())
}
/// Size of memory in pages
pub fn pages(&self) -> u32 {
self.memory.size(&self.store.as_ref().unwrap()) as u32
}
/// Reserve `n` bytes of memory
pub fn alloc(&mut self, n: usize) -> Result<MemoryBlock, Error> {
debug!("Allocating {n} bytes");
for (i, block) in self.free.iter_mut().enumerate() {
if block.length == n {
let block = self.free.swap_remove(i);
self.live_blocks.insert(block.offset, block.length);
debug!("Found block with exact size at offset {}", block.offset);
return Ok(block);
} else if block.length.saturating_sub(n) >= BLOCK_SIZE_THRESHOLD {
let handle = MemoryBlock {
offset: block.offset,
length: n,
};
debug!(
"Using block with size {} at offset {}",
block.length, block.offset
);
block.offset += n;
block.length -= n;
self.live_blocks.insert(handle.offset, handle.length);
return Ok(handle);
}
}
let new_offset = self.position.saturating_add(n);
// If there aren't enough bytes, try to grow the memory size
if new_offset >= self.size() {
debug!("Need more memory");
let bytes_needed = (new_offset as f64 - self.size() as f64) / PAGE_SIZE as f64;
let mut pages_needed = bytes_needed.ceil() as u64;
if pages_needed == 0 {
pages_needed = 1
}
debug!("Requesting {pages_needed} more pages");
// This will fail if we've already allocated the maximum amount of memory allowed
self.memory
.grow(&mut self.store.as_mut().unwrap(), pages_needed)?;
}
let mem = MemoryBlock {
offset: self.position,
length: n,
};
debug!(
"Allocated new block: {} bytes at offset {}",
mem.length, mem.offset
);
self.live_blocks.insert(mem.offset, mem.length);
self.position += n;
Ok(mem)
}
/// Allocate and copy `data` into the wasm memory
pub fn alloc_bytes(&mut self, data: impl AsRef<[u8]>) -> Result<MemoryBlock, Error> {
let handle = self.alloc(data.as_ref().len())?;
self.write(handle, data)?;
Ok(handle)
}
/// Free the block allocated at `offset`
pub fn free(&mut self, offset: usize) {
debug!("Freeing block at {offset}");
if let Some(length) = self.live_blocks.remove(&offset) {
self.free.push(MemoryBlock { offset, length });
} else {
return;
}
let free_size: usize = self.free.iter().map(|x| x.length).sum();
// Perform compaction if there is at least 1kb of free memory available
if free_size >= 1024 {
let mut last: Option<MemoryBlock> = None;
let mut free = Vec::new();
for block in self.free.iter() {
match last {
None => {
free.push(*block);
}
Some(last) => {
if last.offset + last.length == block.offset {
free.push(MemoryBlock {
offset: last.offset,
length: last.length + block.length,
});
}
}
}
last = Some(*block);
}
self.free = free;
}
}
/// Log entire memory as hexdump using the `trace` log level
pub fn dump(&self) {
let data = self.memory.data(self.store.as_ref().unwrap());
trace!("{:?}", data[..self.position].hex_dump());
}
/// Reset memory - clears free-list and live blocks and resets position
pub fn reset(&mut self) {
self.free.clear();
self.live_blocks.clear();
self.position = 1;
}
/// Get memory as a slice of bytes
pub fn data(&self) -> &[u8] {
self.memory.data(self.store.as_ref().unwrap())
}
/// Get memory as a mutable slice of bytes
pub fn data_mut(&mut self) -> &mut [u8] {
self.memory.data_mut(self.store.as_mut().unwrap())
}
/// Get bytes occupied by the provided memory handle
pub fn get(&self, handle: impl ToMemoryBlock) -> Result<&[u8], Error> {
let handle = handle.to_memory_block(self)?;
Ok(&self.memory.data(self.store.as_ref().unwrap())
[handle.offset..handle.offset + handle.length])
}
/// Get mutable bytes occupied by the provided memory handle
pub fn get_mut(&mut self, handle: impl ToMemoryBlock) -> Result<&mut [u8], Error> {
let handle = handle.to_memory_block(self)?;
Ok(&mut self.memory.data_mut(self.store.as_mut().unwrap())
[handle.offset..handle.offset + handle.length])
}
/// Get str occupied by the provided memory handle
pub fn get_str(&self, handle: impl ToMemoryBlock) -> Result<&str, Error> {
let handle = handle.to_memory_block(self)?;
Ok(std::str::from_utf8(
&self.memory.data(self.store.as_ref().unwrap())
[handle.offset..handle.offset + handle.length],
)?)
}
/// Get mutable str occupied by the provided memory handle
pub fn get_mut_str(&mut self, handle: impl ToMemoryBlock) -> Result<&mut str, Error> {
let handle = handle.to_memory_block(self)?;
Ok(std::str::from_utf8_mut(
&mut self.memory.data_mut(self.store.as_mut().unwrap())
[handle.offset..handle.offset + handle.length],
)?)
}
/// Pointer to the provided memory handle
pub fn ptr(&self, handle: impl ToMemoryBlock) -> Result<*mut u8, Error> {
let handle = handle.to_memory_block(self)?;
Ok(unsafe {
self.memory
.data_ptr(&self.store.as_ref().unwrap())
.add(handle.offset)
})
}
/// Get the length of the block starting at `offs`
pub fn block_length(&self, offs: usize) -> Option<usize> {
self.live_blocks.get(&offs).cloned()
}
/// Get the block at the specified offset
pub fn at_offset(&self, offset: usize) -> Option<MemoryBlock> {
let block_length = self.block_length(offset);
block_length.map(|length| MemoryBlock { offset, length })
}
}
#[derive(Clone, Copy)]
pub struct MemoryBlock {
pub offset: usize,
pub length: usize,
}
impl From<(usize, usize)> for MemoryBlock {
fn from(x: (usize, usize)) -> Self {
MemoryBlock {
offset: x.0,
length: x.1,
}
}
}
impl MemoryBlock {
pub fn new(offset: usize, length: usize) -> Self {
MemoryBlock { offset, length }
}
}

View File

@@ -18,177 +18,6 @@ macro_rules! args {
};
}
/// Get the input length
/// Params: none
/// Returns: i64 (length)
pub(crate) fn input_length(
caller: Caller<Internal>,
_input: &[Val],
output: &mut [Val],
) -> Result<(), Error> {
let data: &Internal = caller.data();
output[0] = Val::I64(data.input_length as i64);
Ok(())
}
/// Load a byte from input
/// Params: i64 (offset)
/// Returns: i32 (byte)
pub(crate) fn input_load_u8(
caller: Caller<Internal>,
input: &[Val],
output: &mut [Val],
) -> Result<(), Error> {
let data: &Internal = caller.data();
if data.input.is_null() {
return Ok(());
}
output[0] = unsafe { Val::I32(*data.input.add(input[0].unwrap_i64() as usize) as i32) };
Ok(())
}
/// Load an unsigned 64 bit integer from input
/// Params: i64 (offset)
/// Returns: i64 (int)
pub(crate) fn input_load_u64(
caller: Caller<Internal>,
input: &[Val],
output: &mut [Val],
) -> Result<(), Error> {
let data: &Internal = caller.data();
if data.input.is_null() {
return Ok(());
}
let offs = args!(input, 0, i64) as usize;
let slice = unsafe { std::slice::from_raw_parts(data.input.add(offs), 8) };
let byte = u64::from_ne_bytes(slice.try_into().unwrap());
output[0] = Val::I64(byte as i64);
Ok(())
}
/// Store a byte in memory
/// Params: i64 (offset), i32 (byte)
/// Returns: none
pub(crate) fn store_u8(
mut caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Error> {
let data: &mut Internal = caller.data_mut();
let (offset, byte) = args!(input, (0, i64), (1, i32));
data.memory_mut().store_u8(offset as usize, byte as u8)?;
Ok(())
}
/// Load a byte from memory
/// Params: i64 (offset)
/// Returns: i32 (byte)
pub(crate) fn load_u8(
caller: Caller<Internal>,
input: &[Val],
output: &mut [Val],
) -> Result<(), Error> {
let data: &Internal = caller.data();
let offset = args!(input, 0, i64) as usize;
let byte = data.memory().load_u8(offset)?;
output[0] = Val::I32(byte as i32);
Ok(())
}
/// Store an unsigned 64 bit integer in memory
/// Params: i64 (offset), i64 (int)
/// Returns: none
pub(crate) fn store_u64(
mut caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Error> {
let data: &mut Internal = caller.data_mut();
let (offset, b) = args!(input, (0, i64), (1, i64));
data.memory_mut().store_u64(offset as usize, b as u64)?;
Ok(())
}
/// Load an unsigned 64 bit integer from memory
/// Params: i64 (offset)
/// Returns: i64 (int)
pub(crate) fn load_u64(
caller: Caller<Internal>,
input: &[Val],
output: &mut [Val],
) -> Result<(), Error> {
let data: &Internal = caller.data();
let offset = args!(input, 0, i64) as usize;
let byte = data.memory().load_u64(offset)?;
output[0] = Val::I64(byte as i64);
Ok(())
}
/// Set output offset and length
/// Params: i64 (offset), i64 (length)
/// Returns: none
pub(crate) fn output_set(
mut caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Error> {
let data: &mut Internal = caller.data_mut();
let (offset, length) = args!(input, (0, i64), (1, i64));
data.output_offset = offset as usize;
data.output_length = length as usize;
Ok(())
}
/// Allocate bytes
/// Params: i64 (length)
/// Returns: i64 (offset)
pub(crate) fn alloc(
mut caller: Caller<Internal>,
input: &[Val],
output: &mut [Val],
) -> Result<(), Error> {
let data: &mut Internal = caller.data_mut();
let offs = data.memory_mut().alloc(input[0].unwrap_i64() as _)?;
output[0] = Val::I64(offs.offset as i64);
Ok(())
}
/// Free memory
/// Params: i64 (offset)
/// Returns: none
pub(crate) fn free(
mut caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Error> {
let data: &mut Internal = caller.data_mut();
let offset = args!(input, 0, i64) as usize;
data.memory_mut().free(offset);
Ok(())
}
/// Set the error message, this can be checked by the host program
/// Params: i64 (offset)
/// Returns: none
pub(crate) fn error_set(
mut caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Error> {
let data: &mut Internal = caller.data_mut();
let offset = args!(input, 0, i64) as usize;
if offset == 0 {
*data.last_error.borrow_mut() = None;
return Ok(());
}
let s = data.memory().get_str(offset)?;
data.set_error(s);
Ok(())
}
/// Get a configuration value
/// Params: i64 (offset)
/// Returns: i64 (offset)
@@ -199,21 +28,24 @@ pub(crate) fn config_get(
) -> Result<(), Error> {
let data: &mut Internal = caller.data_mut();
let offset = args!(input, 0, i64) as usize;
let key = data.memory().get_str(offset)?;
let val = data.memory().manifest.as_ref().config.get(key);
let offset = args!(input, 0, i64) as u64;
let key = data.memory_read_str(offset)?;
let key = unsafe {
std::str::from_utf8_unchecked(std::slice::from_raw_parts(key.as_ptr(), key.len()))
};
let val = data.internal().manifest.as_ref().config.get(key);
let ptr = val.map(|x| (x.len(), x.as_ptr()));
let mem = match ptr {
Some((len, ptr)) => {
let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
data.memory_mut().alloc_bytes(bytes)?
data.memory_alloc_bytes(bytes)?
}
None => {
output[0] = Val::I64(0);
return Ok(());
}
};
output[0] = Val::I64(mem.offset as i64);
output[0] = Val::I64(mem as i64);
Ok(())
}
@@ -227,23 +59,24 @@ pub(crate) fn var_get(
) -> Result<(), Error> {
let data: &mut Internal = caller.data_mut();
let offset = args!(input, 0, i64) as usize;
let key = data.memory().get_str(offset)?;
let val = data.vars.get(key);
let offset = args!(input, 0, i64) as u64;
let key = data.memory_read_str(offset)?;
let key = unsafe {
std::str::from_utf8_unchecked(std::slice::from_raw_parts(key.as_ptr(), key.len()))
};
let val = data.internal().vars.get(key);
let ptr = val.map(|x| (x.len(), x.as_ptr()));
let mem = match ptr {
Some((len, ptr)) => {
let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
data.memory_mut().alloc_bytes(bytes)?
data.memory_alloc_bytes(bytes)?
}
None => {
output[0] = Val::I64(0);
return Ok(());
}
};
output[0] = Val::I64(mem.offset as i64);
output[0] = Val::I64(mem as i64);
Ok(())
}
@@ -262,16 +95,16 @@ pub(crate) fn var_set(
size += v.len();
}
let voffset = args!(input, 1, i64) as usize;
let voffset = args!(input, 1, i64) as u64;
// If the store is larger than 100MB then stop adding things
if size > 1024 * 1024 * 100 && voffset != 0 {
return Err(Error::msg("Variable store is full"));
}
let key_offs = args!(input, 0, i64) as usize;
let key_offs = args!(input, 0, i64) as u64;
let key = {
let key = data.memory().get_str(key_offs)?;
let key = data.memory_read_str(key_offs)?;
let key_len = key.len();
let key_ptr = key.as_ptr();
unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(key_ptr, key_len)) }
@@ -283,10 +116,11 @@ pub(crate) fn var_set(
return Ok(());
}
let value = data.memory().get(voffset)?;
let vlen = data.memory_length(voffset);
let value = data.memory_read(voffset, vlen).to_vec();
// Insert the value from memory into the `vars` map
data.vars.insert(key.to_string(), value.to_vec());
data.vars.insert(key.to_string(), value);
Ok(())
}
@@ -312,18 +146,19 @@ pub(crate) fn http_request(
{
use std::io::Read;
let data: &mut Internal = caller.data_mut();
let http_req_offset = args!(input, 0, i64) as usize;
let http_req_offset = args!(input, 0, i64) as u64;
let http_req_len = data.memory_length(http_req_offset);
let req: extism_manifest::HttpRequest =
serde_json::from_slice(data.memory().get(http_req_offset)?)?;
serde_json::from_slice(data.memory_read(http_req_offset, http_req_len))?;
let body_offset = args!(input, 1, i64) as usize;
let body_offset = args!(input, 1, i64) as u64;
let url = match url::Url::parse(&req.url) {
Ok(u) => u,
Err(e) => return Err(Error::msg(format!("Invalid URL: {e:?}"))),
};
let allowed_hosts = &data.memory().manifest.as_ref().allowed_hosts;
let allowed_hosts = &data.internal().manifest.as_ref().allowed_hosts;
let host_str = url.host_str().unwrap_or_default();
let host_matches = if let Some(allowed_hosts) = allowed_hosts {
allowed_hosts.iter().any(|url| {
@@ -352,7 +187,8 @@ pub(crate) fn http_request(
}
let res = if body_offset > 0 {
let buf = data.memory().get(body_offset)?;
let len = data.memory_length(body_offset);
let buf = data.memory_read(body_offset, len);
r.send_bytes(buf)
} else {
r.call()
@@ -379,9 +215,8 @@ pub(crate) fn http_request(
.take(1024 * 1024 * 50) // TODO: make this limit configurable
.read_to_end(&mut buf)?;
let mem = data.memory_mut().alloc_bytes(buf)?;
output[0] = Val::I64(mem.offset as i64);
let mem = data.memory_alloc_bytes(buf)?;
output[0] = Val::I64(mem as i64);
} else {
output[0] = Val::I64(0);
}
@@ -403,39 +238,17 @@ pub(crate) fn http_status_code(
Ok(())
}
/// Get the length of an allocated block given the offset
/// Params: i64 (offset)
/// Returns: i64 (length or 0)
pub(crate) fn length(
mut caller: Caller<Internal>,
input: &[Val],
output: &mut [Val],
) -> Result<(), Error> {
let data: &mut Internal = caller.data_mut();
let offset = args!(input, 0, i64) as usize;
if offset == 0 {
output[0] = Val::I64(0);
return Ok(());
}
let length = match data.memory().block_length(offset) {
Some(x) => x,
None => return Err(Error::msg("Unable to find length for offset")),
};
output[0] = Val::I64(length as i64);
Ok(())
}
pub fn log(
level: log::Level,
caller: Caller<Internal>,
mut caller: Caller<Internal>,
input: &[Val],
_output: &mut [Val],
) -> Result<(), Error> {
let data: &Internal = caller.data();
let offset = args!(input, 0, i64) as usize;
let buf = data.memory().get(offset)?;
let data: &mut Internal = caller.data_mut();
let offset = args!(input, 0, i64) as u64;
let buf = data.memory_read_str(offset);
match std::str::from_utf8(buf) {
match buf {
Ok(buf) => log::log!(level, "{}", buf),
Err(_) => log::log!(level, "{:?}", buf),
}

View File

@@ -9,6 +9,7 @@ pub struct Plugin {
/// Used to define functions and create new instances
pub linker: Linker<Internal>,
pub store: Store<Internal>,
/// Instance provides the ability to call functions in a module
pub instance: Instance,
@@ -18,9 +19,6 @@ pub struct Plugin {
/// actually cleaned up along with a `Store`
pub instantiations: usize,
/// Handles interactions with WASM memory
pub memory: std::cell::UnsafeCell<PluginMemory>,
/// The ID used to identify this plugin with the `Timer`
pub timer_id: uuid::Uuid,
@@ -33,12 +31,24 @@ pub struct Plugin {
}
impl InternalExt for Plugin {
fn memory(&self) -> &PluginMemory {
unsafe { &*self.memory.get() }
fn store(&self) -> &Store<Internal> {
&self.store
}
fn memory_mut(&mut self) -> &mut PluginMemory {
self.memory.get_mut()
fn store_mut(&mut self) -> &mut Store<Internal> {
&mut self.store
}
fn linker(&self) -> &Linker<Internal> {
&self.linker
}
fn linker_mut(&mut self) -> &mut Linker<Internal> {
&mut self.linker
}
fn linker_and_store(&mut self) -> (&mut Linker<Internal>, &mut Store<Internal>) {
(&mut self.linker, &mut self.store)
}
}
@@ -68,9 +78,13 @@ fn calculate_available_memory(
let mut fail_memory_check = false;
let mut total_memory_needed = 0;
for (name, module) in modules.iter() {
if name == "env" {
continue;
}
let mut memories = 0;
for export in module.exports() {
if let Some(memory) = export.ty().memory() {
memories += 1;
let memory_max = memory.maximum();
match memory_max {
None => anyhow::bail!("Unbounded memory in module {name}, when `memory.max_pages` is set in the manifest all modules \
@@ -78,16 +92,15 @@ fn calculate_available_memory(
Some(m) => {
total_memory_needed += m;
if !fail_memory_check {
continue
continue;
}
*available_pages = available_pages.saturating_sub(m as u32);
if *available_pages == 0 {
fail_memory_check = true;
}
},
}
}
memories += 1;
}
}
@@ -132,14 +145,13 @@ impl Plugin {
let mut store = Store::new(
&engine,
Internal::new(&manifest, with_wasi, available_pages)?,
Internal::new(manifest, with_wasi, available_pages)?,
);
store.epoch_deadline_callback(|_internal| Err(Error::msg("timeout")));
// Create memory
let memory = Memory::new(&mut store, MemoryType::new(2, available_pages))?;
let mut memory = PluginMemory::new(store, memory, manifest);
if available_pages.is_some() {
store.limiter(|internal| internal.memory_limiter.as_mut().unwrap());
}
let mut linker = Linker::new(&engine);
linker.allow_shadowing(true);
@@ -177,7 +189,10 @@ impl Plugin {
}
// Add builtins
for (_name, module) in modules.iter() {
for (name, module) in modules.iter() {
if name != main_name {
linker.module(&mut store, name, module)?;
}
for import in module.imports() {
let module_name = import.module();
let name = import.name();
@@ -185,23 +200,11 @@ impl Plugin {
if module_name == EXPORT_MODULE_NAME {
define_funcs!(name, {
alloc(I64) -> I64;
free(I64);
load_u8(I64) -> I32;
load_u64(I64) -> I64;
store_u8(I64, I32);
store_u64(I64, I64);
input_length() -> I64;
input_load_u8(I64) -> I32;
input_load_u64(I64) -> I64;
output_set(I64, I64);
error_set(I64);
config_get(I64) -> I64;
var_get(I64) -> I64;
var_set(I64, I64);
http_request(I64, I64) -> I64;
http_status_code() -> I32;
length(I64) -> I64;
log_warn(I64);
log_info(I64);
log_debug(I64);
@@ -219,20 +222,13 @@ impl Plugin {
}
}
// Add modules to linker
for (name, module) in modules.iter() {
if name != main_name {
linker.module(&mut memory.store_mut(), name, module)?;
}
}
let instance = linker.instantiate(&mut memory.store_mut(), main)?;
let instance = linker.instantiate(&mut store, main)?;
let timer_id = uuid::Uuid::new_v4();
let mut plugin = Plugin {
modules,
linker,
memory: std::cell::UnsafeCell::new(memory),
instance,
store,
instantiations: 1,
runtime: None,
timer_id,
@@ -242,8 +238,8 @@ impl Plugin {
},
};
// Make sure `Internal::memory` is initialized
plugin.internal_mut().memory = plugin.memory.get();
plugin.internal_mut().store = &mut plugin.store;
plugin.internal_mut().linker = &mut plugin.linker;
// Then detect runtime before returning the new plugin
plugin.detect_runtime();
@@ -252,35 +248,55 @@ impl Plugin {
/// Get a function by name
pub fn get_func(&mut self, function: impl AsRef<str>) -> Option<Func> {
self.instance
.get_func(&mut self.memory.get_mut().store_mut(), function.as_ref())
}
// A convenience method to set the plugin error and return a value
pub fn error<E>(&self, e: impl std::fmt::Debug, x: E) -> E {
self.store().data().set_error(e);
x
self.instance.get_func(&mut self.store, function.as_ref())
}
/// Store input in memory and initialize `Internal` pointer
pub fn set_input(&mut self, input: *const u8, mut len: usize) {
pub(crate) fn set_input(
&mut self,
input: *const u8,
mut len: usize,
tx: std::sync::mpsc::SyncSender<TimerAction>,
) -> Result<(), Error> {
if input.is_null() {
len = 0;
}
let ptr = self.memory.get();
let internal = self.internal_mut();
internal.input = input;
internal.input_length = len;
internal.memory = ptr
}
/// Dump memory using trace! logging
pub fn dump_memory(&self) {
self.memory().dump();
{
let store = &mut self.store as *mut _;
let linker = &mut self.linker as *mut _;
let internal = self.internal_mut();
internal.store = store;
internal.linker = linker;
}
let bytes = unsafe { std::slice::from_raw_parts(input, len) };
trace!("Input size: {}", bytes.len());
self.start_timer(&tx)?;
if let Some(f) = self.linker.get(&mut self.store, "env", "extism_reset") {
f.into_func().unwrap().call(&mut self.store, &[], &mut [])?;
}
let offs = self.memory_alloc_bytes(bytes)?;
if let Some(f) = self.linker.get(&mut self.store, "env", "extism_input_set") {
f.into_func().unwrap().call(
&mut self.store,
&[Val::I64(offs as i64), Val::I64(len as i64)],
&mut [],
)?;
}
Ok(())
}
/// Create a new instance from the same modules
pub fn reinstantiate(&mut self) -> Result<(), Error> {
if let Some(limiter) = self.internal_mut().memory_limiter.as_mut() {
limiter.reset();
}
let (main_name, main) = self
.modules
.get("main")
@@ -290,24 +306,34 @@ impl Plugin {
(entry.0.as_str(), entry.1)
});
// Avoid running into resource limits, after 5 instantiations reset the store. This will
// release any old `Instance` objects
if self.instantiations > 5 {
self.memory.get_mut().reinstantiate()?;
let engine = self.store.engine().clone();
let internal = self.internal();
self.store = Store::new(
&engine,
Internal::new(
internal.manifest.clone(),
internal.wasi.is_some(),
internal.available_pages,
)?,
);
self.store
.epoch_deadline_callback(|_internal| Err(Error::msg("timeout")));
if self.internal().available_pages.is_some() {
self.store
.limiter(|internal| internal.memory_limiter.as_mut().unwrap());
}
// Get the `main` module, or the last one if `main` doesn't exist
for (name, module) in self.modules.iter() {
if name != main_name {
self.linker
.module(&mut self.memory.get_mut().store_mut(), name, module)?;
self.linker.module(&mut self.store, name, module)?;
}
}
self.instantiations = 0;
}
let instance = self
.linker
.instantiate(&mut self.memory.get_mut().store_mut(), &main)?;
let instance = self.linker.instantiate(&mut self.store, main)?;
self.instance = instance;
self.detect_runtime();
self.instantiations += 1;
@@ -316,7 +342,7 @@ impl Plugin {
/// Determine if wasi is enabled
pub fn has_wasi(&self) -> bool {
self.memory().store().data().wasi.is_some()
self.internal().wasi.is_some()
}
fn detect_runtime(&mut self) {
@@ -325,13 +351,10 @@ impl Plugin {
// by calling the `hs_init` export
if let Some(init) = self.get_func("hs_init") {
if let Some(cleanup) = self.get_func("hs_exit") {
if init
.typed::<(i32, i32), ()>(&self.memory().store())
.is_err()
{
if init.typed::<(i32, i32), ()>(&self.store()).is_err() {
trace!(
"hs_init function found with type {:?}",
init.ty(&self.memory().store())
init.ty(self.store())
);
}
self.runtime = Some(Runtime::Haskell { init, cleanup });
@@ -339,38 +362,48 @@ impl Plugin {
return;
}
// Check for `__wasm__call_ctors` and `__wasm_call_dtors`, this is used by WASI to
// Check for `__wasm_call_ctors` and `__wasm_call_dtors`, this is used by WASI to
// initialize certain interfaces.
if self.has_wasi() {
if let Some(init) = self.get_func("__wasm_call_ctors") {
if init.typed::<(), ()>(&self.memory().store()).is_err() {
let init = if let Some(init) = self.get_func("__wasm_call_ctors") {
if init.typed::<(), ()>(&self.store()).is_err() {
trace!(
"__wasm_call_ctors function found with type {:?}",
init.ty(&self.memory().store())
init.ty(self.store())
);
return;
}
trace!("WASI runtime detected");
if let Some(cleanup) = self.get_func("__wasm_call_dtors") {
if cleanup.typed::<(), ()>(&self.memory().store()).is_err() {
trace!(
"__wasm_call_dtors function found with type {:?}",
cleanup.ty(&self.memory().store())
);
return;
}
self.runtime = Some(Runtime::Wasi {
init,
cleanup: Some(cleanup),
});
init
} else if let Some(init) = self.get_func("_initialize") {
if init.typed::<(), ()>(&self.store()).is_err() {
trace!(
"_initialize function found with type {:?}",
init.ty(self.store())
);
return;
}
trace!("WASI reactor module detected");
init
} else {
return;
};
self.runtime = Some(Runtime::Wasi {
init,
cleanup: None,
});
}
let cleanup = if let Some(cleanup) = self.get_func("__wasm_call_dtors") {
if cleanup.typed::<(), ()>(&self.store()).is_err() {
trace!(
"__wasm_call_dtors function found with type {:?}",
cleanup.ty(self.store())
);
None
} else {
Some(cleanup)
}
} else {
None
};
self.runtime = Some(Runtime::Wasi { init, cleanup });
return;
}
@@ -378,22 +411,22 @@ impl Plugin {
}
pub(crate) fn initialize_runtime(&mut self) -> Result<(), Error> {
let mut store = &mut self.store;
if let Some(runtime) = &self.runtime {
trace!("Plugin::initialize_runtime");
match runtime {
Runtime::Haskell { init, cleanup: _ } => {
let mut results =
vec![Val::null(); init.ty(&self.memory().store()).results().len()];
let mut results = vec![Val::null(); init.ty(&store).results().len()];
init.call(
&mut self.memory.get_mut().store_mut(),
&mut store,
&[Val::I32(0), Val::I32(0)],
results.as_mut_slice(),
)?;
debug!("Initialized Haskell language runtime");
}
Runtime::Wasi { init, cleanup: _ } => {
debug!("Calling __wasm_call_ctors");
init.call(&mut self.memory.get_mut().store_mut(), &[], &mut [])?;
init.call(&mut store, &[], &mut [])?;
debug!("Initialied WASI runtime");
}
}
}
@@ -411,7 +444,7 @@ impl Plugin {
cleanup: Some(cleanup),
} => {
debug!("Calling __wasm_call_dtors");
cleanup.call(&mut self.memory_mut().store_mut(), &[], &mut [])?;
cleanup.call(self.store_mut(), &[], &mut [])?;
}
Runtime::Wasi {
init: _,
@@ -420,13 +453,8 @@ impl Plugin {
// Cleanup Haskell runtime if `hs_exit` and `hs_exit` are present,
// by calling the `hs_exit` export
Runtime::Haskell { init: _, cleanup } => {
let mut results =
vec![Val::null(); cleanup.ty(&self.memory().store()).results().len()];
cleanup.call(
&mut self.memory_mut().store_mut(),
&[],
results.as_mut_slice(),
)?;
let mut results = vec![Val::null(); cleanup.ty(self.store()).results().len()];
cleanup.call(self.store_mut(), &[], results.as_mut_slice())?;
debug!("Cleaned up Haskell language runtime");
}
}
@@ -442,14 +470,14 @@ impl Plugin {
tx: &std::sync::mpsc::SyncSender<TimerAction>,
) -> Result<(), Error> {
let duration = self
.memory()
.internal()
.manifest
.as_ref()
.timeout_ms
.map(std::time::Duration::from_millis);
self.cancel_handle.epoch_timer_tx = Some(tx.clone());
self.memory_mut().store_mut().set_epoch_deadline(1);
let engine: Engine = self.memory().store().engine().clone();
self.store_mut().set_epoch_deadline(1);
let engine: Engine = self.store().engine().clone();
tx.send(TimerAction::Start {
id: self.timer_id,
duration,

View File

@@ -3,6 +3,7 @@ use crate::*;
// PluginRef is used to access a plugin from a context-scoped plugin registry
pub struct PluginRef<'a> {
pub id: PluginIndex,
running: bool,
pub(crate) epoch_timer_tx: std::sync::mpsc::SyncSender<TimerAction>,
plugin: *mut Plugin,
_t: std::marker::PhantomData<&'a ()>,
@@ -10,23 +11,18 @@ pub struct PluginRef<'a> {
impl<'a> PluginRef<'a> {
/// Initialize the plugin for a new call
///
/// - Resets memory offsets
/// - Updates `input` pointer
pub fn init(mut self, data: *const u8, data_len: usize) -> Self {
trace!("PluginRef::init: {}", self.id,);
pub fn start_call(mut self) -> Self {
trace!("PluginRef::start_call: {}", self.id,);
let plugin = self.as_mut();
plugin.memory_mut().reset();
if plugin.has_wasi() || plugin.runtime.is_some() {
if let Err(e) = plugin.reinstantiate() {
error!("Failed to reinstantiate: {e:?}");
plugin
.internal()
.set_error(format!("Failed to reinstantiate: {e:?}"));
plugin.error(format!("Failed to reinstantiate: {e:?}"), ());
return self;
}
}
plugin.set_input(data, data_len);
self.running = true;
self
}
@@ -45,12 +41,25 @@ impl<'a> PluginRef<'a> {
return ctx.error(format!("Plugin does not exist: {plugin_id}"), None);
};
{
let plugin = unsafe { &mut *plugin };
// Start timer
if let Err(e) = plugin.start_timer(&epoch_timer_tx) {
let id = plugin.timer_id;
plugin.error(
format!("Unable to start timeout manager for {id}: {e:?}"),
(),
);
return None;
}
}
if clear_error {
trace!("Clearing context error");
ctx.error = None;
trace!("Clearing plugin error: {plugin_id}");
unsafe {
(&*plugin).internal().clear_error();
(*plugin).clear_error();
}
}
@@ -59,6 +68,7 @@ impl<'a> PluginRef<'a> {
plugin,
epoch_timer_tx,
_t: std::marker::PhantomData,
running: false,
})
}
}
@@ -78,6 +88,14 @@ impl<'a> AsMut<Plugin> for PluginRef<'a> {
impl<'a> Drop for PluginRef<'a> {
fn drop(&mut self) {
trace!("Dropping PluginRef {}", self.id);
// Cleanup?
if self.running {
let plugin = self.as_mut();
// Stop timer
if let Err(e) = plugin.stop_timer() {
let id = plugin.timer_id;
error!("Failed to stop timeout manager for {id}: {e:?}");
}
}
}
}

View File

@@ -99,7 +99,7 @@ pub unsafe extern "C" fn extism_current_plugin_memory(plugin: *mut Internal) ->
}
let plugin = &mut *plugin;
plugin.memory_mut().data_mut().as_mut_ptr()
plugin.memory_ptr()
}
/// Allocate a memory block in the currently running plugin
@@ -111,16 +111,7 @@ pub unsafe extern "C" fn extism_current_plugin_memory_alloc(plugin: *mut Interna
}
let plugin = &mut *plugin;
let mem = match plugin.memory_mut().alloc(n as usize) {
Ok(x) => x,
Err(e) => {
plugin.set_error(e);
return 0;
}
};
mem.offset as u64
plugin.memory_alloc(n as u64).unwrap_or_default()
}
/// Get the length of an allocated block
@@ -135,11 +126,7 @@ pub unsafe extern "C" fn extism_current_plugin_memory_length(
}
let plugin = &mut *plugin;
match plugin.memory().block_length(n as usize) {
Some(x) => x as Size,
None => 0,
}
plugin.memory_length(n)
}
/// Free an allocated memory block
@@ -151,7 +138,7 @@ pub unsafe extern "C" fn extism_current_plugin_memory_free(plugin: *mut Internal
}
let plugin = &mut *plugin;
plugin.memory_mut().free(ptr as usize);
plugin.memory_free(ptr);
}
/// Create a new host function
@@ -436,21 +423,21 @@ pub unsafe extern "C" fn extism_plugin_config(
}
};
let wasi = &mut plugin.memory.get_mut().store_mut().data_mut().wasi;
let wasi = &mut plugin.internal_mut().wasi;
if let Some(Wasi { ctx, .. }) = wasi {
for (k, v) in json.iter() {
match v {
Some(v) => {
let _ = ctx.push_env(&k, &v);
let _ = ctx.push_env(k, v);
}
None => {
let _ = ctx.push_env(&k, "");
let _ = ctx.push_env(k, "");
}
}
}
}
let config = &mut plugin.memory.get_mut().manifest.as_mut().config;
let config = &mut plugin.internal_mut().manifest.as_mut().config;
for (k, v) in json.into_iter() {
match v {
Some(v) => {
@@ -512,15 +499,11 @@ pub unsafe extern "C" fn extism_plugin_call(
// needed before a new call
let mut plugin_ref = match PluginRef::new(ctx, plugin_id, true) {
None => return -1,
Some(p) => p.init(data, data_len as usize),
Some(p) => p.start_call(),
};
let tx = plugin_ref.epoch_timer_tx.clone();
let plugin = plugin_ref.as_mut();
if plugin.internal().last_error.borrow().is_some() {
return -1;
}
// Find function
let name = std::ffi::CStr::from_ptr(func_name);
let name = match name.to_str() {
@@ -534,15 +517,6 @@ pub unsafe extern "C" fn extism_plugin_call(
None => return plugin.error(format!("Function not found: {name}"), -1),
};
// Start timer
if let Err(e) = plugin.start_timer(&tx) {
let id = plugin.timer_id;
return plugin.error(
format!("Unable to start timeout manager for {id}: {e:?}"),
-1,
);
}
// Check the number of results, reject functions with more than 1 result
let n_results = func.ty(plugin.store()).results().len();
if n_results > 1 {
@@ -559,13 +533,19 @@ pub unsafe extern "C" fn extism_plugin_call(
}
}
if let Err(e) = plugin.set_input(data, data_len as usize, tx) {
return plugin.error(e, -1);
}
if plugin.has_error() {
return -1;
}
debug!("Calling function: {name} in plugin {plugin_id}");
// Call the function
let mut results = vec![wasmtime::Val::null(); n_results];
let res = func.call(&mut plugin.store_mut(), &[], results.as_mut_slice());
plugin.dump_memory();
let res = func.call(plugin.store_mut(), &[], results.as_mut_slice());
// Cleanup runtime
if !is_start {
@@ -574,18 +554,10 @@ pub unsafe extern "C" fn extism_plugin_call(
}
}
// Stop timer
if let Err(e) = plugin.stop_timer() {
let id = plugin.timer_id;
return plugin.error(
format!("Failed to stop timeout manager for {id}: {e:?}"),
-1,
);
}
match res {
Ok(()) => (),
Err(e) => {
plugin.store.set_epoch_deadline(1);
if let Some(exit) = e.downcast_ref::<wasmtime_wasi::I32Exit>() {
trace!("WASI return code: {}", exit.0);
if exit.0 != 0 {
@@ -635,20 +607,27 @@ pub unsafe extern "C" fn extism_error(ctx: *mut Context, plugin: PluginIndex) ->
return get_context_error(ctx);
}
let plugin_ref = match PluginRef::new(ctx, plugin, false) {
let mut plugin_ref = match PluginRef::new(ctx, plugin, false) {
None => return std::ptr::null(),
Some(p) => p,
};
let plugin = plugin_ref.as_ref();
let err = plugin.internal().last_error.borrow();
match err.as_ref() {
Some(e) => e.as_ptr() as *const _,
None => {
trace!("Error is NULL");
std::ptr::null()
}
let plugin = plugin_ref.as_mut();
let output = &mut [Val::I64(0)];
if let Some(f) = plugin
.linker
.get(&mut plugin.store, "env", "extism_error_get")
{
f.into_func()
.unwrap()
.call(&mut plugin.store, &[], output)
.unwrap();
}
if output[0].unwrap_i64() == 0 {
trace!("Error is NULL");
return std::ptr::null();
}
plugin.memory_ptr().add(output[0].unwrap_i64() as usize) as *const _
}
/// Get the length of a plugin's output data
@@ -660,13 +639,20 @@ pub unsafe extern "C" fn extism_plugin_output_length(
trace!("Call to extism_plugin_output_length for plugin {plugin}");
let ctx = &mut *ctx;
let plugin_ref = match PluginRef::new(ctx, plugin, true) {
let mut plugin_ref = match PluginRef::new(ctx, plugin, true) {
None => return 0,
Some(p) => p,
};
let plugin = plugin_ref.as_ref();
let len = plugin.internal().output_length as Size;
let plugin = plugin_ref.as_mut();
let out = &mut [Val::I64(0)];
let _ = plugin
.linker
.get(&mut plugin.store, "env", "extism_output_length")
.unwrap()
.into_func()
.unwrap()
.call(&mut plugin.store_mut(), &[], out);
let len = out[0].unwrap_i64() as Size;
trace!("Output length: {len}");
len
}
@@ -680,20 +666,23 @@ pub unsafe extern "C" fn extism_plugin_output_data(
trace!("Call to extism_plugin_output_data for plugin {plugin}");
let ctx = &mut *ctx;
let plugin_ref = match PluginRef::new(ctx, plugin, true) {
let mut plugin_ref = match PluginRef::new(ctx, plugin, true) {
None => return std::ptr::null(),
Some(p) => p,
};
let plugin = plugin_ref.as_ref();
let internal = plugin.internal();
let plugin = plugin_ref.as_mut();
let ptr = plugin.memory_ptr();
let out = &mut [Val::I64(0)];
let mut store = &mut *(plugin.store_mut() as *mut Store<_>);
plugin
.memory()
.ptr(MemoryBlock::new(
internal.output_offset,
internal.output_length,
))
.map(|x| x as *const _)
.unwrap_or(std::ptr::null())
.linker
.get(&mut store, "env", "extism_output_offset")
.unwrap()
.into_func()
.unwrap()
.call(&mut store, &[], out)
.unwrap();
ptr.add(out[0].unwrap_i64() as usize)
}
/// Set log file and level

View File

@@ -9,7 +9,7 @@ repository = "https://github.com/extism/extism"
description = "Extism Host SDK for Rust"
[dependencies]
extism-manifest = { version = "0.3.0", path = "../manifest" }
extism-manifest = { version = "0.4.0", path = "../manifest" }
extism-runtime = { version = "0.4.0", path = "../runtime"}
serde_json = "1"
log = "0.4"

View File

@@ -1,6 +1,6 @@
pub use extism_manifest::{self as manifest, Manifest};
pub use extism_runtime::{
sdk as bindings, Function, Internal as CurrentPlugin, MemoryBlock, UserData, Val, ValType,
sdk as bindings, Function, Internal as CurrentPlugin, UserData, Val, ValType,
};
mod context;

Binary file not shown.

View File

@@ -13,7 +13,7 @@ pub fn getMemory(self: Self, offset: u64) []const u8 {
const len = c.extism_current_plugin_memory_length(self.c_currplugin, offset);
const c_data = c.extism_current_plugin_memory(self.c_currplugin);
const data: [*:0]u8 = std.mem.span(c_data);
return data[offset .. len + 1];
return data[offset .. offset + len];
}
pub fn alloc(self: *Self, n: u64) u64 {

View File

@@ -81,7 +81,7 @@ fn stringify(
try out_stream.writeByte('{');
var field_output = false;
var child_options = options;
child_options.whitespace.indent_level += 1;
child_options.whitespace = .indent_2;
inline for (S.fields) |Field| {
// don't include void fields
if (Field.type == void) continue;
@@ -103,18 +103,14 @@ fn stringify(
} else {
try out_stream.writeByte(',');
}
try child_options.whitespace.outputIndent(out_stream);
try json.encodeJsonString(Field.name, options, out_stream);
try out_stream.writeByte(':');
if (child_options.whitespace.separator) {
if (child_options.whitespace != .minified) {
try out_stream.writeByte(' ');
}
try stringify(@field(value, Field.name), child_options, out_stream);
}
}
if (field_output) {
try options.whitespace.outputIndent(out_stream);
}
try out_stream.writeByte('}');
return;
},
@@ -133,24 +129,19 @@ fn stringify(
},
// TODO: .Many when there is a sentinel (waiting for https://github.com/ziglang/zig/pull/3972)
.Slice => {
if (ptr_info.child == u8 and options.string == .String and std.unicode.utf8ValidateSlice(value)) {
if (ptr_info.child == u8 and std.unicode.utf8ValidateSlice(value)) {
try json.encodeJsonString(value, options, out_stream);
return;
}
try out_stream.writeByte('[');
var child_options = options;
child_options.whitespace.indent_level += 1;
for (value, 0..) |x, i| {
if (i != 0) {
try out_stream.writeByte(',');
}
try child_options.whitespace.outputIndent(out_stream);
try stringify(x, child_options, out_stream);
}
if (value.len != 0) {
try options.whitespace.outputIndent(out_stream);
}
try out_stream.writeByte(']');
return;
},