extism_runtime/
sdk.rs

1#![allow(clippy::missing_safety_doc)]
2
3use std::os::raw::c_char;
4use std::str::FromStr;
5
6use crate::*;
7
8/// A union type for host function argument/return values
9#[repr(C)]
10pub union ValUnion {
11    i32: i32,
12    i64: i64,
13    f32: f32,
14    f64: f64,
15    // TODO: v128, ExternRef, FuncRef
16}
17
18/// `ExtismVal` holds the type and value of a function argument/return
19#[repr(C)]
20pub struct ExtismVal {
21    t: ValType,
22    v: ValUnion,
23}
24
25/// Wraps host functions
26pub struct ExtismFunction(Function);
27
28impl From<Function> for ExtismFunction {
29    fn from(x: Function) -> Self {
30        ExtismFunction(x)
31    }
32}
33
34/// Host function signature
35pub type ExtismFunctionType = extern "C" fn(
36    plugin: *mut Internal,
37    inputs: *const ExtismVal,
38    n_inputs: Size,
39    outputs: *mut ExtismVal,
40    n_outputs: Size,
41    data: *mut std::ffi::c_void,
42);
43
44impl From<&wasmtime::Val> for ExtismVal {
45    fn from(value: &wasmtime::Val) -> Self {
46        match value.ty() {
47            wasmtime::ValType::I32 => ExtismVal {
48                t: ValType::I32,
49                v: ValUnion {
50                    i32: value.unwrap_i32(),
51                },
52            },
53            wasmtime::ValType::I64 => ExtismVal {
54                t: ValType::I64,
55                v: ValUnion {
56                    i64: value.unwrap_i64(),
57                },
58            },
59            wasmtime::ValType::F32 => ExtismVal {
60                t: ValType::F32,
61                v: ValUnion {
62                    f32: value.unwrap_f32(),
63                },
64            },
65            wasmtime::ValType::F64 => ExtismVal {
66                t: ValType::F64,
67                v: ValUnion {
68                    f64: value.unwrap_f64(),
69                },
70            },
71            t => todo!("{}", t),
72        }
73    }
74}
75
76/// Create a new context
77#[no_mangle]
78pub unsafe extern "C" fn extism_context_new() -> *mut Context {
79    trace!("Creating new Context");
80    Box::into_raw(Box::new(Context::new()))
81}
82
83/// Free a context
84#[no_mangle]
85pub unsafe extern "C" fn extism_context_free(ctx: *mut Context) {
86    trace!("Freeing context");
87    if ctx.is_null() {
88        return;
89    }
90    drop(Box::from_raw(ctx))
91}
92
93/// Returns a pointer to the memory of the currently running plugin
94/// NOTE: this should only be called from host functions.
95#[no_mangle]
96pub unsafe extern "C" fn extism_current_plugin_memory(plugin: *mut Internal) -> *mut u8 {
97    if plugin.is_null() {
98        return std::ptr::null_mut();
99    }
100
101    let plugin = &mut *plugin;
102    plugin.memory_ptr()
103}
104
105/// Allocate a memory block in the currently running plugin
106/// NOTE: this should only be called from host functions.
107#[no_mangle]
108pub unsafe extern "C" fn extism_current_plugin_memory_alloc(plugin: *mut Internal, n: Size) -> u64 {
109    if plugin.is_null() {
110        return 0;
111    }
112
113    let plugin = &mut *plugin;
114    plugin.memory_alloc(n as u64).unwrap_or_default()
115}
116
117/// Get the length of an allocated block
118/// NOTE: this should only be called from host functions.
119#[no_mangle]
120pub unsafe extern "C" fn extism_current_plugin_memory_length(
121    plugin: *mut Internal,
122    n: Size,
123) -> Size {
124    if plugin.is_null() {
125        return 0;
126    }
127
128    let plugin = &mut *plugin;
129    plugin.memory_length(n)
130}
131
132/// Free an allocated memory block
133/// NOTE: this should only be called from host functions.
134#[no_mangle]
135pub unsafe extern "C" fn extism_current_plugin_memory_free(plugin: *mut Internal, ptr: u64) {
136    if plugin.is_null() {
137        return;
138    }
139
140    let plugin = &mut *plugin;
141    plugin.memory_free(ptr);
142}
143
144/// Create a new host function
145///
146/// Arguments
147/// - `name`: function name, this should be valid UTF-8
148/// - `inputs`: argument types
149/// - `n_inputs`: number of argument types
150/// - `outputs`: return types
151/// - `n_outputs`: number of return types
152/// - `func`: the function to call
153/// - `user_data`: a pointer that will be passed to the function when it's called
154///    this value should live as long as the function exists
155/// - `free_user_data`: a callback to release the `user_data` value when the resulting
156///   `ExtismFunction` is freed.
157///
158/// Returns a new `ExtismFunction` or `null` if the `name` argument is invalid.
159#[no_mangle]
160pub unsafe extern "C" fn extism_function_new(
161    name: *const std::ffi::c_char,
162    inputs: *const ValType,
163    n_inputs: Size,
164    outputs: *const ValType,
165    n_outputs: Size,
166    func: ExtismFunctionType,
167    user_data: *mut std::ffi::c_void,
168    free_user_data: Option<extern "C" fn(_: *mut std::ffi::c_void)>,
169) -> *mut ExtismFunction {
170    let name = match std::ffi::CStr::from_ptr(name).to_str() {
171        Ok(x) => x.to_string(),
172        Err(_) => {
173            return std::ptr::null_mut();
174        }
175    };
176
177    let inputs = if inputs.is_null() || n_inputs == 0 {
178        &[]
179    } else {
180        std::slice::from_raw_parts(inputs, n_inputs as usize)
181    }
182    .to_vec();
183
184    let output_types = if outputs.is_null() || n_outputs == 0 {
185        &[]
186    } else {
187        std::slice::from_raw_parts(outputs, n_outputs as usize)
188    }
189    .to_vec();
190
191    let user_data = UserData::new_pointer(user_data, free_user_data);
192    let f = Function::new(
193        name,
194        inputs,
195        output_types.clone(),
196        Some(user_data),
197        move |plugin, inputs, outputs, user_data| {
198            let inputs: Vec<_> = inputs.iter().map(ExtismVal::from).collect();
199            let mut output_tmp: Vec<_> = output_types
200                .iter()
201                .map(|t| ExtismVal {
202                    t: t.clone(),
203                    v: ValUnion { i64: 0 },
204                })
205                .collect();
206
207            func(
208                plugin,
209                inputs.as_ptr(),
210                inputs.len() as Size,
211                output_tmp.as_mut_ptr(),
212                output_tmp.len() as Size,
213                user_data.as_ptr(),
214            );
215
216            for (tmp, out) in output_tmp.iter().zip(outputs.iter_mut()) {
217                match tmp.t {
218                    ValType::I32 => *out = Val::I32(tmp.v.i32),
219                    ValType::I64 => *out = Val::I64(tmp.v.i64),
220                    ValType::F32 => *out = Val::F32(tmp.v.f32 as u32),
221                    ValType::F64 => *out = Val::F64(tmp.v.f64 as u64),
222                    _ => todo!(),
223                }
224            }
225            Ok(())
226        },
227    );
228    Box::into_raw(Box::new(ExtismFunction(f)))
229}
230
231/// Set the namespace of an `ExtismFunction`
232#[no_mangle]
233pub unsafe extern "C" fn extism_function_set_namespace(
234    ptr: *mut ExtismFunction,
235    namespace: *const std::ffi::c_char,
236) {
237    let namespace = std::ffi::CStr::from_ptr(namespace);
238    let f = &mut *ptr;
239    f.0.set_namespace(namespace.to_string_lossy().to_string());
240}
241
242/// Free an `ExtismFunction`
243#[no_mangle]
244pub unsafe extern "C" fn extism_function_free(ptr: *mut ExtismFunction) {
245    drop(Box::from_raw(ptr))
246}
247
248/// Create a new plugin with additional host functions
249///
250/// `wasm`: is a WASM module (wat or wasm) or a JSON encoded manifest
251/// `wasm_size`: the length of the `wasm` parameter
252/// `functions`: an array of `ExtismFunction*`
253/// `n_functions`: the number of functions provided
254/// `with_wasi`: enables/disables WASI
255#[no_mangle]
256pub unsafe extern "C" fn extism_plugin_new(
257    ctx: *mut Context,
258    wasm: *const u8,
259    wasm_size: Size,
260    functions: *mut *const ExtismFunction,
261    n_functions: Size,
262    with_wasi: bool,
263) -> PluginIndex {
264    trace!("Call to extism_plugin_new with wasm pointer {:?}", wasm);
265    let ctx = &mut *ctx;
266    let data = std::slice::from_raw_parts(wasm, wasm_size as usize);
267    let mut funcs = vec![];
268
269    if !functions.is_null() {
270        for i in 0..n_functions {
271            unsafe {
272                let f = *functions.add(i as usize);
273                if f.is_null() {
274                    continue;
275                }
276                let f = &*f;
277                funcs.push(&f.0);
278            }
279        }
280    }
281    ctx.new_plugin(data, funcs, with_wasi)
282}
283
284/// Update a plugin, keeping the existing ID
285///
286/// Similar to `extism_plugin_new` but takes an `index` argument to specify
287/// which plugin to update
288///
289/// Memory for this plugin will be reset upon update
290#[no_mangle]
291pub unsafe extern "C" fn extism_plugin_update(
292    ctx: *mut Context,
293    index: PluginIndex,
294    wasm: *const u8,
295    wasm_size: Size,
296    functions: *mut *const ExtismFunction,
297    nfunctions: Size,
298    with_wasi: bool,
299) -> bool {
300    trace!("Call to extism_plugin_update with wasm pointer {:?}", wasm);
301    let ctx = &mut *ctx;
302
303    let data = std::slice::from_raw_parts(wasm, wasm_size as usize);
304
305    let mut funcs = vec![];
306
307    if !functions.is_null() {
308        for i in 0..nfunctions {
309            unsafe {
310                let f = *functions.add(i as usize);
311                if f.is_null() {
312                    continue;
313                }
314                let f = &*f;
315                funcs.push(&f.0);
316            }
317        }
318    }
319
320    let plugin = match Plugin::new(data, funcs, with_wasi) {
321        Ok(x) => x,
322        Err(e) => {
323            error!("Error creating Plugin: {:?}", e);
324            ctx.set_error(e);
325            return false;
326        }
327    };
328
329    if !ctx.plugins.contains_key(&index) {
330        ctx.set_error("Plugin index does not exist");
331        return false;
332    }
333
334    ctx.plugins.insert(index, plugin);
335
336    debug!("Plugin updated: {index}");
337    true
338}
339
340/// Remove a plugin from the registry and free associated memory
341#[no_mangle]
342pub unsafe extern "C" fn extism_plugin_free(ctx: *mut Context, plugin: PluginIndex) {
343    if plugin < 0 || ctx.is_null() {
344        return;
345    }
346
347    trace!("Freeing plugin {plugin}");
348
349    let ctx = &mut *ctx;
350    ctx.remove(plugin);
351}
352
353pub struct ExtismCancelHandle {
354    pub(crate) epoch_timer_tx: Option<std::sync::mpsc::SyncSender<TimerAction>>,
355    pub id: uuid::Uuid,
356}
357
358/// Get plugin ID for cancellation
359#[no_mangle]
360pub unsafe extern "C" fn extism_plugin_cancel_handle(
361    ctx: *mut Context,
362    plugin: PluginIndex,
363) -> *const ExtismCancelHandle {
364    let ctx = &mut *ctx;
365    let mut plugin = match PluginRef::new(ctx, plugin, true) {
366        None => return std::ptr::null_mut(),
367        Some(p) => p,
368    };
369    let plugin = plugin.as_mut();
370    &plugin.cancel_handle as *const _
371}
372
373/// Cancel a running plugin
374#[no_mangle]
375pub unsafe extern "C" fn extism_plugin_cancel(handle: *const ExtismCancelHandle) -> bool {
376    let handle = &*handle;
377    if let Some(tx) = &handle.epoch_timer_tx {
378        return tx.send(TimerAction::Cancel { id: handle.id }).is_ok();
379    }
380
381    false
382}
383
384/// Remove all plugins from the registry
385#[no_mangle]
386pub unsafe extern "C" fn extism_context_reset(ctx: *mut Context) {
387    let ctx = &mut *ctx;
388
389    trace!(
390        "Resetting context, plugins cleared: {:?}",
391        ctx.plugins.keys().collect::<Vec<&i32>>()
392    );
393
394    ctx.plugins.clear();
395}
396
397/// Update plugin config values, this will merge with the existing values
398#[no_mangle]
399pub unsafe extern "C" fn extism_plugin_config(
400    ctx: *mut Context,
401    plugin: PluginIndex,
402    json: *const u8,
403    json_size: Size,
404) -> bool {
405    let ctx = &mut *ctx;
406    let mut plugin_ref = match PluginRef::new(ctx, plugin, true) {
407        None => return false,
408        Some(p) => p,
409    };
410    trace!(
411        "Call to extism_plugin_config for {} with json pointer {:?}",
412        plugin_ref.id,
413        json
414    );
415    let plugin = plugin_ref.as_mut();
416
417    let data = std::slice::from_raw_parts(json, json_size as usize);
418    let json: std::collections::BTreeMap<String, Option<String>> =
419        match serde_json::from_slice(data) {
420            Ok(x) => x,
421            Err(e) => {
422                return plugin.error(e, false);
423            }
424        };
425
426    let wasi = &mut plugin.internal_mut().wasi;
427    if let Some(Wasi { ctx, .. }) = wasi {
428        for (k, v) in json.iter() {
429            match v {
430                Some(v) => {
431                    let _ = ctx.push_env(k, v);
432                }
433                None => {
434                    let _ = ctx.push_env(k, "");
435                }
436            }
437        }
438    }
439
440    let config = &mut plugin.internal_mut().manifest.as_mut().config;
441    for (k, v) in json.into_iter() {
442        match v {
443            Some(v) => {
444                trace!("Config, adding {k}");
445                config.insert(k, v);
446            }
447            None => {
448                trace!("Config, removing {k}");
449                config.remove(&k);
450            }
451        }
452    }
453
454    true
455}
456
457/// Returns true if `func_name` exists
458#[no_mangle]
459pub unsafe extern "C" fn extism_plugin_function_exists(
460    ctx: *mut Context,
461    plugin: PluginIndex,
462    func_name: *const c_char,
463) -> bool {
464    let ctx = &mut *ctx;
465    let mut plugin = match PluginRef::new(ctx, plugin, true) {
466        None => return false,
467        Some(p) => p,
468    };
469
470    let name = std::ffi::CStr::from_ptr(func_name);
471    trace!("Call to extism_plugin_function_exists for: {:?}", name);
472
473    let name = match name.to_str() {
474        Ok(x) => x,
475        Err(e) => {
476            return plugin.as_mut().error(e, false);
477        }
478    };
479
480    plugin.as_mut().get_func(name).is_some()
481}
482
483/// Call a function
484///
485/// `func_name`: is the function to call
486/// `data`: is the input data
487/// `data_len`: is the length of `data`
488#[no_mangle]
489pub unsafe extern "C" fn extism_plugin_call(
490    ctx: *mut Context,
491    plugin_id: PluginIndex,
492    func_name: *const c_char,
493    data: *const u8,
494    data_len: Size,
495) -> i32 {
496    let ctx = &mut *ctx;
497
498    // Get function name
499    let name = std::ffi::CStr::from_ptr(func_name);
500    let name = match name.to_str() {
501        Ok(name) => name,
502        Err(e) => return ctx.error(e, -1),
503    };
504    let is_start = name == "_start";
505
506    // Get a `PluginRef` and call `init` to set up the plugin input and memory, this is only
507    // needed before a new call
508    let mut plugin_ref = match PluginRef::new(ctx, plugin_id, true) {
509        None => return -1,
510        Some(p) => p.start_call(is_start),
511    };
512    let tx = plugin_ref.epoch_timer_tx.clone();
513    let plugin = plugin_ref.as_mut();
514
515    let func = match plugin.get_func(name) {
516        Some(x) => x,
517        None => return plugin.error(format!("Function not found: {name}"), -1),
518    };
519
520    // Check the number of results, reject functions with more than 1 result
521    let n_results = func.ty(plugin.store()).results().len();
522    if n_results > 1 {
523        return plugin.error(
524            format!("Function {name} has {n_results} results, expected 0 or 1"),
525            -1,
526        );
527    }
528
529    if let Err(e) = plugin.set_input(data, data_len as usize) {
530        return plugin.error(e, -1);
531    }
532
533    if plugin.has_error() {
534        return -1;
535    }
536
537    // Start timer, this will be stopped when PluginRef goes out of scope
538    if let Err(e) = plugin.start_timer(&tx) {
539        return plugin.error(e, -1);
540    }
541
542    debug!("Calling function: {name} in plugin {plugin_id}");
543
544    // Call the function
545    let mut results = vec![wasmtime::Val::null(); n_results];
546    let res = func.call(plugin.store_mut(), &[], results.as_mut_slice());
547
548    match res {
549        Ok(()) => (),
550        Err(e) => {
551            plugin.store.set_epoch_deadline(1);
552            if let Some(exit) = e.downcast_ref::<wasmtime_wasi::I32Exit>() {
553                trace!("WASI return code: {}", exit.0);
554                if exit.0 != 0 {
555                    return plugin.error(&e, exit.0);
556                }
557                return exit.0;
558            }
559
560            if e.root_cause().to_string() == "timeout" {
561                return plugin.error("timeout", -1);
562            }
563
564            error!("Call: {e:?}");
565            return plugin.error(e.context("Call failed"), -1);
566        }
567    };
568
569    // If `results` is empty and the return value wasn't a WASI exit code then
570    // the call succeeded
571    if results.is_empty() {
572        return 0;
573    }
574
575    // Return result to caller
576    results[0].unwrap_i32()
577}
578
579pub fn get_context_error(ctx: &Context) -> *const c_char {
580    match &ctx.error {
581        Some(e) => e.as_ptr() as *const _,
582        None => {
583            trace!("Context error is NULL");
584            std::ptr::null()
585        }
586    }
587}
588
589/// Get the error associated with a `Context` or `Plugin`, if `plugin` is `-1` then the context
590/// error will be returned
591#[no_mangle]
592pub unsafe extern "C" fn extism_error(ctx: *mut Context, plugin: PluginIndex) -> *const c_char {
593    trace!("Call to extism_error for plugin {plugin}");
594
595    let ctx = &mut *ctx;
596
597    if !ctx.plugin_exists(plugin) {
598        return get_context_error(ctx);
599    }
600
601    let mut plugin_ref = match PluginRef::new(ctx, plugin, false) {
602        None => return std::ptr::null(),
603        Some(p) => p,
604    };
605    let plugin = plugin_ref.as_mut();
606    let output = &mut [Val::I64(0)];
607    if let Some(f) = plugin
608        .linker
609        .get(&mut plugin.store, "env", "extism_error_get")
610    {
611        f.into_func()
612            .unwrap()
613            .call(&mut plugin.store, &[], output)
614            .unwrap();
615    }
616    if output[0].unwrap_i64() == 0 {
617        trace!("Error is NULL");
618        return std::ptr::null();
619    }
620
621    plugin.memory_ptr().add(output[0].unwrap_i64() as usize) as *const _
622}
623
624/// Get the length of a plugin's output data
625#[no_mangle]
626pub unsafe extern "C" fn extism_plugin_output_length(
627    ctx: *mut Context,
628    plugin: PluginIndex,
629) -> Size {
630    trace!("Call to extism_plugin_output_length for plugin {plugin}");
631
632    let ctx = &mut *ctx;
633    let mut plugin_ref = match PluginRef::new(ctx, plugin, true) {
634        None => return 0,
635        Some(p) => p,
636    };
637    let plugin = plugin_ref.as_mut();
638    let out = &mut [Val::I64(0)];
639    let _ = plugin
640        .linker
641        .get(&mut plugin.store, "env", "extism_output_length")
642        .unwrap()
643        .into_func()
644        .unwrap()
645        .call(&mut plugin.store_mut(), &[], out);
646    let len = out[0].unwrap_i64() as Size;
647    trace!("Output length: {len}");
648    len
649}
650
651/// Get a pointer to the output data
652#[no_mangle]
653pub unsafe extern "C" fn extism_plugin_output_data(
654    ctx: *mut Context,
655    plugin: PluginIndex,
656) -> *const u8 {
657    trace!("Call to extism_plugin_output_data for plugin {plugin}");
658
659    let ctx = &mut *ctx;
660    let mut plugin_ref = match PluginRef::new(ctx, plugin, true) {
661        None => return std::ptr::null(),
662        Some(p) => p,
663    };
664    let plugin = plugin_ref.as_mut();
665    let ptr = plugin.memory_ptr();
666    let out = &mut [Val::I64(0)];
667    let mut store = &mut *(plugin.store_mut() as *mut Store<_>);
668    plugin
669        .linker
670        .get(&mut store, "env", "extism_output_offset")
671        .unwrap()
672        .into_func()
673        .unwrap()
674        .call(&mut store, &[], out)
675        .unwrap();
676
677    let offs = out[0].unwrap_i64() as usize;
678    trace!("Output offset: {}", offs);
679    ptr.add(offs)
680}
681
682/// Set log file and level
683#[no_mangle]
684pub unsafe extern "C" fn extism_log_file(
685    filename: *const c_char,
686    log_level: *const c_char,
687) -> bool {
688    use log::LevelFilter;
689    use log4rs::append::console::ConsoleAppender;
690    use log4rs::append::file::FileAppender;
691    use log4rs::config::{Appender, Config, Logger, Root};
692    use log4rs::encode::pattern::PatternEncoder;
693
694    let file = if !filename.is_null() {
695        let file = std::ffi::CStr::from_ptr(filename);
696        match file.to_str() {
697            Ok(x) => x,
698            Err(_) => {
699                return false;
700            }
701        }
702    } else {
703        "stderr"
704    };
705
706    let level = if !log_level.is_null() {
707        let level = std::ffi::CStr::from_ptr(log_level);
708        match level.to_str() {
709            Ok(x) => x,
710            Err(_) => {
711                return false;
712            }
713        }
714    } else {
715        "error"
716    };
717
718    let level = match LevelFilter::from_str(level) {
719        Ok(x) => x,
720        Err(_) => {
721            return false;
722        }
723    };
724
725    let encoder = Box::new(PatternEncoder::new("{t} {l} {d} - {m}\n"));
726
727    let logfile: Box<dyn log4rs::append::Append> =
728        if file == "-" || file == "stdout" || file == "stderr" {
729            let target = if file == "-" || file == "stdout" {
730                log4rs::append::console::Target::Stdout
731            } else {
732                log4rs::append::console::Target::Stderr
733            };
734            let console = ConsoleAppender::builder().target(target).encoder(encoder);
735            Box::new(console.build())
736        } else {
737            match FileAppender::builder().encoder(encoder).build(file) {
738                Ok(x) => Box::new(x),
739                Err(_) => {
740                    return false;
741                }
742            }
743        };
744
745    let config = match Config::builder()
746        .appender(Appender::builder().build("logfile", logfile))
747        .logger(
748            Logger::builder()
749                .appender("logfile")
750                .build("extism_runtime", level),
751        )
752        .build(Root::builder().build(LevelFilter::Off))
753    {
754        Ok(x) => x,
755        Err(_) => {
756            return false;
757        }
758    };
759
760    if log4rs::init_config(config).is_err() {
761        return false;
762    }
763    true
764}
765
766const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");
767
768/// Get the Extism version string
769#[no_mangle]
770pub unsafe extern "C" fn extism_version() -> *const c_char {
771    VERSION.as_ptr() as *const _
772}