| 1 | //! [![github]](https://github.com/dtolnay/inventory) [![crates-io]](https://crates.io/crates/inventory) [![docs-rs]](https://docs.rs/inventory) |
| 2 | //! |
| 3 | //! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github |
| 4 | //! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust |
| 5 | //! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs |
| 6 | //! |
| 7 | //! <br> |
| 8 | //! |
| 9 | //! **Typed distributed plugin registration.** |
| 10 | //! |
| 11 | //! This crate provides a way to set up a plugin registry into which plugins |
| 12 | //! can be registered from any source file linked into your application. There |
| 13 | //! does not need to be a central list of all the plugins. |
| 14 | //! |
| 15 | //! # Examples |
| 16 | //! |
| 17 | //! Suppose we are writing a command line flags library and want to allow any |
| 18 | //! source file in the application to register command line flags that are |
| 19 | //! relevant to it. |
| 20 | //! |
| 21 | //! This is the flag registration style used by [gflags] and is better suited |
| 22 | //! for large scale development than maintaining a single central list of flags, |
| 23 | //! as the central list would become an endless source of merge conflicts in an |
| 24 | //! application developed simultaneously by thousands of developers. |
| 25 | //! |
| 26 | //! [gflags]: https://gflags.github.io/gflags/ |
| 27 | //! |
| 28 | //! ## Instantiating the plugin registry |
| 29 | //! |
| 30 | //! Let's use a `struct Flag` as the plugin type, which will contain the short |
| 31 | //! name of the flag like `-v`, the full name like `--verbose`, and maybe other |
| 32 | //! information like argument type and help text. We instantiate a plugin |
| 33 | //! registry with an invocation of `inventory::collect!`. |
| 34 | //! |
| 35 | //! ``` |
| 36 | //! pub struct Flag { |
| 37 | //! short: char, |
| 38 | //! name: &'static str, |
| 39 | //! /* ... */ |
| 40 | //! } |
| 41 | //! |
| 42 | //! impl Flag { |
| 43 | //! pub const fn new(short: char, name: &'static str) -> Self { |
| 44 | //! Flag { short, name } |
| 45 | //! } |
| 46 | //! } |
| 47 | //! |
| 48 | //! inventory::collect!(Flag); |
| 49 | //! ``` |
| 50 | //! |
| 51 | //! This `collect!` call must be in the same crate that defines the plugin type. |
| 52 | //! This macro does not "run" anything so place it outside of any function body. |
| 53 | //! |
| 54 | //! ## Registering plugins |
| 55 | //! |
| 56 | //! Now any crate with access to the `Flag` type can register flags as a plugin. |
| 57 | //! Plugins can be registered by the same crate that declares the plugin type, |
| 58 | //! or by any downstream crate. |
| 59 | //! |
| 60 | //! ``` |
| 61 | //! # struct Flag; |
| 62 | //! # |
| 63 | //! # impl Flag { |
| 64 | //! # const fn new(short: char, name: &'static str) -> Self { |
| 65 | //! # Flag |
| 66 | //! # } |
| 67 | //! # } |
| 68 | //! # |
| 69 | //! # inventory::collect!(Flag); |
| 70 | //! # |
| 71 | //! inventory::submit! { |
| 72 | //! Flag::new('v' , "verbose" ) |
| 73 | //! } |
| 74 | //! # |
| 75 | //! # fn main() {} |
| 76 | //! ``` |
| 77 | //! |
| 78 | //! The `submit!` macro does not "run" anything so place it outside of any |
| 79 | //! function body. In particular, note that all `submit!` invocations across all |
| 80 | //! source files linked into your application all take effect simultaneously. A |
| 81 | //! `submit!` invocation is not a statement that needs to be called from `main` |
| 82 | //! in order to execute. |
| 83 | //! |
| 84 | //! ## Iterating over plugins |
| 85 | //! |
| 86 | //! The value `inventory::iter::<T>` is an iterator with element type `&'static |
| 87 | //! T` that iterates over all plugins registered of type `T`. |
| 88 | //! |
| 89 | //! ``` |
| 90 | //! # struct Flag { |
| 91 | //! # short: char, |
| 92 | //! # name: &'static str, |
| 93 | //! # } |
| 94 | //! # |
| 95 | //! # inventory::collect!(Flag); |
| 96 | //! # |
| 97 | //! for flag in inventory::iter::<Flag> { |
| 98 | //! println!("-{}, --{}" , flag.short, flag.name); |
| 99 | //! } |
| 100 | //! ``` |
| 101 | //! |
| 102 | //! There is no guarantee about the order that plugins of the same type are |
| 103 | //! visited by the iterator. They may be visited in any order. |
| 104 | //! |
| 105 | //! ## WebAssembly and constructors |
| 106 | //! |
| 107 | //! `inventory` supports all WebAssembly targets, including |
| 108 | //! `wasm*-unknown-unknown`. However, in unusual circumstances, ensuring that |
| 109 | //! constructors run may require some extra effort. The Wasm linker will |
| 110 | //! synthesize a function `extern "C" unsafe fn __wasm_call_ctors()` which calls |
| 111 | //! all constructors when invoked; this function will *not* be exported from the |
| 112 | //! module unless you do so explicitly. Depending on the result of a heuristic, |
| 113 | //! the linker may or may not insert a call to this function from the beginning |
| 114 | //! of every function that your module exports. Specifically, it regards a |
| 115 | //! module as having "command-style linkage" if: |
| 116 | //! |
| 117 | //! * it is not relocatable; |
| 118 | //! * it is not a position-independent executable; |
| 119 | //! * and it does not call `__wasm_call_ctors`, directly or indirectly, from any |
| 120 | //! exported function. |
| 121 | //! |
| 122 | //! The linker expects that the embedder will call into a command-style module |
| 123 | //! only once per instantiation. Violation of this expectation can result in |
| 124 | //! `__wasm_call_ctors` being called multiple times. This is dangerous in |
| 125 | //! general, but safe and mostly harmless in the case of constructors generated |
| 126 | //! by `inventory`, which are idempotent. |
| 127 | //! |
| 128 | //! If you are building a module which relies on constructors and may be called |
| 129 | //! into multiple times per instance, you should export `__wasm_call_ctors` (or |
| 130 | //! a wrapper around it) and ensure that the embedder calls it immediately after |
| 131 | //! instantiation. Even though `inventory` may work fine without this, it is |
| 132 | //! still good practice, because it avoids unnecessary overhead from repeated |
| 133 | //! constructor invocation. It also can prevent unsoundness if some of your |
| 134 | //! constructors are generated by other crates or other programming languages. |
| 135 | //! |
| 136 | //! ``` |
| 137 | //! #[cfg(target_family = "wasm" )] |
| 138 | //! unsafe extern "C" { |
| 139 | //! fn __wasm_call_ctors(); |
| 140 | //! } |
| 141 | //! |
| 142 | //! fn main() { |
| 143 | //! #[cfg (target_family = "wasm" )] |
| 144 | //! unsafe { |
| 145 | //! __wasm_call_ctors(); |
| 146 | //! } |
| 147 | //! } |
| 148 | //! ``` |
| 149 | |
| 150 | #![doc (html_root_url = "https://docs.rs/inventory/0.3.20" )] |
| 151 | #![no_std ] |
| 152 | #![deny (unsafe_op_in_unsafe_fn)] |
| 153 | #![allow ( |
| 154 | clippy::doc_markdown, |
| 155 | clippy::empty_enum, |
| 156 | clippy::expl_impl_clone_on_copy, |
| 157 | clippy::let_underscore_untyped, |
| 158 | clippy::let_unit_value, |
| 159 | clippy::must_use_candidate, |
| 160 | clippy::new_without_default, |
| 161 | clippy::semicolon_if_nothing_returned, // https://github.com/rust-lang/rust-clippy/issues/7324 |
| 162 | )] |
| 163 | |
| 164 | // Not public API. |
| 165 | #[doc (hidden)] |
| 166 | pub extern crate core; |
| 167 | |
| 168 | use core::cell::UnsafeCell; |
| 169 | use core::marker::PhantomData; |
| 170 | use core::ops::Deref; |
| 171 | use core::ptr; |
| 172 | #[cfg (target_family = "wasm" )] |
| 173 | use core::sync::atomic::AtomicBool; |
| 174 | use core::sync::atomic::{AtomicPtr, Ordering}; |
| 175 | |
| 176 | // Not public API. Used by generated code. |
| 177 | #[doc (hidden)] |
| 178 | pub struct Registry { |
| 179 | head: AtomicPtr<Node>, |
| 180 | } |
| 181 | |
| 182 | // Not public API. Used by generated code. |
| 183 | #[doc (hidden)] |
| 184 | pub struct Node { |
| 185 | pub value: &'static dyn ErasedNode, |
| 186 | pub next: UnsafeCell<Option<&'static Node>>, |
| 187 | #[cfg (target_family = "wasm" )] |
| 188 | pub initialized: AtomicBool, |
| 189 | } |
| 190 | |
| 191 | // The `value` is Sync, and `next` is only mutated during submit, which is prior |
| 192 | // to any reads. |
| 193 | unsafe impl Sync for Node {} |
| 194 | |
| 195 | // Not public API. Used by generated code. |
| 196 | #[doc (hidden)] |
| 197 | pub trait ErasedNode: Sync { |
| 198 | // SAFETY: requires *node.value is of type Self. |
| 199 | unsafe fn submit(&self, node: &'static Node); |
| 200 | } |
| 201 | |
| 202 | impl<T: Collect> ErasedNode for T { |
| 203 | unsafe fn submit(&self, node: &'static Node) { |
| 204 | unsafe { |
| 205 | T::registry().submit(new:node); |
| 206 | } |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | /// Trait bound corresponding to types that can be iterated by inventory::iter. |
| 211 | /// |
| 212 | /// This trait cannot be implemented manually. Instead use the [`collect`] macro |
| 213 | /// which expands to an implementation of this trait for the given type. |
| 214 | /// |
| 215 | /// # Examples |
| 216 | /// |
| 217 | /// ``` |
| 218 | /// use inventory::Collect; |
| 219 | /// |
| 220 | /// fn count_plugins<T: Collect>() -> usize { |
| 221 | /// inventory::iter::<T>.into_iter().count() |
| 222 | /// } |
| 223 | /// ``` |
| 224 | pub trait Collect: Sync + Sized + 'static { |
| 225 | #[doc (hidden)] |
| 226 | fn registry() -> &'static Registry; |
| 227 | } |
| 228 | |
| 229 | impl Registry { |
| 230 | // Not public API. Used by generated code. |
| 231 | pub const fn new() -> Self { |
| 232 | Registry { |
| 233 | head: AtomicPtr::new(ptr::null_mut()), |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | // SAFETY: requires type of *new.value matches the $ty surrounding the |
| 238 | // declaration of this registry in inventory::collect macro. |
| 239 | unsafe fn submit(&'static self, new: &'static Node) { |
| 240 | // The WebAssembly linker uses an unreliable heuristic to determine |
| 241 | // whether a module is a "command-style" linkage, for which it will |
| 242 | // insert a call to `__wasm_call_ctors` at the top of every exported |
| 243 | // function. It expects that the embedder will call into such modules |
| 244 | // only once per instantiation. If this heuristic goes wrong, we can end |
| 245 | // up having our constructors invoked multiple times, which without this |
| 246 | // safeguard would lead to our registry's linked list becoming circular. |
| 247 | // On non-Wasm platforms, this check is unnecessary, so we skip it. |
| 248 | #[cfg (target_family = "wasm" )] |
| 249 | if new.initialized.swap(true, Ordering::Relaxed) { |
| 250 | return; |
| 251 | } |
| 252 | |
| 253 | let mut head = self.head.load(Ordering::Relaxed); |
| 254 | loop { |
| 255 | unsafe { |
| 256 | *new.next.get() = head.as_ref(); |
| 257 | } |
| 258 | let new_ptr = new as *const Node as *mut Node; |
| 259 | match self |
| 260 | .head |
| 261 | .compare_exchange(head, new_ptr, Ordering::Release, Ordering::Relaxed) |
| 262 | { |
| 263 | Ok(_) => return, |
| 264 | Err(prev) => head = prev, |
| 265 | } |
| 266 | } |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | /// An iterator over plugins registered of a given type. |
| 271 | /// |
| 272 | /// The value `inventory::iter::<T>` is an iterator with element type `&'static |
| 273 | /// T`. |
| 274 | /// |
| 275 | /// There is no guarantee about the order that plugins of the same type are |
| 276 | /// visited by the iterator. They may be visited in any order. |
| 277 | /// |
| 278 | /// # Examples |
| 279 | /// |
| 280 | /// ``` |
| 281 | /// # struct Flag { |
| 282 | /// # short: char, |
| 283 | /// # name: &'static str, |
| 284 | /// # } |
| 285 | /// # |
| 286 | /// # inventory::collect!(Flag); |
| 287 | /// # |
| 288 | /// # const IGNORE: &str = stringify! { |
| 289 | /// use my_flags::Flag; |
| 290 | /// # }; |
| 291 | /// |
| 292 | /// fn main() { |
| 293 | /// for flag in inventory::iter::<Flag> { |
| 294 | /// println!("-{}, --{}" , flag.short, flag.name); |
| 295 | /// } |
| 296 | /// } |
| 297 | /// ``` |
| 298 | /// |
| 299 | /// Refer to the [crate level documentation](index.html) for a complete example |
| 300 | /// of instantiating a plugin registry and submitting plugins. |
| 301 | #[allow (non_camel_case_types)] |
| 302 | pub type iter<T> = private::iter<T>; |
| 303 | |
| 304 | mod void_iter { |
| 305 | enum Void {} |
| 306 | |
| 307 | #[repr (C, packed)] |
| 308 | pub struct Iter<T>([*const T; 0], Void); |
| 309 | |
| 310 | unsafe impl<T> Send for Iter<T> {} |
| 311 | unsafe impl<T> Sync for Iter<T> {} |
| 312 | } |
| 313 | |
| 314 | mod value_iter { |
| 315 | #[doc (hidden)] |
| 316 | pub use crate::private::iter::iter; |
| 317 | } |
| 318 | |
| 319 | mod private { |
| 320 | // Based on https://github.com/dtolnay/ghost |
| 321 | #[allow (non_camel_case_types)] |
| 322 | pub enum iter<T> { |
| 323 | __Phantom(crate::void_iter::Iter<T>), |
| 324 | iter, |
| 325 | } |
| 326 | |
| 327 | #[doc (hidden)] |
| 328 | pub use crate::value_iter::*; |
| 329 | } |
| 330 | |
| 331 | #[doc (hidden)] |
| 332 | pub use crate::private::*; |
| 333 | |
| 334 | const _: () = { |
| 335 | fn into_iter<T: Collect>() -> Iter<T> { |
| 336 | let head = T::registry().head.load(Ordering::Acquire); |
| 337 | Iter { |
| 338 | // Head pointer is always null or valid &'static Node. |
| 339 | node: unsafe { head.as_ref() }, |
| 340 | marker: PhantomData, |
| 341 | } |
| 342 | } |
| 343 | |
| 344 | impl<T: Collect> IntoIterator for iter<T> { |
| 345 | type Item = &'static T; |
| 346 | type IntoIter = Iter<T>; |
| 347 | |
| 348 | fn into_iter(self) -> Self::IntoIter { |
| 349 | into_iter() |
| 350 | } |
| 351 | } |
| 352 | |
| 353 | #[doc (hidden)] |
| 354 | impl<T: Collect> Deref for iter<T> { |
| 355 | type Target = fn() -> Iter<T>; |
| 356 | fn deref(&self) -> &Self::Target { |
| 357 | &(into_iter as fn() -> Iter<T>) |
| 358 | } |
| 359 | } |
| 360 | |
| 361 | pub struct Iter<T: 'static> { |
| 362 | node: Option<&'static Node>, |
| 363 | marker: PhantomData<T>, |
| 364 | } |
| 365 | |
| 366 | impl<T: 'static> Iterator for Iter<T> { |
| 367 | type Item = &'static T; |
| 368 | |
| 369 | fn next(&mut self) -> Option<Self::Item> { |
| 370 | let node = self.node?; |
| 371 | unsafe { |
| 372 | let value_ptr = (node.value as *const dyn ErasedNode).cast::<T>(); |
| 373 | self.node = *node.next.get(); |
| 374 | Some(&*value_ptr) |
| 375 | } |
| 376 | } |
| 377 | } |
| 378 | |
| 379 | impl<T> Clone for Iter<T> { |
| 380 | fn clone(&self) -> Self { |
| 381 | Self { |
| 382 | node: self.node, |
| 383 | marker: PhantomData, |
| 384 | } |
| 385 | } |
| 386 | } |
| 387 | }; |
| 388 | |
| 389 | /// Associate a plugin registry with the specified type. |
| 390 | /// |
| 391 | /// This call must be in the same crate that defines the plugin type. This macro |
| 392 | /// does not "run" anything so place it outside of any function body. |
| 393 | /// |
| 394 | /// # Examples |
| 395 | /// |
| 396 | /// Suppose we are writing a command line flags library and want to allow any |
| 397 | /// source file in the application to register command line flags that are |
| 398 | /// relevant to it. |
| 399 | /// |
| 400 | /// This is the flag registration style used by [gflags] and is better suited |
| 401 | /// for large scale development than maintaining a single central list of flags, |
| 402 | /// as the central list would become an endless source of merge conflicts. |
| 403 | /// |
| 404 | /// [gflags]: https://gflags.github.io/gflags/ |
| 405 | /// |
| 406 | /// ``` |
| 407 | /// pub struct Flag { |
| 408 | /// short: char, |
| 409 | /// name: &'static str, |
| 410 | /// /* ... */ |
| 411 | /// } |
| 412 | /// |
| 413 | /// inventory::collect!(Flag); |
| 414 | /// ``` |
| 415 | /// |
| 416 | /// Refer to the [crate level documentation](index.html) for a complete example |
| 417 | /// of submitting plugins and iterating a plugin registry. |
| 418 | #[macro_export ] |
| 419 | macro_rules! collect { |
| 420 | ($ty:ty) => { |
| 421 | impl $crate::Collect for $ty { |
| 422 | #[inline] |
| 423 | fn registry() -> &'static $crate::Registry { |
| 424 | static REGISTRY: $crate::Registry = $crate::Registry::new(); |
| 425 | ®ISTRY |
| 426 | } |
| 427 | } |
| 428 | }; |
| 429 | } |
| 430 | |
| 431 | /// Enter an element into the plugin registry corresponding to its type. |
| 432 | /// |
| 433 | /// This call may be in the same crate that defines the type, or downstream in |
| 434 | /// any crate that depends on that crate. |
| 435 | /// |
| 436 | /// This macro does not "run" anything so place it outside of any function body. |
| 437 | /// In particular, note that all `submit!` invocations across all source files |
| 438 | /// linked into your application all take effect simultaneously. A `submit!` |
| 439 | /// invocation is not a statement that needs to be called from `main` in order |
| 440 | /// to execute. |
| 441 | /// |
| 442 | /// # Examples |
| 443 | /// |
| 444 | /// Put `submit!` invocations outside of any function body. |
| 445 | /// |
| 446 | /// ``` |
| 447 | /// # struct Flag; |
| 448 | /// # |
| 449 | /// # impl Flag { |
| 450 | /// # const fn new(short: char, name: &'static str) -> Self { |
| 451 | /// # Flag |
| 452 | /// # } |
| 453 | /// # } |
| 454 | /// # |
| 455 | /// # inventory::collect!(Flag); |
| 456 | /// # |
| 457 | /// inventory::submit! { |
| 458 | /// Flag::new('v' , "verbose" ) |
| 459 | /// } |
| 460 | /// # |
| 461 | /// # fn main() {} |
| 462 | /// ``` |
| 463 | /// |
| 464 | /// Do not try to invoke `submit!` from inside of a function body as it does not |
| 465 | /// do what you want. |
| 466 | /// |
| 467 | /// ```compile_fail |
| 468 | /// // Do not do this. |
| 469 | /// fn submit_flags(has_verbose_flag: bool) { |
| 470 | /// if has_verbose_flag { |
| 471 | /// inventory::submit! { |
| 472 | /// Flag::new('v' , "verbose" ) |
| 473 | /// } |
| 474 | /// } |
| 475 | /// } |
| 476 | /// ``` |
| 477 | /// |
| 478 | /// Refer to the [crate level documentation](index.html) for a complete example |
| 479 | /// of instantiating and iterating a plugin registry. |
| 480 | #[macro_export ] |
| 481 | macro_rules! submit { |
| 482 | ($($value:tt)*) => { |
| 483 | $crate::__do_submit! { |
| 484 | { $($value)* } |
| 485 | { $($value)* } |
| 486 | } |
| 487 | }; |
| 488 | } |
| 489 | |
| 490 | // Not public API. |
| 491 | #[cfg (target_family = "wasm" )] |
| 492 | #[doc (hidden)] |
| 493 | pub mod __private { |
| 494 | #[doc (hidden)] |
| 495 | pub use rustversion::attr; |
| 496 | } |
| 497 | |
| 498 | // Not public API. |
| 499 | #[doc (hidden)] |
| 500 | #[macro_export ] |
| 501 | macro_rules! __do_submit { |
| 502 | (used={ $($used:tt)+ } $($value:tt)*) => { |
| 503 | #[allow(non_upper_case_globals)] |
| 504 | const _: () = { |
| 505 | static __INVENTORY: $crate::Node = $crate::Node { |
| 506 | value: &{ $($value)* }, |
| 507 | next: $crate::core::cell::UnsafeCell::new($crate::core::option::Option::None), |
| 508 | #[cfg(target_family = "wasm" )] |
| 509 | initialized: $crate::core::sync::atomic::AtomicBool::new(false), |
| 510 | }; |
| 511 | |
| 512 | #[cfg_attr(any(target_os = "linux" , target_os = "android" ), link_section = ".text.startup" )] |
| 513 | unsafe extern "C" fn __ctor() { |
| 514 | unsafe { $crate::ErasedNode::submit(__INVENTORY.value, &__INVENTORY) } |
| 515 | } |
| 516 | |
| 517 | // Linux/ELF: https://www.exploit-db.com/papers/13234 |
| 518 | // |
| 519 | // macOS: https://blog.timac.org/2016/0716-constructor-and-destructor-attributes/ |
| 520 | // |
| 521 | // Why .CRT$XCU on Windows? https://www.cnblogs.com/sunkang/archive/2011/05/24/2055635.html |
| 522 | // 'I'=C init, 'C'=C++ init, 'P'=Pre-terminators and 'T'=Terminators |
| 523 | $($used)+ |
| 524 | #[cfg_attr( |
| 525 | all( |
| 526 | not(target_family = "wasm" ), |
| 527 | any( |
| 528 | target_os = "linux" , |
| 529 | target_os = "android" , |
| 530 | target_os = "dragonfly" , |
| 531 | target_os = "freebsd" , |
| 532 | target_os = "haiku" , |
| 533 | target_os = "illumos" , |
| 534 | target_os = "netbsd" , |
| 535 | target_os = "openbsd" , |
| 536 | target_os = "none" , |
| 537 | ) |
| 538 | ), |
| 539 | link_section = ".init_array" , |
| 540 | )] |
| 541 | #[cfg_attr( |
| 542 | target_family = "wasm" , |
| 543 | $crate::__private::attr( |
| 544 | any(all(stable, since(1.85)), since(2024-12-18)), |
| 545 | link_section = ".init_array" , |
| 546 | ), |
| 547 | )] |
| 548 | #[cfg_attr( |
| 549 | any(target_os = "macos" , target_os = "ios" ), |
| 550 | link_section = "__DATA,__mod_init_func" , |
| 551 | )] |
| 552 | #[cfg_attr(windows, link_section = ".CRT$XCU" )] |
| 553 | static __CTOR: unsafe extern "C" fn() = __ctor; |
| 554 | }; |
| 555 | }; |
| 556 | |
| 557 | ({ #![used($($used:tt)+)] $($value:tt)* } { $pound:tt $bang:tt $brackets:tt $($dup:tt)* }) => { |
| 558 | $crate::__do_submit! { |
| 559 | used={ $pound $brackets } |
| 560 | $($value)* |
| 561 | } |
| 562 | }; |
| 563 | |
| 564 | ({ $($value:tt)* } { $($dup:tt)* }) => { |
| 565 | $crate::__do_submit! { |
| 566 | used={ #[used] } |
| 567 | $($value)* |
| 568 | } |
| 569 | }; |
| 570 | } |
| 571 | |