memflow_derive/lib.rs
1mod batcher;
2
3use darling::{ast::NestedMeta, FromMeta};
4use proc_macro::TokenStream;
5use proc_macro_crate::*;
6use quote::{format_ident, quote};
7use syn::{parse_macro_input, Data, DeriveInput, Fields, ItemFn};
8
9/// Auto implement a function to batch read fields from a struct.
10///
11/// # Example
12///
13/// ```rust,ignore
14/// # use ::memflow::prelude::v1::*;
15///
16/// #[derive(Batcher)]
17/// struct FooBar {
18/// #[batch(offset = 0x20)]
19/// foo: u32,
20/// #[batch(offset = 0x1f0)]
21/// bar: u32,
22/// }
23/// ```
24#[proc_macro_derive(Batcher, attributes(batch))]
25pub fn batcher_derive(input: TokenStream) -> TokenStream {
26 batcher::batcher_derive(input)
27}
28
29#[derive(Debug, FromMeta)]
30struct ConnectorFactoryArgs {
31 name: String,
32 #[darling(default)]
33 version: Option<String>,
34 #[darling(default)]
35 description: Option<String>,
36 #[darling(default)]
37 help_fn: Option<String>,
38 #[darling(default)]
39 target_list_fn: Option<String>,
40 #[darling(default)]
41 accept_input: bool,
42 #[darling(default)]
43 return_wrapped: bool,
44 #[darling(default)]
45 no_default_cache: bool,
46}
47
48#[derive(Debug, FromMeta)]
49struct OsFactoryArgs {
50 name: String,
51 #[darling(default)]
52 version: Option<String>,
53 #[darling(default)]
54 description: Option<String>,
55 #[darling(default)]
56 help_fn: Option<String>,
57 #[darling(default)]
58 accept_input: bool,
59 #[darling(default)]
60 return_wrapped: bool,
61}
62
63fn validate_plugin_name(name: &str) {
64 if !name
65 .chars()
66 .all(|c| char::is_alphanumeric(c) || c == '-' || c == '_')
67 {
68 panic!("plugin name must only contain alphanumeric characters");
69 }
70}
71
72/// Creates a memflow connector plugin.
73/// This function takes care of supplying all necessary underlying structures
74/// for exposing a memflow connector plugin in the form of a dylib.
75///
76/// Remarks:
77///
78/// We should add conditional compilation for the crate-type here
79/// so our rust libraries who use a connector wont export those functions
80/// again by themselves (e.g. the ffi).
81///
82/// This would also lead to possible duplicated symbols if
83/// multiple connectors are imported.
84///
85/// See <https://github.com/rust-lang/rust/issues/20267> for the tracking issue.
86///
87/// #[cfg(crate_type = "cdylib")]
88///
89/// Macro Parameters:
90///
91/// `name` - The name of the plugin
92/// `version` - The version of the plugin
93/// `description` - Short description of the plugin
94/// `help_fn` - Name of the function that provides a help text to the user
95/// `target_list_fn` - Name of the function that provides a list of all targets to the user
96/// `accept_input` - Wether or not this Connector is able to accept an Os-Plugin as an input
97/// `return_wrapped` - Wether or not the return value is an already wrapped cglue object or if the macro needs to construct it
98/// `no_default_cache` - Disables the default caching behavior if no cache configuration is supplied by the user.
99///
100/// Caching:
101///
102/// By default the proc macro will call `memflow::plugins::connector::create_instance` internally which will handle the caching functionality.
103/// Either the user did not specify any caching, which results in the default caching configuration being used, or the user
104/// did choose a custom caching configuration which will override the default caching configuration.
105///
106/// In case `no_default_cache` is used the default behavior will be to use no caching. If the user supplies a cache configuration even
107/// if `no_default_cache` is set the `memflow::plugins::connector::create_instance` function will still instantiate the requested configuration.
108///
109/// In case `return_wrapped` is set to true the caching behavior has to be handled by the end user simply by
110/// calling `memflow::plugins::connector::create_instance` with the appropiate arguments.
111///
112/// Examples:
113///
114/// Simple usage:
115/// ```rust,ignore
116/// # use ::memflow::prelude::v1::*;
117/// # use ::memflow::dummy::*;
118/// #[connector(name = "dummy_conn", version = "1.0.0", description = "Dummy Plugin for Testing purposes")]
119/// pub fn create_connector(_args: &ConnectorArgs) -> Result<DummyMemory> {
120/// Ok(DummyMemory::new(size::mb(16)))
121/// }
122/// ```
123///
124/// Disable default caching:
125/// ```rust,ignore
126/// # use ::memflow::prelude::v1::*;
127/// # use ::memflow::dummy::*;
128/// #[connector(name = "dummy_conn", no_default_cache = true)]
129/// pub fn create_connector(_args: &ConnectorArgs) -> Result<DummyMemory> {
130/// Ok(DummyMemory::new(size::mb(16)))
131/// }
132/// ```
133///
134/// Custom help function:
135/// ```rust,ignore
136/// # use ::memflow::prelude::v1::*;
137/// # use ::memflow::dummy::*;
138/// #[connector(name = "dummy_conn", help_fn = "help")]
139/// pub fn create_connector(_args: &ConnectorArgs) -> Result<DummyMemory> {
140/// Ok(DummyMemory::new(size::mb(16)))
141/// }
142///
143/// pub fn help() -> String {
144/// "Dummy Plugin for Testing purposes".to_string()
145/// }
146/// ```
147///
148/// Custom target list function:
149/// ```rust,ignore
150/// # use ::memflow::prelude::v1::*;
151/// # use ::memflow::dummy::*;
152/// # use std::vec::Vec;
153/// #[connector(name = "dummy_conn", target_list_fn = "target_list")]
154/// pub fn create_connector(_args: &ConnectorArgs) -> Result<DummyMemory> {
155/// Ok(DummyMemory::new(size::mb(16)))
156/// }
157///
158/// pub fn target_list() -> Result<Vec<TargetInfo>> {
159/// Ok(Vec::new())
160/// }
161/// ```
162///
163/// Wrapped return with manually created connector instance:
164/// ```rust,ignore
165/// # use ::memflow::prelude::v1::*;
166/// # use ::memflow::dummy::*;
167/// #[connector(name = "dummy_conn", return_wrapped = true)]
168/// pub fn create_connector(
169/// args: &ConnectorArgs,
170/// lib: LibArc,
171/// ) -> Result<ConnectorInstanceArcBox<'static>> {
172/// let connector = DummyMemory::new(size::mb(16));
173/// Ok(memflow::plugins::connector::create_instance(connector, lib, args, false))
174/// }
175/// ```
176///
177/// Connector with input parameter:
178/// ```rust,ignore
179/// # use ::memflow::prelude::v1::*;
180/// # use ::memflow::dummy::*;
181/// #[connector(name = "dummy_conn", accept_input = true)]
182/// pub fn create_connector(
183/// _args: &ConnectorArgs,
184/// _os: Option<OsInstanceArcBox<'static>>,
185/// ) -> Result<DummyMemory> {
186/// Ok(DummyMemory::new(size::mb(16)))
187/// }
188/// ```
189///
190/// Connector with input parameter and manually created connector instance:
191/// ```rust,ignore
192/// # use ::memflow::prelude::v1::*;
193/// # use ::memflow::dummy::*;
194/// #[connector(name = "dummy_conn", accept_input = true, return_wrapped = true)]
195/// pub fn create_connector<'a>(
196/// args: &ConnectorArgs,
197/// _os: Option<OsInstanceArcBox<'static>>,
198/// lib: LibArc,
199/// ) -> Result<ConnectorInstanceArcBox<'static>> {
200/// let connector = DummyMemory::new(size::mb(16));
201/// Ok(memflow::plugins::connector::create_instance(connector, lib, args, false))
202/// }
203/// ```
204#[proc_macro_attribute]
205pub fn connector(args: TokenStream, input: TokenStream) -> TokenStream {
206 let crate_path = crate_path();
207
208 let attr_args = match NestedMeta::parse_meta_list(args.into()) {
209 Ok(v) => v,
210 Err(e) => return TokenStream::from(darling::Error::from(e).write_errors()),
211 };
212 let args = match ConnectorFactoryArgs::from_list(&attr_args) {
213 Ok(v) => v,
214 Err(e) => return TokenStream::from(e.write_errors()),
215 };
216
217 let connector_name = args.name;
218 validate_plugin_name(&connector_name);
219
220 let version_gen = args
221 .version
222 .map_or_else(|| quote! { env!("CARGO_PKG_VERSION") }, |v| quote! { #v });
223
224 let description_gen = args.description.map_or_else(
225 || quote! { env!("CARGO_PKG_DESCRIPTION") },
226 |d| quote! { #d },
227 );
228
229 let help_gen = if args.help_fn.is_some() {
230 quote! { Some(mf_help_callback) }
231 } else {
232 quote! { None }
233 };
234
235 let target_list_gen = if args.target_list_fn.is_some() {
236 quote! { Some(mf_target_list_callback) }
237 } else {
238 quote! { None }
239 };
240
241 let connector_descriptor: proc_macro2::TokenStream =
242 ["MEMFLOW_CONNECTOR_", &connector_name.to_uppercase()]
243 .concat()
244 .parse()
245 .unwrap();
246
247 let func = parse_macro_input!(input as ItemFn);
248 let func_name = &func.sig.ident;
249
250 let func_accept_input = args.accept_input;
251 let func_return_wrapped = args.return_wrapped;
252
253 let no_default_cache = args.no_default_cache;
254
255 // create wrapping function according to input/output configuration
256 #[allow(clippy::collapsible_else_if)]
257 let create_fn_gen_inner = if func_accept_input {
258 if !func_return_wrapped {
259 // args + os
260 quote! {
261 #crate_path::plugins::wrap_with_input(args, os.into(), lib, logger, out, |a, os, lib| {
262 Ok(#crate_path::plugins::connector::create_instance(#func_name(a, os)?, lib, a, #no_default_cache))
263 })
264 }
265 } else {
266 // args + os + lib
267 quote! {
268 #crate_path::plugins::wrap_with_input(args, os.into(), lib, logger, out, #func_name)
269 }
270 }
271 } else {
272 if !func_return_wrapped {
273 // args
274 quote! {
275 #crate_path::plugins::wrap(args, lib, logger, out, |a, lib| {
276 Ok(#crate_path::plugins::connector::create_instance(#func_name(a)?, lib, a, #no_default_cache))
277 })
278 }
279 } else {
280 // args + lib
281 quote! {
282 #crate_path::plugins::wrap(args, lib, logger, out, #func_name)
283 }
284 }
285 };
286
287 let create_fn_gen = quote! {
288 #[doc(hidden)]
289 extern "C" fn mf_create(
290 args: Option<&#crate_path::plugins::connector::ConnectorArgs>,
291 os: #crate_path::cglue::option::COption<#crate_path::plugins::os::OsInstanceArcBox<'static>>,
292 lib: #crate_path::plugins::LibArc,
293 logger: Option<&'static #crate_path::plugins::PluginLogger>,
294 out: &mut #crate_path::plugins::connector::MuConnectorInstanceArcBox<'static>
295 ) -> i32 {
296 #create_fn_gen_inner
297 }
298 };
299
300 let help_fn_gen = args.help_fn.map(|v| v.parse().unwrap()).map_or_else(
301 proc_macro2::TokenStream::new,
302 |func_name: proc_macro2::TokenStream| {
303 quote! {
304 #[doc(hidden)]
305 extern "C" fn mf_help_callback(
306 mut callback: #crate_path::plugins::HelpCallback,
307 ) {
308 let helpstr = #func_name();
309 let _ = callback.call(helpstr.into());
310 }
311 }
312 },
313 );
314
315 let target_list_fn_gen = args.target_list_fn.map(|v| v.parse().unwrap()).map_or_else(
316 proc_macro2::TokenStream::new,
317 |func_name: proc_macro2::TokenStream| {
318 quote! {
319 #[doc(hidden)]
320 extern "C" fn mf_target_list_callback(
321 mut callback: #crate_path::plugins::TargetCallback,
322 ) -> i32 {
323 #func_name()
324 .map(|mut targets| {
325 targets
326 .into_iter()
327 .take_while(|t| callback.call(t.clone()))
328 .for_each(|_| ());
329 })
330 .into_int_result()
331 }
332 }
333 },
334 );
335
336 let gen = quote! {
337 #[doc(hidden)]
338 #[no_mangle]
339 pub static #connector_descriptor: #crate_path::plugins::ConnectorDescriptor = #crate_path::plugins::ConnectorDescriptor {
340 plugin_version: #crate_path::plugins::MEMFLOW_PLUGIN_VERSION,
341 accept_input: #func_accept_input,
342 input_layout: <<#crate_path::plugins::LoadableConnector as #crate_path::plugins::Loadable>::CInputArg as #crate_path::abi_stable::StableAbi>::LAYOUT,
343 output_layout: <<#crate_path::plugins::LoadableConnector as #crate_path::plugins::Loadable>::Instance as #crate_path::abi_stable::StableAbi>::LAYOUT,
344 name: #crate_path::cglue::CSliceRef::from_str(#connector_name),
345 version: #crate_path::cglue::CSliceRef::from_str(#version_gen),
346 description: #crate_path::cglue::CSliceRef::from_str(#description_gen),
347 help_callback: #help_gen,
348 target_list_callback: #target_list_gen,
349 create: mf_create,
350 };
351
352 #create_fn_gen
353
354 #help_fn_gen
355
356 #target_list_fn_gen
357
358 #func
359 };
360
361 gen.into()
362}
363
364/// Creates a memflow os plugin.
365/// This function takes care of supplying all necessary underlying structures
366/// for exposing a memflow os plugin in the form of a dylib.
367///
368/// Macro Parameters:
369///
370/// `name` - The name of the plugin
371/// `version` - The version of the plugin
372/// `description` - Short description of the plugin
373/// `help_fn` - Name of the function that provides a help text to the user
374/// `accept_input` - Wether or not this Os-Plugin is able to accept a connector as an input
375/// `return_wrapped` - Wether or not the return value is an already wrapped cglue object or if the macro needs to construct it
376///
377/// Examples:
378///
379/// Simple usage:
380/// ```rust,ignore
381/// # use ::memflow::prelude::v1::*;
382/// # use ::memflow::dummy::*;
383/// #[os(name = "dummy_os", version = "1.0.0", description = "Dummy Plugin for Testing purposes")]
384/// pub fn create_os(
385/// _args: &OsArgs,
386/// ) -> Result<DummyOs> {
387/// let phys_mem = DummyMemory::new(size::mb(16));
388/// Ok(DummyOs::new(phys_mem))
389/// }
390///
391/// ```
392/// Custom help function:
393/// ```rust,ignore
394/// # use ::memflow::prelude::v1::*;
395/// # use ::memflow::dummy::*;
396/// #[os(name = "dummy_os", help_fn = "help")]
397/// pub fn create_os(
398/// _args: &OsArgs,
399/// ) -> Result<DummyOs> {
400/// let phys_mem = DummyMemory::new(size::mb(16));
401/// Ok(DummyOs::new(phys_mem))
402/// }
403///
404/// pub fn help() -> String {
405/// "Dummy Plugin for Testing purposes".to_string()
406/// }
407/// ```
408///
409/// Wrapped return with manually created os instance:
410/// ```rust,ignore
411/// # use ::memflow::prelude::v1::*;
412/// # use ::memflow::dummy::*;
413/// #[os(name = "dummy_os", return_wrapped = true)]
414/// pub fn create_os(
415/// args: &OsArgs,
416/// lib: LibArc,
417/// ) -> Result<OsInstanceArcBox<'static>> {
418/// let phys_mem = DummyMemory::new(size::mb(16));
419/// let os = DummyOs::new(phys_mem);
420/// Ok(memflow::plugins::os::create_instance(os, lib, args))
421/// }
422/// ```
423///
424/// Os with input parameter:
425/// ```rust,ignore
426/// # use ::memflow::prelude::v1::*;
427/// # use ::memflow::dummy::*;
428/// #[os(name = "dummy_os", accept_input = true)]
429/// pub fn create_os(
430/// args: &OsArgs,
431/// _connector: Option<ConnectorInstanceArcBox<'static>>,
432/// ) -> Result<DummyOs> {
433/// let phys_mem = DummyMemory::new(size::mb(16));
434/// Ok(DummyOs::new(phys_mem))
435/// }
436/// ```
437///
438/// Os with input parameter and manually created os instance:
439/// ```rust,ignore
440/// # use ::memflow::prelude::v1::*;
441/// # use ::memflow::dummy::*;
442/// #[os(name = "dummy_os", accept_input = true, return_wrapped = true)]
443/// pub fn create_os(
444/// args: &OsArgs,
445/// _connector: Option<ConnectorInstanceArcBox<'static>>,
446/// lib: LibArc,
447/// ) -> Result<OsInstanceArcBox<'static>> {
448/// let phys_mem = DummyMemory::new(size::mb(16));
449/// let os = DummyOs::new(phys_mem);
450/// Ok(memflow::plugins::os::create_instance(os, lib, args))
451/// }
452/// ```
453#[proc_macro_attribute]
454pub fn os(args: TokenStream, input: TokenStream) -> TokenStream {
455 let crate_path = crate_path();
456
457 let attr_args = match NestedMeta::parse_meta_list(args.into()) {
458 Ok(v) => v,
459 Err(e) => return TokenStream::from(darling::Error::from(e).write_errors()),
460 };
461 let args = match OsFactoryArgs::from_list(&attr_args) {
462 Ok(v) => v,
463 Err(e) => return TokenStream::from(e.write_errors()),
464 };
465
466 let os_name = args.name;
467 validate_plugin_name(&os_name);
468
469 let version_gen = args
470 .version
471 .map_or_else(|| quote! { env!("CARGO_PKG_VERSION") }, |v| quote! { #v });
472
473 let description_gen = args.description.map_or_else(
474 || quote! { env!("CARGO_PKG_DESCRIPTION") },
475 |d| quote! { #d },
476 );
477
478 let help_gen = if args.help_fn.is_some() {
479 quote! { Some(mf_help_callback) }
480 } else {
481 quote! { None }
482 };
483
484 let os_descriptor: proc_macro2::TokenStream = ["MEMFLOW_OS_", &os_name.to_uppercase()]
485 .concat()
486 .parse()
487 .unwrap();
488
489 let func = parse_macro_input!(input as ItemFn);
490 let func_name = &func.sig.ident;
491
492 let func_accept_input = args.accept_input;
493 let func_return_wrapped = args.return_wrapped;
494
495 // create wrapping function according to input/output configuration
496 #[allow(clippy::collapsible_else_if)]
497 let create_fn_gen_inner = if func_accept_input {
498 if !func_return_wrapped {
499 // inputs: args + connector
500 quote! {
501 #crate_path::plugins::wrap_with_input(args, connector.into(), lib, logger, out, |a, os, lib| {
502 Ok(#crate_path::plugins::os::create_instance(#func_name(a, os)?, lib, a))
503 })
504 }
505 } else {
506 // inputs: args + connector + lib
507 quote! {
508 #crate_path::plugins::wrap_with_input(args, connector.into(), lib, logger, out, #func_name)
509 }
510 }
511 } else {
512 if !func_return_wrapped {
513 // inputs: args
514 quote! {
515 #crate_path::plugins::wrap(args, lib, logger, out, |a, lib| {
516 Ok(#crate_path::plugins::os::create_instance(#func_name(a)?, lib, a))
517 })
518 }
519 } else {
520 // inputs: args + lib
521 quote! {
522 #crate_path::plugins::wrap(args, lib, logger, out, #func_name)
523 }
524 }
525 };
526
527 let create_fn_gen = quote! {
528 #[doc(hidden)]
529 extern "C" fn mf_create(
530 args: Option<&#crate_path::plugins::os::OsArgs>,
531 connector: #crate_path::cglue::COption<#crate_path::plugins::connector::ConnectorInstanceArcBox<'static>>,
532 lib: #crate_path::plugins::LibArc,
533 logger: Option<&'static #crate_path::plugins::PluginLogger>,
534 out: &mut #crate_path::plugins::os::MuOsInstanceArcBox<'static>
535 ) -> i32 {
536 #create_fn_gen_inner
537 }
538 };
539
540 let help_fn_gen = args.help_fn.map(|v| v.parse().unwrap()).map_or_else(
541 proc_macro2::TokenStream::new,
542 |func_name: proc_macro2::TokenStream| {
543 quote! {
544 #[doc(hidden)]
545 extern "C" fn mf_help_callback(
546 mut callback: #crate_path::plugins::HelpCallback,
547 ) {
548 let helpstr = #func_name();
549 let _ = callback.call(helpstr.into());
550 }
551 }
552 },
553 );
554
555 let gen = quote! {
556 #[doc(hidden)]
557 #[no_mangle]
558 pub static #os_descriptor: #crate_path::plugins::os::OsDescriptor = #crate_path::plugins::os::OsDescriptor {
559 plugin_version: #crate_path::plugins::MEMFLOW_PLUGIN_VERSION,
560 accept_input: #func_accept_input,
561 input_layout: <<#crate_path::plugins::os::LoadableOs as #crate_path::plugins::Loadable>::CInputArg as #crate_path::abi_stable::StableAbi>::LAYOUT,
562 output_layout: <<#crate_path::plugins::os::LoadableOs as #crate_path::plugins::Loadable>::Instance as #crate_path::abi_stable::StableAbi>::LAYOUT,
563 name: #crate_path::cglue::CSliceRef::from_str(#os_name),
564 version: #crate_path::cglue::CSliceRef::from_str(#version_gen),
565 description: #crate_path::cglue::CSliceRef::from_str(#description_gen),
566 help_callback: #help_gen,
567 target_list_callback: None, // non existent on Os Plugins
568 create: mf_create,
569 };
570
571 #create_fn_gen
572
573 #help_fn_gen
574
575 #func
576 };
577
578 gen.into()
579}
580
581/// Auto derive the `Pod` trait for structs.
582///
583/// The type is checked for requirements of the `Pod` trait:
584///
585/// * Be annotated with `repr(C)` or `repr(transparent)`.
586///
587/// * Have every field's type implement `Pod` itself.
588///
589/// * Not have any padding between its fields.
590///
591/// # Compile errors
592///
593/// Error reporting is not very ergonomic due to how errors are detected:
594///
595/// * `error[E0277]: the trait bound $TYPE: Pod is not satisfied`
596///
597/// The struct contains a field whose type does not implement `Pod`.
598///
599/// * `error[E0512]: cannot transmute between types of different sizes, or dependently-sized types`
600///
601/// This error means your struct has padding as its size is not equal to a byte array of length equal to the sum of the size of its fields.
602///
603/// * `error: no rules expected the token <`
604///
605/// The struct contains generic parameters which are not supported. It may still be possible to manually implement `Pod` but extra care should be taken to ensure its invariants are upheld.
606///
607/// # Remarks:
608/// This custom derive macro is required because the dataview proc macro searches for ::dataview::derive_pod!().
609/// See <https://github.com/CasualX/dataview/blob/master/derive_pod/lib.rs> for the original implementation.
610#[proc_macro_derive(Pod)]
611pub fn pod_derive(input: TokenStream) -> TokenStream {
612 let crate_path = crate_path();
613
614 format!("{crate_path}::dataview::derive_pod!{{ {input} }}")
615 .parse()
616 .unwrap()
617}
618
619#[proc_macro_derive(ByteSwap)]
620pub fn byteswap_derive(input: TokenStream) -> TokenStream {
621 let crate_path = crate_path();
622
623 let input = parse_macro_input!(input as DeriveInput);
624 let name = &input.ident;
625 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
626
627 let mut gen_inner = quote!();
628 match input.data {
629 Data::Struct(data) => match data.fields {
630 Fields::Named(named) => {
631 for field in named.named.iter() {
632 let name = field.ident.as_ref().unwrap();
633 gen_inner.extend(quote!(
634 self.#name.byte_swap();
635 ));
636 }
637 }
638 _ => unimplemented!(),
639 },
640 _ => unimplemented!(),
641 };
642
643 let gen = quote!(
644 impl #impl_generics #crate_path::types::byte_swap::ByteSwap for #name #ty_generics #where_clause {
645 fn byte_swap(&mut self) {
646 #gen_inner
647 }
648 }
649 );
650
651 gen.into()
652}
653
654fn crate_path() -> proc_macro2::TokenStream {
655 let (col, ident) = crate_path_ident();
656 quote!(#col #ident)
657}
658
659fn crate_path_ident() -> (Option<syn::token::PathSep>, proc_macro2::Ident) {
660 let found_crate = crate_name("memflow").expect("memflow found in `Cargo.toml`");
661 match found_crate {
662 FoundCrate::Itself => (None, format_ident!("memflow")), // we can use memflow instead of crate due to the alias in lib.rs
663 FoundCrate::Name(name) => (Some(Default::default()), format_ident!("{}", name)),
664 }
665}