Skip to main content

barbacane_plugin_macros/
lib.rs

1//! Proc macros for building Barbacane WASM plugins.
2//!
3//! This crate provides the `#[barbacane_middleware]` and `#[barbacane_dispatcher]`
4//! attribute macros that generate the necessary WASM exports for plugin entry points.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use barbacane_plugin_sdk::prelude::*;
10//!
11//! #[barbacane_middleware]
12//! struct RateLimiter {
13//!     quota: u32,
14//!     window: u32,
15//! }
16//!
17//! impl RateLimiter {
18//!     fn on_request(&mut self, req: Request) -> Action<Request> {
19//!         // Rate limiting logic
20//!         Action::Continue(req)
21//!     }
22//!
23//!     fn on_response(&mut self, resp: Response) -> Response {
24//!         resp
25//!     }
26//! }
27//! ```
28
29use proc_macro::TokenStream;
30use quote::quote;
31use syn::{parse_macro_input, ItemStruct};
32
33/// Generates WASM exports for a middleware plugin.
34///
35/// The annotated struct must implement:
36/// - `fn on_request(&mut self, req: Request) -> Action<Request>`
37/// - `fn on_response(&mut self, resp: Response) -> Response`
38///
39/// The macro generates:
40/// - `init(ptr, len) -> i32` - Initialize with JSON config
41/// - `on_request(ptr, len) -> i32` - Process request (0=continue, 1=short-circuit)
42/// - `on_response(ptr, len) -> i32` - Process response
43///
44/// Bodies travel via side-channel host functions (host_body_read/host_body_set),
45/// not embedded in JSON. The glue code handles this transparently.
46#[proc_macro_attribute]
47pub fn barbacane_middleware(_attr: TokenStream, item: TokenStream) -> TokenStream {
48    let input = parse_macro_input!(item as ItemStruct);
49    let struct_name = &input.ident;
50
51    let expanded = quote! {
52        #input
53
54        // WASM ABI glue — only compiled when targeting wasm32.
55        // On native targets (e.g. cargo test), only the struct and its
56        // impl blocks are compiled, enabling unit testing of plugin logic.
57        #[cfg(target_arch = "wasm32")]
58        mod __barbacane_wasm_abi {
59            use super::*;
60
61            // Global state for the plugin instance
62            static mut PLUGIN_INSTANCE: Option<#struct_name> = None;
63
64            /// Initialize the plugin with JSON config.
65            #[no_mangle]
66            pub extern "C" fn init(ptr: i32, len: i32) -> i32 {
67                let config_bytes = unsafe {
68                    core::slice::from_raw_parts(ptr as *const u8, len as usize)
69                };
70
71                match serde_json::from_slice::<#struct_name>(config_bytes) {
72                    Ok(instance) => {
73                        unsafe { PLUGIN_INSTANCE = Some(instance); }
74                        0 // Success
75                    }
76                    Err(_) => 1 // Failed to parse config
77                }
78            }
79
80            /// Lightweight serialization wrapper — avoids the heap cost of
81            /// an intermediate `serde_json::Value` tree.
82            #[derive(serde::Serialize)]
83            struct ActionOutput<T: serde::Serialize> { action: i32, data: T }
84
85            /// Process an incoming request.
86            /// Returns 0 to continue, 1 to short-circuit with response.
87            #[no_mangle]
88            pub extern "C" fn on_request(ptr: i32, len: i32) -> i32 {
89                let request_bytes = unsafe {
90                    core::slice::from_raw_parts(ptr as *const u8, len as usize)
91                };
92
93                let mut request: barbacane_plugin_sdk::prelude::Request = match serde_json::from_slice(request_bytes) {
94                    Ok(r) => r,
95                    Err(_) => return 1, // Parse error, short-circuit
96                };
97
98                // Free the input buffer — data is now owned by `request`.
99                dealloc(ptr, len);
100
101                // Read body from side-channel (host_body_read).
102                request.body = barbacane_plugin_sdk::body::read_request_body();
103
104                let instance = unsafe {
105                    match PLUGIN_INSTANCE.as_mut() {
106                        Some(i) => i,
107                        None => return 1, // Not initialized
108                    }
109                };
110
111                match instance.on_request(request) {
112                    barbacane_plugin_sdk::prelude::Action::Continue(mut req) => {
113                        // Extract body and send via side-channel.
114                        match req.body.take() {
115                            Some(body) => barbacane_plugin_sdk::body::set_response_body(&body),
116                            None => barbacane_plugin_sdk::body::clear_response_body(),
117                        }
118                        if let Ok(output) = serde_json::to_vec(&ActionOutput { action: 0, data: req }) {
119                            set_output(&output);
120                        }
121                        0 // Continue
122                    }
123                    barbacane_plugin_sdk::prelude::Action::ShortCircuit(mut resp) => {
124                        // Extract body and send via side-channel.
125                        match resp.body.take() {
126                            Some(body) => barbacane_plugin_sdk::body::set_response_body(&body),
127                            None => barbacane_plugin_sdk::body::clear_response_body(),
128                        }
129                        if let Ok(output) = serde_json::to_vec(&ActionOutput { action: 1, data: resp }) {
130                            set_output(&output);
131                        }
132                        1 // Short-circuit
133                    }
134                }
135            }
136
137            /// Process an outgoing response.
138            #[no_mangle]
139            pub extern "C" fn on_response(ptr: i32, len: i32) -> i32 {
140                let response_bytes = unsafe {
141                    core::slice::from_raw_parts(ptr as *const u8, len as usize)
142                };
143
144                let mut response: barbacane_plugin_sdk::prelude::Response = match serde_json::from_slice(response_bytes) {
145                    Ok(r) => r,
146                    Err(_) => return 1,
147                };
148
149                // Free the input buffer.
150                dealloc(ptr, len);
151
152                // Read body from side-channel (host_body_read).
153                response.body = barbacane_plugin_sdk::body::read_request_body();
154
155                let instance = unsafe {
156                    match PLUGIN_INSTANCE.as_mut() {
157                        Some(i) => i,
158                        None => return 1,
159                    }
160                };
161
162                let mut result = instance.on_response(response);
163
164                // Extract body and send via side-channel.
165                match result.body.take() {
166                    Some(body) => barbacane_plugin_sdk::body::set_response_body(&body),
167                    None => barbacane_plugin_sdk::body::clear_response_body(),
168                }
169
170                if let Ok(output) = serde_json::to_vec(&result) {
171                    set_output(&output);
172                }
173
174                0
175            }
176
177            /// Allocate `size` bytes via the plugin's allocator and return the pointer.
178            ///
179            /// Called by the host before writing input data into linear memory so that
180            /// dlmalloc is aware of the allocation and will not reuse the region.
181            #[no_mangle]
182            pub extern "C" fn alloc(size: i32) -> i32 {
183                let mut buf = Vec::<u8>::with_capacity(size as usize);
184                let ptr = buf.as_mut_ptr();
185                core::mem::forget(buf);
186                ptr as i32
187            }
188
189            /// Free a region previously returned by `alloc`.
190            #[no_mangle]
191            pub extern "C" fn dealloc(ptr: i32, size: i32) {
192                unsafe {
193                    drop(Vec::from_raw_parts(ptr as *mut u8, 0, size as usize));
194                }
195            }
196
197            /// Helper to set output via host function.
198            fn set_output(data: &[u8]) {
199                #[link(wasm_import_module = "barbacane")]
200                extern "C" {
201                    fn host_set_output(ptr: i32, len: i32);
202                }
203                unsafe {
204                    host_set_output(data.as_ptr() as i32, data.len() as i32);
205                }
206            }
207        }
208    };
209
210    TokenStream::from(expanded)
211}
212
213/// Generates WASM exports for a dispatcher plugin.
214///
215/// The annotated struct must implement:
216/// - `fn dispatch(&mut self, req: Request) -> Response`
217///
218/// The macro generates:
219/// - `init(ptr, len) -> i32` - Initialize with JSON config
220/// - `dispatch(ptr, len) -> i32` - Handle request and return response
221///
222/// Bodies travel via side-channel host functions (host_body_read/host_body_set),
223/// not embedded in JSON. The glue code handles this transparently.
224#[proc_macro_attribute]
225pub fn barbacane_dispatcher(_attr: TokenStream, item: TokenStream) -> TokenStream {
226    let input = parse_macro_input!(item as ItemStruct);
227    let struct_name = &input.ident;
228
229    let expanded = quote! {
230        #input
231
232        // WASM ABI glue — only compiled when targeting wasm32.
233        #[cfg(target_arch = "wasm32")]
234        mod __barbacane_wasm_abi {
235            use super::*;
236
237            // Global state for the plugin instance
238            static mut PLUGIN_INSTANCE: Option<#struct_name> = None;
239
240            /// Initialize the plugin with JSON config.
241            #[no_mangle]
242            pub extern "C" fn init(ptr: i32, len: i32) -> i32 {
243                let config_bytes = unsafe {
244                    core::slice::from_raw_parts(ptr as *const u8, len as usize)
245                };
246
247                match serde_json::from_slice::<#struct_name>(config_bytes) {
248                    Ok(instance) => {
249                        unsafe { PLUGIN_INSTANCE = Some(instance); }
250                        0 // Success
251                    }
252                    Err(_) => 1 // Failed to parse config
253                }
254            }
255
256            /// Dispatch a request and return a response.
257            #[no_mangle]
258            pub extern "C" fn dispatch(ptr: i32, len: i32) -> i32 {
259                let request_bytes = unsafe {
260                    core::slice::from_raw_parts(ptr as *const u8, len as usize)
261                };
262
263                let mut request: barbacane_plugin_sdk::prelude::Request = match serde_json::from_slice(request_bytes) {
264                    Ok(r) => r,
265                    Err(_) => {
266                        // Free input buffer before returning error.
267                        dealloc(ptr, len);
268                        let error_resp = barbacane_plugin_sdk::prelude::Response {
269                            status: 500,
270                            headers: std::collections::BTreeMap::new(),
271                            body: Some(br#"{"error":"failed to parse request"}"#.to_vec()),
272                        };
273                        // Send error body via side-channel.
274                        if let Some(ref body) = error_resp.body {
275                            barbacane_plugin_sdk::body::set_response_body(body);
276                        }
277                        let mut err_resp_for_json = error_resp;
278                        err_resp_for_json.body = None;
279                        if let Ok(output) = serde_json::to_vec(&err_resp_for_json) {
280                            set_output(&output);
281                        }
282                        return 1;
283                    }
284                };
285
286                // Free the input buffer — data is now owned by `request`.
287                dealloc(ptr, len);
288
289                // Read body from side-channel (host_body_read).
290                request.body = barbacane_plugin_sdk::body::read_request_body();
291
292                let instance = unsafe {
293                    match PLUGIN_INSTANCE.as_mut() {
294                        Some(i) => i,
295                        None => {
296                            let error_body = br#"{"error":"plugin not initialized"}"#;
297                            barbacane_plugin_sdk::body::set_response_body(error_body);
298                            let error_resp = barbacane_plugin_sdk::prelude::Response {
299                                status: 500,
300                                headers: std::collections::BTreeMap::new(),
301                                body: None,
302                            };
303                            if let Ok(output) = serde_json::to_vec(&error_resp) {
304                                set_output(&output);
305                            }
306                            return 1;
307                        }
308                    }
309                };
310
311                let mut response = instance.dispatch(request);
312
313                // Extract body and send via side-channel.
314                match response.body.take() {
315                    Some(body) => barbacane_plugin_sdk::body::set_response_body(&body),
316                    None => barbacane_plugin_sdk::body::clear_response_body(),
317                }
318
319                if let Ok(output) = serde_json::to_vec(&response) {
320                    set_output(&output);
321                }
322
323                0
324            }
325
326            /// Allocate `size` bytes via the plugin's allocator and return the pointer.
327            #[no_mangle]
328            pub extern "C" fn alloc(size: i32) -> i32 {
329                let mut buf = Vec::<u8>::with_capacity(size as usize);
330                let ptr = buf.as_mut_ptr();
331                core::mem::forget(buf);
332                ptr as i32
333            }
334
335            /// Free a region previously returned by `alloc`.
336            #[no_mangle]
337            pub extern "C" fn dealloc(ptr: i32, size: i32) {
338                unsafe {
339                    drop(Vec::from_raw_parts(ptr as *mut u8, 0, size as usize));
340                }
341            }
342
343            /// Helper to set output via host function.
344            fn set_output(data: &[u8]) {
345                #[link(wasm_import_module = "barbacane")]
346                extern "C" {
347                    fn host_set_output(ptr: i32, len: i32);
348                }
349                unsafe {
350                    host_set_output(data.as_ptr() as i32, data.len() as i32);
351                }
352            }
353        }
354    };
355
356    TokenStream::from(expanded)
357}