Compare commits

..

5 Commits

Author SHA1 Message Date
zach
8bafdfb710 cleanup: add cost to each host function 2023-12-21 15:26:22 -08:00
zach
e1d33800f0 wip: add TimeoutManager::cost 2023-12-20 14:15:56 -08:00
zach
6084b69790 cleanup: remove scale factor from TimeoutManager 2023-12-18 12:44:20 -08:00
zach
5896729cfb feat: add extism_current_plugin_timeout_add_ms 2023-12-18 12:44:20 -08:00
zach
0f93c5ef9d feat: add TimeoutManager to add/subtract from a plugins timeout 2023-12-18 12:44:20 -08:00
20 changed files with 330 additions and 263 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 465 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 465 KiB

126
README.md
View File

@@ -1,111 +1,65 @@
<div align="center">
<a href="https://extism.org">
<picture>
<source media="(prefers-color-scheme: dark)" srcset=".github/assets/logo-horizontal-darkmode.png">
<img alt="Extism - the WebAssembly framework" width="75%" style="max-width: 600px" src=".github/assets/logo-horizontal.png">
</picture>
</a>
# [Extism](https://extism.org)
[![Discord](https://img.shields.io/discord/1011124058408112148?color=%23404eed&label=Community%20Chat&logo=Discord&logoColor=%23404eed)](https://extism.org/discord)
[![Discord](https://img.shields.io/discord/1011124058408112148?color=%23404eed&label=Community%20Chat&logo=Discord&logoColor=%23404eed)](https://discord.gg/cx3usBCWnc)
![GitHub Org's stars](https://img.shields.io/github/stars/extism)
![GitHub all releases](https://img.shields.io/github/downloads/extism/extism/total)
![GitHub License](https://img.shields.io/github/license/extism/extism)
![GitHub release (with filter)](https://img.shields.io/github/v/release/extism/extism)
</div>
The universal plug-in system. Run WebAssembly extensions inside your app. Use idiomatic Host SDKs for [Go](https://github.com/extism/go-sdk#readme),
[Ruby](https://github.com/extism/ruby-sdk#readme),
[Python](https://github.com/extism/python-sdk#readme),
[JavaScript](https://github.com/extism/js-sdk#readme),
[Rust](/runtime/#readme),
[C](libextism/#readme),
[C++](https://github.com/extism/cpp-sdk/#readme),
[OCaml](https://github.com/extism/ocaml-sdk#readme),
[Haskell](https://github.com/extism/haskell-sdk#readme),
[PHP](https://github.com/extism/php-sdk#readme),
[Elixir](https://github.com/extism/elixir-sdk#readme),
[.NET](https://github.com/extism/dotnet-sdk#readme),
[Java](https://github.com/extism/java-sdk#readme),
[Zig](https://github.com/extism/zig-sdk#readme),
[D](https://github.com/extism/d-sdk#readme),
&amp; more (others coming soon).
# Overview
Plug-in development kits (PDK) for plug-in authors supported in [Rust](https://github.com/extism/rust-pdk#readme), [AssemblyScript](https://github.com/extism/assemblyscript-pdk#readme), [Go](https://github.com/extism/go-pdk#readme), [C/C++](https://github.com/extism/c-pdk#readme), [Haskell](https://github.com/extism/haskell-pdk#readme), [JavaScript](https://github.com/extism/js-pdk#readme), [C#](https://github.com/extism/dotnet-pdk#readme), [F#](https://github.com/extism/dotnet-pdk#readme) and [Zig](https://github.com/extism/zig-pdk#readme).
Extism is a lightweight framework for building with WebAssembly (Wasm). It
supports running Wasm code on servers, the edge, CLIs, IoT, browsers and
everything in between. Extism is designed to be "universal" in that it supports
a common interface, no matter where it runs.
<p align="center">
<img style="width: 70%;" src="https://user-images.githubusercontent.com/7517515/210286900-39b144fd-1b26-4dd0-b7a9-2b5755bc174d.png" alt="Extism embedded SDK language support"/>
</p>
> **Note:** One of the primary use cases for Extism is **building extensible
> software & plugins**. You want to be able to execute arbitrary, untrusted code
> from your users? Extism makes this safe and practical to do.
Add a flexible, secure, and _bLaZiNg FaSt_ plug-in system to your project. Server, desktop, mobile, web, database -- you name it. Enable users to write and execute safe extensions to your software in **3 easy steps:**
Additionally, Extism adds some extra utilities on top of standard Wasm runtimes.
For example, we support persistent memory/module-scope variables, secure &
host-controlled HTTP without WASI, runtime limiters & timers, simpler host
function linking, and more. Extism users build:
### 1. Import
- plug-in systems
- FaaS platforms
- code generators
- web applications
- & much more...
Import an Extism Host SDK into your code as a library dependency.
# Run WebAssembly In Your App
### 2. Integrate
Pick a SDK to import into your program, and refer to the documentation to get
started:
Identify the place(s) in your code where some arbitrary logic should run (the plug-in!), returning your code some results.
| Type | Language | Source Code | Package |
| ----------- | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| Rust SDK | <img alt="Rust SDK" src="https://extism.org/img/sdk-languages/rust.svg" width="50px"/> | https://github.com/extism/extism/tree/main/runtime | [Crates.io](https://crates.io/crates/extism) |
| JS SDK | <img alt="JS SDK" src="https://extism.org/img/sdk-languages/js.svg" width="50px"/> | https://github.com/extism/js-sdk <br/>(supports Web, Node, Deno & Bun!) | [NPM](https://www.npmjs.com/package/@extism/extism) |
| Elixir SDK | <img alt="Elixir SDK" src="https://extism.org/img/sdk-languages/elixir.svg" width="50px"/> | https://github.com/extism/elixir-sdk | [Hex](https://hex.pm/packages/extism) |
| Go SDK | <img alt="Go SDK" src="https://extism.org/img/sdk-languages/go.svg" width="50px"/> | https://github.com/extism/go-sdk | [Go mod](https://pkg.go.dev/github.com/extism/go-sdk) |
| Haskell SDK | <img alt="Haskell SDK" src="https://extism.org/img/sdk-languages/haskell.svg" width="50px"/> | https://github.com/extism/haskell-sdk | [Hackage](https://hackage.haskell.org/package/extism) |
| Java SDK | <img alt="Java SDK" src="https://extism.org/img/sdk-languages/java-android.svg" width="50px"/> | https://github.com/extism/java-sdk | [Sonatype](https://central.sonatype.com/artifact/org.extism.sdk/extism) |
| .NET SDK | <img alt=".NET SDK" src="https://extism.org/img/sdk-languages/dotnet.svg" width="50px"/> | https://github.com/extism/dotnet-sdk <br/>(supports C# & F#!) | [Nuget](https://www.nuget.org/packages/Extism.Sdk) |
| OCaml SDK | <img alt="OCaml SDK" src="https://extism.org/img/sdk-languages/ocaml.svg" width="50px"/> | https://github.com/extism/ocaml-sdk | [opam](https://opam.ocaml.org/packages/extism/) |
| PHP SDK | <img alt="PHP SDK" src="https://extism.org/img/sdk-languages/php.svg" width="50px"/> | https://github.com/extism/php-sdk | [Packagist](https://packagist.org/packages/extism/extism) |
| Python SDK | <img alt="Python SDK" src="https://extism.org/img/sdk-languages/python.svg" width="50px"/> | https://github.com/extism/python-sdk | [PyPi](https://pypi.org/project/extism/) |
| Ruby SDK | <img alt="Ruby SDK" src="https://extism.org/img/sdk-languages/ruby.svg" width="50px"/> | https://github.com/extism/ruby-sdk | [RubyGems](https://rubygems.org/gems/extism) |
| Zig SDK | <img alt="Zig SDK" src="https://extism.org/img/sdk-languages/zig.svg" width="50px"/> | https://github.com/extism/zig-sdk | N/A |
| C SDK | <img alt="C SDK" src="https://extism.org/img/sdk-languages/c.svg" width="50px"/> | https://github.com/extism/extism/tree/main/libextism | N/A |
| C++ SDK | <img alt="C++ SDK" src="https://extism.org/img/sdk-languages/cpp.svg" width="50px"/> | https://github.com/extism/cpp-sdk | N/A |
### 3. Execute
# Compile WebAssembly to run in Extism Hosts
Load WebAssembly modules at any time in your app's lifetime and Extism will execute them in a secure sandbox, fully isolated from your program's memory.
Extism Hosts (running the SDK) must execute WebAssembly code that has a PDK
library compiled in to the `.wasm` binary. PDKs make it easy for plug-in /
extension code authors to read input from the host and return data back, read
provided configuration, set/get variables, make outbound HTTP calls if allowed,
and more.
# API Status
Pick a PDK to import into your Wasm program, and refer to the documentation to
get started:
**Please note:** This project still under active development and APIs are still changing. We are aiming for a stable 1.0 release in January, 2024.
The main branch may have breaking changes until that point, but if you starting today, a 1.0.0-rcx release is the best place to start.
| Type | Language | Source Code | Package |
| ------------------ | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | --------------------------------------------------------- |
| Rust PDK | <img alt="Rust PDK" src="https://extism.org/img/sdk-languages/rust.svg" width="50px"/> | https://github.com/extism/rust-pdk | [Crates.io](https://crates.io/crates/extism-pdk) |
| JS PDK | <img alt="JS PDK" src="https://extism.org/img/sdk-languages/js.svg" width="50px"/> | https://github.com/extism/js-pdk | N/A |
| Go PDK | <img alt="Go PDK" src="https://extism.org/img/sdk-languages/go.svg" width="50px"/> | https://github.com/extism/go-pdk | [Go mod](https://pkg.go.dev/github.com/extism/go-pdk) |
| Haskell PDK | <img alt="Haskell PDK" src="https://extism.org/img/sdk-languages/haskell.svg" width="50px"/> | https://github.com/extism/haskell-pdk | [Hackage](https://hackage.haskell.org/package/extism-pdk) |
| AssemblyScript PDK | <img alt="AssemblyScript PDK" src="https://extism.org/img/sdk-languages/assemblyscript.svg" width="50px"/> | https://github.com/extism/assemblyscript-pdk | [NPM](https://www.npmjs.com/package/@extism/as-pdk) |
| C PDK | <img alt="C PDK" src="https://extism.org/img/sdk-languages/c.svg" width="50px"/> | https://github.com/extism/c-pdk | N/A |
| Zig PDK | <img alt="Zig PDK" src="https://extism.org/img/sdk-languages/zig.svg" width="50px"/> | https://github.com/extism/zig-pdk | N/A |
| .NET PDK | <img alt=".NET PDK" src="https://extism.org/img/sdk-languages/dotnet.svg" width="50px"/> | https://github.com/extism/dotnet-pdk <br/>(supports C# & F#!) | N/A |
# Support
## Discord
If you experience any problems or have any questions, please join our
[Discord](https://extism.org/discord) and let us know. Our community is very
responsive and happy to help get you started.
If you experience any problems or have any questions, please join our [Discord](https://discord.gg/cx3usBCWnc) and let us know.
Our community is very responsive and happy to help get you started.
## Usage
Head to the [project website](https://extism.org) for more information and docs.
Also, consider reading an [overview](https://extism.org/docs/overview) of Extism
and its goals & approach.
Head to the [project website](https://extism.org) for more information and docs. Also, consider reading an [overview](https://extism.org/docs/overview) of Extism and its goals & approach.
## Contribution
Thank you for considering a contribution to Extism, we are happy to help you
make a PR or find something to work on!
Thank you for considering a contribution to Extism, we are happy to help you make a PR or find something to work on!
The easiest way to start would be to join the
[Discord](https://extism.org/discord) or open an issue on the
[`extism/proposals`](https://github.com/extism/proposals) issue tracker, which
can eventually become an Extism Improvement Proposal (EIP).
For more information, please read the
[Contributing](https://extism.org/docs/concepts/contributing) guide.
The easiest way to start would be to join the [Discord](https://discord.gg/cx3usBCWnc) or open an issue on the [`extism/proposals`](https://github.com/extism/proposals) issue tracker, which can eventually become an Extism Improvement Proposal (EIP).
---
@@ -114,8 +68,8 @@ For more information, please read the
Extism is an open-source product from the team at:
<p align="left">
<a href="https://dylibso.com" _target="blanks"><img width="200px" src="https://user-images.githubusercontent.com/7517515/198204119-5afdebb9-a5d8-4322-bd2a-46179c8d7b24.svg"/></a>
<a href="https://dylib.so" _target="blanks"><img width="200px" src="https://user-images.githubusercontent.com/7517515/198204119-5afdebb9-a5d8-4322-bd2a-46179c8d7b24.svg"/></a>
</p>
_Reach out and tell us what you're building! We'd love to help:_
<a href="mailto:hello@dylibso.com">hello@dylibso.com</a>
_Reach out and tell us what you're building! We'd love to help._

View File

@@ -14,7 +14,6 @@ anyhow = "1.0.75"
base64 = "~0.21"
bytemuck = {version = "1.14.0", optional = true }
prost = { version = "0.12.0", optional = true }
protobuf = { version = "3.2.0", optional = true }
rmp-serde = { version = "1.1.2", optional = true }
serde = "1.0.186"
serde_json = "1.0.105"
@@ -23,6 +22,7 @@ serde_json = "1.0.105"
serde = { version = "1.0.186", features = ["derive"] }
[features]
default = ["msgpack", "prost", "raw"]
default = ["msgpack", "protobuf", "raw"]
msgpack = ["rmp-serde"]
protobuf = ["prost"]
raw = ["bytemuck"]

View File

@@ -112,19 +112,19 @@ impl FromBytesOwned for Base64<String> {
/// Protobuf encoding
///
/// Allows for `prost` Protobuf messages to be used as arguments to Extism plugin calls
#[cfg(feature = "prost")]
#[cfg(feature = "protobuf")]
#[derive(Debug)]
pub struct Prost<T: prost::Message>(pub T);
pub struct Protobuf<T: prost::Message>(pub T);
#[cfg(feature = "prost")]
impl<T: prost::Message> From<T> for Prost<T> {
#[cfg(feature = "protobuf")]
impl<T: prost::Message> From<T> for Protobuf<T> {
fn from(data: T) -> Self {
Self(data)
}
}
#[cfg(feature = "prost")]
impl<'a, T: prost::Message> ToBytes<'a> for Prost<T> {
#[cfg(feature = "protobuf")]
impl<'a, T: prost::Message> ToBytes<'a> for Protobuf<T> {
type Bytes = Vec<u8>;
fn to_bytes(&self) -> Result<Self::Bytes, Error> {
@@ -132,32 +132,10 @@ impl<'a, T: prost::Message> ToBytes<'a> for Prost<T> {
}
}
#[cfg(feature = "prost")]
impl<T: Default + prost::Message> FromBytesOwned for Prost<T> {
#[cfg(feature = "protobuf")]
impl<T: Default + prost::Message> FromBytesOwned for Protobuf<T> {
fn from_bytes_owned(data: &[u8]) -> Result<Self, Error> {
Ok(Prost(T::decode(data)?))
}
}
/// Protobuf encoding
///
/// Allows for `rust-protobuf` Protobuf messages to be used as arguments to Extism plugin calls
#[cfg(feature = "protobuf")]
pub struct Protobuf<T: protobuf::Message>(pub T);
#[cfg(feature = "protobuf")]
impl<'a, T: protobuf::Message> ToBytes<'a> for Protobuf<T> {
type Bytes = Vec<u8>;
fn to_bytes(&self) -> Result<Self::Bytes, Error> {
Ok(self.0.write_to_bytes()?)
}
}
#[cfg(feature = "protobuf")]
impl<T: Default + protobuf::Message> FromBytesOwned for Protobuf<T> {
fn from_bytes_owned(data: &[u8]) -> Result<Self, Error> {
Ok(Protobuf(T::parse_from_bytes(data)?))
Ok(Protobuf(T::decode(data)?))
}
}

View File

@@ -18,9 +18,6 @@ pub use encoding::{Base64, Json};
#[cfg(feature = "msgpack")]
pub use encoding::Msgpack;
#[cfg(feature = "prost")]
pub use encoding::Prost;
#[cfg(feature = "protobuf")]
pub use encoding::Protobuf;

View File

@@ -39,7 +39,7 @@
use core::sync::atomic::*;
pub type Pointer = u64;
pub type Handle = u64;
pub type Length = u64;
/// WebAssembly page size
const PAGE_SIZE: usize = 65536;
@@ -81,13 +81,13 @@ pub struct MemoryRoot {
/// Offset of error block
pub error: AtomicU64,
/// Input position in memory
pub input_offset: Handle,
pub input_offset: Pointer,
/// Input length
pub input_length: u64,
pub input_length: Length,
/// Output position in memory
pub output_offset: Pointer,
/// Output length
pub output_length: u64,
pub output_length: Length,
/// A pointer to the start of the first block
pub blocks: [MemoryBlock; 0],
}
@@ -206,7 +206,7 @@ impl MemoryRoot {
// is used to avoid loading the allocators position more than once when performing an allocation.
unsafe fn find_free_block(
&mut self,
length: u64,
length: Length,
self_position: u64,
) -> Option<&'static mut MemoryBlock> {
// Get the first block
@@ -252,7 +252,7 @@ impl MemoryRoot {
/// 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: u64) -> Option<&'static mut MemoryBlock> {
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);
@@ -350,21 +350,21 @@ impl MemoryBlock {
/// Allocate a block of memory and return the offset
#[no_mangle]
pub unsafe fn alloc(n: u64) -> Handle {
pub unsafe fn alloc(n: Length) -> Pointer {
if n == 0 {
return 0;
}
let region = MemoryRoot::new();
let block = region.alloc(n);
match block {
Some(block) => block.data.as_mut_ptr() as Handle,
Some(block) => block.data.as_mut_ptr() as Pointer,
None => 0,
}
}
/// Free allocated memory
#[no_mangle]
pub unsafe fn free(p: Handle) {
pub unsafe fn free(p: Pointer) {
if p == 0 {
return;
}
@@ -382,42 +382,13 @@ pub unsafe fn free(p: Handle) {
}
/// Get the length of an allocated memory block
///
/// Note: this should only be called on memory handles returned
/// by a call to `alloc` - it will return garbage on invalid offsets
#[no_mangle]
pub unsafe fn length_unsafe(p: Handle) -> u64 {
pub unsafe fn length(p: Pointer) -> Length {
if p == 0 {
return 0;
}
if !MemoryRoot::pointer_in_bounds_fast(p) {
return 0;
}
let ptr = p - core::mem::size_of::<MemoryBlock>() as u64;
let block = &mut *(ptr as *mut MemoryBlock);
// Simplest sanity check to verify the pointer is a block
if block.status.load(Ordering::Acquire) != MemoryStatus::Active as u8 {
return 0;
}
block.used as u64
}
/// Get the length but returns 0 if the offset is not a valid handle.
///
/// Note: this function walks each node in the allocations list, which ensures correctness, but is also
/// slow
#[no_mangle]
pub unsafe fn length(p: Pointer) -> u64 {
if p == 0 {
return 0;
}
if let Some(block) = MemoryRoot::new().find_block(p) {
block.used as u64
block.used as Length
} else {
0
}
@@ -445,24 +416,24 @@ pub unsafe fn load_u64(p: Pointer) -> u64 {
/// Load a byte from the input data
#[no_mangle]
pub unsafe fn input_load_u8(offset: u64) -> u8 {
pub unsafe fn input_load_u8(p: Pointer) -> u8 {
let root = MemoryRoot::new();
#[cfg(feature = "bounds-checking")]
if offset >= root.input_length {
if p >= root.input_length {
return 0;
}
*((root.input_offset + offset) as *mut u8)
*((root.input_offset + p) as *mut u8)
}
/// Load a u64 from the input data
#[no_mangle]
pub unsafe fn input_load_u64(offset: u64) -> u64 {
pub unsafe fn input_load_u64(p: Pointer) -> u64 {
let root = MemoryRoot::new();
#[cfg(feature = "bounds-checking")]
if offset + core::mem::size_of::<u64>() as u64 > root.input_length {
if p + core::mem::size_of::<u64>() as Pointer > root.input_length {
return 0;
}
*((root.input_offset + offset) as *mut u64)
*((root.input_offset + p) as *mut u64)
}
/// Write a byte in Extism-managed memory
@@ -486,24 +457,22 @@ pub unsafe fn store_u64(p: Pointer, x: u64) {
}
/// Set the range of the input data in memory
/// h must always be a handle so that length works on it
/// len must match length(handle)
#[no_mangle]
pub unsafe fn input_set(h: Handle, len: u64) {
pub unsafe fn input_set(p: Pointer, len: Length) {
let root = MemoryRoot::new();
#[cfg(feature = "bounds-checking")]
{
if !root.pointer_in_bounds(h) || !root.pointer_in_bounds(h + len - 1) {
if !root.pointer_in_bounds(p) || !root.pointer_in_bounds(p + len - 1) {
return;
}
}
root.input_offset = h;
root.input_offset = p;
root.input_length = len;
}
/// Set the range of the output data in memory
#[no_mangle]
pub unsafe fn output_set(p: Pointer, len: u64) {
pub unsafe fn output_set(p: Pointer, len: Length) {
let root = MemoryRoot::new();
#[cfg(feature = "bounds-checking")]
{
@@ -517,25 +486,25 @@ pub unsafe fn output_set(p: Pointer, len: u64) {
/// Get the input length
#[no_mangle]
pub fn input_length() -> u64 {
pub fn input_length() -> Length {
unsafe { MemoryRoot::new().input_length }
}
/// Get the input offset in Exitsm-managed memory
#[no_mangle]
pub fn input_offset() -> Handle {
pub fn input_offset() -> Length {
unsafe { MemoryRoot::new().input_offset }
}
/// Get the output length
#[no_mangle]
pub fn output_length() -> u64 {
pub fn output_length() -> Length {
unsafe { MemoryRoot::new().output_length }
}
/// Get the output offset in Extism-managed memory
#[no_mangle]
pub unsafe fn output_offset() -> Pointer {
pub unsafe fn output_offset() -> Length {
MemoryRoot::new().output_offset
}
@@ -547,30 +516,30 @@ pub unsafe fn reset() {
/// Set the error message offset
#[no_mangle]
pub unsafe fn error_set(h: Handle) {
pub unsafe fn error_set(ptr: Pointer) {
let root = MemoryRoot::new();
// Allow ERROR to be set to 0
if h == 0 {
root.error.store(h, Ordering::SeqCst);
if ptr == 0 {
root.error.store(ptr, Ordering::SeqCst);
return;
}
#[cfg(feature = "bounds-checking")]
if !root.pointer_in_bounds(h) {
if !root.pointer_in_bounds(ptr) {
return;
}
root.error.store(h, Ordering::SeqCst);
root.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 error_get() -> Handle {
pub unsafe fn error_get() -> Pointer {
MemoryRoot::new().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 memory_bytes() -> u64 {
pub unsafe fn memory_bytes() -> Length {
MemoryRoot::new().length.load(Ordering::Acquire)
}

View File

@@ -9,8 +9,8 @@ repository.workspace = true
version.workspace = true
[dependencies]
wasmtime = ">= 14.0.0, < 17.0.0"
wasmtime-wasi = ">= 14.0.0, < 17.0.0"
wasmtime = ">= 14.0.0, < 16.0.0"
wasmtime-wasi = ">= 14.0.0, < 16.0.0"
anyhow = "1"
serde = {version = "1", features = ["derive"]}
serde_json = "1"
@@ -33,7 +33,7 @@ register-filesystem = [] # enables wasm to be loaded from disk
http = ["ureq"] # enables extism_http_request
[build-dependencies]
cbindgen = { version = "0.26", default-features = false }
cbindgen = "0.26"
[dev-dependencies]
criterion = "0.5.1"

View File

@@ -173,6 +173,13 @@ void extism_function_free(ExtismFunction *f);
*/
void extism_function_set_namespace(ExtismFunction *ptr, const char *namespace_);
/**
* Set the cost of an `ExtismFunction`, when set to 0 this has no effect, when set to `1` this will add
* the runtime of the function back to the plugin timer.
*/
void extism_function_set_cost(ExtismFunction *ptr,
double cost);
/**
* Create a new plugin with host functions, the functions passed to this function no longer need to be manually freed using
*

View File

@@ -246,26 +246,6 @@ impl CurrentPlugin {
Ok(len)
}
pub fn memory_length_unsafe(&mut self, offs: u64) -> Result<u64, Error> {
let (linker, mut store) = self.linker_and_store();
let output = &mut [Val::I64(0)];
if let Some(f) = linker.get(&mut store, EXTISM_ENV_MODULE, "length_unsafe") {
f.into_func()
.unwrap()
.call(&mut store, &[Val::I64(offs as i64)], output)?;
} else {
anyhow::bail!("unable to locate an extism kernel function: length_unsafe",)
}
let len = output[0].unwrap_i64() as u64;
trace!(
plugin = self.id.to_string(),
"memory_length_unsafe({}) = {}",
offs,
len
);
Ok(len)
}
/// Access a plugin's variables
pub fn vars(&self) -> &std::collections::BTreeMap<String, Vec<u8>> {
&self.vars

Binary file not shown.

View File

@@ -155,7 +155,7 @@ unsafe impl<T> Sync for UserData<T> {}
unsafe impl Send for CPtr {}
unsafe impl Sync for CPtr {}
type FunctionInner = dyn Fn(wasmtime::Caller<CurrentPlugin>, &[wasmtime::Val], &mut [wasmtime::Val]) -> Result<(), Error>
pub(crate) type FunctionInner = dyn Fn(wasmtime::Caller<CurrentPlugin>, &[wasmtime::Val], &mut [wasmtime::Val]) -> Result<(), Error>
+ Sync
+ Send;
@@ -174,6 +174,9 @@ pub struct Function {
/// Function handle
pub(crate) f: Arc<FunctionInner>,
/// Function cost (in terms of time)
pub(crate) cost: f64,
/// UserData
pub(crate) _user_data: UserDataHandle,
}
@@ -204,10 +207,12 @@ impl Function {
ty,
f: Arc::new(
move |mut caller: Caller<_>, inp: &[Val], outp: &mut [Val]| {
let plugin = caller.data_mut();
let x = data.clone();
f(caller.data_mut(), inp, outp, x)
f(plugin, inp, outp, x)
},
),
cost: 0.0,
namespace: None,
_user_data: match &user_data {
UserData::C(ptr) => UserDataHandle::C(ptr.clone()),
@@ -239,6 +244,23 @@ impl Function {
self
}
/// Function cost
pub fn cost(&self) -> f64 {
self.cost
}
/// Set host function cost
pub fn set_cost(&mut self, cost: f64) {
trace!("Setting cost for {} to {cost}", self.name);
self.cost = cost;
}
/// Update host function cost
pub fn with_cost(mut self, cost: f64) -> Self {
self.set_cost(cost);
self
}
/// Get function type
pub fn ty(&self) -> &wasmtime::FuncType {
&self.ty

View File

@@ -14,6 +14,7 @@ pub(crate) mod manifest;
pub(crate) mod pdk;
mod plugin;
mod plugin_builder;
mod timeout_manager;
mod timer;
/// Extism C API
@@ -27,6 +28,7 @@ pub use plugin::{CancelHandle, Plugin, WasmInput, EXTISM_ENV_MODULE, EXTISM_USER
pub use plugin_builder::{DebugOptions, PluginBuilder};
pub(crate) use internal::{Internal, Wasi};
pub(crate) use timeout_manager::TimeoutManager;
pub(crate) use timer::{Timer, TimerAction};
pub(crate) use tracing::{debug, error, trace, warn};

View File

@@ -117,17 +117,10 @@ pub(crate) fn load(
match input {
WasmInput::Data(data) => {
let has_magic = data.len() >= 4 && data[0..4] == WASM_MAGIC;
let s = std::str::from_utf8(&data);
let is_wat = s.is_ok_and(|s| {
let s = s.trim_start();
let starts_with_module = s.len() > 2
&& data[0] == b'(' // First character is `(`
&& s[1..].trim_start().starts_with("module"); // Then `module` (after any whitespace)
starts_with_module || s.starts_with(";;") || s.starts_with("(;")
});
let is_wat = data.starts_with(b"(module") || data.starts_with(b";;");
if !has_magic && !is_wat {
trace!("Loading manifest");
if let Ok(s) = s {
if let Ok(s) = std::str::from_utf8(&data) {
let t = if let Ok(t) = toml::from_str::<extism_manifest::Manifest>(s) {
trace!("Manifest is TOML");
modules(engine, &t, &mut mods)?;

View File

@@ -238,6 +238,20 @@ impl Plugin {
CurrentPlugin::new(manifest, with_wasi, available_pages, id)?,
);
store.set_epoch_deadline(1);
store.call_hook(|data, hook| {
if hook.entering_host() {
let tx = Timer::tx();
tx.send(TimerAction::EnterHost {
id: data.id.clone(),
})?;
} else if hook.exiting_host() {
let tx = Timer::tx();
tx.send(TimerAction::ExitHost {
id: data.id.clone(),
})?;
}
Ok(())
});
let mut linker = Linker::new(&engine);
linker.allow_shadowing(true);
@@ -284,9 +298,13 @@ impl Plugin {
for f in &mut imports {
let name = f.name().to_string();
let ns = f.namespace().unwrap_or(EXTISM_USER_MODULE);
unsafe {
linker.func_new(ns, &name, f.ty().clone(), &*(f.f.as_ref() as *const _))?;
}
let cost = f.cost();
let inner: &function::FunctionInner = unsafe { &*(f.f.as_ref() as *const _) };
linker.func_new(ns, &name, f.ty().clone(), move |caller, params, results| {
let _timeout =
TimeoutManager::new(caller.data()).map(|x| x.with_cost(cost.clone()));
inner(caller, params, results)
})?;
}
let instance_pre = linker.instantiate_pre(main)?;
@@ -824,24 +842,6 @@ impl Plugin {
.and_then(move |_| self.output())
}
/// Similar to `Plugin::call`, but returns the Extism error code along with the
/// `Error`. It is assumed if `Ok(_)` is returned that the error code was `0`.
///
/// All Extism plugin calls return an error code, `Plugin::call` consumes the error code,
/// while `Plugin::call_get_error_code` preserves it - this function should only be used
/// when you need to inspect the actual return value of a plugin function when it fails.
pub fn call_get_error_code<'a, 'b, T: ToBytes<'a>, U: FromBytes<'b>>(
&'b mut self,
name: impl AsRef<str>,
input: T,
) -> Result<U, (Error, i32)> {
let lock = self.instance.clone();
let mut lock = lock.lock().unwrap();
let data = input.to_bytes().map_err(|e| (e, -1))?;
self.raw_call(&mut lock, name, data)
.and_then(move |_| self.output().map_err(|e| (e, -1)))
}
/// Get a `CancelHandle`, which can be used from another thread to cancel a running plugin
pub fn cancel_handle(&self) -> CancelHandle {
self.cancel_handle.clone()

View File

@@ -258,6 +258,18 @@ pub unsafe extern "C" fn extism_function_set_namespace(
}
}
/// Set the cost of an `ExtismFunction`, when set to 0 this has no effect, when set to `1` this will add
/// the runtime of the function back to the plugin timer.
#[no_mangle]
pub unsafe extern "C" fn extism_function_set_cost(ptr: *mut ExtismFunction, cost: f64) {
let f = &mut *ptr;
if let Some(x) = f.0.get_mut() {
x.set_cost(cost);
} else {
debug!("Trying to set the cost of already registered function")
}
}
/// Create a new plugin with host functions, the functions passed to this function no longer need to be manually freed using
///
/// `wasm`: is a WASM module (wat or wasm) or a JSON encoded manifest

View File

@@ -22,20 +22,6 @@ fn extism_length<T>(mut store: &mut wasmtime::Store<T>, instance: &mut Instance,
out[0].unwrap_i64() as u64
}
fn extism_length_unsafe<T>(
mut store: &mut wasmtime::Store<T>,
instance: &mut Instance,
p: u64,
) -> u64 {
let out = &mut [Val::I64(0)];
instance
.get_func(&mut store, "length_unsafe")
.unwrap()
.call(&mut store, &[Val::I64(p as i64)], out)
.unwrap();
out[0].unwrap_i64() as u64
}
fn extism_load_u8<T>(mut store: &mut wasmtime::Store<T>, instance: &mut Instance, p: u64) -> u8 {
let out = &mut [Val::I32(0)];
instance
@@ -187,7 +173,6 @@ fn test_kernel_allocations() {
let first_alloc = p;
assert!(p > 0);
assert_eq!(extism_length(&mut store, instance, p), 1);
assert_eq!(extism_length_unsafe(&mut store, instance, p), 1);
extism_free(&mut store, instance, p);
// 2 bytes
@@ -195,21 +180,18 @@ fn test_kernel_allocations() {
assert!(x > 0);
assert!(x != p);
assert_eq!(extism_length(&mut store, instance, x), 2);
assert_eq!(extism_length_unsafe(&mut store, instance, x), 2);
extism_free(&mut store, instance, x);
for i in 0..64 {
let p = extism_alloc(&mut store, instance, 64 - i);
assert!(p > 0);
assert_eq!(extism_length(&mut store, instance, p), 64 - i);
assert_eq!(extism_length_unsafe(&mut store, instance, p), 64 - i);
extism_free(&mut store, instance, p);
// should re-use the last allocation
let q = extism_alloc(&mut store, instance, 64 - i);
assert_eq!(p, q);
assert_eq!(extism_length(&mut store, instance, q), 64 - i);
assert_eq!(extism_length_unsafe(&mut store, instance, q), 64 - i);
extism_free(&mut store, instance, q);
}

View File

@@ -235,6 +235,43 @@ fn test_timeout() {
assert!(err == "timeout");
}
fn hello_world_timeout_manager(
plugin: &mut CurrentPlugin,
inputs: &[Val],
outputs: &mut [Val],
_user_data: UserData<()>,
) -> Result<(), Error> {
let mgr = plugin.timeout_manager().map(|x| x.add_on_drop());
std::thread::sleep(std::time::Duration::from_secs(3));
outputs[0] = inputs[0].clone();
drop(mgr);
Ok(())
}
#[test]
fn test_timeout_manager() {
let f = Function::new(
"hello_world",
[PTR],
[PTR],
UserData::default(),
hello_world_timeout_manager,
);
let manifest = Manifest::new([extism_manifest::Wasm::data(WASM)])
.with_timeout(std::time::Duration::from_secs(1));
let mut plugin = Plugin::new(manifest, [f], true).unwrap();
let start = std::time::Instant::now();
let output: Result<&[u8], Error> = plugin.call("count_vowels", "testing");
println!("Result {:?}", output);
assert!(output.is_ok());
let end = std::time::Instant::now();
let time = end - start;
println!("Plugin ran for {:?}", time);
assert!(time.as_secs() >= 3);
}
typed_plugin!(pub TestTypedPluginGenerics {
count_vowels<T: FromBytes<'a>>(&str) -> T
});

View File

@@ -0,0 +1,59 @@
use crate::*;
/// `TimeoutManager` is used to control `Plugin` timeouts from within host functions
///
/// It can be used to add or subtract time from a plug-in's timeout. If a plugin is not
/// configured to have a timeout then this will have no effect.
pub(crate) struct TimeoutManager {
start_time: std::time::Instant,
id: uuid::Uuid,
tx: std::sync::mpsc::Sender<TimerAction>,
cost: f64,
}
impl TimeoutManager {
/// Create a new `TimeoutManager` from the `CurrentPlugin`, this will return `None` if no timeout
/// is configured
pub fn new(plugin: &CurrentPlugin) -> Option<TimeoutManager> {
plugin.manifest.timeout_ms.map(|_| TimeoutManager {
start_time: std::time::Instant::now(),
id: plugin.id.clone(),
tx: Timer::tx(),
cost: 0.0,
})
}
/// Add the amount of time this value has existed to the configured timeout
pub fn add_elapsed(&mut self) -> Result<(), Error> {
let cost = self.cost.abs();
let d = self.start_time.elapsed().mul_f64(cost);
let mut d: timer::ExtendTimeout = d.into();
if self.cost.is_sign_negative() {
d = -d;
}
self.tx.send(TimerAction::Extend {
id: self.id.clone(),
duration: d,
})?;
self.start_time = std::time::Instant::now();
Ok(())
}
pub fn with_cost(mut self, cost: f64) -> Self {
self.cost = cost;
self
}
}
impl Drop for TimeoutManager {
fn drop(&mut self) {
let x = self.add_elapsed();
if let Err(e) = x {
error!(
plugin = self.id.to_string(),
"unable to extend timeout: {}",
e.to_string()
);
}
}
}

View File

@@ -1,17 +1,51 @@
use crate::*;
#[derive(Debug)]
pub(crate) struct ExtendTimeout {
duration: std::time::Duration,
negative: bool,
}
impl From<std::time::Duration> for ExtendTimeout {
fn from(value: std::time::Duration) -> Self {
ExtendTimeout {
duration: value,
negative: false,
}
}
}
impl std::ops::Neg for ExtendTimeout {
type Output = ExtendTimeout;
fn neg(mut self) -> Self::Output {
self.negative = !self.negative;
self
}
}
pub(crate) enum TimerAction {
Start {
id: uuid::Uuid,
engine: Engine,
duration: Option<std::time::Duration>,
},
Extend {
id: uuid::Uuid,
duration: ExtendTimeout,
},
Stop {
id: uuid::Uuid,
},
Cancel {
id: uuid::Uuid,
},
EnterHost {
id: uuid::Uuid,
},
ExitHost {
id: uuid::Uuid,
},
Shutdown,
}
@@ -31,6 +65,8 @@ extern "C" fn cleanup_timer() {
static mut TIMER: std::sync::Mutex<Option<Timer>> = std::sync::Mutex::new(None);
type TimerMap = std::collections::BTreeMap<uuid::Uuid, (Engine, Option<std::time::Instant>)>;
impl Timer {
pub(crate) fn tx() -> std::sync::mpsc::Sender<TimerAction> {
let mut timer = match unsafe { TIMER.lock() } {
@@ -49,7 +85,8 @@ impl Timer {
pub fn init(timer: &mut Option<Timer>) -> std::sync::mpsc::Sender<TimerAction> {
let (tx, rx) = std::sync::mpsc::channel();
let thread = std::thread::spawn(move || {
let mut plugins = std::collections::BTreeMap::new();
let mut plugins = TimerMap::new();
let mut in_host = TimerMap::new();
macro_rules! handle {
($x:expr) => {
@@ -70,12 +107,16 @@ impl Timer {
TimerAction::Stop { id } => {
trace!(plugin = id.to_string(), "handling stop event");
plugins.remove(&id);
in_host.remove(&id);
}
TimerAction::Cancel { id } => {
trace!(plugin = id.to_string(), "handling cancel event");
if let Some((engine, _)) = plugins.remove(&id) {
engine.increment_epoch();
}
if let Some((engine, _)) = in_host.remove(&id) {
engine.increment_epoch();
}
}
TimerAction::Shutdown => {
trace!("Shutting down timer");
@@ -83,8 +124,42 @@ impl Timer {
trace!(plugin = id.to_string(), "handling shutdown event");
engine.increment_epoch();
}
for (id, (engine, _)) in in_host.iter() {
trace!(plugin = id.to_string(), "handling shutdown event");
engine.increment_epoch();
}
return;
}
TimerAction::Extend { id, duration } => {
if let Some((_engine, Some(timeout))) = plugins.get_mut(&id) {
let x = if duration.negative {
timeout.checked_sub(duration.duration)
} else {
timeout.checked_add(duration.duration)
};
if let Some(t) = x {
*timeout = t;
} else {
error!(
plugin = id.to_string(),
"unable to extend timeout by {:?}", duration.duration
);
}
}
}
TimerAction::EnterHost { id } => {
trace!(plugin = id.to_string(), "enter host function");
if let Some(x) = plugins.remove(&id) {
in_host.insert(id, x);
}
}
TimerAction::ExitHost { id } => {
trace!(plugin = id.to_string(), "exit host function");
if let Some(x) = in_host.remove(&id) {
plugins.insert(id, x);
}
}
}
};
}
@@ -96,6 +171,10 @@ impl Timer {
}
}
for x in rx.try_iter() {
handle!(x)
}
plugins = plugins
.into_iter()
.filter(|(_k, (engine, end))| {
@@ -109,10 +188,6 @@ impl Timer {
true
})
.collect();
for x in rx.try_iter() {
handle!(x)
}
}
});
*timer = Some(Timer {