Skip to main content

aiway_plugin/
macros.rs

1//! WASM 插件导出宏
2//!
3//! 为插件开发者生成 C ABI 导出函数,供网关 wasmtime 运行时调用。
4
5/// 格式化日志宏(ERROR 级别)
6///
7/// # 用法
8/// ```ignore
9/// log_error!(ctx, "request failed: {}", err);
10/// ```
11#[macro_export]
12macro_rules! log_error {
13    ($ctx:expr, $($arg:tt)*) => {
14        $ctx.log_error(&format!($($arg)*))
15    };
16}
17
18/// WARN日志
19#[macro_export]
20macro_rules! log_warn {
21    ($ctx:expr, $($arg:tt)*) => {
22        $ctx.log_warn(&format!($($arg)*))
23    };
24}
25
26/// INFO日志
27#[macro_export]
28macro_rules! log_info {
29    ($ctx:expr, $($arg:tt)*) => {
30        $ctx.log_info(&format!($($arg)*))
31    };
32}
33
34/// DEBUG日志
35#[macro_export]
36macro_rules! log_debug {
37    ($ctx:expr, $($arg:tt)*) => {
38        $ctx.log_debug(&format!($($arg)*))
39    };
40}
41
42/// TRACE日志
43#[macro_export]
44macro_rules! log_trace {
45    ($ctx:expr, $($arg:tt)*) => {
46        $ctx.log_trace(&format!($($arg)*))
47    };
48}
49
50/// 导出 WASM 插件
51///
52/// 生成以下导出函数:
53/// - `plugin_info()` -> i64:返回插件元信息(bincode 编码)
54/// - `aiway_call(hook_id, input_ptr, input_len)` -> i64:插件钩子调用入口
55/// - `aiway_alloc(size)` -> i32:内存分配
56/// - `aiway_dealloc(ptr, size)`:内存释放
57///
58/// # 用法
59/// ```ignore
60/// struct MyPlugin;
61/// impl aiway_plugin::Plugin for MyPlugin { /* ... */ }
62///
63/// aiway_plugin::export_wasm!(MyPlugin);
64/// ```
65#[macro_export]
66macro_rules! export_wasm {
67    ($plugin_type:ty) => {
68        /// 插件实例(全局单例,因为 WASM 插件应无状态)
69        static PLUGIN: std::sync::LazyLock<$plugin_type> =
70            std::sync::LazyLock::new(|| <$plugin_type>::new());
71
72        /// 分配内存(供 Host 写入输入数据)
73        #[unsafe(no_mangle)]
74        pub extern "C" fn aiway_alloc(size: i32) -> i32 {
75            let layout = std::alloc::Layout::from_size_align(size as usize, 1).unwrap();
76            unsafe {
77                let ptr = std::alloc::alloc(layout);
78                ptr as i32
79            }
80        }
81
82        /// 释放内存
83        #[unsafe(no_mangle)]
84        pub extern "C" fn aiway_dealloc(ptr: i32, size: i32) {
85            let layout = std::alloc::Layout::from_size_align(size as usize, 1).unwrap();
86            unsafe {
87                std::alloc::dealloc(ptr as *mut u8, layout);
88            }
89        }
90
91        /// 返回插件元信息
92        ///
93        /// 返回 i64,高 32 位 = 数据指针,低 32 位 = 数据长度
94        #[unsafe(no_mangle)]
95        pub extern "C" fn plugin_info() -> i64 {
96            let info = aiway_plugin::wasm_types::WasmPluginInfo {
97                name: PLUGIN.name().to_string(),
98                version: PLUGIN.info().version.to_string(),
99                description: PLUGIN.info().description.clone(),
100                default_config: aiway_plugin::serde_json::to_string(&PLUGIN.info().default_config)
101                    .unwrap_or_default(),
102                readme:  PLUGIN.info().readme.clone(),
103            };
104
105            let bytes = bincode::serialize(&info).unwrap();
106            let len = bytes.len();
107            let ptr = bytes.as_ptr() as i32;
108            std::mem::forget(bytes);
109
110            ((ptr as i64) << 32) | (len as i64)
111        }
112
113        /// 插件钩子调用入口
114        ///
115        /// # 参数
116        /// - `hook_id`: 钩子 ID(见 `wasm_types` 常量)
117        /// - `input_ptr`: 输入数据在 WASM 内存中的指针
118        /// - `input_len`: 输入数据长度
119        ///
120        /// # 返回
121        /// i64,高 32 位 = 结果指针,低 32 位 = 结果长度。
122        /// 若高 32 位为 0,表示错误,低 32 位是错误信息长度(数据在 ptr=1 处)。
123        #[unsafe(no_mangle)]
124        pub extern "C" fn aiway_call(hook_id: i32, input_ptr: i32, input_len: i32) -> i64 {
125            // 读取输入数据
126            let input_slice =
127                unsafe { std::slice::from_raw_parts(input_ptr as *const u8, input_len as usize) };
128
129            let input: aiway_plugin::wasm_types::WasmInput = match bincode::deserialize(input_slice)
130            {
131                Ok(v) => v,
132                Err(e) => return encode_error(&format!("deserialize input failed: {}", e)),
133            };
134
135            // 根据 hook_id 分发
136            let result: Result<aiway_plugin::wasm_types::WasmOutput, String> = match hook_id {
137                aiway_plugin::wasm_types::HOOK_ON_REQUEST => handle_on_request(&PLUGIN, &input),
138                aiway_plugin::wasm_types::HOOK_ON_REQUEST_BODY => {
139                    handle_on_request_body(&PLUGIN, &input)
140                }
141                aiway_plugin::wasm_types::HOOK_ON_RESPONSE => handle_on_response(&PLUGIN, &input),
142                aiway_plugin::wasm_types::HOOK_ON_RESPONSE_BODY => {
143                    handle_on_response_body(&PLUGIN, &input)
144                }
145                aiway_plugin::wasm_types::HOOK_ON_LOGGING => handle_on_logging(&PLUGIN, &input),
146                _ => Err(format!("unknown hook_id: {}", hook_id)),
147            };
148
149            match result {
150                Ok(output) => encode_output(&output),
151                Err(err_bytes) => encode_reject_error(err_bytes.as_bytes()),
152            }
153        }
154
155        /// 编码成功输出
156        fn encode_output(output: &aiway_plugin::wasm_types::WasmOutput) -> i64 {
157            match bincode::serialize(output) {
158                Ok(bytes) => {
159                    let len = bytes.len();
160                    let ptr = bytes.as_ptr() as i32;
161                    std::mem::forget(bytes);
162                    ((ptr as i64) << 32) | (len as i64)
163                }
164                Err(e) => encode_error(&format!("serialize output failed: {}", e)),
165            }
166        }
167
168        /// 编码错误信息(ptr=0, len=错误信息长度)
169        fn encode_error(msg: &str) -> i64 {
170            let bytes = msg.as_bytes();
171            // 将错误信息写入 ptr=1 处
172            let dst = unsafe { std::slice::from_raw_parts_mut(1 as *mut u8, bytes.len()) };
173            dst.copy_from_slice(bytes);
174            (0i64 << 32) | (bytes.len() as i64)
175        }
176
177        /// 编码错误信息,Reject 错误携带状态码前缀
178        ///
179        /// 错误缓冲区格式(写入 ptr=1):
180        /// - Reject: `[status_u16_be (4 bytes)] [message bytes]`
181        /// - 其他:   `[message bytes]`
182        fn encode_reject_error(err_bytes: &[u8]) -> i64 {
183            let dst = unsafe { std::slice::from_raw_parts_mut(1 as *mut u8, err_bytes.len()) };
184            dst.copy_from_slice(err_bytes);
185            (0i64 << 32) | (err_bytes.len() as i64)
186        }
187
188        /// 将 PluginError 编码为字节:Reject 带 4 字节状态码前缀,其他为纯消息
189        fn encode_plugin_error(e: aiway_plugin::PluginError) -> String {
190            match e {
191                aiway_plugin::PluginError::Reject(status, message) => {
192                    let msg_bytes = message.as_bytes();
193                    let mut buf = Vec::with_capacity(4 + msg_bytes.len());
194                    buf.extend_from_slice(&(status as u32).to_be_bytes());
195                    buf.extend_from_slice(msg_bytes);
196                    // SAFETY: 前 4 字节是 u32 BE,后面是 UTF-8 消息
197                    unsafe { String::from_utf8_unchecked(buf) }
198                }
199                other => format!("{}", other),
200            }
201        }
202
203        /// 解析 JSON 配置字符串
204        fn parse_config(config_str: &str) -> Result<aiway_plugin::serde_json::Value, String> {
205            aiway_plugin::serde_json::from_str(config_str)
206                .map_err(|e| format!("parse config failed: {}", e))
207        }
208
209        /// 处理 on_request
210        fn handle_on_request(
211            plugin: &$plugin_type,
212            input: &aiway_plugin::wasm_types::WasmInput,
213        ) -> Result<aiway_plugin::wasm_types::WasmOutput, String> {
214            let config = parse_config(&input.config)?;
215            let mut dummy_head = build_dummy_request_parts(input.head.as_ref().unwrap());
216            let mut ctx = aiway_plugin::WasmHttpContext;
217
218            aiway_plugin::block_on(async {
219                plugin.on_request(&config, &mut dummy_head, &mut ctx).await
220            })
221            .map_err(encode_plugin_error)?;
222
223            let output_head = aiway_plugin::wasm_types::WasmHead::from_request_parts(&dummy_head);
224
225            Ok(aiway_plugin::wasm_types::WasmOutput {
226                head: Some(output_head),
227                body: None,
228            })
229        }
230
231        /// 处理 on_request_body
232        fn handle_on_request_body(
233            plugin: &$plugin_type,
234            input: &aiway_plugin::wasm_types::WasmInput,
235        ) -> Result<aiway_plugin::wasm_types::WasmOutput, String> {
236            let config = parse_config(&input.config)?;
237            let mut body = input
238                .body
239                .as_ref()
240                .map(|b| aiway_plugin::Bytes::from(b.clone()));
241            let mut ctx = aiway_plugin::WasmHttpContext;
242
243            aiway_plugin::block_on(async {
244                plugin.on_request_body(&config, &mut body, &mut ctx).await
245            })
246            .map_err(encode_plugin_error)?;
247
248            Ok(aiway_plugin::wasm_types::WasmOutput {
249                head: None,
250                body: body.map(|b| b.to_vec()),
251            })
252        }
253
254        /// 处理 on_response
255        fn handle_on_response(
256            plugin: &$plugin_type,
257            input: &aiway_plugin::wasm_types::WasmInput,
258        ) -> Result<aiway_plugin::wasm_types::WasmOutput, String> {
259            let config = parse_config(&input.config)?;
260            let mut dummy_head = build_dummy_response_parts(input.head.as_ref().unwrap());
261            let mut ctx = aiway_plugin::WasmHttpContext;
262
263            aiway_plugin::block_on(async {
264                plugin.on_response(&config, &mut dummy_head, &mut ctx).await
265            })
266            .map_err(encode_plugin_error)?;
267
268            let output_head = aiway_plugin::wasm_types::WasmHead::from_response_parts(&dummy_head);
269
270            Ok(aiway_plugin::wasm_types::WasmOutput {
271                head: Some(output_head),
272                body: None,
273            })
274        }
275
276        /// 处理 on_response_body
277        fn handle_on_response_body(
278            plugin: &$plugin_type,
279            input: &aiway_plugin::wasm_types::WasmInput,
280        ) -> Result<aiway_plugin::wasm_types::WasmOutput, String> {
281            let config = parse_config(&input.config)?;
282            let mut body = input
283                .body
284                .as_ref()
285                .map(|b| aiway_plugin::Bytes::from(b.clone()));
286            let mut ctx = aiway_plugin::WasmHttpContext;
287
288            aiway_plugin::block_on(async {
289                plugin.on_response_body(&config, &mut body, &mut ctx).await
290            })
291            .map_err(encode_plugin_error)?;
292
293            Ok(aiway_plugin::wasm_types::WasmOutput {
294                head: None,
295                body: body.map(|b| b.to_vec()),
296            })
297        }
298
299        /// 处理 on_logging
300        fn handle_on_logging(
301            plugin: &$plugin_type,
302            input: &aiway_plugin::wasm_types::WasmInput,
303        ) -> Result<aiway_plugin::wasm_types::WasmOutput, String> {
304            let config = parse_config(&input.config)?;
305            let mut ctx = aiway_plugin::WasmHttpContext;
306
307            aiway_plugin::block_on(async {
308                plugin.on_logging(&config, &mut ctx).await;
309            });
310
311            Ok(aiway_plugin::wasm_types::WasmOutput {
312                head: None,
313                body: None,
314            })
315        }
316
317        /// 构建虚拟的请求 Parts(用于传递给插件)
318        fn build_dummy_request_parts(
319            head: &aiway_plugin::wasm_types::WasmHead,
320        ) -> aiway_plugin::http::request::Parts {
321            let method: aiway_plugin::http::Method = head
322                .method
323                .as_deref()
324                .unwrap_or("GET")
325                .parse()
326                .unwrap_or(aiway_plugin::http::Method::GET);
327
328            let uri: aiway_plugin::http::Uri = head
329                .uri
330                .as_deref()
331                .unwrap_or("/")
332                .parse()
333                .unwrap_or_default();
334
335            let mut headers = aiway_plugin::http::HeaderMap::new();
336            for (k, v) in &head.headers {
337                if let (Ok(name), Ok(value)) = (
338                    aiway_plugin::http::HeaderName::from_bytes(k.as_bytes()),
339                    aiway_plugin::http::HeaderValue::from_str(v),
340                ) {
341                    headers.insert(name, value);
342                }
343            }
344
345            let (mut parts, _) = aiway_plugin::http::Request::builder()
346                .method(method)
347                .uri(uri)
348                .body(())
349                .unwrap()
350                .into_parts();
351            parts.headers = headers;
352            parts
353        }
354
355        /// 构建虚拟的响应 Parts(用于传递给插件)
356        fn build_dummy_response_parts(
357            head: &aiway_plugin::wasm_types::WasmHead,
358        ) -> aiway_plugin::http::response::Parts {
359            let status = aiway_plugin::http::StatusCode::from_u16(head.status.unwrap_or(200))
360                .unwrap_or(aiway_plugin::http::StatusCode::OK);
361
362            let mut headers = aiway_plugin::http::HeaderMap::new();
363            for (k, v) in &head.headers {
364                if let (Ok(name), Ok(value)) = (
365                    aiway_plugin::http::HeaderName::from_bytes(k.as_bytes()),
366                    aiway_plugin::http::HeaderValue::from_str(v),
367                ) {
368                    headers.insert(name, value);
369                }
370            }
371
372            let (mut parts, _) = aiway_plugin::http::Response::builder()
373                .status(status)
374                .body(())
375                .unwrap()
376                .into_parts();
377            parts.headers = headers;
378            parts
379        }
380    };
381}