mirror of
https://github.com/extism/extism.git
synced 2026-01-12 07:18:02 -05:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7230657f6f | ||
|
|
de81040c99 | ||
|
|
b79e74d516 | ||
|
|
52c160b9ec | ||
|
|
f68a548df4 | ||
|
|
3d15c76115 | ||
|
|
0f4c32e68d | ||
|
|
9e5729b103 | ||
|
|
04cf39e751 | ||
|
|
7133dfc4e0 | ||
|
|
d1ba15484e | ||
|
|
dedd81d90f | ||
|
|
2732ca198d | ||
|
|
30b4a7d2d3 |
11
.github/workflows/release-dotnet-native.yaml
vendored
11
.github/workflows/release-dotnet-native.yaml
vendored
@@ -20,10 +20,13 @@ jobs:
|
||||
uses: actions/setup-dotnet@v3.0.3
|
||||
with:
|
||||
dotnet-version: 7.x
|
||||
- uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
workflow: release.yml
|
||||
name: release-artifacts
|
||||
- name: download release
|
||||
run: |
|
||||
tag='${{ github.ref }}'
|
||||
tag="${tag/refs\/tags\//}"
|
||||
gh release download "$tag" -p 'libextism-*.tar.gz'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Extract Archive
|
||||
run: |
|
||||
extract_archive() {
|
||||
|
||||
@@ -13,7 +13,7 @@ description = "Traits to make Rust types usable with Extism"
|
||||
anyhow = "1.0.75"
|
||||
base64 = "~0.22"
|
||||
bytemuck = {version = "1.14.0", optional = true }
|
||||
prost = { version = "0.13.1", optional = true }
|
||||
prost = { version = "0.14.1", optional = true }
|
||||
protobuf = { version = "3.2.0", optional = true }
|
||||
rmp-serde = { version = "1.1.2", optional = true }
|
||||
serde = "1.0.186"
|
||||
|
||||
@@ -9,35 +9,54 @@ repository.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
wasmtime = {version = ">= 27.0.0, < 31.0.0"}
|
||||
wasi-common = {version = ">= 27.0.0, < 31.0.0"}
|
||||
wiggle = {version = ">= 27.0.0, < 31.0.0"}
|
||||
wasmtime = { version="37", default-features = false, features = [
|
||||
'cache',
|
||||
'gc',
|
||||
'gc-drc',
|
||||
'cranelift',
|
||||
'coredump',
|
||||
'wat',
|
||||
'parallel-compilation',
|
||||
'pooling-allocator',
|
||||
'demangle',
|
||||
] }
|
||||
wasi-common = "37"
|
||||
wiggle = "37"
|
||||
anyhow = "1"
|
||||
serde = {version = "1", features = ["derive"]}
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
toml = "0.8"
|
||||
toml = "0.9"
|
||||
sha2 = "0.10"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = {version = "0.3.18", features = ["std", "env-filter", "fmt"]}
|
||||
tracing-subscriber = { version = "0.3.18", features = [
|
||||
"std",
|
||||
"env-filter",
|
||||
"fmt",
|
||||
] }
|
||||
url = "2"
|
||||
glob = "0.3"
|
||||
ureq = {version = "3.0", optional=true}
|
||||
ureq = { version = "3.0", optional = true }
|
||||
extism-manifest = { workspace = true }
|
||||
extism-convert = { workspace = true, features = ["extism-path"] }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
libc = "0.2"
|
||||
|
||||
[features]
|
||||
default = ["http", "register-http", "register-filesystem"]
|
||||
register-http = ["ureq"] # enables wasm to be downloaded using http
|
||||
register-filesystem = [] # enables wasm to be loaded from disk
|
||||
http = ["ureq"] # enables extism_http_request
|
||||
default = ["http", "register-http", "register-filesystem", "wasmtime-default-features"]
|
||||
register-http = ["ureq"] # enables wasm to be downloaded using http
|
||||
register-filesystem = [] # enables wasm to be loaded from disk
|
||||
http = ["ureq"] # enables extism_http_request
|
||||
wasmtime-exceptions = [] # enables exception-handling proposal in wasmtime (requires wasmtime gc feature)
|
||||
wasmtime-default-features = [
|
||||
'wasmtime/default',
|
||||
]
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
cbindgen = { version = "0.29", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.6.0"
|
||||
criterion = "0.7.0"
|
||||
quickcheck = "1"
|
||||
rand = "0.9.0"
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ There are a few environment variables that can be used for debugging purposes:
|
||||
- `EXTISM_COREDUMP=extism.core`: write [coredump](https://github.com/WebAssembly/tool-conventions/blob/main/Coredump.md) to a file when a WebAssembly function traps
|
||||
- `EXTISM_DEBUG=1`: generate debug information
|
||||
- `EXTISM_PROFILE=perf|jitdump|vtune`: enable Wasmtime profiling
|
||||
- `EXTISM_CACHE_CONFIG=path/to/config.toml`: enable Wasmtime cache, see [the docs](https://docs.wasmtime.dev/cli-cache.html) for details about configuration. Setting this to an empty string will disable caching.
|
||||
- `EXTISM_CACHE_CONFIG=path/to/config.toml`: enable Wasmtime cache, details [here](#wasmtime-caching)
|
||||
|
||||
> *Note*: The debug and coredump info will only be written if the plug-in has an error.
|
||||
|
||||
@@ -112,7 +112,8 @@ let mut plugin = Plugin::new(&manifest, [], true);
|
||||
let res = plugin.call::<&str, &str>("count_vowels", "Yellow, world!").unwrap();
|
||||
println!("{}", res);
|
||||
# => {"count": 3, "total": 3, "vowels": "aeiouAEIOU"}
|
||||
let mut plugin = Plugin::new(&manifest, [], true).with_config_key("vowels", "aeiouyAEIOUY");
|
||||
let manifest = Manifest::new([url]).with_config_key("vowels", "aeiouyAEIOUY");
|
||||
let mut plugin = Plugin::new(&manifest, [], true).unwrap();
|
||||
let res = plugin.call::<&str, &str>("count_vowels", "Yellow, world!").unwrap();
|
||||
println!("{}", res);
|
||||
# => {"count": 4, "total": 4, "vowels": "aeiouyAEIOUY"}
|
||||
@@ -229,3 +230,42 @@ Inside your host application, the rust-sdk emits these as [tracing](https://gith
|
||||
tracing_subscriber::fmt::init();
|
||||
```
|
||||
|
||||
### Wasmtime Caching
|
||||
|
||||
To enable or disable caching for plugin compilation, you need to provide a configuration file that will be used by the [wasmtime crate](https://github.com/bytecodealliance/wasmtime).
|
||||
|
||||
For more information and values that can be used for configuring caching, take a look at [the docs](https://docs.wasmtime.dev/cli-cache.html).
|
||||
|
||||
> *Note*: As of now extism uses wasmtime [`version = ">= 27.0.0, < 31.0.0"`](https://github.com/extism/extism/blob/v1.11.1/runtime/Cargo.toml#L12), but the `enabled` key requirement [was removed](https://github.com/bytecodealliance/wasmtime/pull/10859) from `wasmtime` and its documentation, this could explain the `failed to parse config file` error you might encounter without it.
|
||||
|
||||
An example configuration for caching would be:
|
||||
|
||||
```toml
|
||||
[cache]
|
||||
enabled = true # This value is required
|
||||
directory = "/some/path"
|
||||
```
|
||||
|
||||
You can :
|
||||
- [Create a global `wasmtime` configuration file](#using-a-configuration-file) in `$HOME/.config/wasmtime/config.toml`.
|
||||
- [Set the `EXTISM_CACHE_CONFIG` environment variable](#using-an-environment-variable)
|
||||
- [Set the configuration file path using `PluginBuilder`](#using-pluginbuilder)
|
||||
|
||||
#### Using a configuration file
|
||||
|
||||
The [wasmtime](https://github.com/bytecodealliance/wasmtime) crate, by default, will look for a configuration file in your systems' default configuration directory (for example on UNIX systems: `$HOME/.config/wasmtime/config.toml`),
|
||||
for more [information on this behaviour](`https://docs.rs/wasmtime/31.0.0/wasmtime/struct.Config.html#method.cache_config_load_default`).
|
||||
|
||||
#### Using an environment variable
|
||||
|
||||
You can set the `EXTISM_CACHE_CONFIG=path/to/config.toml` environment variable to set the path of the configuration file used by [wasmtime](https://github.com/bytecodealliance/wasmtime).
|
||||
Setting the variable to an empty string will disable caching (it won't load any configuration file).
|
||||
|
||||
> *Note*: If the environment variable is not set, `wasmtime` will still try to read from a configuration file that may exist in your system's default configuration folder (e.g. `$HOME/.config/wasmtime/config.toml`).
|
||||
|
||||
The environment variable does not override the path you might have set using `PluginBuilder`. will only be checked for if you did not specify a cache configuration path in `PluginBuilder`.
|
||||
|
||||
#### Using PluginBuilder
|
||||
|
||||
If you use a [PluginBuilder](https://docs.rs/extism/latest/extism/struct.PluginBuilder.html), you can set the `wasmtime` configuration path using the [with_cache_config](https://docs.rs/extism/latest/extism/struct.PluginBuilder.html#method.with_cache_config) method.
|
||||
This will override the `EXTISM_CACHE_CONFIG` environment variable if it's set, so you could have a "global" and per plugin configuration if needed.
|
||||
|
||||
@@ -16,7 +16,7 @@ fn main() {
|
||||
|
||||
let res = plugin.call::<&str, &str>("try_read", "").unwrap();
|
||||
|
||||
println!("{:?}", res);
|
||||
println!("{res:?}");
|
||||
|
||||
println!("-----------------------------------------------------");
|
||||
|
||||
@@ -30,7 +30,7 @@ fn main() {
|
||||
);
|
||||
let res2 = plugin.call::<&str, &str>("try_write", &line).unwrap();
|
||||
|
||||
println!("{:?}", res2);
|
||||
println!("{res2:?}");
|
||||
|
||||
println!("done!");
|
||||
}
|
||||
|
||||
@@ -30,6 +30,6 @@ fn main() {
|
||||
let res = plugin
|
||||
.call::<&str, &str>("reflect", "Hello, world!")
|
||||
.unwrap();
|
||||
println!("{}", res);
|
||||
println!("{res}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@ fn main() {
|
||||
println!("Dumping logs");
|
||||
|
||||
for line in LOGS.lock().unwrap().iter() {
|
||||
print!("{}", line);
|
||||
print!("{line}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,6 @@ fn main() {
|
||||
let res = plugin
|
||||
.call::<&str, &str>("count_vowels", "Hello, world!")
|
||||
.unwrap();
|
||||
println!("{}", res);
|
||||
println!("{res}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +191,17 @@ ExtismCompiledPlugin *extism_compiled_plugin_new(const uint8_t *wasm,
|
||||
bool with_wasi,
|
||||
char **errmsg);
|
||||
|
||||
/**
|
||||
* Pre-compile an Extism plugin and set the number of instructions a plugin is allowed to execute
|
||||
*/
|
||||
ExtismCompiledPlugin *extism_compiled_plugin_new_with_fuel_limit(const uint8_t *wasm,
|
||||
ExtismSize wasm_size,
|
||||
const ExtismFunction **functions,
|
||||
ExtismSize n_functions,
|
||||
bool with_wasi,
|
||||
uint64_t fuel_limit,
|
||||
char **errmsg);
|
||||
|
||||
/**
|
||||
* Free `ExtismCompiledPlugin`
|
||||
*/
|
||||
|
||||
@@ -29,6 +29,7 @@ pub(crate) mod manifest;
|
||||
pub(crate) mod pdk;
|
||||
mod plugin;
|
||||
mod plugin_builder;
|
||||
mod pool;
|
||||
mod readonly_dir;
|
||||
mod timer;
|
||||
|
||||
@@ -43,6 +44,7 @@ pub use plugin::{
|
||||
CancelHandle, CompiledPlugin, Plugin, WasmInput, EXTISM_ENV_MODULE, EXTISM_USER_MODULE,
|
||||
};
|
||||
pub use plugin_builder::{DebugOptions, PluginBuilder};
|
||||
pub use pool::{Pool, PoolBuilder, PoolPlugin};
|
||||
|
||||
pub(crate) use internal::{Internal, Wasi};
|
||||
pub(crate) use timer::{Timer, TimerAction};
|
||||
@@ -96,7 +98,7 @@ pub fn set_log_callback<F: 'static + Clone + Fn(&str)>(
|
||||
let x = tracing_subscriber::EnvFilter::builder()
|
||||
.with_default_directive(tracing::Level::ERROR.into());
|
||||
if is_level {
|
||||
x.parse_lossy(format!("extism={}", filter))
|
||||
x.parse_lossy(format!("extism={filter}"))
|
||||
} else {
|
||||
x.parse_lossy(filter)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::*;
|
||||
fn hex(data: &[u8]) -> String {
|
||||
let mut s = String::new();
|
||||
for &byte in data {
|
||||
write!(&mut s, "{:02x}", byte).unwrap();
|
||||
write!(&mut s, "{byte:02x}").unwrap();
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
@@ -258,12 +258,16 @@ pub(crate) fn http_request(
|
||||
};
|
||||
let buf: &[u8] = data.memory_bytes(handle)?;
|
||||
let agent = ureq::agent();
|
||||
let config = agent.configure_request(r.body(buf)?);
|
||||
let config = agent
|
||||
.configure_request(r.body(buf)?)
|
||||
.http_status_as_error(false);
|
||||
let req = config.timeout_global(timeout).build();
|
||||
ureq::run(req)
|
||||
} else {
|
||||
let agent = ureq::agent();
|
||||
let config = agent.configure_request(r.body(())?);
|
||||
let config = agent
|
||||
.configure_request(r.body(())?)
|
||||
.http_status_as_error(false);
|
||||
let req = config.timeout_global(timeout).build();
|
||||
ureq::run(req)
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{
|
||||
any::Any,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
path::PathBuf,
|
||||
sync::TryLockError,
|
||||
};
|
||||
|
||||
@@ -60,26 +61,16 @@ impl CompiledPlugin {
|
||||
.wasm_tail_call(true)
|
||||
.wasm_function_references(true)
|
||||
.wasm_gc(true);
|
||||
#[cfg(feature = "wasmtime-exceptions")]
|
||||
{
|
||||
config.wasm_exceptions(true);
|
||||
}
|
||||
|
||||
if builder.options.fuel.is_some() {
|
||||
config.consume_fuel(true);
|
||||
}
|
||||
|
||||
match &builder.options.cache_config {
|
||||
Some(None) => (),
|
||||
Some(Some(path)) => {
|
||||
config.cache_config_load(path)?;
|
||||
}
|
||||
None => {
|
||||
if let Ok(env) = std::env::var("EXTISM_CACHE_CONFIG") {
|
||||
if !env.is_empty() {
|
||||
config.cache_config_load(&env)?;
|
||||
}
|
||||
} else {
|
||||
config.cache_config_load_default()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
config.cache(Self::configure_cache(&builder.options.cache_config)?);
|
||||
|
||||
let engine = Engine::new(&config)?;
|
||||
|
||||
@@ -97,6 +88,43 @@ impl CompiledPlugin {
|
||||
engine,
|
||||
})
|
||||
}
|
||||
|
||||
/// Return optional cache according to builder options.
|
||||
fn configure_cache(
|
||||
cache_opt: &Option<Option<std::path::PathBuf>>,
|
||||
) -> Result<Option<wasmtime::Cache>, Error> {
|
||||
match cache_opt {
|
||||
// Explicitly disabled
|
||||
Some(None) => Ok(None),
|
||||
|
||||
// Explicit path
|
||||
Some(Some(p)) => {
|
||||
let cache = wasmtime::Cache::from_file(Some(p.as_path()))?;
|
||||
Ok(Some(cache))
|
||||
}
|
||||
|
||||
// Unspecified, try environment, then system fallback
|
||||
None => {
|
||||
match std::env::var_os("EXTISM_CACHE_CONFIG") {
|
||||
Some(val) => {
|
||||
if val.is_empty() {
|
||||
// Disable cache if env var exists but is empty
|
||||
Ok(None)
|
||||
} else {
|
||||
let p = PathBuf::from(val);
|
||||
let cache = wasmtime::Cache::from_file(Some(p.as_path()))?;
|
||||
Ok(Some(cache))
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// load cache configuration from the system default path
|
||||
let cache = wasmtime::Cache::from_file(None)?;
|
||||
Ok(Some(cache))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Plugin contains everything needed to execute a WASM function
|
||||
@@ -191,6 +219,7 @@ pub(crate) fn profiling_strategy() -> ProfilingStrategy {
|
||||
/// Defines an input type for Wasm data.
|
||||
///
|
||||
/// Types that implement `Into<WasmInput>` can be passed directly into `Plugin::new`
|
||||
#[derive(Clone)]
|
||||
pub enum WasmInput<'a> {
|
||||
/// Raw Wasm module
|
||||
Data(std::borrow::Cow<'a, [u8]>),
|
||||
@@ -343,6 +372,7 @@ fn relink(
|
||||
let (kind, ty) = match import.ty() {
|
||||
ExternType::Func(t) => ("function", t.to_string()),
|
||||
ExternType::Global(t) => ("global", t.content().to_string()),
|
||||
ExternType::Tag(t) => ("tag", t.ty().to_string()),
|
||||
ExternType::Table(t) => ("table", t.element().to_string()),
|
||||
ExternType::Memory(_) => ("memory", String::new()),
|
||||
};
|
||||
@@ -962,8 +992,7 @@ impl Plugin {
|
||||
}
|
||||
Err(msg) => {
|
||||
res = Err(Error::msg(format!(
|
||||
"unable to load error message from memory: {}",
|
||||
msg,
|
||||
"unable to load error message from memory: {msg}",
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ impl Default for DebugOptions {
|
||||
}
|
||||
|
||||
/// PluginBuilder is used to configure and create `Plugin` instances
|
||||
#[derive(Clone)]
|
||||
pub struct PluginBuilder<'a> {
|
||||
pub(crate) source: WasmInput<'a>,
|
||||
pub(crate) config: Option<wasmtime::Config>,
|
||||
|
||||
195
runtime/src/pool.rs
Normal file
195
runtime/src/pool.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
use crate::{Error, FromBytesOwned, Plugin, ToBytes};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
// `PoolBuilder` is used to configure and create `Pool`s
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PoolBuilder {
|
||||
/// Max number of concurrent instances for a plugin - by default this is set to
|
||||
/// the output of `std::thread::available_parallelism`
|
||||
pub max_instances: usize,
|
||||
}
|
||||
|
||||
impl PoolBuilder {
|
||||
/// Create a `PoolBuilder` with default values
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set the max number of parallel instances
|
||||
pub fn with_max_instances(mut self, n: usize) -> Self {
|
||||
self.max_instances = n;
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a new `Pool` with the given configuration
|
||||
pub fn build<F: 'static + Fn() -> Result<Plugin, Error>>(self, source: F) -> Pool {
|
||||
Pool::new_from_builder(source, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PoolBuilder {
|
||||
fn default() -> Self {
|
||||
PoolBuilder {
|
||||
max_instances: std::thread::available_parallelism()
|
||||
.expect("available parallelism")
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `PoolPlugin` is used by the pool to track the number of live instances of a particular plugin
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PoolPlugin(std::rc::Rc<std::cell::RefCell<Plugin>>);
|
||||
|
||||
impl PoolPlugin {
|
||||
fn new(plugin: Plugin) -> Self {
|
||||
Self(std::rc::Rc::new(std::cell::RefCell::new(plugin)))
|
||||
}
|
||||
|
||||
/// Access the underlying plugin
|
||||
pub fn plugin(&self) -> std::cell::RefMut<'_, Plugin> {
|
||||
self.0.borrow_mut()
|
||||
}
|
||||
|
||||
/// Helper to call a plugin function on the underlying plugin
|
||||
pub fn call<'a, Input: ToBytes<'a>, Output: FromBytesOwned>(
|
||||
&self,
|
||||
name: impl AsRef<str>,
|
||||
input: Input,
|
||||
) -> Result<Output, Error> {
|
||||
self.plugin().call(name.as_ref(), input)
|
||||
}
|
||||
|
||||
/// Helper to get the underlying plugin's ID
|
||||
pub fn id(&self) -> uuid::Uuid {
|
||||
self.plugin().id
|
||||
}
|
||||
}
|
||||
|
||||
type PluginSource = dyn Fn() -> Result<Plugin, Error>;
|
||||
|
||||
struct PoolInner {
|
||||
plugin_source: Box<PluginSource>,
|
||||
instances: Vec<PoolPlugin>,
|
||||
}
|
||||
|
||||
unsafe impl Send for PoolInner {}
|
||||
unsafe impl Sync for PoolInner {}
|
||||
|
||||
/// `Pool` manages threadsafe access to a limited number of instances of multiple plugins
|
||||
#[derive(Clone)]
|
||||
pub struct Pool {
|
||||
config: PoolBuilder,
|
||||
inner: Arc<std::sync::Mutex<PoolInner>>,
|
||||
existing_functions: Arc<RwLock<HashMap<String, bool>>>,
|
||||
}
|
||||
|
||||
unsafe impl Send for Pool {}
|
||||
unsafe impl Sync for Pool {}
|
||||
|
||||
impl Pool {
|
||||
/// Create a new pool with the default configuration
|
||||
pub fn new<F: 'static + Fn() -> Result<Plugin, Error>>(source: F) -> Self {
|
||||
Self::new_from_builder(Box::new(source), PoolBuilder::default())
|
||||
}
|
||||
|
||||
/// Create a new pool configured using a `PoolBuilder`
|
||||
pub fn new_from_builder<F: 'static + Fn() -> Result<Plugin, Error>>(
|
||||
source: F,
|
||||
builder: PoolBuilder,
|
||||
) -> Self {
|
||||
Pool {
|
||||
config: builder,
|
||||
inner: Arc::new(std::sync::Mutex::new(PoolInner {
|
||||
plugin_source: Box::new(source),
|
||||
instances: Default::default(),
|
||||
})),
|
||||
existing_functions: RwLock::new(HashMap::default()).into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_available(&self) -> Result<Option<PoolPlugin>, Error> {
|
||||
let pool = self.inner.lock().unwrap();
|
||||
for instance in pool.instances.iter() {
|
||||
if std::rc::Rc::strong_count(&instance.0) == 1 {
|
||||
return Ok(Some(instance.clone()));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Get the number of live instances for a plugin
|
||||
pub fn count(&self) -> usize {
|
||||
self.inner.lock().unwrap().instances.len()
|
||||
}
|
||||
|
||||
/// Get access to a plugin, this will create a new instance if needed (and allowed by the specified
|
||||
/// max_instances). `Ok(None)` is returned if the timeout is reached before an available plugin could be
|
||||
/// acquired
|
||||
pub fn get(&self, timeout: std::time::Duration) -> Result<Option<PoolPlugin>, Error> {
|
||||
let start = std::time::Instant::now();
|
||||
let max = self.config.max_instances;
|
||||
if let Some(avail) = self.find_available()? {
|
||||
return Ok(Some(avail));
|
||||
}
|
||||
|
||||
{
|
||||
let mut pool = self.inner.lock().unwrap();
|
||||
if pool.instances.len() < max {
|
||||
let plugin = (*pool.plugin_source)()?;
|
||||
let instance = PoolPlugin::new(plugin);
|
||||
pool.instances.push(instance);
|
||||
return Ok(Some(pool.instances.last().unwrap().clone()));
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
if let Ok(Some(x)) = self.find_available() {
|
||||
return Ok(Some(x));
|
||||
}
|
||||
if std::time::Instant::now() - start > timeout {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
|
||||
/// Access a plugin in a callback function. This calls `Pool::get` then the provided
|
||||
/// callback. `Ok(None)` is returned if the timeout is reached before an available
|
||||
/// plugin could be acquired
|
||||
pub fn with_plugin<T>(
|
||||
&self,
|
||||
timeout: std::time::Duration,
|
||||
f: impl FnOnce(&mut Plugin) -> Result<T, Error>,
|
||||
) -> Result<Option<T>, Error> {
|
||||
if let Some(plugin) = self.get(timeout)? {
|
||||
return f(&mut plugin.plugin()).map(Some);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns `true` if the given function exists, otherwise `false`. Results are cached
|
||||
/// after the first call.
|
||||
pub fn function_exists(&self, name: &str, timeout: std::time::Duration) -> Result<bool, Error> {
|
||||
// read current value if any
|
||||
let read = self.existing_functions.read().unwrap();
|
||||
let exists_opt = read.get(name).cloned();
|
||||
drop(read);
|
||||
if let Some(exists) = exists_opt {
|
||||
Ok(exists)
|
||||
} else {
|
||||
// load plugin and call function_exists
|
||||
let plugin = self.get(timeout)?;
|
||||
let exists = plugin.unwrap().0.borrow().function_exists(name);
|
||||
|
||||
// write result to hashmap
|
||||
let mut write = self.existing_functions.write().unwrap();
|
||||
write.insert(name.to_string(), exists);
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -363,6 +363,70 @@ pub unsafe extern "C" fn extism_compiled_plugin_new(
|
||||
})
|
||||
}
|
||||
|
||||
/// Pre-compile an Extism plugin and set the number of instructions a plugin is allowed to execute
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_compiled_plugin_new_with_fuel_limit(
|
||||
wasm: *const u8,
|
||||
wasm_size: Size,
|
||||
functions: *mut *const ExtismFunction,
|
||||
n_functions: Size,
|
||||
with_wasi: bool,
|
||||
fuel_limit: u64,
|
||||
errmsg: *mut *mut std::ffi::c_char,
|
||||
) -> *mut CompiledPlugin {
|
||||
trace!("Call to extism_plugin_new with wasm pointer {:?}", wasm);
|
||||
let data = std::slice::from_raw_parts(wasm, wasm_size as usize);
|
||||
|
||||
let mut builder = PluginBuilder::new(data)
|
||||
.with_wasi(with_wasi)
|
||||
.with_fuel_limit(fuel_limit);
|
||||
|
||||
if !functions.is_null() {
|
||||
let funcs = (0..n_functions)
|
||||
.map(|i| unsafe { *functions.add(i as usize) })
|
||||
.map(|ptr| {
|
||||
if ptr.is_null() {
|
||||
return Err("Cannot pass null pointer");
|
||||
}
|
||||
|
||||
let ExtismFunction(func) = &*ptr;
|
||||
let Some(func) = func.take() else {
|
||||
return Err("Function cannot be registered with multiple different Plugins");
|
||||
};
|
||||
|
||||
Ok(func)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.unwrap_or_else(|e| {
|
||||
if !errmsg.is_null() {
|
||||
let e = std::ffi::CString::new(e.to_string()).unwrap();
|
||||
*errmsg = e.into_raw();
|
||||
}
|
||||
Vec::new()
|
||||
});
|
||||
|
||||
if funcs.len() != n_functions as usize {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
|
||||
builder = builder.with_functions(funcs);
|
||||
}
|
||||
|
||||
CompiledPlugin::new(builder)
|
||||
.map(|v| Box::into_raw(Box::new(v)))
|
||||
.unwrap_or_else(|e| {
|
||||
if !errmsg.is_null() {
|
||||
let e = std::ffi::CString::new(format!(
|
||||
"Unable to compile Extism plugin: {}",
|
||||
e.root_cause(),
|
||||
))
|
||||
.unwrap();
|
||||
*errmsg = e.into_raw();
|
||||
}
|
||||
std::ptr::null_mut()
|
||||
})
|
||||
}
|
||||
|
||||
/// Free `ExtismCompiledPlugin`
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn extism_compiled_plugin_free(plugin: *mut CompiledPlugin) {
|
||||
@@ -871,7 +935,7 @@ fn set_log_file(log_file: impl Into<std::path::PathBuf>, filter: &str) -> Result
|
||||
let x = tracing_subscriber::EnvFilter::builder()
|
||||
.with_default_directive(tracing::Level::ERROR.into());
|
||||
if is_level {
|
||||
x.parse_lossy(format!("extism={}", filter))
|
||||
x.parse_lossy(format!("extism={filter}"))
|
||||
} else {
|
||||
x.parse_lossy(filter)
|
||||
}
|
||||
@@ -926,7 +990,7 @@ unsafe fn set_log_buffer(filter: &str) -> Result<(), Error> {
|
||||
let x = tracing_subscriber::EnvFilter::builder()
|
||||
.with_default_directive(tracing::Level::ERROR.into());
|
||||
if is_level {
|
||||
x.parse_lossy(format!("extism={}", filter))
|
||||
x.parse_lossy(format!("extism={filter}"))
|
||||
} else {
|
||||
x.parse_lossy(filter)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ fn test_issue_620() {
|
||||
// Call test method, this does not work
|
||||
let p = plugin.call::<(), String>("test", ()).unwrap();
|
||||
|
||||
println!("{}", p);
|
||||
println!("{p}");
|
||||
}
|
||||
|
||||
// https://github.com/extism/extism/issues/619
|
||||
@@ -53,5 +53,5 @@ fn test_issue_775() {
|
||||
Ok(code) => Err(code),
|
||||
}
|
||||
.unwrap();
|
||||
println!("{}", p);
|
||||
println!("{p}");
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod issues;
|
||||
mod kernel;
|
||||
mod pool;
|
||||
mod runtime;
|
||||
|
||||
63
runtime/src/tests/pool.rs
Normal file
63
runtime/src/tests/pool.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use crate::*;
|
||||
use std::time::Duration;
|
||||
|
||||
fn run_thread(p: Pool, i: u64) -> std::thread::JoinHandle<()> {
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(Duration::from_millis(i));
|
||||
let s: String = p
|
||||
.get(Duration::from_secs(1))
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.call("count_vowels", "abc")
|
||||
.unwrap();
|
||||
println!("{s}");
|
||||
})
|
||||
}
|
||||
|
||||
fn init(max_instances: usize) -> Pool {
|
||||
let data = include_bytes!("../../../wasm/code.wasm");
|
||||
let plugin_builder =
|
||||
extism::PluginBuilder::new(extism::Manifest::new([extism::Wasm::data(data)]))
|
||||
.with_wasi(true);
|
||||
PoolBuilder::new()
|
||||
.with_max_instances(max_instances)
|
||||
.build(move || plugin_builder.clone().build())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_threads() {
|
||||
for i in 1..=3 {
|
||||
let pool = init(i);
|
||||
let threads = vec![
|
||||
run_thread(pool.clone(), 1000),
|
||||
run_thread(pool.clone(), 1000),
|
||||
run_thread(pool.clone(), 1000),
|
||||
run_thread(pool.clone(), 1000),
|
||||
run_thread(pool.clone(), 1000),
|
||||
run_thread(pool.clone(), 1000),
|
||||
run_thread(pool.clone(), 500),
|
||||
run_thread(pool.clone(), 500),
|
||||
run_thread(pool.clone(), 500),
|
||||
run_thread(pool.clone(), 500),
|
||||
run_thread(pool.clone(), 500),
|
||||
run_thread(pool.clone(), 0),
|
||||
];
|
||||
|
||||
for t in threads {
|
||||
t.join().unwrap();
|
||||
}
|
||||
|
||||
assert!(pool.count() <= i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_exists() -> Result<(), Error> {
|
||||
let pool = init(1);
|
||||
let timeout = Duration::from_secs(1);
|
||||
assert!(pool.function_exists("count_vowels", timeout)?);
|
||||
assert!(pool.function_exists("count_vowels", timeout)?);
|
||||
assert!(!pool.function_exists("not_existing", timeout)?);
|
||||
assert!(!pool.function_exists("not_existing", timeout)?);
|
||||
Ok(())
|
||||
}
|
||||
@@ -133,10 +133,7 @@ fn it_works() {
|
||||
.unwrap();
|
||||
let native_avg: std::time::Duration = native_sum / native_num_tests as u32;
|
||||
|
||||
println!(
|
||||
"native function call (avg, N = {}): {:?}",
|
||||
native_num_tests, native_avg
|
||||
);
|
||||
println!("native function call (avg, N = {native_num_tests}): {native_avg:?}");
|
||||
|
||||
let num_tests = test_times.len();
|
||||
let sum: std::time::Duration = test_times
|
||||
@@ -145,7 +142,7 @@ fn it_works() {
|
||||
.unwrap();
|
||||
let avg: std::time::Duration = sum / num_tests as u32;
|
||||
|
||||
println!("wasm function call (avg, N = {}): {:?}", num_tests, avg);
|
||||
println!("wasm function call (avg, N = {num_tests}): {avg:?}");
|
||||
|
||||
// Check that log file was written to
|
||||
if log {
|
||||
@@ -212,7 +209,7 @@ fn test_cancel() {
|
||||
let _output: Result<&[u8], Error> = plugin.call("loop_forever", "abc123");
|
||||
let end = std::time::Instant::now();
|
||||
let time = end - start;
|
||||
println!("Cancelled plugin ran for {:?}", time);
|
||||
println!("Cancelled plugin ran for {time:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +268,7 @@ fn test_fuel_consumption() {
|
||||
assert!(output.is_err());
|
||||
|
||||
let fuel_consumed = plugin.fuel_consumed().unwrap();
|
||||
println!("Fuel consumed: {}", fuel_consumed);
|
||||
println!("Fuel consumed: {fuel_consumed}");
|
||||
assert!(fuel_consumed > 0);
|
||||
}
|
||||
|
||||
@@ -440,7 +437,7 @@ fn test_memory_max() {
|
||||
assert!(output.is_err());
|
||||
|
||||
let err = output.unwrap_err().root_cause().to_string();
|
||||
println!("{:?}", err);
|
||||
println!("{err:?}");
|
||||
assert_eq!(err, "oom");
|
||||
|
||||
// Should pass with memory.max set to a large enough number
|
||||
@@ -503,7 +500,7 @@ fn test_extism_error() {
|
||||
let mut plugin = Plugin::new(&manifest, [f], true).unwrap();
|
||||
let output: Result<String, Error> = plugin.call("count_vowels", "a".repeat(1024));
|
||||
assert!(output.is_err());
|
||||
println!("{:?}", output);
|
||||
println!("{output:?}");
|
||||
assert_eq!(output.unwrap_err().root_cause().to_string(), "TEST");
|
||||
}
|
||||
|
||||
@@ -823,7 +820,7 @@ fn test_http_response_headers() {
|
||||
.unwrap();
|
||||
let req = HttpRequest::new("https://extism.org");
|
||||
let Json(res): Json<HashMap<String, String>> = plugin.call("http_get", Json(req)).unwrap();
|
||||
println!("{:?}", res);
|
||||
println!("{res:?}");
|
||||
assert_eq!(res["content-type"], "text/html; charset=utf-8");
|
||||
}
|
||||
|
||||
@@ -838,6 +835,6 @@ fn test_http_response_headers_disabled() {
|
||||
.unwrap();
|
||||
let req = HttpRequest::new("https://extism.org");
|
||||
let Json(res): Json<HashMap<String, String>> = plugin.call("http_get", Json(req)).unwrap();
|
||||
println!("{:?}", res);
|
||||
println!("{res:?}");
|
||||
assert!(res.is_empty());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user