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;