diff --git a/Cargo.toml b/Cargo.toml index 7d63d19..4432caa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,4 +3,5 @@ members = [ "manifest", "runtime", "wasm/rust-pdk", + "rust", ] diff --git a/extism.go b/extism.go index f4bf834..cbff477 100644 --- a/extism.go +++ b/extism.go @@ -48,6 +48,15 @@ type Manifest struct { Config map[string]string `json:"config,omitempty"` } +func SetLogFile(filename string, level string) bool { + name := C.CString(filename) + l := C.CString(level) + r := C.extism_log_file(name, l) + C.free(unsafe.Pointer(name)) + C.free(unsafe.Pointer(l)) + return bool(r) +} + func register(data []byte, wasi bool) (Plugin, error) { plugin := C.extism_plugin_register( (*C.uchar)(unsafe.Pointer(&data[0])), @@ -80,13 +89,25 @@ func LoadPlugin(module io.Reader, wasi bool) (Plugin, error) { return register(wasm, wasi) } +func (plugin Plugin) SetConfig(data map[string][]byte) error { + s, err := json.Marshal(data) + if err != nil { + return err + } + C.extism_plugin_config(C.int(plugin.id), (*C.uchar)(unsafe.Pointer(&s[0])), C.uint64_t(len(s))) + return nil +} + func (plugin Plugin) Call(functionName string, input []byte) ([]byte, error) { + name := C.CString(functionName) rc := C.extism_call( C.int32_t(plugin.id), - C.CString(functionName), + name, (*C.uchar)(unsafe.Pointer(&input[0])), C.uint64_t(len(input)), ) + C.free(unsafe.Pointer(name)) + if rc != 0 { error := C.extism_error(C.int32_t(plugin.id)) if error != nil { diff --git a/manifest/README.md b/manifest/README.md new file mode 100644 index 0000000..74e3c49 --- /dev/null +++ b/manifest/README.md @@ -0,0 +1,5 @@ +# extsim-manifest + +This crate defines the manifest type for [Extism](https://github.com/extism/extism). + +The JSON Schema definition can be found in [schema.json](https://github.com/extism/extism/blob/main/manifest/schema.json) diff --git a/manifest/examples/json_schema.rs b/manifest/examples/json_schema.rs new file mode 100644 index 0000000..a5c0225 --- /dev/null +++ b/manifest/examples/json_schema.rs @@ -0,0 +1,7 @@ +use extism_manifest::Manifest; +use schemars::schema_for; + +fn main() { + let schema = schema_for!(Manifest); + println!("{}", serde_json::to_string_pretty(&schema).unwrap()); +} diff --git a/manifest/json_schema.sh b/manifest/json_schema.sh new file mode 100755 index 0000000..85df934 --- /dev/null +++ b/manifest/json_schema.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +cargo run --example json_schema --features=json_schema > schema.json diff --git a/manifest/schema.json b/manifest/schema.json new file mode 100644 index 0000000..a71519e --- /dev/null +++ b/manifest/schema.json @@ -0,0 +1,133 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Manifest", + "type": "object", + "properties": { + "config": { + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "memory": { + "default": { + "max": null + }, + "allOf": [ + { + "$ref": "#/definitions/ManifestMemory" + } + ] + }, + "wasm": { + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/ManifestWasm" + } + } + }, + "definitions": { + "ManifestMemory": { + "type": "object", + "properties": { + "max": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + } + }, + "ManifestWasm": { + "anyOf": [ + { + "type": "object", + "required": [ + "path" + ], + "properties": { + "hash": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "path": { + "type": "string" + } + } + }, + { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "string", + "format": "string" + }, + "hash": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + } + } + }, + { + "type": "object", + "required": [ + "url" + ], + "properties": { + "hash": { + "type": [ + "string", + "null" + ] + }, + "header": { + "default": {}, + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "method": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "url": { + "type": "string" + } + } + } + ] + } + } +} diff --git a/manifest/src/lib.rs b/manifest/src/lib.rs index dc9aa97..45f983b 100644 --- a/manifest/src/lib.rs +++ b/manifest/src/lib.rs @@ -1,11 +1,22 @@ use std::collections::BTreeMap; #[derive(Default, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))] pub struct ManifestMemory { pub max: Option, } #[derive(serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))] +pub struct HttpRequest { + pub url: String, + #[serde(default)] + pub header: std::collections::BTreeMap, + pub method: Option, +} + +#[derive(serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))] #[serde(untagged)] pub enum ManifestWasm { File { @@ -15,23 +26,31 @@ pub enum ManifestWasm { }, Data { #[serde(with = "base64")] + #[cfg_attr(feature = "json_schema", schemars(schema_with = "base64_schema"))] data: Vec, name: Option, hash: Option, }, Url { - url: String, - #[serde(default)] - header: BTreeMap, + #[serde(flatten)] + req: HttpRequest, name: Option, - method: Option, hash: Option, }, } +#[cfg(feature = "json_schema")] +fn base64_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { + use schemars::{schema::SchemaObject, JsonSchema}; + let mut schema: SchemaObject = ::json_schema(gen).into(); + schema.format = Some("string".to_owned()); + schema.into() +} + #[derive(Default, serde::Serialize, serde::Deserialize)] +#[cfg_attr(feature = "json_schema", derive(schemars::JsonSchema))] pub struct Manifest { - #[serde(default = "Vec::new")] + #[serde(default)] pub wasm: Vec, #[serde(default)] pub memory: ManifestMemory, diff --git a/node/index.js b/node/index.js index 0e5641a..7a1c805 100644 --- a/node/index.js +++ b/node/index.js @@ -11,12 +11,18 @@ var lib = ffi.Library( extism_error: ['char*', ['int32']], extism_call: ['int32', ['int32', 'string', 'string', 'uint64']], extism_output_length: ['uint64', ['int32']], - extism_output_get: ['void', ['int32', 'char*', 'uint64']] + extism_output_get: ['void', ['int32', 'char*', 'uint64']], + extism_log_file: ['bool', ['string', 'char*']], + extism_plugin_config: ['void', ['int32', 'char*', 'uint64']], } ) +export function set_log_file(filename, level = null) { + lib.extism_log_file(filename, level) +} + export class Plugin { - constructor(data, wasi = false) { + constructor(data, wasi = false, config = null) { if (typeof data === "object" && data.wasm) { data = JSON.stringify(data); } @@ -25,6 +31,11 @@ export class Plugin { throw "Unable to load plugin"; } this.id = plugin; + + if (config != null) { + let s = JSON.stringify(config); + lib.extism_plugin_config(this.id, s, s.length); + } } call(name, input) { diff --git a/ocaml/lib/extism.ml b/ocaml/lib/extism.ml index 0c2638a..efaebb6 100644 --- a/ocaml/lib/extism.ml +++ b/ocaml/lib/extism.ml @@ -59,6 +59,9 @@ module Bindings = struct let extism_output_get = fn "extism_output_get" (int32_t @-> ptr char @-> uint64_t @-> returning void) + + let extism_log_file = + fn "extism_log_file" (string @-> string_opt @-> returning bool) end type error = [ `Msg of string ] @@ -125,6 +128,9 @@ end exception Failed_to_load_plugin +let set_log_file ?level filename = + Bindings.extism_log_file filename level + let register ?(wasi = false) wasm = let id = Bindings.extism_plugin_register wasm diff --git a/ocaml/lib/extism.mli b/ocaml/lib/extism.mli index 35d0268..6f56bfb 100644 --- a/ocaml/lib/extism.mli +++ b/ocaml/lib/extism.mli @@ -42,6 +42,7 @@ module Manifest : sig val json: t -> string end +val set_log_file: ?level:string -> string -> bool val register: ?wasi:bool -> string -> t val register_manifest: ?wasi:bool -> Manifest.t -> t val call_bigstring: t -> name:string -> Bigstringaf.t -> (Bigstringaf.t, error) result diff --git a/python/extism/extism.py b/python/extism/extism.py index faef927..9aa2bd6 100644 --- a/python/extism/extism.py +++ b/python/extism/extism.py @@ -86,9 +86,18 @@ class Base64Encoder(json.JSONEncoder): return json.JSONEncoder.default(self, o) +def set_log_file(file, level=ffi.NULL): + if isinstance(level, str): + level = level.encode() + lib.extism_log_file(file.encode(), level) + + class Plugin: - def __init__(self, plugin: Union[str, bytes, dict], wasi=False): + def __init__(self, + plugin: Union[str, bytes, dict], + wasi=False, + config=None): if isinstance(plugin, str) and os.path.exists(plugin): with open(plugin, 'rb') as f: wasm = f.read() @@ -102,6 +111,10 @@ class Plugin: # Register plugin self.plugin = lib.extism_plugin_register(wasm, len(wasm), wasi) + if config is not None: + s = json.dumps(config).encode() + lib.extism_plugin_config(s, len(s)) + def _check_error(self, rc): if rc != 0: error = lib.extism_error(self.plugin) diff --git a/ruby/lib/extism.rb b/ruby/lib/extism.rb index bc64f3b..2693f5a 100644 --- a/ruby/lib/extism.rb +++ b/ruby/lib/extism.rb @@ -9,26 +9,40 @@ module C attach_function :extism_call, [:int32, :string, :pointer, :uint64], :int32 attach_function :extism_output_length, [:int32], :uint64 attach_function :extism_output_get, [:int32, :pointer, :uint64], :void + attack_function :extism_log_file, [:string, :pointer], :void end class Error < StandardError end +def set_log_file(name, level=FFI::NIL) + if level != FFI::NIL then + level = FFI::MemoryPointer::from_string(level) + end + C.extism_log_file(name, level) +end + class Plugin - def initialize(wasm, wasi=false) + def initialize(wasm, wasi=false, config=nil) if wasm.class == Hash then wasm = JSON.generate(wasm) end code = FFI::MemoryPointer.new(:char, wasm.bytesize) - code.put_bytes(0, wasm) + code.put_bytes(0, wasm) @plugin = C.extism_plugin_register(code, wasm.bytesize, wasi) + + if config != nil and @plugin >= 0 then + s = JSON.generate(config) + ptr = FFI::MemoryPointer::from_string(s) + C.extism_plugin_config(@plugin, ptr, s.bytesize) + end end - + def call(name, data) input = FFI::MemoryPointer::from_string(data) rc = C.extism_call(@plugin, name, input, data.bytesize) - if rc != 0 then + if rc != 0 then err = C.extism_error(@plugin) if err.empty? then raise Error.new "extism_call failed" diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 86a3d8f..f08f43c 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -22,6 +22,8 @@ serde = { version = "1", features=["derive"] } toml = "0.5" serde_json = "1" sha2 = "0.10" +log = "0.4" +log4rs = "1.1" ureq = {version = "2.5", optional=true} extism-manifest = { version = "0.0.1-alpha", path = "../manifest" } pretty-hex = { version = "0.3", optional = true } diff --git a/runtime/extism.h b/runtime/extism.h index 692d632..d5b9b19 100644 --- a/runtime/extism.h +++ b/runtime/extism.h @@ -15,7 +15,7 @@ bool extism_plugin_config(ExtismPlugin plugin, const uint8_t *json, ExtismSize j bool extism_function_exists(ExtismPlugin plugin, const char *func_name); -int32_t extism_call(ExtismPlugin plugin, +int32_t extism_call(ExtismPlugin plugin_id, const char *func_name, const uint8_t *data, ExtismSize data_len); @@ -25,3 +25,5 @@ const char *extism_error(ExtismPlugin plugin); ExtismSize extism_output_length(ExtismPlugin plugin); void extism_output_get(ExtismPlugin plugin, uint8_t *buf, ExtismSize len); + +bool extism_log_file(const char *filename, const char *log_level); diff --git a/runtime/src/export.rs b/runtime/src/export.rs index dbd43aa..ba70503 100644 --- a/runtime/src/export.rs +++ b/runtime/src/export.rs @@ -261,14 +261,6 @@ pub(crate) fn var_set( Ok(()) } -#[derive(serde::Serialize, serde::Deserialize)] -struct HttpRequest { - url: String, - #[serde(default)] - header: std::collections::BTreeMap, - method: Option, -} - pub(crate) fn http_request( #[allow(unused_mut)] mut caller: Caller, input: &[Val], @@ -277,7 +269,10 @@ pub(crate) fn http_request( #[cfg(not(feature = "http"))] { let _ = (caller, input, output); - panic!("HTTP requests have been disabled"); + + output[0] = Val::I64(0 as i64); + error!("http_request is not enabled"); + return Ok(()); } #[cfg(feature = "http")] @@ -288,25 +283,37 @@ pub(crate) fn http_request( let length = match memory!(data).block_length(offset) { Some(x) => x, - None => return Err(Trap::new("Invalid offset in call to config_get")), + None => return Err(Trap::new("Invalid offset in call to http_request")), }; let buf = memory!(data).get((offset, length)); - let req: HttpRequest = + let req: extism_manifest::HttpRequest = serde_json::from_slice(buf).map_err(|_| Trap::new("Invalid http request"))?; + let body_offset = input[1].unwrap_i64() as usize; + let mut r = ureq::request(req.method.as_deref().unwrap_or("GET"), &req.url); for (k, v) in req.header.iter() { r = r.set(k, v); } - let mut r = r - .call() - .map_err(|e| Trap::new(format!("{:?}", e)))? - .into_reader(); + let mut res = if body_offset > 0 { + let length = match memory!(data).block_length(body_offset) { + Some(x) => x, + None => return Err(Trap::new("Invalid offset in call to http_request")), + }; + let buf = memory!(data).get((offset, length)); + r.send_bytes(buf) + .map_err(|e| Trap::new(&format!("Request error: {e:?}")))? + .into_reader() + } else { + r.call() + .map_err(|e| Trap::new(format!("{:?}", e)))? + .into_reader() + }; let mut buf = Vec::new(); - r.read_to_end(&mut buf) + res.read_to_end(&mut buf) .map_err(|e| Trap::new(format!("{:?}", e)))?; let mem = memory!(mut data).alloc_bytes(buf)?; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 844b7bd..e464be3 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -15,3 +15,5 @@ pub use plugin_ref::PluginRef; pub type Size = u64; pub type PluginIndex = i32; + +pub(crate) use log::{debug, error, info}; diff --git a/runtime/src/manifest.rs b/runtime/src/manifest.rs index 2ac0a06..61684e3 100644 --- a/runtime/src/manifest.rs +++ b/runtime/src/manifest.rs @@ -100,9 +100,12 @@ fn to_module( #[allow(unused)] extism_manifest::ManifestWasm::Url { name, - url, - header, - method, + req: + extism_manifest::HttpRequest { + url, + header, + method, + }, hash, } => { let file_name = url.split('/').last().unwrap(); diff --git a/runtime/src/memory.rs b/runtime/src/memory.rs index daa0dd8..23b2ea5 100644 --- a/runtime/src/memory.rs +++ b/runtime/src/memory.rs @@ -128,16 +128,24 @@ impl PluginMemory { /// Reserve `n` bytes of memory pub fn alloc(&mut self, n: usize) -> Result { + 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 - 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); @@ -147,6 +155,8 @@ impl PluginMemory { // If there aren't enough bytes, try to grow the memory size if self.position + n >= self.size() { + debug!("Need more memory"); + let bytes_needed = (self.position as f64 + n as f64 - self.memory.data_size(&self.store) as f64) / PAGE_SIZE as f64; @@ -155,6 +165,7 @@ impl PluginMemory { pages_needed = 1 } + info!("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, pages_needed)?; } @@ -178,6 +189,7 @@ impl PluginMemory { /// Free the block allocated at `offset` pub fn free(&mut self, offset: usize) { + info!("Freeing block at {offset}"); if let Some(length) = self.live_blocks.remove(&offset) { self.free.push(MemoryBlock { offset, length }); } else { diff --git a/runtime/src/plugin.rs b/runtime/src/plugin.rs index 7426639..b7f9b2d 100644 --- a/runtime/src/plugin.rs +++ b/runtime/src/plugin.rs @@ -91,12 +91,12 @@ impl Plugin { // Add builtins for (_name, module) in modules.iter() { for import in module.imports() { - let m = import.module(); - let n = import.name(); + let module_name = import.module(); + let name = import.name(); use ValType::*; - if m == EXPORT_MODULE_NAME { - define_funcs!(n, { + if module_name == EXPORT_MODULE_NAME { + define_funcs!(name, { alloc(I64) -> I64; free(I64); load_u8(I64) -> I32; @@ -111,23 +111,15 @@ impl Plugin { config_get(I64) -> I64; var_get(I64) -> I64; var_set(I64, I64); - http_request(I64) -> I64; + http_request(I64, I64) -> I64; length(I64) -> I64; }); } // Define memory or check to ensure the symbol is exported by another module // since it doesn't match one of our known exports - match import.ty() { - ExternType::Memory(t) => { - linker.define(m, n, Extern::Memory(Memory::new(&mut memory.store, t)?))?; - } - ExternType::Func(_f) => { - if !m.starts_with("wasi") && !exports.contains_key(n) { - panic!("Invalid export: {m}::{n}") - } - } - _ => (), + if !module_name.starts_with("wasi") && !exports.contains_key(name) { + return Err(anyhow::format_err!("Invalid export: {module_name}::{name}")); } } } diff --git a/runtime/src/plugin_ref.rs b/runtime/src/plugin_ref.rs index 3f27f7b..50140df 100644 --- a/runtime/src/plugin_ref.rs +++ b/runtime/src/plugin_ref.rs @@ -18,12 +18,14 @@ impl<'a> PluginRef<'a> { /// /// This function is used to access the static `PLUGINS` registry pub unsafe fn new(plugin: PluginIndex) -> Self { - let mut plugins = PLUGINS - .lock() - .expect("Unable to acquire lock on plugin registry"); + let mut plugins = match PLUGINS.lock() { + Ok(p) => p, + Err(e) => e.into_inner(), + }; if plugin < 0 || plugin as usize >= plugins.len() { - panic!("Invalid PluginIndex {plugin}") + drop(plugins); + panic!("Invalid PluginIndex {plugin}"); } let plugin = plugins.get_unchecked_mut(plugin as usize) as *mut _; diff --git a/runtime/src/sdk.rs b/runtime/src/sdk.rs index b901e22..2a08c5b 100644 --- a/runtime/src/sdk.rs +++ b/runtime/src/sdk.rs @@ -1,6 +1,7 @@ #![allow(clippy::missing_safety_doc)] use std::os::raw::c_char; +use std::str::FromStr; use crate::*; @@ -14,18 +15,21 @@ pub unsafe extern "C" fn extism_plugin_register( let plugin = match Plugin::new(data, with_wasi) { Ok(x) => x, Err(e) => { - eprintln!("Error creating Plugin: {:?}", e); + error!("Error creating Plugin: {:?}", e); return -1; } }; - // Acquire lock and add plugin to registry - if let Ok(mut plugins) = PLUGINS.lock() { - plugins.push(plugin); - return (plugins.len() - 1) as PluginIndex; - } + let mut plugins = match PLUGINS.lock() { + Ok(p) => p, + Err(e) => e.into_inner(), + }; - -1 + // Acquire lock and add plugin to registry + plugins.push(plugin); + let id = (plugins.len() - 1) as PluginIndex; + info!("New plugin added: {id}"); + return id; } #[no_mangle] @@ -64,26 +68,37 @@ pub unsafe extern "C" fn extism_function_exists( let mut plugin = PluginRef::new(plugin); let name = std::ffi::CStr::from_ptr(func_name); - let name = name.to_str().expect("Invalid function name"); + let name = match name.to_str() { + Ok(x) => x, + Err(_) => return false, + }; + plugin.as_mut().get_func(name).is_some() } #[no_mangle] pub unsafe extern "C" fn extism_call( - plugin: PluginIndex, + plugin_id: PluginIndex, func_name: *const c_char, data: *const u8, data_len: Size, ) -> i32 { - let mut plugin = PluginRef::new(plugin).init(); + let mut plugin = PluginRef::new(plugin_id).init(); let plugin = plugin.as_mut(); // Find function let name = std::ffi::CStr::from_ptr(func_name); - let name = name.to_str().expect("Invalid function name"); - let func = plugin - .get_func(name) - .unwrap_or_else(|| panic!("Function not found {name}")); + let name = match name.to_str() { + Ok(name) => name, + Err(e) => return plugin.error(e, -1), + }; + + debug!("Calling function: {name} in plugin {plugin_id}"); + + let func = match plugin.get_func(name) { + Some(x) => x, + None => return plugin.error(format!("Function not found: {name}"), -1), + }; // Write input to memory let data = std::slice::from_raw_parts(data, data_len as usize); @@ -99,14 +114,14 @@ pub unsafe extern "C" fn extism_call( plugin.set_input(handle); // Call function with offset+length pointing to input data. - // TODO: In the future this could be a JSON or Protobuf payload. let mut results = vec![Val::I32(0)]; match func.call(&mut plugin.memory.store, &[], results.as_mut_slice()) { Ok(r) => r, Err(e) => { #[cfg(feature = "debug")] plugin.dump_memory(); - return plugin.error(e.context("Invalid write"), -1); + error!("Call: {e:?}"); + return plugin.error(e.context("Call failed"), -1); } }; @@ -148,3 +163,66 @@ pub unsafe extern "C" fn extism_output_get(plugin: PluginIndex, buf: *mut u8, le ) .expect("Out of bounds read in extism_output_get"); } + +#[no_mangle] +pub unsafe extern "C" fn extism_log_file( + filename: *const c_char, + log_level: *const c_char, +) -> bool { + use log::LevelFilter; + use log4rs::append::file::FileAppender; + use log4rs::config::{Appender, Config, Root}; + use log4rs::encode::pattern::PatternEncoder; + + let file = std::ffi::CStr::from_ptr(filename); + let file = match file.to_str() { + Ok(x) => x, + Err(_) => { + return false; + } + }; + + let level = if log_level.is_null() { + "error" + } else { + let level = std::ffi::CStr::from_ptr(log_level); + match level.to_str() { + Ok(x) => x, + Err(_) => { + return false; + } + } + }; + + let level = match LevelFilter::from_str(level) { + Ok(x) => x, + Err(_) => { + return false; + } + }; + + let logfile = match FileAppender::builder() + .encoder(Box::new(PatternEncoder::new("{d}: {l} - {m}\n"))) + .build(file) + { + Ok(x) => x, + Err(e) => { + error!("Unable to set up log encoder: {e:?}"); + return false; + } + }; + + let config = match Config::builder() + .appender(Appender::builder().build("logfile", Box::new(logfile))) + .build(Root::builder().appender("logfile").build(level)) + { + Ok(x) => x, + Err(e) => { + error!("Unable to configure log file: {e:?}"); + return false; + } + }; + + log4rs::init_config(config).expect("Log initialization failed"); + true +} diff --git a/rust/Cargo.toml b/rust/Cargo.toml index bd63586..adbde74 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -11,4 +11,5 @@ description = "Extism Host SDK for Rust" [dependencies] extism-manifest = { version = "0.0.1-alpha", path = "../manifest" } -serde_json = "1" \ No newline at end of file +serde_json = "1" +log = "0.4" \ No newline at end of file diff --git a/rust/src/bindings.rs b/rust/src/bindings.rs index caba08c..e533a1d 100644 --- a/rust/src/bindings.rs +++ b/rust/src/bindings.rs @@ -42,3 +42,9 @@ extern "C" { extern "C" { pub fn extism_output_get(plugin: ExtismPlugin, buf: *mut u8, len: ExtismSize); } +extern "C" { + pub fn extism_log_file( + filename: *const ::std::os::raw::c_char, + log_level: *const ::std::os::raw::c_char, + ) -> bool; +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 41980f3..deb3f5c 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -55,6 +55,34 @@ impl Plugin { Ok(()) } + pub fn with_config(self, config: &BTreeMap) -> Result { + self.set_config(config)?; + Ok(self) + } + + pub fn set_log_file( + &self, + filename: impl AsRef, + log_level: Option, + ) { + let log_level = log_level.map(|x| x.as_str()); + unsafe { + bindings::extism_log_file( + filename.as_ref().as_os_str().to_string_lossy().as_ptr() as *const _, + log_level.map(|x| x.as_ptr()).unwrap_or(std::ptr::null()) as *const _, + ); + } + } + + pub fn with_log_file( + self, + filename: impl AsRef, + log_level: Option, + ) -> Self { + self.set_log_file(filename, log_level); + self + } + pub fn has_function(&self, name: impl AsRef) -> bool { let name = std::ffi::CString::new(name.as_ref()).expect("Invalid function name"); unsafe { bindings::extism_function_exists(self.0 as i32, name.as_ptr() as *const _) } @@ -100,7 +128,9 @@ mod tests { fn it_works() { let wasm = include_bytes!("../../wasm/code.wasm"); let wasm_start = Instant::now(); - let plugin = Plugin::new(wasm, false).unwrap(); + let plugin = Plugin::new(wasm, false) + .unwrap() + .with_log_file("test.log", Some(log::LevelFilter::Info)); println!("register loaded plugin: {:?}", wasm_start.elapsed()); let repeat = 1182; @@ -143,7 +173,7 @@ mod tests { // .collect::>() // .len(); - let mut _native_vowel_count = 0; + let mut native_vowel_count2 = 0; let input: &[u8] = input.as_ref(); for i in 0..input.len() { if input[i] == b'A' @@ -157,7 +187,7 @@ mod tests { || input[i] == b'o' || input[i] == b'u' { - _native_vowel_count += 1; + native_vowel_count2 += 1; } } native_start.elapsed() diff --git a/wasm/rust-pdk/Cargo.toml b/wasm/rust-pdk/Cargo.toml index 9b9d304..f22c0be 100644 --- a/wasm/rust-pdk/Cargo.toml +++ b/wasm/rust-pdk/Cargo.toml @@ -8,6 +8,7 @@ homepage = "https://extism.org" repository = "https://github.com/extism/extism" description = "Extism Plug-in Development Kit (PDK) for Rust" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +extism-manifest = {path = "../../manifest"} +serde_json = "1" diff --git a/wasm/rust-pdk/src/bindings.rs b/wasm/rust-pdk/src/bindings.rs index 52e94d6..b0366b3 100644 --- a/wasm/rust-pdk/src/bindings.rs +++ b/wasm/rust-pdk/src/bindings.rs @@ -16,6 +16,7 @@ extern "C" { pub fn extism_config_get(offs: u64) -> u64; pub fn extism_kv_get(offs: u64) -> u64; pub fn extism_kv_set(offs: u64, offs1: u64); + pub fn extism_http_request(req: u64, body: u64) -> u64; } /// # Safety diff --git a/wasm/rust-pdk/src/lib.rs b/wasm/rust-pdk/src/lib.rs index e9eb86b..8200ced 100644 --- a/wasm/rust-pdk/src/lib.rs +++ b/wasm/rust-pdk/src/lib.rs @@ -103,6 +103,24 @@ impl Host { unsafe { std::str::from_utf8_unchecked(self.input.as_slice()) } } + pub fn http_request( + &self, + req: &extism_manifest::HttpRequest, + body: Option<&[u8]>, + ) -> Result, serde_json::Error> { + let enc = serde_json::to_vec(req)?; + let req = self.alloc_bytes(&enc); + let body = match body { + Some(b) => self.alloc_bytes(b).offset, + None => 0, + }; + let res = unsafe { extism_http_request(req.offset, body) }; + let len = unsafe { extism_length(res) }; + let mut dest = vec![0; len as usize]; + unsafe { bindings::extism_load(res, &mut dest) }; + Ok(dest) + } + pub fn output(&self, data: impl AsRef<[u8]>) { let len = data.as_ref().len(); unsafe {