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}