Skip to main content

dpp_plugin_sdk/
lib.rs

1//! Guest-side SDK for Odal Node Wasm sector plugins.
2//!
3//! A sector plugin author implements the [`DppSectorPlugin`](dpp_plugin_traits::DppSectorPlugin) trait from
4//! `dpp-plugin-traits` and invokes [`export_plugin!`] once. The macro generates
5//! the full low-level Wasm ABI (`alloc`, `dealloc`, `metadata`, `describe`,
6//! `validate`, `calculate_metrics`, `generate_passport`) and wires each export
7//! to the corresponding trait method. Plugins no longer hand-roll the ABI shim
8//! or redefine their own output structs — they speak the shared contract.
9//!
10//! ## Why `describe()`
11//!
12//! The host calls `describe()` immediately after loading a plugin to read its
13//! [`PluginCapabilities`](dpp_plugin_traits::PluginCapabilities) (ABI version, supported schema versions, feature
14//! capabilities) and run `dpp_plugin_traits::check_compatibility` *before*
15//! dispatching any work. This is what makes the version registry enforceable at
16//! the Wasm boundary rather than aspirational.
17//!
18//! ## ABI summary
19//!
20//! | Export | Signature | Returns (JSON) |
21//! |--------|-----------|----------------|
22//! | `alloc` | `(len: u32) -> u32` | pointer to `len` bytes |
23//! | `dealloc` | `(ptr: u32, len: u32)` | — |
24//! | `metadata` | `() -> u64` | [`PluginMeta`](dpp_plugin_traits::PluginMeta) |
25//! | `describe` | `() -> u64` | [`PluginCapabilities`](dpp_plugin_traits::PluginCapabilities) |
26//! | `validate` | `(ptr: u32, len: u32) -> u64` | [`AbiResult`](dpp_plugin_traits::AbiResult) (`ok: null` / `error`) |
27//! | `calculate_metrics` | `(ptr: u32, len: u32) -> u64` | [`AbiResult`](dpp_plugin_traits::AbiResult) (`ok: PluginResult`) |
28//! | `generate_passport` | `(ptr: u32, len: u32) -> u64` | [`AbiResult`](dpp_plugin_traits::AbiResult) (`ok: payload`) |
29//!
30//! Every `-> u64` return packs the output buffer as `(out_ptr << 32) | out_len`.
31//! The host reads the JSON, then frees the buffer via `dealloc`.
32//!
33//! ## Module layout
34//!
35//! - [`abi`] — the low-level linear-memory ABI (`alloc`/`dealloc`/buffer packing).
36//! - `codec` — pure, host-testable JSON glue (`*_bytes` functions).
37//! - `entry` — the `run_*` ABI entry-point wrappers `export_plugin!` calls.
38//! - [`validate`] — shared field-validation helpers.
39
40/// Re-export of the shared host/guest contract so plugins need only one
41/// path dependency.
42pub use dpp_plugin_traits as traits;
43
44/// Re-export of the shared cross-field regulatory rules ([`dpp_rules`]), so a
45/// plugin uses the same rule implementation as `dpp-domain` rather than
46/// reimplementing it.
47pub use dpp_rules as rules;
48
49pub mod abi;
50mod codec;
51mod entry;
52#[cfg(test)]
53mod tests;
54pub mod validate;
55
56#[cfg(test)]
57use dpp_plugin_traits::{AbiResult, DppSectorPlugin, PluginError, PluginInput};
58pub use dpp_plugin_traits::{
59    METRIC_CO2E_SCORE, METRIC_RECYCLED_CONTENT_PCT, METRIC_REPAIRABILITY_INDEX,
60    PluginComplianceStatus,
61};
62
63pub use codec::{
64    calculate_metrics_bytes, describe_bytes, generate_passport_bytes, metadata_bytes,
65    validate_bytes,
66};
67pub use entry::{
68    run_calculate_metrics, run_describe, run_generate_passport, run_metadata, run_validate,
69};
70
71// ─── Export macro ─────────────────────────────────────────────────────────────
72
73/// Generate the full Wasm ABI for a sector plugin.
74///
75/// `$plugin` must implement [`DppSectorPlugin`](dpp_plugin_traits::DppSectorPlugin)
76/// and [`Default`] (plugins are deterministic and stateless, so the instance is
77/// constructed per call). Invoke once at the crate root:
78///
79/// ```ignore
80/// use dpp_plugin_sdk::{export_plugin, traits::*};
81///
82/// #[derive(Default)]
83/// struct BatteryPlugin;
84/// impl DppSectorPlugin for BatteryPlugin { /* ... */ }
85///
86/// export_plugin!(BatteryPlugin);
87/// ```
88#[macro_export]
89macro_rules! export_plugin {
90    ($plugin:ty) => {
91        #[unsafe(no_mangle)]
92        pub extern "C" fn alloc(len: u32) -> u32 {
93            $crate::abi::host_alloc(len)
94        }
95
96        #[unsafe(no_mangle)]
97        pub extern "C" fn dealloc(ptr: u32, len: u32) {
98            $crate::abi::host_dealloc(ptr, len)
99        }
100
101        #[unsafe(no_mangle)]
102        pub extern "C" fn metadata() -> u64 {
103            $crate::run_metadata(&<$plugin as ::core::default::Default>::default())
104        }
105
106        #[unsafe(no_mangle)]
107        pub extern "C" fn describe() -> u64 {
108            $crate::run_describe(&<$plugin as ::core::default::Default>::default())
109        }
110
111        #[unsafe(no_mangle)]
112        pub extern "C" fn validate(ptr: u32, len: u32) -> u64 {
113            // SAFETY: the host guarantees `ptr`/`len` describe a buffer it wrote via `alloc`.
114            unsafe {
115                $crate::run_validate(&<$plugin as ::core::default::Default>::default(), ptr, len)
116            }
117        }
118
119        #[unsafe(no_mangle)]
120        pub extern "C" fn calculate_metrics(ptr: u32, len: u32) -> u64 {
121            // SAFETY: the host guarantees `ptr`/`len` describe a buffer it wrote via `alloc`.
122            unsafe {
123                $crate::run_calculate_metrics(
124                    &<$plugin as ::core::default::Default>::default(),
125                    ptr,
126                    len,
127                )
128            }
129        }
130
131        #[unsafe(no_mangle)]
132        pub extern "C" fn generate_passport(ptr: u32, len: u32) -> u64 {
133            // SAFETY: the host guarantees `ptr`/`len` describe a buffer it wrote via `alloc`.
134            unsafe {
135                $crate::run_generate_passport(
136                    &<$plugin as ::core::default::Default>::default(),
137                    ptr,
138                    len,
139                )
140            }
141        }
142    };
143}
144
145/// Compile-checks this crate's README examples.
146///
147/// A README example is a public claim about the API, and nothing else in the
148/// build compiles one. Without this, a README can advertise a function that
149/// does not exist — which is exactly what happened before this harness landed.
150#[cfg(doctest)]
151#[doc = include_str!("../README.md")]
152struct ReadmeDoctests;