Skip to main content

hush_plugin/
lib.rs

1//! hush-plugin — Macro for building Hush workflow op plugins as cdylib crates.
2//!
3//! Provides the `hush_plugin!` macro that generates the C ABI exports
4//! required for hush-serve to load your ops at runtime via `libloading`.
5//!
6//! # Usage
7//!
8//! ```rust,ignore
9//! // In your Cargo.toml:
10//! // [lib]
11//! // crate-type = ["cdylib"]
12//! //
13//! // [dependencies]
14//! // serde_json = "1"
15//! // hush-plugin = { path = "../../rust/hush-plugin" }
16//!
17//! mod math;
18//! mod text;
19//!
20//! hush_plugin::hush_plugin! {
21//!     call: {
22//!         "double" => math::double,
23//!         "clean_text" => text::clean_text,
24//!     },
25//!     call_generator: {
26//!         "range_gen" => math::range_gen,
27//!     }
28//! }
29//! ```
30//!
31//! # C ABI Contract
32//!
33//! The macro generates three `extern "C"` functions:
34//!
35//! - `hush_call(name, inputs_json) -> *mut c_char` — call a regular op
36//! - `hush_call_generator(name, inputs_json) -> *mut c_char` — call a generator op
37//! - `hush_free_str(ptr)` — free a string returned by the above
38//!
39//! Data crosses the FFI boundary as null-terminated JSON strings.
40//! Returns null if the op name is not found.
41
42pub use serde_json;
43
44/// Generate the cdylib FFI exports for a set of ops and generators.
45///
46/// # Syntax
47///
48/// ```rust,ignore
49/// hush_plugin::hush_plugin! {
50///     call: {
51///         "op_name" => module::function,
52///         ...
53///     },
54///     call_generator: {
55///         "gen_name" => module::gen_function,
56///         ...
57///     }
58/// }
59/// ```
60///
61/// The `call_generator` section is optional.
62#[macro_export]
63macro_rules! hush_plugin {
64    // With both call and call_generator
65    (
66        call: { $($name:literal => $func:path),* $(,)? },
67        call_generator: { $($gen_name:literal => $gen_func:path),* $(,)? }
68    ) => {
69        $crate::_hush_plugin_impl!($($name => $func),*; $($gen_name => $gen_func),*);
70    };
71    // call only, no generators
72    (
73        call: { $($name:literal => $func:path),* $(,)? }
74    ) => {
75        $crate::_hush_plugin_impl!($($name => $func),*;);
76    };
77}
78
79#[doc(hidden)]
80#[macro_export]
81macro_rules! _hush_plugin_impl {
82    ($($name:literal => $func:path),*; $($gen_name:literal => $gen_func:path),*) => {
83        /// Dispatch a regular op by name. Returns JSON string or null.
84        ///
85        /// # Safety
86        /// Caller must pass valid null-terminated UTF-8 strings.
87        /// Caller must free the returned pointer via `hush_free_str`.
88        #[no_mangle]
89        pub unsafe extern "C" fn hush_call(
90            name_ptr: *const std::ffi::c_char,
91            input_ptr: *const std::ffi::c_char,
92        ) -> *mut std::ffi::c_char {
93            let name = match unsafe { std::ffi::CStr::from_ptr(name_ptr) }.to_str() {
94                Ok(s) => s,
95                Err(_) => return std::ptr::null_mut(),
96            };
97            let input_str = match unsafe { std::ffi::CStr::from_ptr(input_ptr) }.to_str() {
98                Ok(s) => s,
99                Err(_) => return std::ptr::null_mut(),
100            };
101            let inputs: $crate::serde_json::Value = match $crate::serde_json::from_str(input_str) {
102                Ok(v) => v,
103                Err(_) => return std::ptr::null_mut(),
104            };
105
106            let result: Option<$crate::serde_json::Value> = match name {
107                $($name => Some($func(&inputs)),)*
108                _ => None,
109            };
110
111            match result {
112                Some(val) => {
113                    let json_str = $crate::serde_json::to_string(&val).unwrap_or_default();
114                    match std::ffi::CString::new(json_str) {
115                        Ok(cs) => cs.into_raw(),
116                        Err(_) => std::ptr::null_mut(),
117                    }
118                }
119                None => std::ptr::null_mut(),
120            }
121        }
122
123        /// Dispatch a generator op by name. Returns JSON array string or null.
124        ///
125        /// # Safety
126        /// Same as `hush_call`.
127        #[no_mangle]
128        pub unsafe extern "C" fn hush_call_generator(
129            name_ptr: *const std::ffi::c_char,
130            input_ptr: *const std::ffi::c_char,
131        ) -> *mut std::ffi::c_char {
132            let name = match unsafe { std::ffi::CStr::from_ptr(name_ptr) }.to_str() {
133                Ok(s) => s,
134                Err(_) => return std::ptr::null_mut(),
135            };
136            let input_str = match unsafe { std::ffi::CStr::from_ptr(input_ptr) }.to_str() {
137                Ok(s) => s,
138                Err(_) => return std::ptr::null_mut(),
139            };
140            let inputs: $crate::serde_json::Value = match $crate::serde_json::from_str(input_str) {
141                Ok(v) => v,
142                Err(_) => return std::ptr::null_mut(),
143            };
144
145            let result: Option<$crate::serde_json::Value> = match name {
146                $($gen_name => Some($gen_func(&inputs)),)*
147                _ => None,
148            };
149
150            match result {
151                Some(val) => {
152                    // Ensure it's an array — if the function returns Value::Array, serialize directly.
153                    // If it returns something else, wrap in an array.
154                    let arr = if val.is_array() { val } else { $crate::serde_json::Value::Array(vec![val]) };
155                    let json_str = $crate::serde_json::to_string(&arr).unwrap_or_default();
156                    match std::ffi::CString::new(json_str) {
157                        Ok(cs) => cs.into_raw(),
158                        Err(_) => std::ptr::null_mut(),
159                    }
160                }
161                None => std::ptr::null_mut(),
162            }
163        }
164
165        /// Free a string returned by `hush_call` or `hush_call_generator`.
166        ///
167        /// # Safety
168        /// Pointer must have been returned by `hush_call` or `hush_call_generator`.
169        #[no_mangle]
170        pub unsafe extern "C" fn hush_free_str(ptr: *mut std::ffi::c_char) {
171            if !ptr.is_null() {
172                drop(unsafe { std::ffi::CString::from_raw(ptr) });
173            }
174        }
175    };
176}