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}