Skip to main content

sen_plugin_sdk/
lib.rs

1//! sen-plugin-sdk: SDK for creating WASM plugins
2//!
3//! This SDK provides utilities and helpers for creating WASM plugins
4//! with minimal boilerplate. Using this SDK, you can create a fully functional
5//! plugin in under 30 lines of code.
6//!
7//! # Table of Contents
8//!
9//! - [Project Setup](#project-setup)
10//! - [Quick Start](#quick-start)
11//! - [Arguments](#arguments)
12//! - [Error Handling](#error-handling)
13//! - [Advanced Usage](#advanced-usage)
14//! - [Manual Implementation](#manual-implementation)
15//! - [Best Practices](#best-practices)
16//! - [Troubleshooting](#troubleshooting)
17//!
18//! # Project Setup
19//!
20//! ## 1. Create a New Plugin Project
21//!
22//! ```bash
23//! cargo new --lib my-plugin
24//! cd my-plugin
25//! ```
26//!
27//! ## 2. Configure Cargo.toml
28//!
29//! Your complete `Cargo.toml` should look like:
30//!
31//! ```toml
32//! [package]
33//! name = "my-plugin"
34//! version = "0.1.0"
35//! edition = "2021"
36//!
37//! [lib]
38//! crate-type = ["cdylib"]  # Required for WASM output
39//!
40//! [dependencies]
41//! sen-plugin-sdk = { version = "0.7" }
42//!
43//! # Optimize for size (optional but recommended)
44//! [profile.release]
45//! opt-level = "s"
46//! lto = true
47//! strip = true
48//! ```
49//!
50//! ## 3. Install WASM Target (One-Time)
51//!
52//! ```bash
53//! rustup target add wasm32-unknown-unknown
54//! ```
55//!
56//! ## 4. Build Your Plugin
57//!
58//! ```bash
59//! cargo build --release --target wasm32-unknown-unknown
60//! ```
61//!
62//! The output file will be at:
63//! `target/wasm32-unknown-unknown/release/my_plugin.wasm`
64//!
65//! # Quick Start
66//!
67//! A minimal plugin requires:
68//! 1. A struct implementing the [`Plugin`] trait
69//! 2. The [`export_plugin!`] macro to generate WASM exports
70//!
71//! ```rust,ignore
72//! use sen_plugin_sdk::prelude::*;
73//!
74//! struct HelloPlugin;
75//!
76//! impl Plugin for HelloPlugin {
77//!     fn manifest() -> PluginManifest {
78//!         PluginManifest::new(
79//!             CommandSpec::new("hello", "Says hello to the world")
80//!                 .version("1.0.0")
81//!                 .arg(ArgSpec::positional("name").help("Name to greet"))
82//!         )
83//!     }
84//!
85//!     fn execute(args: Vec<String>) -> ExecuteResult {
86//!         let name = args.first().map(|s| s.as_str()).unwrap_or("World");
87//!         ExecuteResult::success(format!("Hello, {}!", name))
88//!     }
89//! }
90//!
91//! export_plugin!(HelloPlugin);
92//! ```
93//!
94//! # Arguments
95//!
96//! ## Positional Arguments
97//!
98//! Positional arguments are passed in order:
99//!
100//! ```rust,ignore
101//! CommandSpec::new("copy", "Copy files")
102//!     .arg(ArgSpec::positional("source").required().help("Source file"))
103//!     .arg(ArgSpec::positional("dest").required().help("Destination file"))
104//! ```
105//!
106//! Usage: `copy src.txt dst.txt`
107//!
108//! In `execute()`, args are: `["src.txt", "dst.txt"]`
109//!
110//! ## Options (Flags with Values)
111//!
112//! Named options with long and short forms:
113//!
114//! ```rust,ignore
115//! CommandSpec::new("greet", "Greet someone")
116//!     .arg(ArgSpec::positional("name").default("World"))
117//!     .arg(
118//!         ArgSpec::option("greeting", "greeting")
119//!             .short('g')
120//!             .help("Custom greeting message")
121//!             .default("Hello")
122//!     )
123//!     .arg(
124//!         ArgSpec::option("count", "count")
125//!             .short('n')
126//!             .help("Number of times to greet")
127//!             .default("1")
128//!     )
129//! ```
130//!
131//! Usage: `greet Alice -g "Good morning" --count 3`
132//!
133//! ## Required Arguments
134//!
135//! Mark arguments as required:
136//!
137//! ```rust,ignore
138//! ArgSpec::positional("file")
139//!     .required()
140//!     .help("Input file (required)")
141//! ```
142//!
143//! ## Default Values
144//!
145//! Provide fallback values:
146//!
147//! ```rust,ignore
148//! ArgSpec::option("format", "format")
149//!     .short('f')
150//!     .default("json")
151//!     .help("Output format [default: json]")
152//! ```
153//!
154//! ## Argument Parsing in execute()
155//!
156//! Arguments are passed as a `Vec<String>` in the order they appear.
157//! The host handles option parsing; your plugin receives resolved values:
158//!
159//! ```rust,ignore
160//! fn execute(args: Vec<String>) -> ExecuteResult {
161//!     // For: greet Alice -g "Hi"
162//!     // args = ["Alice", "Hi"]
163//!
164//!     let name = args.get(0).map(|s| s.as_str()).unwrap_or("World");
165//!     let greeting = args.get(1).map(|s| s.as_str()).unwrap_or("Hello");
166//!
167//!     ExecuteResult::success(format!("{}, {}!", greeting, name))
168//! }
169//! ```
170//!
171//! # Error Handling
172//!
173//! Plugins return [`ExecuteResult`] which can be:
174//!
175//! ## Success
176//!
177//! ```rust,ignore
178//! ExecuteResult::success("Operation completed successfully")
179//! ```
180//!
181//! ## User Error (Exit Code 1)
182//!
183//! For expected errors like invalid input:
184//!
185//! ```rust,ignore
186//! fn execute(args: Vec<String>) -> ExecuteResult {
187//!     let file = match args.first() {
188//!         Some(f) => f,
189//!         None => return ExecuteResult::user_error("Missing required argument: file"),
190//!     };
191//!
192//!     if !is_valid_format(file) {
193//!         return ExecuteResult::user_error(format!(
194//!             "Invalid file format: {}. Expected .json or .yaml",
195//!             file
196//!         ));
197//!     }
198//!
199//!     ExecuteResult::success("File processed")
200//! }
201//! ```
202//!
203//! ## System Error (Exit Code 101)
204//!
205//! For unexpected internal errors:
206//!
207//! ```rust,ignore
208//! fn execute(args: Vec<String>) -> ExecuteResult {
209//!     match process_data(&args) {
210//!         Ok(result) => ExecuteResult::success(result),
211//!         Err(e) => ExecuteResult::system_error(format!("Internal error: {}", e)),
212//!     }
213//! }
214//! ```
215//!
216//! # Advanced Usage
217//!
218//! ## Subcommands
219//!
220//! Create nested command structures:
221//!
222//! ```rust,ignore
223//! CommandSpec::new("db", "Database operations")
224//!     .subcommand(
225//!         CommandSpec::new("create", "Create a new database")
226//!             .arg(ArgSpec::positional("name").required())
227//!     )
228//!     .subcommand(
229//!         CommandSpec::new("drop", "Drop a database")
230//!             .arg(ArgSpec::positional("name").required())
231//!     )
232//!     .subcommand(
233//!         CommandSpec::new("list", "List all databases")
234//!     )
235//! ```
236//!
237//! ## Plugin Metadata
238//!
239//! Add author and version information:
240//!
241//! ```rust,ignore
242//! CommandSpec::new("mytool", "My awesome tool")
243//!     .version("2.1.0")
244//!     // Note: author is set on CommandSpec, not PluginManifest
245//! ```
246//!
247//! # Manual Implementation
248//!
249//! If you need more control, you can implement the WASM exports manually
250//! instead of using the SDK. This is what the `export_plugin!` macro generates:
251//!
252//! ```rust,ignore
253//! use sen_plugin_api::{ArgSpec, CommandSpec, ExecuteResult, PluginManifest, API_VERSION};
254//! use std::alloc::{alloc, dealloc, Layout};
255//!
256//! // 1. Memory allocator for host-guest communication
257//! #[no_mangle]
258//! pub extern "C" fn plugin_alloc(size: i32) -> i32 {
259//!     if size <= 0 { return 0; }
260//!     let layout = Layout::from_size_align(size as usize, 1).unwrap();
261//!     unsafe { alloc(layout) as i32 }
262//! }
263//!
264//! // 2. Memory deallocator
265//! #[no_mangle]
266//! pub extern "C" fn plugin_dealloc(ptr: i32, size: i32) {
267//!     if ptr == 0 || size <= 0 { return; }
268//!     let layout = Layout::from_size_align(size as usize, 1).unwrap();
269//!     unsafe { dealloc(ptr as *mut u8, layout) }
270//! }
271//!
272//! // 3. Return plugin manifest (command specification)
273//! #[no_mangle]
274//! pub extern "C" fn plugin_manifest() -> i64 {
275//!     let manifest = PluginManifest {
276//!         api_version: API_VERSION,
277//!         command: CommandSpec::new("hello", "Says hello")
278//!             .arg(ArgSpec::positional("name").default("World")),
279//!     };
280//!     serialize_to_memory(&manifest)
281//! }
282//!
283//! // 4. Execute the command
284//! #[no_mangle]
285//! pub extern "C" fn plugin_execute(args_ptr: i32, args_len: i32) -> i64 {
286//!     let args: Vec<String> = unsafe {
287//!         let slice = std::slice::from_raw_parts(args_ptr as *const u8, args_len as usize);
288//!         rmp_serde::from_slice(slice).unwrap_or_default()
289//!     };
290//!
291//!     let name = args.first().map(|s| s.as_str()).unwrap_or("World");
292//!     let result = ExecuteResult::success(format!("Hello, {}!", name));
293//!     serialize_to_memory(&result)
294//! }
295//!
296//! // Helper: Pack pointer and length into i64
297//! fn pack_ptr_len(ptr: i32, len: i32) -> i64 {
298//!     ((ptr as i64) << 32) | (len as i64 & 0xFFFFFFFF)
299//! }
300//!
301//! // Helper: Serialize value to guest memory
302//! fn serialize_to_memory<T: serde::Serialize>(value: &T) -> i64 {
303//!     let bytes = rmp_serde::to_vec(value).expect("Serialization failed");
304//!     let len = bytes.len() as i32;
305//!     let ptr = plugin_alloc(len);
306//!     if ptr == 0 { return 0; }
307//!     unsafe {
308//!         std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr as *mut u8, bytes.len());
309//!     }
310//!     pack_ptr_len(ptr, len)
311//! }
312//! ```
313//!
314//! # Best Practices
315//!
316//! ## Do
317//!
318//! - **Keep plugins focused**: One plugin, one responsibility
319//! - **Validate inputs early**: Check arguments at the start of `execute()`
320//! - **Return meaningful errors**: Include context in error messages
321//! - **Use default values**: Make common cases convenient
322//! - **Document your commands**: Use `.help()` on all arguments
323//!
324//! ## Don't
325//!
326//! - **Don't panic**: Always return `ExecuteResult::user_error` or `system_error`
327//! - **Don't use unwrap()**: Prefer `unwrap_or`, `unwrap_or_default`, or match
328//! - **Don't allocate excessively**: WASM has limited memory
329//! - **Don't block forever**: The host has CPU limits (fuel)
330//!
331//! ## Example: Robust Argument Handling
332//!
333//! ```rust,ignore
334//! fn execute(args: Vec<String>) -> ExecuteResult {
335//!     // Validate required arguments
336//!     let file = match args.get(0) {
337//!         Some(f) if !f.is_empty() => f,
338//!         _ => return ExecuteResult::user_error("Missing required argument: file"),
339//!     };
340//!
341//!     // Parse optional numeric argument with default
342//!     let count: usize = args.get(1)
343//!         .and_then(|s| s.parse().ok())
344//!         .unwrap_or(1);
345//!
346//!     // Validate value range
347//!     if count == 0 || count > 100 {
348//!         return ExecuteResult::user_error(
349//!             "Count must be between 1 and 100"
350//!         );
351//!     }
352//!
353//!     ExecuteResult::success(format!("Processing {} {} time(s)", file, count))
354//! }
355//! ```
356//!
357//! # Troubleshooting
358//!
359//! ## Build Errors
360//!
361//! **Error: `can't find crate for std`**
362//!
363//! Make sure you're building for the correct target:
364//! ```bash
365//! cargo build --release --target wasm32-unknown-unknown
366//! ```
367//!
368//! **Error: `crate-type must be cdylib`**
369//!
370//! Add to your `Cargo.toml`:
371//! ```toml
372//! [lib]
373//! crate-type = ["cdylib"]
374//! ```
375//!
376//! ## Runtime Errors
377//!
378//! **Error: `API version mismatch`**
379//!
380//! Your plugin was built with a different API version. Rebuild with the
381//! matching `sen-plugin-sdk` version.
382//!
383//! **Error: `Function not found: plugin_manifest`**
384//!
385//! Make sure you have `export_plugin!(YourPlugin);` at the end of your lib.rs.
386//!
387//! **Error: `Fuel exhausted`**
388//!
389//! Your plugin is taking too long (possible infinite loop). The host limits
390//! CPU usage to prevent runaway plugins.
391//!
392//! ## Debugging Tips
393//!
394//! 1. **Test locally first**: Write unit tests for your `execute()` logic
395//! 2. **Check WASM size**: Large plugins may have unnecessary dependencies
396//! 3. **Simplify arguments**: Start with positional args, add options later
397//!
398//! # Examples
399//!
400//! See the `examples/` directory for complete working plugins:
401//!
402//! - `examples/hello-plugin/`: Manual implementation (no SDK)
403//! - `examples/greet-plugin/`: SDK-based with options
404
405use std::alloc::{alloc, dealloc, Layout};
406
407// Re-export everything from sen-plugin-api
408pub use sen_plugin_api::*;
409
410/// Prelude module for convenient imports
411pub mod prelude {
412    pub use crate::{export_plugin, memory, Plugin};
413    pub use sen_plugin_api::{
414        ArgSpec, Capabilities, CommandSpec, Effect, EffectResult, ExecuteError, ExecuteResult,
415        HttpResponse, NetPattern, PathPattern, PluginManifest, StdioCapability, API_VERSION,
416    };
417}
418
419/// Trait that plugins must implement
420pub trait Plugin {
421    /// Returns the plugin manifest describing the command
422    fn manifest() -> PluginManifest;
423
424    /// Executes the plugin with the given arguments
425    fn execute(args: Vec<String>) -> ExecuteResult;
426
427    /// Resume execution after an effect completes
428    ///
429    /// Called by the host when an effect (HTTP request, sleep, etc.) completes.
430    /// The plugin should continue its work with the effect result.
431    ///
432    /// Default implementation returns an error - override if using effects.
433    ///
434    /// # Arguments
435    /// * `effect_id` - The ID of the completed effect
436    /// * `result` - The result of the effect
437    ///
438    /// # Example
439    ///
440    /// ```rust,ignore
441    /// fn resume(effect_id: u32, result: EffectResult) -> ExecuteResult {
442    ///     match result {
443    ///         EffectResult::Http(response) => {
444    ///             if response.is_success() {
445    ///                 ExecuteResult::success(response.body)
446    ///             } else {
447    ///                 ExecuteResult::user_error(format!("HTTP {}", response.status))
448    ///             }
449    ///         }
450    ///         EffectResult::Error(e) => ExecuteResult::user_error(e),
451    ///         _ => ExecuteResult::system_error("Unexpected effect result"),
452    ///     }
453    /// }
454    /// ```
455    fn resume(_effect_id: u32, _result: EffectResult) -> ExecuteResult {
456        ExecuteResult::system_error("Plugin does not support effects")
457    }
458}
459
460/// Memory utilities for Wasm plugin development
461///
462/// # Platform
463/// These functions are designed for **WASM32 targets only**.
464/// Pointer values are represented as `i32`, which is correct for WASM32's
465/// 32-bit linear memory address space. Do not use on 64-bit native targets.
466pub mod memory {
467    use super::*;
468
469    /// Allocate memory in the Wasm linear memory
470    ///
471    /// # Platform
472    /// WASM32 only. Pointer is returned as `i32` (32-bit address).
473    ///
474    /// # Returns
475    /// - Pointer to allocated memory as `i32`
476    /// - `0` (null pointer) on allocation failure or invalid size
477    ///
478    /// # Safety
479    /// This function is safe to call from the host.
480    #[inline]
481    pub fn plugin_alloc(size: i32) -> i32 {
482        if size <= 0 {
483            return 0;
484        }
485        // Safe: size > 0 is checked above, and positive i32 always fits in usize
486        let size_usize = size as usize;
487        let layout = match Layout::from_size_align(size_usize, 1) {
488            Ok(l) => l,
489            Err(_) => return 0, // Invalid layout, return null pointer
490        };
491        // SAFETY:
492        // 1. Layout is valid (checked above with from_size_align)
493        // 2. Layout has non-zero size (size > 0 checked above)
494        // 3. The returned pointer will be properly aligned (alignment = 1)
495        unsafe { alloc(layout) as i32 }
496    }
497
498    /// Deallocate memory in the Wasm linear memory
499    ///
500    /// # Safety
501    /// The ptr must have been allocated by `plugin_alloc` with the same size.
502    #[inline]
503    pub fn plugin_dealloc(ptr: i32, size: i32) {
504        if ptr == 0 || size <= 0 {
505            return;
506        }
507        // Safe: size > 0 is checked above
508        let size_usize = size as usize;
509        let layout = match Layout::from_size_align(size_usize, 1) {
510            Ok(l) => l,
511            Err(_) => return, // Invalid layout, skip deallocation
512        };
513        // SAFETY:
514        // 1. ptr was allocated by plugin_alloc with the same layout (caller's responsibility)
515        // 2. ptr is non-null (checked above: ptr == 0 returns early)
516        // 3. Layout matches the allocation (same size, alignment = 1)
517        // 4. The memory block has not been deallocated yet (caller's responsibility)
518        unsafe { dealloc(ptr as *mut u8, layout) }
519    }
520
521    /// Pack a pointer and length into a single i64 value
522    ///
523    /// This is the standard way to return two values from a Wasm function
524    /// since wasm32-unknown-unknown doesn't support multi-value returns.
525    #[inline]
526    pub fn pack_ptr_len(ptr: i32, len: i32) -> i64 {
527        ((ptr as i64) << 32) | (len as i64 & 0xFFFFFFFF)
528    }
529
530    /// Serialize data and return it as an allocated buffer
531    ///
532    /// Returns a packed i64 containing the pointer and length.
533    /// Returns (0, 0) on serialization failure or if data exceeds i32::MAX bytes.
534    ///
535    /// Uses named serialization for compatibility with `skip_serializing_if` attributes.
536    pub fn serialize_and_return<T: serde::Serialize>(data: &T) -> i64 {
537        // Use to_vec_named for proper handling of optional/skipped fields
538        let bytes = match rmp_serde::to_vec_named(data) {
539            Ok(b) => b,
540            Err(_) => return pack_ptr_len(0, 0),
541        };
542
543        // Check for integer overflow before casting
544        let len: i32 = match bytes.len().try_into() {
545            Ok(l) => l,
546            Err(_) => return pack_ptr_len(0, 0), // Data too large for i32
547        };
548
549        let ptr = plugin_alloc(len);
550
551        if ptr != 0 && len > 0 {
552            // SAFETY:
553            // 1. src (bytes.as_ptr()) is valid for reads of len bytes
554            // 2. dst (ptr) is valid for writes of len bytes (allocated by plugin_alloc)
555            // 3. Both pointers are properly aligned (alignment = 1 for u8)
556            // 4. Memory regions do not overlap (src is stack/heap, dst is Wasm linear memory)
557            unsafe {
558                std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr as *mut u8, len as usize);
559            }
560        }
561
562        pack_ptr_len(ptr, len)
563    }
564
565    /// Error type for deserialization failures
566    #[derive(Debug)]
567    pub enum DeserializeError {
568        /// Null pointer or invalid length provided
569        InvalidPointer { ptr: i32, len: i32 },
570        /// MessagePack deserialization failed
571        DeserializeFailed(rmp_serde::decode::Error),
572    }
573
574    impl std::fmt::Display for DeserializeError {
575        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
576            match self {
577                Self::InvalidPointer { ptr, len } => {
578                    write!(f, "invalid pointer/length: ptr={}, len={}", ptr, len)
579                }
580                Self::DeserializeFailed(e) => write!(f, "deserialization failed: {}", e),
581            }
582        }
583    }
584
585    impl std::error::Error for DeserializeError {
586        fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
587            match self {
588                Self::DeserializeFailed(e) => Some(e),
589                _ => None,
590            }
591        }
592    }
593
594    /// Deserialize data from a raw pointer and length
595    ///
596    /// # Platform
597    /// WASM32 only. Expects pointer as `i32` (32-bit address).
598    ///
599    /// # Errors
600    /// - `InvalidPointer` if ptr is 0 or len <= 0
601    /// - `DeserializeFailed` if MessagePack deserialization fails
602    ///
603    /// # Safety
604    /// Caller must ensure:
605    /// 1. `ptr` points to a valid memory region in Wasm linear memory
606    /// 2. The memory region is at least `len` bytes
607    /// 3. The memory contains valid MessagePack data
608    /// 4. The memory will not be modified during deserialization
609    pub unsafe fn deserialize_from_ptr<T: serde::de::DeserializeOwned>(
610        ptr: i32,
611        len: i32,
612    ) -> Result<T, DeserializeError> {
613        if ptr == 0 || len <= 0 {
614            return Err(DeserializeError::InvalidPointer { ptr, len });
615        }
616        // SAFETY: Caller guarantees ptr is valid for len bytes (see function docs)
617        let slice = std::slice::from_raw_parts(ptr as *const u8, len as usize);
618        rmp_serde::from_slice(slice).map_err(DeserializeError::DeserializeFailed)
619    }
620}
621
622/// Macro to export all required plugin functions
623///
624/// This macro generates the `plugin_manifest`, `plugin_execute`, `plugin_resume`,
625/// `plugin_alloc`, and `plugin_dealloc` functions required by the host.
626///
627/// # Example
628///
629/// ```rust,ignore
630/// struct MyPlugin;
631///
632/// impl Plugin for MyPlugin {
633///     fn manifest() -> PluginManifest { /* ... */ }
634///     fn execute(args: Vec<String>) -> ExecuteResult { /* ... */ }
635/// }
636///
637/// export_plugin!(MyPlugin);
638/// ```
639///
640/// # Effects (Optional)
641///
642/// For plugins that use effects (HTTP, sleep, etc.), implement `resume`:
643///
644/// ```rust,ignore
645/// impl Plugin for MyPlugin {
646///     // ...
647///     fn resume(effect_id: u32, result: EffectResult) -> ExecuteResult {
648///         match result {
649///             EffectResult::Http(resp) => ExecuteResult::success(resp.body),
650///             EffectResult::Error(e) => ExecuteResult::user_error(e),
651///             _ => ExecuteResult::system_error("Unexpected result"),
652///         }
653///     }
654/// }
655/// ```
656#[macro_export]
657macro_rules! export_plugin {
658    ($plugin:ty) => {
659        #[no_mangle]
660        pub extern "C" fn plugin_manifest() -> i64 {
661            let manifest = <$plugin as $crate::Plugin>::manifest();
662            $crate::memory::serialize_and_return(&manifest)
663        }
664
665        #[no_mangle]
666        pub extern "C" fn plugin_execute(args_ptr: i32, args_len: i32) -> i64 {
667            let args: Vec<String> = unsafe {
668                match $crate::memory::deserialize_from_ptr(args_ptr, args_len) {
669                    Ok(v) => v,
670                    Err(_e) => {
671                        // Return error result for invalid/corrupted arguments
672                        let result =
673                            $crate::ExecuteResult::system_error("Failed to deserialize arguments");
674                        return $crate::memory::serialize_and_return(&result);
675                    }
676                }
677            };
678            let result = <$plugin as $crate::Plugin>::execute(args);
679            $crate::memory::serialize_and_return(&result)
680        }
681
682        /// Resume execution after an effect completes
683        ///
684        /// # Arguments
685        /// * `effect_id` - The ID of the completed effect
686        /// * `result_ptr` - Pointer to serialized EffectResult
687        /// * `result_len` - Length of serialized data
688        #[no_mangle]
689        pub extern "C" fn plugin_resume(effect_id: u32, result_ptr: i32, result_len: i32) -> i64 {
690            let effect_result: $crate::EffectResult = unsafe {
691                match $crate::memory::deserialize_from_ptr(result_ptr, result_len) {
692                    Ok(v) => v,
693                    Err(_e) => {
694                        let result = $crate::ExecuteResult::system_error(
695                            "Failed to deserialize effect result",
696                        );
697                        return $crate::memory::serialize_and_return(&result);
698                    }
699                }
700            };
701            let result = <$plugin as $crate::Plugin>::resume(effect_id, effect_result);
702            $crate::memory::serialize_and_return(&result)
703        }
704
705        #[no_mangle]
706        pub extern "C" fn plugin_alloc(size: i32) -> i32 {
707            $crate::memory::plugin_alloc(size)
708        }
709
710        #[no_mangle]
711        pub extern "C" fn plugin_dealloc(ptr: i32, size: i32) {
712            $crate::memory::plugin_dealloc(ptr, size)
713        }
714    };
715}
716
717#[cfg(test)]
718mod tests {
719    use super::*;
720
721    #[test]
722    fn test_pack_ptr_len() {
723        let ptr = 0x12345678_i32;
724        let len = 0x00000100_i32;
725        let packed = memory::pack_ptr_len(ptr, len);
726
727        // Verify the packed value
728        let unpacked_ptr = (packed >> 32) as i32;
729        let unpacked_len = (packed & 0xFFFFFFFF) as i32;
730
731        assert_eq!(unpacked_ptr, ptr);
732        assert_eq!(unpacked_len, len);
733    }
734
735    #[test]
736    fn test_alloc_edge_cases() {
737        // Test zero/negative edge cases - these should return 0
738        assert_eq!(memory::plugin_alloc(0), 0);
739        assert_eq!(memory::plugin_alloc(-1), 0);
740    }
741
742    // Note: Full allocation tests run via integration tests with actual Wasm plugins.
743    // The memory functions are designed for Wasm linear memory and may behave
744    // differently in native test environments.
745}