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 = 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_msg) => encode_error(&err_msg),
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 处(预留 1 字节作为 null 标记)
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        /// 解析 JSON 配置字符串
178        fn parse_config(config_str: &str) -> Result<aiway_plugin::serde_json::Value, String> {
179            aiway_plugin::serde_json::from_str(config_str)
180                .map_err(|e| format!("parse config failed: {}", e))
181        }
182
183        /// 处理 on_request
184        fn handle_on_request(
185            plugin: &$plugin_type,
186            input: &aiway_plugin::wasm_types::WasmInput,
187        ) -> Result<aiway_plugin::wasm_types::WasmOutput, String> {
188            let config = parse_config(&input.config)?;
189            let mut dummy_head = build_dummy_request_parts(input.head.as_ref().unwrap());
190            let mut ctx = aiway_plugin::WasmHttpContext;
191
192            aiway_plugin::block_on(async {
193                plugin.on_request(&config, &mut dummy_head, &mut ctx).await
194            })
195            .map_err(|e| format!("{}", e))?;
196
197            let output_head = aiway_plugin::wasm_types::WasmHead::from_request_parts(&dummy_head);
198
199            Ok(aiway_plugin::wasm_types::WasmOutput {
200                head: Some(output_head),
201                body: None,
202            })
203        }
204
205        /// 处理 on_request_body
206        fn handle_on_request_body(
207            plugin: &$plugin_type,
208            input: &aiway_plugin::wasm_types::WasmInput,
209        ) -> Result<aiway_plugin::wasm_types::WasmOutput, String> {
210            let config = parse_config(&input.config)?;
211            let mut body = input
212                .body
213                .as_ref()
214                .map(|b| aiway_plugin::Bytes::from(b.clone()));
215            let mut ctx = aiway_plugin::WasmHttpContext;
216
217            aiway_plugin::block_on(async {
218                plugin.on_request_body(&config, &mut body, &mut ctx).await
219            })
220            .map_err(|e| format!("{}", e))?;
221
222            Ok(aiway_plugin::wasm_types::WasmOutput {
223                head: None,
224                body: body.map(|b| b.to_vec()),
225            })
226        }
227
228        /// 处理 on_response
229        fn handle_on_response(
230            plugin: &$plugin_type,
231            input: &aiway_plugin::wasm_types::WasmInput,
232        ) -> Result<aiway_plugin::wasm_types::WasmOutput, String> {
233            let config = parse_config(&input.config)?;
234            let mut dummy_head = build_dummy_response_parts(input.head.as_ref().unwrap());
235            let mut ctx = aiway_plugin::WasmHttpContext;
236
237            aiway_plugin::block_on(async {
238                plugin.on_response(&config, &mut dummy_head, &mut ctx).await
239            })
240            .map_err(|e| format!("{}", e))?;
241
242            let output_head = aiway_plugin::wasm_types::WasmHead::from_response_parts(&dummy_head);
243
244            Ok(aiway_plugin::wasm_types::WasmOutput {
245                head: Some(output_head),
246                body: None,
247            })
248        }
249
250        /// 处理 on_response_body
251        /// 因为 pingora 仅支持同步的,所以这里也需要同步
252        fn handle_on_response_body(
253            plugin: &$plugin_type,
254            input: &aiway_plugin::wasm_types::WasmInput,
255        ) -> Result<aiway_plugin::wasm_types::WasmOutput, String> {
256            let config = parse_config(&input.config)?;
257            let mut body = input
258                .body
259                .as_ref()
260                .map(|b| aiway_plugin::Bytes::from(b.clone()));
261            let mut ctx = aiway_plugin::WasmHttpContext;
262
263            plugin
264                .on_response_body(&config, &mut body, &mut ctx)
265                .map_err(|e| format!("{}", e))?;
266
267            Ok(aiway_plugin::wasm_types::WasmOutput {
268                head: None,
269                body: body.map(|b| b.to_vec()),
270            })
271        }
272
273        /// 处理 on_logging
274        fn handle_on_logging(
275            plugin: &$plugin_type,
276            input: &aiway_plugin::wasm_types::WasmInput,
277        ) -> Result<aiway_plugin::wasm_types::WasmOutput, String> {
278            let config = parse_config(&input.config)?;
279            let mut ctx = aiway_plugin::WasmHttpContext;
280
281            aiway_plugin::block_on(async {
282                plugin.on_logging(&config, &mut ctx).await;
283            });
284
285            Ok(aiway_plugin::wasm_types::WasmOutput {
286                head: None,
287                body: None,
288            })
289        }
290
291        /// 构建虚拟的请求 Parts(用于传递给插件)
292        fn build_dummy_request_parts(
293            head: &aiway_plugin::wasm_types::WasmHead,
294        ) -> aiway_plugin::http::request::Parts {
295            let method: aiway_plugin::http::Method = head
296                .method
297                .as_deref()
298                .unwrap_or("GET")
299                .parse()
300                .unwrap_or(aiway_plugin::http::Method::GET);
301
302            let uri: aiway_plugin::http::Uri = head
303                .uri
304                .as_deref()
305                .unwrap_or("/")
306                .parse()
307                .unwrap_or_default();
308
309            let mut headers = aiway_plugin::http::HeaderMap::new();
310            for (k, v) in &head.headers {
311                if let (Ok(name), Ok(value)) = (
312                    aiway_plugin::http::HeaderName::from_bytes(k.as_bytes()),
313                    aiway_plugin::http::HeaderValue::from_str(v),
314                ) {
315                    headers.insert(name, value);
316                }
317            }
318
319            let (mut parts, _) = aiway_plugin::http::Request::builder()
320                .method(method)
321                .uri(uri)
322                .body(())
323                .unwrap()
324                .into_parts();
325            parts.headers = headers;
326            parts
327        }
328
329        /// 构建虚拟的响应 Parts(用于传递给插件)
330        fn build_dummy_response_parts(
331            head: &aiway_plugin::wasm_types::WasmHead,
332        ) -> aiway_plugin::http::response::Parts {
333            let status = aiway_plugin::http::StatusCode::from_u16(head.status.unwrap_or(200))
334                .unwrap_or(aiway_plugin::http::StatusCode::OK);
335
336            let mut headers = aiway_plugin::http::HeaderMap::new();
337            for (k, v) in &head.headers {
338                if let (Ok(name), Ok(value)) = (
339                    aiway_plugin::http::HeaderName::from_bytes(k.as_bytes()),
340                    aiway_plugin::http::HeaderValue::from_str(v),
341                ) {
342                    headers.insert(name, value);
343                }
344            }
345
346            let (mut parts, _) = aiway_plugin::http::Response::builder()
347                .status(status)
348                .body(())
349                .unwrap()
350                .into_parts();
351            parts.headers = headers;
352            parts
353        }
354    };
355}