1use crate::{
8 PluginCapabilities, PluginContext, PluginError, PluginHealth, PluginId, PluginManifest,
9 PluginMetrics, PluginResult, PluginState, Result,
10};
11use std::collections::HashMap;
12use std::path::Path;
13use std::sync::{Arc, OnceLock};
14use tokio::sync::RwLock;
15use tracing;
16use wasmtime::{
17 Config, Engine, Linker, Module, PoolingAllocationConfig, ResourceLimiter, Store, StoreLimits,
18 StoreLimitsBuilder,
19};
20use wasmtime_wasi::p2::WasiCtxBuilder;
21use wasmtime_wasi::preview1::{self, WasiP1Ctx};
22use wasmtime_wasi::{DirPerms, FilePerms};
23
24pub struct PluginRuntime {
26 engine: OnceLock<Engine>,
28 plugins: RwLock<HashMap<PluginId, Arc<RwLock<PluginInstance>>>>,
30 config: RuntimeConfig,
32}
33
34impl PluginRuntime {
35 pub fn new(config: RuntimeConfig) -> Result<Self> {
40 Ok(Self {
41 engine: OnceLock::new(),
42 plugins: RwLock::new(HashMap::new()),
43 config,
44 })
45 }
46
47 fn get_engine(&self) -> &Engine {
49 self.engine.get_or_init(|| {
50 let mut config = Config::new();
53
54 config.consume_fuel(true);
56
57 config.epoch_interruption(true);
59
60 config.max_wasm_stack(2 * 1024 * 1024); config.wasm_threads(false);
65 config.wasm_bulk_memory(true); config.wasm_simd(false); config.wasm_multi_memory(false);
68
69 config.allocation_strategy(wasmtime::InstanceAllocationStrategy::Pooling(
71 PoolingAllocationConfig::default(),
72 ));
73
74 Engine::new(&config).expect("Failed to create WASM engine with security config")
75 })
76 }
77
78 pub async fn load_plugin(
80 &self,
81 plugin_id: PluginId,
82 manifest: PluginManifest,
83 wasm_path: &Path,
84 ) -> Result<()> {
85 self.validate_plugin_path(wasm_path)?;
87
88 self.validate_file_size(wasm_path)?;
90
91 let plugin_capabilities = PluginCapabilities::from_strings(&manifest.capabilities);
93 self.validate_capabilities(&plugin_capabilities)?;
94
95 self.validate_manifest_security(&manifest)?;
97
98 let engine = self.get_engine();
101 let module = Module::from_file(engine, wasm_path)
102 .map_err(|e| PluginError::wasm(format!("Failed to load WASM module: {}", e)))?;
103
104 ModuleValidator::validate_module(&module, &plugin_capabilities)?;
106
107 self.validate_module_security(&module)?;
109
110 let instance =
112 PluginInstance::new(plugin_id.clone(), manifest, module, self.config.clone()).await?;
113
114 let mut plugins = self.plugins.write().await;
116 #[allow(clippy::arc_with_non_send_sync)]
117 plugins.insert(plugin_id, Arc::new(RwLock::new(instance)));
118
119 Ok(())
120 }
121
122 pub async fn unload_plugin(&self, plugin_id: &PluginId) -> Result<()> {
124 let mut plugins = self.plugins.write().await;
125 if let Some(instance) = plugins.remove(plugin_id) {
126 let mut instance = instance.write().await;
127 instance.unload().await?;
128 }
129 Ok(())
130 }
131
132 pub async fn execute_plugin_function<T>(
134 &self,
135 plugin_id: &PluginId,
136 function_name: &str,
137 context: &PluginContext,
138 input: &[u8],
139 ) -> Result<PluginResult<T>>
140 where
141 T: serde::de::DeserializeOwned,
142 {
143 let plugins = self.plugins.read().await;
144 let instance = plugins
145 .get(plugin_id)
146 .ok_or_else(|| PluginError::execution("Plugin not found"))?;
147
148 let mut instance = instance.write().await;
149 instance.execute_function(function_name, context, input).await
150 }
151
152 pub async fn get_plugin_health(&self, plugin_id: &PluginId) -> Result<PluginHealth> {
154 let plugins = self.plugins.read().await;
155 let instance = plugins
156 .get(plugin_id)
157 .ok_or_else(|| PluginError::execution("Plugin not found"))?;
158
159 let instance = instance.read().await;
160 Ok(instance.get_health().await)
161 }
162
163 pub async fn list_plugins(&self) -> Vec<PluginId> {
165 let plugins = self.plugins.read().await;
166 plugins.keys().cloned().collect()
167 }
168
169 pub async fn get_plugin_metrics(&self, plugin_id: &PluginId) -> Result<PluginMetrics> {
171 let plugins = self.plugins.read().await;
172 let instance = plugins
173 .get(plugin_id)
174 .ok_or_else(|| PluginError::execution("Plugin not found"))?;
175
176 let instance = instance.read().await;
177 Ok(instance.metrics.clone())
178 }
179
180 fn validate_capabilities(&self, capabilities: &PluginCapabilities) -> Result<()> {
182 if capabilities.resources.max_memory_bytes > self.config.max_memory_per_plugin {
184 return Err(PluginError::security(format!(
185 "Plugin memory limit {} exceeds runtime limit {}",
186 capabilities.resources.max_memory_bytes, self.config.max_memory_per_plugin
187 )));
188 }
189
190 if capabilities.resources.max_cpu_percent > self.config.max_cpu_per_plugin {
192 return Err(PluginError::security(format!(
193 "Plugin CPU limit {:.2}% exceeds runtime limit {:.2}%",
194 capabilities.resources.max_cpu_percent, self.config.max_cpu_per_plugin
195 )));
196 }
197
198 if capabilities.resources.max_execution_time_ms > self.config.max_execution_time_ms {
200 return Err(PluginError::security(format!(
201 "Plugin execution time limit {}ms exceeds runtime limit {}ms",
202 capabilities.resources.max_execution_time_ms, self.config.max_execution_time_ms
203 )));
204 }
205
206 if capabilities.network.allow_http && !self.config.allow_network_access {
208 return Err(PluginError::security(
209 "Plugin requires network access but runtime disallows it",
210 ));
211 }
212
213 Ok(())
214 }
215
216 fn validate_plugin_path(&self, wasm_path: &Path) -> Result<()> {
218 let canonicalized = wasm_path
219 .canonicalize()
220 .map_err(|e| PluginError::security(format!("Invalid plugin path: {}", e)))?;
221
222 if self.config.allowed_fs_paths.is_empty() {
224 return Err(PluginError::security("No allowed plugin paths configured"));
225 }
226
227 for allowed_path in &self.config.allowed_fs_paths {
228 if canonicalized.starts_with(allowed_path) {
229 return Ok(());
230 }
231 }
232
233 Err(PluginError::security(format!(
234 "Plugin path {} is not within allowed directories",
235 canonicalized.display()
236 )))
237 }
238
239 fn validate_file_size(&self, wasm_path: &Path) -> Result<()> {
241 let metadata = std::fs::metadata(wasm_path).map_err(|e| {
242 PluginError::security(format!("Cannot read plugin file metadata: {}", e))
243 })?;
244
245 const MAX_PLUGIN_SIZE: u64 = 50 * 1024 * 1024; if metadata.len() > MAX_PLUGIN_SIZE {
247 return Err(PluginError::security(format!(
248 "Plugin file size {} exceeds maximum allowed size {}",
249 metadata.len(),
250 MAX_PLUGIN_SIZE
251 )));
252 }
253
254 Ok(())
255 }
256
257 fn validate_manifest_security(&self, manifest: &PluginManifest) -> Result<()> {
259 if !manifest.info.name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
261 return Err(PluginError::security("Plugin name contains unsafe characters"));
262 }
263
264 let dangerous_caps = ["raw_syscalls", "kernel_access", "direct_memory"];
266 for cap in &manifest.capabilities {
267 if dangerous_caps.contains(&cap.as_str()) {
268 return Err(PluginError::security(format!(
269 "Dangerous capability not allowed: {}",
270 cap
271 )));
272 }
273 }
274
275 if manifest.info.author.name.is_empty() || manifest.info.author.name.len() > 100 {
277 return Err(PluginError::security("Invalid author field in manifest"));
278 }
279
280 if manifest.info.id.0.is_empty() || manifest.info.id.0.len() > 100 {
282 return Err(PluginError::security("Invalid plugin ID format"));
283 }
284
285 if manifest.info.description.len() > 1000 {
287 return Err(PluginError::security("Plugin description too long"));
288 }
289
290 Ok(())
291 }
292
293 fn validate_module_security(&self, module: &Module) -> Result<()> {
295 for import in module.imports() {
297 match import.module() {
298 "env" => {
299 match import.name() {
301 "memory" | "table" => continue,
302 name if name.starts_with("__") => {
303 return Err(PluginError::security(format!(
304 "Dangerous import function: {}",
305 name
306 )));
307 }
308 _ => continue,
309 }
310 }
311 "wasi_snapshot_preview1" => {
312 continue;
314 }
315 module_name => {
316 return Err(PluginError::security(format!(
317 "Dangerous import module: {}",
318 module_name
319 )));
320 }
321 }
322 }
323
324 let mut has_init = false;
326 let mut has_process = false;
327
328 for export in module.exports() {
329 match export.name() {
330 "init" => has_init = true,
331 "process" => has_process = true,
332 name if name.starts_with("_") => {
333 return Err(PluginError::security(format!(
334 "Private export function not allowed: {}",
335 name
336 )));
337 }
338 _ => continue,
339 }
340 }
341
342 if !has_init || !has_process {
343 return Err(PluginError::security("Plugin must export 'init' and 'process' functions"));
344 }
345
346 Ok(())
347 }
348}
349
350#[derive(Debug, Clone)]
352pub struct RuntimeConfig {
353 pub max_memory_per_plugin: usize,
355 pub max_cpu_per_plugin: f64,
357 pub max_execution_time_ms: u64,
359 pub allow_network_access: bool,
361 pub allowed_fs_paths: Vec<String>,
363 pub max_concurrent_executions: usize,
365 pub cache_dir: Option<String>,
367 pub debug_logging: bool,
369}
370
371impl Default for RuntimeConfig {
372 fn default() -> Self {
373 Self {
374 max_memory_per_plugin: 10 * 1024 * 1024, max_cpu_per_plugin: 0.5, max_execution_time_ms: 5000, allow_network_access: false,
378 allowed_fs_paths: vec![],
379 max_concurrent_executions: 10,
380 cache_dir: None,
381 debug_logging: false,
382 }
383 }
384}
385
386pub struct WasiCtxWithLimits {
388 wasi: WasiP1Ctx,
390 limits: StoreLimits,
392}
393
394impl WasiCtxWithLimits {
395 fn new(wasi: WasiP1Ctx, limits: StoreLimits) -> Self {
396 Self { wasi, limits }
397 }
398}
399
400impl ResourceLimiter for WasiCtxWithLimits {
402 fn memory_growing(
403 &mut self,
404 current: usize,
405 desired: usize,
406 _maximum: Option<usize>,
407 ) -> anyhow::Result<bool> {
408 self.limits.memory_growing(current, desired, _maximum)
410 }
411
412 fn table_growing(
413 &mut self,
414 current: usize,
415 desired: usize,
416 _maximum: Option<usize>,
417 ) -> anyhow::Result<bool> {
418 self.limits.table_growing(current, desired, _maximum)
420 }
421}
422
423pub struct PluginInstance {
425 #[allow(dead_code)]
427 plugin_id: PluginId,
428 #[allow(dead_code)]
430 manifest: PluginManifest,
431 instance: wasmtime::Instance,
433 store: Store<WasiCtxWithLimits>,
435 state: PluginState,
437 metrics: PluginMetrics,
439 #[allow(dead_code)]
441 config: RuntimeConfig,
442 #[allow(dead_code)]
444 created_at: chrono::DateTime<chrono::Utc>,
445 limits: ExecutionLimits,
447}
448
449impl PluginInstance {
450 async fn new(
452 plugin_id: PluginId,
453 manifest: PluginManifest,
454 module: Module,
455 config: RuntimeConfig,
456 ) -> Result<Self> {
457 let limits = ExecutionLimits {
459 memory_limit: config.max_memory_per_plugin,
460 cpu_time_limit: config.max_execution_time_ms * 1_000_000, wall_time_limit: config.max_execution_time_ms * 2 * 1_000_000, fuel_limit: (config.max_execution_time_ms * 1_000), };
464
465 let store_limits = StoreLimitsBuilder::new()
467 .memory_size(limits.memory_limit)
468 .table_elements(1000) .instances(1) .tables(10) .memories(1) .build();
473
474 let mut wasi_ctx_builder = WasiCtxBuilder::new();
476
477 let wasi_ctx_builder = wasi_ctx_builder.inherit_stdio();
479
480 for path in &config.allowed_fs_paths {
482 wasi_ctx_builder.preopened_dir(
483 Path::new(path),
484 path.as_str(),
485 DirPerms::all(),
486 FilePerms::all(),
487 )?;
488 }
489
490 let wasi_ctx = wasi_ctx_builder.build_p1();
491
492 let ctx_with_limits = WasiCtxWithLimits::new(wasi_ctx, store_limits);
494
495 let mut store = Store::new(module.engine(), ctx_with_limits);
497
498 store.limiter(|ctx| &mut ctx.limits);
500
501 store
503 .set_fuel(limits.fuel_limit)
504 .map_err(|e| PluginError::wasm(format!("Failed to set fuel limit: {}", e)))?;
505
506 store.set_epoch_deadline(1);
509
510 let mut linker = Linker::<WasiCtxWithLimits>::new(module.engine());
512 preview1::add_to_linker_sync(&mut linker, |t| &mut t.wasi)
513 .map_err(|e| PluginError::wasm(format!("Failed to add WASI to linker: {}", e)))?;
514
515 let instance = linker.instantiate(&mut store, &module).map_err(|e| {
517 PluginError::wasm(format!("Failed to instantiate WASM module with WASI: {}", e))
518 })?;
519
520 Ok(Self {
524 plugin_id,
525 manifest,
526 instance,
527 store,
528 state: PluginState::Loaded,
529 metrics: PluginMetrics::default(),
530 config,
531 created_at: chrono::Utc::now(),
532 limits,
533 })
534 }
535
536 async fn execute_function<T>(
538 &mut self,
539 function_name: &str,
540 context: &PluginContext,
541 _input: &[u8],
542 ) -> Result<PluginResult<T>>
543 where
544 T: serde::de::DeserializeOwned,
545 {
546 let start_time = std::time::Instant::now();
547
548 self.state = PluginState::Executing;
550 self.metrics.total_executions += 1;
551
552 self.store
554 .set_fuel(self.limits.fuel_limit)
555 .map_err(|e| PluginError::execution(format!("Failed to reset fuel: {}", e)))?;
556
557 self.store.set_epoch_deadline(1);
559
560 let context_json = serde_json::to_string(context)
562 .map_err(|e| PluginError::execution(format!("Failed to serialize context: {}", e)))?;
563
564 let result = self.call_plugin_function(function_name, &context_json).await;
567
568 let fuel_consumed = match self.store.get_fuel() {
570 Ok(remaining) => self.limits.fuel_limit.saturating_sub(remaining),
571 Err(_) => 0, };
573
574 let execution_time = start_time.elapsed();
576 self.metrics.avg_execution_time_ms = (self.metrics.avg_execution_time_ms
577 * (self.metrics.total_executions - 1) as f64
578 + execution_time.as_millis() as f64)
579 / self.metrics.total_executions as f64;
580
581 if execution_time.as_millis() as u64 > self.metrics.max_execution_time_ms {
582 self.metrics.max_execution_time_ms = execution_time.as_millis() as u64;
583 }
584
585 self.state = PluginState::Ready;
587
588 match result {
589 Ok(output) => {
590 self.metrics.successful_executions += 1;
591 match serde_json::from_slice::<T>(&output) {
592 Ok(data) => {
593 tracing::debug!(
594 "Plugin execution completed: {} fuel consumed, {}ms elapsed",
595 fuel_consumed,
596 execution_time.as_millis()
597 );
598 Ok(PluginResult::success(data, execution_time.as_millis() as u64))
599 }
600 Err(e) => {
601 self.metrics.failed_executions += 1;
602 Err(PluginError::execution(format!("Failed to deserialize result: {}", e)))
603 }
604 }
605 }
606 Err(e) => {
607 self.metrics.failed_executions += 1;
608 tracing::error!(
609 "Plugin execution failed: {} fuel consumed, {}ms elapsed, error: {}",
610 fuel_consumed,
611 execution_time.as_millis(),
612 e
613 );
614 Err(e)
615 }
616 }
617 }
618
619 async fn call_plugin_function(&mut self, function_name: &str, input: &str) -> Result<Vec<u8>> {
621 let func = self.instance.get_func(&mut self.store, function_name).ok_or_else(|| {
623 PluginError::execution(format!("Function '{}' not found in WASM module", function_name))
624 })?;
625
626 let input_bytes = input.as_bytes();
628 let input_len = input_bytes.len() as i32;
629
630 let alloc_func = self.instance.get_func(&mut self.store, "alloc").ok_or_else(|| {
632 PluginError::execution(
633 "WASM module must export an 'alloc' function for memory allocation",
634 )
635 })?;
636
637 let mut alloc_result = [wasmtime::Val::I32(0)];
638 alloc_func
639 .call(&mut self.store, &[wasmtime::Val::I32(input_len)], &mut alloc_result)
640 .map_err(|e| {
641 PluginError::execution(format!("Failed to allocate memory for input: {}", e))
642 })?;
643
644 let input_ptr = match alloc_result[0] {
645 wasmtime::Val::I32(ptr) => ptr,
646 _ => {
647 return Err(PluginError::execution("alloc function did not return a valid pointer"))
648 }
649 };
650
651 let memory = self
653 .instance
654 .get_memory(&mut self.store, "memory")
655 .ok_or_else(|| PluginError::execution("WASM module must export a 'memory'"))?;
656
657 memory.write(&mut self.store, input_ptr as usize, input_bytes).map_err(|e| {
658 PluginError::execution(format!("Failed to write input to WASM memory: {}", e))
659 })?;
660
661 let mut func_result = [wasmtime::Val::I32(0), wasmtime::Val::I32(0)];
663 func.call(
664 &mut self.store,
665 &[wasmtime::Val::I32(input_ptr), wasmtime::Val::I32(input_len)],
666 &mut func_result,
667 )
668 .map_err(|e| {
669 PluginError::execution(format!(
670 "Failed to call WASM function '{}': {}",
671 function_name, e
672 ))
673 })?;
674
675 let output_ptr = match func_result[0] {
677 wasmtime::Val::I32(ptr) => ptr,
678 _ => {
679 return Err(PluginError::execution(format!(
680 "Function '{}' did not return a valid output pointer",
681 function_name
682 )))
683 }
684 };
685
686 let output_len = match func_result[1] {
687 wasmtime::Val::I32(len) => len,
688 _ => {
689 return Err(PluginError::execution(format!(
690 "Function '{}' did not return a valid output length",
691 function_name
692 )))
693 }
694 };
695
696 let mut output_bytes = vec![0u8; output_len as usize];
698 memory
699 .read(&mut self.store, output_ptr as usize, &mut output_bytes)
700 .map_err(|e| {
701 PluginError::execution(format!("Failed to read output from WASM memory: {}", e))
702 })?;
703
704 if let Some(dealloc_func) = self.instance.get_func(&mut self.store, "dealloc") {
706 let _ = dealloc_func.call(
707 &mut self.store,
708 &[wasmtime::Val::I32(input_ptr), wasmtime::Val::I32(input_len)],
709 &mut [],
710 );
711 let _ = dealloc_func.call(
712 &mut self.store,
713 &[
714 wasmtime::Val::I32(output_ptr),
715 wasmtime::Val::I32(output_len),
716 ],
717 &mut [],
718 );
719 }
720
721 Ok(output_bytes)
722 }
723
724 async fn get_health(&self) -> PluginHealth {
726 PluginHealth::healthy("Plugin is running".to_string(), self.metrics.clone())
727 }
728
729 async fn unload(&mut self) -> Result<()> {
731 self.state = PluginState::Unloading;
732 self.state = PluginState::Unloaded;
734 Ok(())
735 }
736}
737
738pub struct ExecutionLimits {
740 pub memory_limit: usize,
742 pub cpu_time_limit: u64,
744 pub wall_time_limit: u64,
746 pub fuel_limit: u64,
748}
749
750impl Default for ExecutionLimits {
751 fn default() -> Self {
752 Self {
753 memory_limit: 10 * 1024 * 1024, cpu_time_limit: 5_000_000_000, wall_time_limit: 10_000_000_000, fuel_limit: 1_000_000, }
758 }
759}
760
761pub struct SecurityContext {
763 pub allowed_syscalls: Vec<String>,
765 pub blocked_syscalls: Vec<String>,
767 pub network_policy: NetworkPolicy,
769 pub filesystem_policy: FilesystemPolicy,
771}
772
773impl Default for SecurityContext {
774 fn default() -> Self {
775 Self {
776 allowed_syscalls: vec![
777 "fd_write".to_string(),
778 "fd_read".to_string(),
779 "random_get".to_string(),
780 "clock_time_get".to_string(),
781 ],
782 blocked_syscalls: vec![
783 "path_open".to_string(),
784 "sock_open".to_string(),
785 "proc_exec".to_string(),
786 ],
787 network_policy: NetworkPolicy::DenyAll,
788 filesystem_policy: FilesystemPolicy::DenyAll,
789 }
790 }
791}
792
793#[derive(Debug, Clone)]
795pub enum NetworkPolicy {
796 AllowAll,
798 DenyAll,
800 AllowHosts(Vec<String>),
802}
803
804#[derive(Debug, Clone)]
806pub enum FilesystemPolicy {
807 AllowAll,
809 DenyAll,
811 AllowPaths(Vec<String>),
813}
814
815pub struct ModuleValidator;
817
818impl ModuleValidator {
819 pub fn validate_module(module: &Module, capabilities: &PluginCapabilities) -> Result<()> {
821 Self::validate_imports(module, capabilities)?;
823
824 Ok(())
825 }
826
827 fn validate_imports(module: &Module, capabilities: &PluginCapabilities) -> Result<()> {
829 for import in module.imports() {
830 let module_name = import.module();
831 let field_name = import.name();
832
833 match module_name {
834 "wasi_snapshot_preview1" | "wasi:io/streams" | "wasi:filesystem/types" => {
835 Self::validate_wasi_import(field_name, capabilities)?;
836 }
837 "mockforge:plugin/host" => {
838 Self::validate_host_import(field_name)?;
840 }
841 _ => {
842 return Err(PluginError::security(format!(
843 "Disallowed import module: {}",
844 module_name
845 )));
846 }
847 }
848 }
849
850 Ok(())
851 }
852
853 fn validate_wasi_import(field_name: &str, capabilities: &PluginCapabilities) -> Result<()> {
855 let filesystem_functions = [
857 "fd_read",
858 "fd_write",
859 "fd_close",
860 "fd_fdstat_get",
861 "path_open",
862 "path_readlink",
863 "path_filestat_get",
864 ];
865
866 if filesystem_functions.contains(&field_name)
867 && capabilities.filesystem.read_paths.is_empty()
868 && capabilities.filesystem.write_paths.is_empty()
869 {
870 return Err(PluginError::security(format!(
871 "Plugin imports filesystem function '{}' but has no filesystem capabilities",
872 field_name
873 )));
874 }
875
876 let allowed_functions = [
878 "fd_read",
879 "fd_write",
880 "fd_close",
881 "fd_fdstat_get",
882 "path_open",
883 "path_readlink",
884 "path_filestat_get",
885 "clock_time_get",
886 "proc_exit",
887 "random_get",
888 ];
889
890 if !allowed_functions.contains(&field_name) {
891 return Err(PluginError::security(format!("Disallowed WASI function: {}", field_name)));
892 }
893
894 Ok(())
895 }
896
897 fn validate_host_import(field_name: &str) -> Result<()> {
899 let allowed_functions = [
900 "log_message",
901 "get_config_value",
902 "store_data",
903 "retrieve_data",
904 ];
905
906 if !allowed_functions.contains(&field_name) {
907 return Err(PluginError::security(format!("Disallowed host function: {}", field_name)));
908 }
909
910 Ok(())
911 }
912
913 pub fn extract_plugin_interface(module: &Module) -> Result<PluginInterface> {
915 let mut functions = Vec::new();
916
917 for export in module.exports() {
919 if let wasmtime::ExternType::Func(func_type) = export.ty() {
920 let parameters: Vec<ValueType> = func_type
922 .params()
923 .filter_map(|param| match param {
924 wasmtime::ValType::I32 => Some(ValueType::I32),
925 wasmtime::ValType::I64 => Some(ValueType::I64),
926 wasmtime::ValType::F32 => Some(ValueType::F32),
927 wasmtime::ValType::F64 => Some(ValueType::F64),
928 _ => {
929 None
932 }
933 })
934 .collect();
935
936 let return_type = func_type.results().next().and_then(|result| match result {
938 wasmtime::ValType::I32 => Some(ValueType::I32),
939 wasmtime::ValType::I64 => Some(ValueType::I64),
940 wasmtime::ValType::F32 => Some(ValueType::F32),
941 wasmtime::ValType::F64 => Some(ValueType::F64),
942 _ => {
943 None
945 }
946 });
947
948 functions.push(PluginFunction {
949 name: export.name().to_string(),
950 signature: FunctionSignature {
951 parameters,
952 return_type,
953 },
954 documentation: None, });
956 }
957 }
958
959 Ok(PluginInterface { functions })
960 }
961}
962
963#[derive(Debug, Clone)]
965pub struct PluginInterface {
966 pub functions: Vec<PluginFunction>,
968}
969
970#[derive(Debug, Clone)]
972pub struct PluginFunction {
973 pub name: String,
975 pub signature: FunctionSignature,
977 pub documentation: Option<String>,
979}
980
981#[derive(Debug, Clone)]
983pub struct FunctionSignature {
984 pub parameters: Vec<ValueType>,
986 pub return_type: Option<ValueType>,
988}
989
990#[derive(Debug, Clone)]
992pub enum ValueType {
993 I32,
995 I64,
997 F32,
999 F64,
1001}
1002
1003#[cfg(test)]
1004mod tests {
1005
1006 #[test]
1007 fn test_module_compiles() {
1008 }
1010}