File
Blob: src/rust/jsg-macros/lib.rs
| 1 | // Copyright (c) 2026 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | |
| 5 | //! Procedural macros for the JSG Rust bindings. |
| 6 | //! |
| 7 | //! # Macros |
| 8 | //! |
| 9 | //! | Macro | Apply to | Purpose | |
| 10 | //! |--------------------------|------------------|----------------------------------------------------------------| |
| 11 | //! | `#[jsg_resource]` | struct / impl | Expose a Rust type to JavaScript as a GC resource | |
| 12 | //! | `#[jsg_method]` | fn inside impl | Register a method (instance or static) on a resource | |
| 13 | //! | `#[jsg_constructor]` | fn inside impl | Register `new MyResource(…)` JavaScript constructor | |
| 14 | //! | `#[jsg_static_constant]` | const inside impl | Expose a numeric constant on both constructor and prototype | |
| 15 | //! | `#[jsg_property]` | fn inside impl | Register a resource accessor property (getter/setter) | |
| 16 | //! | `#[jsg_inspect_property]` | fn inside impl | Register a debug-inspect-only symbol property | |
| 17 | //! | `#[jsg_struct]` | struct | Expose a Rust struct as a plain JavaScript object | |
| 18 | //! | `#[jsg_oneof]` | enum | Accept one of several JavaScript types (`kj::OneOf`) | |
| 19 | //! |
| 20 | //! See [`jsg/README.md`](../jsg/README.md) for full usage documentation. |
| 21 | |
| 22 | mod resource; |
| 23 | mod trace; |
| 24 | mod utils; |
| 25 | |
| 26 | use proc_macro::TokenStream; |
| 27 | use quote::quote; |
| 28 | use resource::generate_resource_impl; |
| 29 | use resource::generate_resource_struct; |
| 30 | use syn::Data; |
| 31 | use syn::DeriveInput; |
| 32 | use syn::Fields; |
| 33 | use syn::FnArg; |
| 34 | use syn::ItemFn; |
| 35 | use syn::ItemImpl; |
| 36 | use syn::parse_macro_input; |
| 37 | use utils::error; |
| 38 | use utils::extract_name_attribute; |
| 39 | use utils::extract_named_fields; |
| 40 | use utils::is_lock_ref; |
| 41 | use utils::is_result_type; |
| 42 | |
| 43 | // ============================================================================= |
| 44 | // #[jsg_struct] |
| 45 | // ============================================================================= |
| 46 | |
| 47 | /// Generates `jsg::Struct`, `jsg::Type`, `jsg::ToJS`, and `jsg::FromJS` |
| 48 | /// implementations for a plain data struct. |
| 49 | /// |
| 50 | /// Only `pub` fields are projected into the JavaScript object. |
| 51 | /// Use `#[jsg_struct(name = "MyName")]` to override `Type::class_name()` |
| 52 | /// metadata (used in diagnostics/type reporting), not to define a JS class. |
| 53 | #[proc_macro_attribute] |
| 54 | pub fn jsg_struct(attr: TokenStream, item: TokenStream) -> TokenStream { |
| 55 | let input = parse_macro_input!(item as DeriveInput); |
| 56 | let name = &input.ident; |
| 57 | let class_name = extract_name_attribute(attr).unwrap_or_else(|| name.to_string()); |
| 58 | |
| 59 | let named_fields = match extract_named_fields(&input, "jsg_struct") { |
| 60 | Ok(fields) => fields, |
| 61 | Err(err) => return err, |
| 62 | }; |
| 63 | |
| 64 | let mut field_assignments = Vec::new(); |
| 65 | let mut field_extractions = Vec::new(); |
| 66 | let mut field_names = Vec::new(); |
| 67 | |
| 68 | for field in &named_fields { |
| 69 | // Only public fields are projected into JavaScript objects. |
| 70 | if !matches!(field.vis, syn::Visibility::Public(_)) { |
| 71 | continue; |
| 72 | } |
| 73 | // Named fields always have an ident; guard is purely defensive. |
| 74 | let Some(field_name) = field.ident.as_ref() else { |
| 75 | continue; |
| 76 | }; |
| 77 | let field_name_str = field_name.to_string(); |
| 78 | let field_type = &field.ty; |
| 79 | |
| 80 | field_assignments.push(quote! { |
| 81 | let #field_name = jsg::v8::ToLocalValue::to_local(&this.#field_name, lock); |
| 82 | obj.set(lock, #field_name_str, #field_name); |
| 83 | }); |
| 84 | field_extractions.push(quote! { |
| 85 | let #field_name = { |
| 86 | let prop = obj.get(lock, #field_name_str) |
| 87 | .ok_or_else(|| jsg::Error::new_type_error( |
| 88 | format!("Missing property '{}'", #field_name_str) |
| 89 | ))?; |
| 90 | <#field_type as jsg::FromJS>::from_js(lock, prop)? |
| 91 | }; |
| 92 | }); |
| 93 | field_names.push(field_name); |
| 94 | } |
| 95 | |
| 96 | quote! { |
| 97 | #input |
| 98 | |
| 99 | impl jsg::Type for #name { |
| 100 | fn class_name() -> &'static str { #class_name } |
| 101 | |
| 102 | fn is_exact(value: &jsg::v8::Local<jsg::v8::Value>) -> bool { |
| 103 | value.is_object() |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | impl jsg::ToJS for #name { |
| 108 | fn to_js<'a, 'b>(self, lock: &'a mut jsg::Lock) -> jsg::v8::Local<'b, jsg::v8::Value> |
| 109 | where |
| 110 | 'b: 'a, |
| 111 | { |
| 112 | // TODO(soon): Use a precached ObjectTemplate instance to create the object, |
| 113 | // similar to how C++ JSG optimizes object creation. This would avoid recreating |
| 114 | // the object shape on every wrap() call and improve performance. |
| 115 | { |
| 116 | let this = self; |
| 117 | let mut obj = lock.new_object(); |
| 118 | #(#field_assignments)* |
| 119 | obj.into() |
| 120 | } |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | impl jsg::FromJS for #name { |
| 125 | type ResultType = Self; |
| 126 | |
| 127 | fn from_js(lock: &mut jsg::Lock, value: jsg::v8::Local<jsg::v8::Value>) -> Result<Self::ResultType, jsg::Error> { |
| 128 | if !value.is_object() { |
| 129 | return Err(jsg::Error::new_type_error( |
| 130 | format!("Expected object but got {}", value.type_of()) |
| 131 | )); |
| 132 | } |
| 133 | let obj: jsg::v8::Local<'_, jsg::v8::Object> = value.into(); |
| 134 | #(#field_extractions)* |
| 135 | Ok(Self { #(#field_names),* }) |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | impl jsg::Struct for #name {} |
| 140 | |
| 141 | #[automatically_derived] |
| 142 | impl jsg::Traced for #name {} |
| 143 | } |
| 144 | .into() |
| 145 | } |
| 146 | |
| 147 | // ============================================================================= |
| 148 | // #[jsg_method] |
| 149 | // ============================================================================= |
| 150 | |
| 151 | /// Generates a V8 `FunctionCallback` for a JSG resource method. |
| 152 | /// |
| 153 | /// Parameters are extracted from JavaScript arguments via `jsg::FromJS`. |
| 154 | /// Return values are converted via `jsg::ToJS`. |
| 155 | /// `Result<T, E>` return types automatically throw exceptions on `Err`. |
| 156 | /// |
| 157 | /// The first typed parameter may be `&mut Lock` (or `&mut jsg::Lock`) to receive |
| 158 | /// the isolate lock directly; it is not counted as a JavaScript argument. |
| 159 | /// |
| 160 | /// Use `#[jsg_method(name = "jsName")]` to override the default `camelCase` |
| 161 | /// conversion of the Rust function name. |
| 162 | #[proc_macro_attribute] |
| 163 | pub fn jsg_method(_attr: TokenStream, item: TokenStream) -> TokenStream { |
| 164 | let input_fn = parse_macro_input!(item as ItemFn); |
| 165 | let fn_name = &input_fn.sig.ident; |
| 166 | let fn_vis = &input_fn.vis; |
| 167 | let fn_sig = &input_fn.sig; |
| 168 | let fn_block = &input_fn.block; |
| 169 | let callback_name = syn::Ident::new(&format!("{fn_name}_callback"), fn_name.span()); |
| 170 | |
| 171 | // Methods with a receiver (&self, &mut self) become instance methods on the prototype. |
| 172 | // Methods without a receiver become static methods on the constructor. |
| 173 | let has_self = fn_sig |
| 174 | .inputs |
| 175 | .iter() |
| 176 | .any(|arg| matches!(arg, FnArg::Receiver(_))); |
| 177 | |
| 178 | let params: Vec<_> = fn_sig |
| 179 | .inputs |
| 180 | .iter() |
| 181 | .filter_map(|arg| match arg { |
| 182 | FnArg::Typed(pat_type) => Some(&pat_type.ty), |
| 183 | FnArg::Receiver(_) => None, |
| 184 | }) |
| 185 | .collect(); |
| 186 | |
| 187 | // Check if the first typed parameter is `&mut Lock` — if so, pass `&mut lock` |
| 188 | // directly instead of extracting it from JS args (like C++ jsg::Lock&). |
| 189 | let has_lock_param = params.first().is_some_and(|ty| is_lock_ref(ty)); |
| 190 | let js_arg_offset = usize::from(has_lock_param); |
| 191 | |
| 192 | let (unwraps, arg_exprs): (Vec<_>, Vec<_>) = params |
| 193 | .iter() |
| 194 | .enumerate() |
| 195 | .map(|(i, ty)| { |
| 196 | // First param is &mut Lock — pass the callback's lock directly. |
| 197 | if i == 0 && has_lock_param { |
| 198 | return (quote! {}, quote! { &mut lock }); |
| 199 | } |
| 200 | |
| 201 | let js_index = i - js_arg_offset; |
| 202 | let arg = syn::Ident::new(&format!("arg{js_index}"), fn_name.span()); |
| 203 | let unwrap = quote! { |
| 204 | let #arg = match <#ty as jsg::FromJS>::from_js(&mut lock, args.get(#js_index)) { |
| 205 | Ok(v) => v, |
| 206 | Err(err) => { |
| 207 | lock.throw_exception(&err); |
| 208 | return; |
| 209 | } |
| 210 | }; |
| 211 | }; |
| 212 | // For reference types (like &str), FromJS returns an owned type (String), |
| 213 | // so we need to borrow it when passing to the function. |
| 214 | let is_ref = matches!(ty.as_ref(), syn::Type::Reference(_)); |
| 215 | let arg_expr = if is_ref { |
| 216 | quote! { &#arg } |
| 217 | } else { |
| 218 | quote! { #arg } |
| 219 | }; |
| 220 | (unwrap, arg_expr) |
| 221 | }) |
| 222 | .unzip(); |
| 223 | |
| 224 | // Check if return type is Result<T, E> |
| 225 | let is_result = matches!(&fn_sig.output, syn::ReturnType::Type(_, ty) if is_result_type(ty)); |
| 226 | |
| 227 | let result_handling = if is_result { |
| 228 | quote! { |
| 229 | match result { |
| 230 | Ok(value) => args.set_return_value(jsg::ToJS::to_js(value, &mut lock)), |
| 231 | Err(err) => lock.throw_exception(&err.into()), |
| 232 | } |
| 233 | } |
| 234 | } else { |
| 235 | quote! { |
| 236 | args.set_return_value(jsg::ToJS::to_js(result, &mut lock)); |
| 237 | } |
| 238 | }; |
| 239 | |
| 240 | let invocation = if has_self { |
| 241 | quote! { |
| 242 | let this = args.this(); |
| 243 | // SAFETY: `v8::Signature` (passed to `FunctionTemplate::New` in |
| 244 | // `create_resource_template`) enforces that V8 only dispatches this |
| 245 | // callback when `this` is an instance of the resource's own |
| 246 | // `FunctionTemplate`. If the caller destructures the method and calls |
| 247 | // it with a wrong receiver (e.g. `const {abort} = ac; abort()`), V8 |
| 248 | // throws a `TypeError: Illegal invocation` *before* reaching this |
| 249 | // code. Given that guarantee, `from_js` / `resolve_resource` perform |
| 250 | // a belt-and-suspenders `TypeId` check; the `.expect` panics |
| 251 | // (aborting the isolate) rather than triggering UB on a mismatch. |
| 252 | // The `&mut` is sound because V8 is single-threaded and no other |
| 253 | // Rust code can alias the resource during the callback. |
| 254 | let self_: &mut Self = unsafe { |
| 255 | let wrappable = jsg::v8::WrappableRc::from_js(lock.isolate(), this) |
| 256 | .expect("receiver is not a Rust-wrapped resource"); |
| 257 | &mut *wrappable.resolve_resource::<Self>() |
| 258 | .expect("type mismatch in resource callback") |
| 259 | .as_ptr() |
| 260 | }; |
| 261 | let result = self_.#fn_name(#(#arg_exprs),*); |
| 262 | } |
| 263 | } else { |
| 264 | quote! { |
| 265 | let result = Self::#fn_name(#(#arg_exprs),*); |
| 266 | } |
| 267 | }; |
| 268 | |
| 269 | quote! { |
| 270 | #fn_vis #fn_sig { #fn_block } |
| 271 | |
| 272 | #[automatically_derived] |
| 273 | #[expect(clippy::undocumented_unsafe_blocks)] |
| 274 | extern "C" fn #callback_name(args: *mut jsg::v8::ffi::FunctionCallbackInfo) { |
| 275 | let mut lock = unsafe { jsg::Lock::from_args(args) }; |
| 276 | jsg::catch_panic(&mut lock, || { |
| 277 | let mut lock = unsafe { jsg::Lock::from_args(args) }; |
| 278 | let mut args = unsafe { jsg::v8::FunctionCallbackInfo::from_ffi(args) }; |
| 279 | #(#unwraps)* |
| 280 | #invocation |
| 281 | #result_handling |
| 282 | }); |
| 283 | } |
| 284 | } |
| 285 | .into() |
| 286 | } |
| 287 | |
| 288 | // ============================================================================= |
| 289 | // #[jsg_resource] |
| 290 | // ============================================================================= |
| 291 | |
| 292 | /// Generates JSG boilerplate for a resource type or its impl block. |
| 293 | /// |
| 294 | /// **On a struct** — emits `jsg::Type`, `jsg::ToJS`, `jsg::FromJS`, |
| 295 | /// `jsg::Traced`, and `jsg::GarbageCollected`. |
| 296 | /// |
| 297 | /// The generated `Traced::trace` body simply delegates to `Traced::trace()` on |
| 298 | /// every named field. |
| 299 | /// |
| 300 | /// **On an impl block** — emits `jsg::Resource::members()` registering every |
| 301 | /// `#[jsg_method]`, `#[jsg_property]`, `#[jsg_inspect_property]`, |
| 302 | /// `#[jsg_constructor]`, and `#[jsg_static_constant]` item. |
| 303 | /// |
| 304 | /// Use `#[jsg_resource(name = "JSName")]` on the struct to override the default |
| 305 | /// JavaScript class name. |
| 306 | #[proc_macro_attribute] |
| 307 | pub fn jsg_resource(attr: TokenStream, item: TokenStream) -> TokenStream { |
| 308 | if let Ok(impl_block) = syn::parse::<ItemImpl>(item.clone()) { |
| 309 | return generate_resource_impl(&impl_block); |
| 310 | } |
| 311 | let input = parse_macro_input!(item as DeriveInput); |
| 312 | generate_resource_struct(attr, &input) |
| 313 | } |
| 314 | |
| 315 | // ============================================================================= |
| 316 | // #[jsg_static_constant] (marker only — processed by #[jsg_resource]) |
| 317 | // ============================================================================= |
| 318 | |
| 319 | /// Marks a `const` item inside a `#[jsg_resource]` impl block as a static |
| 320 | /// constant exposed to JavaScript on both the constructor and its prototype. |
| 321 | /// |
| 322 | /// The constant name is used as-is (no camelCase conversion), matching the |
| 323 | /// convention that constants are `UPPER_SNAKE_CASE` in both Rust and JavaScript. |
| 324 | /// Only numeric types (`i8`..`i64`, `u8`..`u64`, `f32`, `f64`) are supported. |
| 325 | /// |
| 326 | /// ```ignore |
| 327 | /// #[jsg_resource] |
| 328 | /// impl WebSocket { |
| 329 | /// #[jsg_static_constant] |
| 330 | /// pub const CONNECTING: i32 = 0; |
| 331 | /// } |
| 332 | /// // JS: WebSocket.CONNECTING === 0 / instance.CONNECTING === 0 |
| 333 | /// ``` |
| 334 | #[proc_macro_attribute] |
| 335 | pub fn jsg_static_constant(_attr: TokenStream, item: TokenStream) -> TokenStream { |
| 336 | // Marker only — registration is handled by #[jsg_resource] on the impl block. |
| 337 | item |
| 338 | } |
| 339 | |
| 340 | // ============================================================================= |
| 341 | // #[jsg_constructor] (marker only — processed by #[jsg_resource]) |
| 342 | // ============================================================================= |
| 343 | |
| 344 | /// Marks a static method as the JavaScript constructor for a `#[jsg_resource]`. |
| 345 | /// |
| 346 | /// The method must have no `self` receiver and must return `Self`. |
| 347 | /// An optional first parameter of `&mut Lock` (or `&mut jsg::Lock`) receives |
| 348 | /// the isolate lock and is not exposed as a JavaScript argument. |
| 349 | /// |
| 350 | /// Only one `#[jsg_constructor]` is allowed per impl block. Without it, |
| 351 | /// `new MyResource()` throws `Illegal constructor`, matching C++ JSG behaviour. |
| 352 | /// |
| 353 | /// ```ignore |
| 354 | /// #[jsg_resource] |
| 355 | /// impl Greeting { |
| 356 | /// #[jsg_constructor] |
| 357 | /// fn constructor(message: String) -> Self { |
| 358 | /// Self { message } |
| 359 | /// } |
| 360 | /// } |
| 361 | /// // JS: let g = new Greeting("hello"); |
| 362 | /// ``` |
| 363 | #[proc_macro_attribute] |
| 364 | pub fn jsg_constructor(_attr: TokenStream, item: TokenStream) -> TokenStream { |
| 365 | // Marker only — registration is handled by #[jsg_resource] on the impl block. |
| 366 | item |
| 367 | } |
| 368 | |
| 369 | /// Registers a method as a JavaScript property getter or setter on a |
| 370 | /// `#[jsg_resource]` type. |
| 371 | /// |
| 372 | /// Supported arguments: |
| 373 | /// - `prototype` or `instance` (optional, defaults to `prototype`) |
| 374 | /// - `name = "..."` for an explicit JS property name |
| 375 | /// - `readonly` to require no matching setter |
| 376 | /// |
| 377 | /// Methods must start with `get_` (getter) or `set_` (setter). If `name` is |
| 378 | /// omitted, the prefix is stripped and the remainder is converted |
| 379 | /// `snake_case` -> `camelCase`. |
| 380 | #[proc_macro_attribute] |
| 381 | pub fn jsg_property(_attr: TokenStream, item: TokenStream) -> TokenStream { |
| 382 | // Reuse jsg_method callback generation; registration as Member::Property is |
| 383 | // handled by #[jsg_resource] on the enclosing impl block. |
| 384 | jsg_method(TokenStream::new(), item) |
| 385 | } |
| 386 | |
| 387 | /// Registers a method as a debug-inspect-only property on a `#[jsg_resource]` |
| 388 | /// type. This maps to `jsg::PropertyKind::Inspect`. |
| 389 | /// |
| 390 | /// Optional argument: `name = "..."` to set the symbol description. |
| 391 | /// |
| 392 | /// Inspect properties are always read-only; setters are rejected at compile time. |
| 393 | #[proc_macro_attribute] |
| 394 | pub fn jsg_inspect_property(_attr: TokenStream, item: TokenStream) -> TokenStream { |
| 395 | // Reuse jsg_method callback generation; registration as Member::Property is |
| 396 | // handled by #[jsg_resource] on the enclosing impl block. |
| 397 | jsg_method(TokenStream::new(), item) |
| 398 | } |
| 399 | |
| 400 | // ============================================================================= |
| 401 | // #[jsg_oneof] |
| 402 | // ============================================================================= |
| 403 | |
| 404 | /// Generates `jsg::Type` and `jsg::FromJS` for a union enum, equivalent to |
| 405 | /// `kj::OneOf<…>` in C++ JSG. |
| 406 | /// |
| 407 | /// Each variant must be a single-field tuple variant whose inner type implements |
| 408 | /// `jsg::Type` and `jsg::FromJS`. The macro tries each variant in declaration |
| 409 | /// order using exact-type matching and returns the first that succeeds. |
| 410 | /// |
| 411 | /// ```ignore |
| 412 | /// #[jsg_oneof] |
| 413 | /// #[derive(Debug, Clone)] |
| 414 | /// enum StringOrNumber { |
| 415 | /// String(String), |
| 416 | /// Number(jsg::Number), |
| 417 | /// } |
| 418 | /// ``` |
| 419 | #[proc_macro_attribute] |
| 420 | pub fn jsg_oneof(_attr: TokenStream, item: TokenStream) -> TokenStream { |
| 421 | let input = parse_macro_input!(item as DeriveInput); |
| 422 | let name = &input.ident; |
| 423 | |
| 424 | let Data::Enum(data) = &input.data else { |
| 425 | return error(&input, "#[jsg_oneof] can only be applied to enums"); |
| 426 | }; |
| 427 | |
| 428 | let mut variants = Vec::new(); |
| 429 | for variant in &data.variants { |
| 430 | let variant_name = &variant.ident; |
| 431 | let Fields::Unnamed(fields) = &variant.fields else { |
| 432 | return error( |
| 433 | variant, |
| 434 | "#[jsg_oneof] variants must be tuple variants (e.g., `Variant(Type)`)", |
| 435 | ); |
| 436 | }; |
| 437 | if fields.unnamed.len() != 1 { |
| 438 | return error(variant, "#[jsg_oneof] variants must have exactly one field"); |
| 439 | } |
| 440 | let inner_type = &fields.unnamed[0].ty; |
| 441 | variants.push((variant_name, inner_type)); |
| 442 | } |
| 443 | |
| 444 | if variants.is_empty() { |
| 445 | return error(&input, "#[jsg_oneof] requires at least one variant"); |
| 446 | } |
| 447 | |
| 448 | let type_checks: Vec<_> = variants |
| 449 | .iter() |
| 450 | .map(|(variant_name, inner_type)| { |
| 451 | quote! { |
| 452 | if let Some(result) = <#inner_type as jsg::FromJS>::try_from_js_exact(lock, &value) { |
| 453 | return result.map(Self::#variant_name); |
| 454 | } |
| 455 | } |
| 456 | }) |
| 457 | .collect(); |
| 458 | |
| 459 | let type_names: Vec<_> = variants |
| 460 | .iter() |
| 461 | .map(|(_, inner_type)| quote! { <#inner_type as jsg::Type>::class_name() }) |
| 462 | .collect(); |
| 463 | |
| 464 | let is_exact_checks: Vec<_> = variants |
| 465 | .iter() |
| 466 | .map(|(_, inner_type)| quote! { <#inner_type as jsg::Type>::is_exact(value) }) |
| 467 | .collect(); |
| 468 | |
| 469 | let error_msg = quote! { |
| 470 | let expected: Vec<&str> = vec![#(#type_names),*]; |
| 471 | let msg = format!( |
| 472 | "Expected one of [{}] but got {}", |
| 473 | expected.join(", "), |
| 474 | value.type_of() |
| 475 | ); |
| 476 | Err(jsg::Error::new_type_error(msg)) |
| 477 | }; |
| 478 | |
| 479 | quote! { |
| 480 | #input |
| 481 | |
| 482 | #[automatically_derived] |
| 483 | impl jsg::Type for #name { |
| 484 | fn class_name() -> &'static str { |
| 485 | stringify!(#name) |
| 486 | } |
| 487 | |
| 488 | fn is_exact(value: &jsg::v8::Local<jsg::v8::Value>) -> bool { |
| 489 | #(#is_exact_checks)||* |
| 490 | } |
| 491 | } |
| 492 | |
| 493 | #[automatically_derived] |
| 494 | impl jsg::FromJS for #name { |
| 495 | type ResultType = Self; |
| 496 | |
| 497 | fn from_js(lock: &mut jsg::Lock, value: jsg::v8::Local<jsg::v8::Value>) -> Result<Self::ResultType, jsg::Error> { |
| 498 | #(#type_checks)* |
| 499 | #error_msg |
| 500 | } |
| 501 | } |
| 502 | } |
| 503 | .into() |
| 504 | } |