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            };
103
104            let bytes = bincode::serialize(&info).unwrap();
105            let len = bytes.len();
106            let ptr = bytes.as_ptr() as i32;
107            std::mem::forget(bytes);
108
109            ((ptr as i64) << 32) | (len as i64)
110        }
111
112        /// 插件钩子调用入口
113        ///
114        /// # 参数
115        /// - `hook_id`: 钩子 ID(见 `wasm_types` 常量)
116        /// - `input_ptr`: 输入数据在 WASM 内存中的指针
117        /// - `input_len`: 输入数据长度
118        ///
119        /// # 返回
120        /// i64,高 32 位 = 结果指针,低 32 位 = 结果长度。
121        /// 若高 32 位为 0,表示错误,低 32 位是错误信息长度(数据在 ptr=1 处)。
122        #[unsafe(no_mangle)]
123        pub extern "C" fn aiway_call(hook_id: i32, input_ptr: i32, input_len: i32) -> i64 {
124            // 读取输入数据
125            let input_slice =
126                unsafe { std::slice::from_raw_parts(input_ptr as *const u8, input_len as usize) };
127
128            let input: aiway_plugin::wasm_types::WasmInput = match bincode::deserialize(input_slice)
129            {
130                Ok(v) => v,
131                Err(e) => return encode_error(&format!("deserialize input failed: {}", e)),
132            };
133
134            // 根据 hook_id 分发
135            let result = match hook_id {
136                aiway_plugin::wasm_types::HOOK_ON_REQUEST => handle_on_request(&PLUGIN, &input),
137                aiway_plugin::wasm_types::HOOK_ON_REQUEST_BODY => {
138                    handle_on_request_body(&PLUGIN, &input)
139                }
140                aiway_plugin::wasm_types::HOOK_ON_RESPONSE => handle_on_response(&PLUGIN, &input),
141                aiway_plugin::wasm_types::HOOK_ON_RESPONSE_BODY => {
142                    handle_on_response_body(&PLUGIN, &input)
143                }
144                aiway_plugin::wasm_types::HOOK_ON_LOGGING => handle_on_logging(&PLUGIN, &input),
145                _ => Err(format!("unknown hook_id: {}", hook_id)),
146            };
147
148            match result {
149                Ok(output) => encode_output(&output),
150                Err(err_msg) => encode_error(&err_msg),
151            }
152        }
153
154        /// 编码成功输出
155        fn encode_output(output: &aiway_plugin::wasm_types::WasmOutput) -> i64 {
156            match bincode::serialize(output) {
157                Ok(bytes) => {
158                    let len = bytes.len();
159                    let ptr = bytes.as_ptr() as i32;
160                    std::mem::forget(bytes);
161                    ((ptr as i64) << 32) | (len as i64)
162                }
163                Err(e) => encode_error(&format!("serialize output failed: {}", e)),
164            }
165        }
166
167        /// 编码错误信息(ptr=0, len=错误信息长度)
168        fn encode_error(msg: &str) -> i64 {
169            let bytes = msg.as_bytes();
170            // 将错误信息写入 ptr=1 处(预留 1 字节作为 null 标记)
171            let dst = unsafe { std::slice::from_raw_parts_mut(1 as *mut u8, bytes.len()) };
172            dst.copy_from_slice(bytes);
173            (0i64 << 32) | (bytes.len() as i64)
174        }
175
176        /// 解析 JSON 配置字符串
177        fn parse_config(config_str: &str) -> Result<aiway_plugin::serde_json::Value, String> {
178            aiway_plugin::serde_json::from_str(config_str)
179                .map_err(|e| format!("parse config failed: {}", e))
180        }
181
182        /// 处理 on_request
183        fn handle_on_request(
184            plugin: &$plugin_type,
185            input: &aiway_plugin::wasm_types::WasmInput,
186        ) -> Result<aiway_plugin::wasm_types::WasmOutput, String> {
187            let config = parse_config(&input.config)?;
188            let mut dummy_head = build_dummy_request_parts(input.head.as_ref().unwrap());
189            let mut ctx = aiway_plugin::WasmHttpContext;
190
191            aiway_plugin::block_on(async {
192                plugin.on_request(&config, &mut dummy_head, &mut ctx).await
193            })
194            .map_err(|e| format!("{}", e))?;
195
196            let output_head = aiway_plugin::wasm_types::WasmHead::from_request_parts(&dummy_head);
197
198            Ok(aiway_plugin::wasm_types::WasmOutput {
199                head: Some(output_head),
200                body: None,
201            })
202        }
203
204        /// 处理 on_request_body
205        fn handle_on_request_body(
206            plugin: &$plugin_type,
207            input: &aiway_plugin::wasm_types::WasmInput,
208        ) -> Result<aiway_plugin::wasm_types::WasmOutput, String> {
209            let config = parse_config(&input.config)?;
210            let mut body = input
211                .body
212                .as_ref()
213                .map(|b| aiway_plugin::Bytes::from(b.clone()));
214            let mut ctx = aiway_plugin::WasmHttpContext;
215
216            aiway_plugin::block_on(async {
217                plugin.on_request_body(&config, &mut body, &mut ctx).await
218            })
219            .map_err(|e| format!("{}", e))?;
220
221            Ok(aiway_plugin::wasm_types::WasmOutput {
222                head: None,
223                body: body.map(|b| b.to_vec()),
224            })
225        }
226
227        /// 处理 on_response
228        fn handle_on_response(
229            plugin: &$plugin_type,
230            input: &aiway_plugin::wasm_types::WasmInput,
231        ) -> Result<aiway_plugin::wasm_types::WasmOutput, String> {
232            let config = parse_config(&input.config)?;
233            let mut dummy_head = build_dummy_response_parts(input.head.as_ref().unwrap());
234            let mut ctx = aiway_plugin::WasmHttpContext;
235
236            aiway_plugin::block_on(async {
237                plugin.on_response(&config, &mut dummy_head, &mut ctx).await
238            })
239            .map_err(|e| format!("{}", e))?;
240
241            let output_head = aiway_plugin::wasm_types::WasmHead::from_response_parts(&dummy_head);
242
243            Ok(aiway_plugin::wasm_types::WasmOutput {
244                head: Some(output_head),
245                body: None,
246            })
247        }
248
249        /// 处理 on_response_body
250        /// 因为 pingora 仅支持同步的,所以这里也需要同步
251        fn handle_on_response_body(
252            plugin: &$plugin_type,
253            input: &aiway_plugin::wasm_types::WasmInput,
254        ) -> Result<aiway_plugin::wasm_types::WasmOutput, String> {
255            let config = parse_config(&input.config)?;
256            let mut body = input
257                .body
258                .as_ref()
259                .map(|b| aiway_plugin::Bytes::from(b.clone()));
260            let mut ctx = aiway_plugin::WasmHttpContext;
261
262            plugin
263                .on_response_body(&config, &mut body, &mut ctx)
264                .map_err(|e| format!("{}", e))?;
265
266            Ok(aiway_plugin::wasm_types::WasmOutput {
267                head: None,
268                body: body.map(|b| b.to_vec()),
269            })
270        }
271
272        /// 处理 on_logging
273        fn handle_on_logging(
274            plugin: &$plugin_type,
275            input: &aiway_plugin::wasm_types::WasmInput,
276        ) -> Result<aiway_plugin::wasm_types::WasmOutput, String> {
277            let config = parse_config(&input.config)?;
278            let mut ctx = aiway_plugin::WasmHttpContext;
279
280            aiway_plugin::block_on(async {
281                plugin.on_logging(&config, &mut ctx).await;
282            });
283
284            Ok(aiway_plugin::wasm_types::WasmOutput {
285                head: None,
286                body: None,
287            })
288        }
289
290        /// 构建虚拟的请求 Parts(用于传递给插件)
291        fn build_dummy_request_parts(
292            head: &aiway_plugin::wasm_types::WasmHead,
293        ) -> aiway_plugin::http::request::Parts {
294            let method: aiway_plugin::http::Method = head
295                .method
296                .as_deref()
297                .unwrap_or("GET")
298                .parse()
299                .unwrap_or(aiway_plugin::http::Method::GET);
300
301            let uri: aiway_plugin::http::Uri = head
302                .uri
303                .as_deref()
304                .unwrap_or("/")
305                .parse()
306                .unwrap_or_default();
307
308            let mut headers = aiway_plugin::http::HeaderMap::new();
309            for (k, v) in &head.headers {
310                if let (Ok(name), Ok(value)) = (
311                    aiway_plugin::http::HeaderName::from_bytes(k.as_bytes()),
312                    aiway_plugin::http::HeaderValue::from_str(v),
313                ) {
314                    headers.insert(name, value);
315                }
316            }
317
318            let (mut parts, _) = aiway_plugin::http::Request::builder()
319                .method(method)
320                .uri(uri)
321                .body(())
322                .unwrap()
323                .into_parts();
324            parts.headers = headers;
325            parts
326        }
327
328        /// 构建虚拟的响应 Parts(用于传递给插件)
329        fn build_dummy_response_parts(
330            head: &aiway_plugin::wasm_types::WasmHead,
331        ) -> aiway_plugin::http::response::Parts {
332            let status = aiway_plugin::http::StatusCode::from_u16(head.status.unwrap_or(200))
333                .unwrap_or(aiway_plugin::http::StatusCode::OK);
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::Response::builder()
346                .status(status)
347                .body(())
348                .unwrap()
349                .into_parts();
350            parts.headers = headers;
351            parts
352        }
353    };
354}