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}