Skip to main content

wasmtime_bench_api/
lib.rs

1//! A C API for benchmarking Wasmtime's WebAssembly compilation, instantiation,
2//! and execution.
3//!
4//! The API expects calls that match the following state machine:
5//!
6//! ```text
7//!               |
8//!               |
9//!               V
10//! .---> wasm_bench_create
11//! |        |        |
12//! |        |        |
13//! |        |        V
14//! |        |   wasm_bench_compile
15//! |        |     |            |
16//! |        |     |            |     .----.
17//! |        |     |            |     |    |
18//! |        |     |            V     V    |
19//! |        |     |     wasm_bench_instantiate <------.
20//! |        |     |            |        |             |
21//! |        |     |            |        |             |
22//! |        |     |            |        |             |
23//! |        |     |     .------'        '-----> wasm_bench_execute
24//! |        |     |     |                             |
25//! |        |     |     |                             |
26//! |        V     V     V                             |
27//! '------ wasm_bench_free <--------------------------'
28//!               |
29//!               |
30//!               V
31//! ```
32//!
33//! All API calls must happen on the same thread.
34//!
35//! Functions which return pointers use null as an error value. Function which
36//! return `int` use `0` as OK and non-zero as an error value.
37//!
38//! # Example
39//!
40//! ```
41//! use std::ptr;
42//! use wasmtime_bench_api::*;
43//!
44//! let working_dir = std::env::current_dir().unwrap().display().to_string();
45//! let stdout_path = "./stdout.log";
46//! let stderr_path = "./stderr.log";
47//!
48//! // Functions to start/end timers for compilation.
49//! //
50//! // The `compilation_timer` pointer configured in the `WasmBenchConfig` is
51//! // passed through.
52//! extern "C" fn compilation_start(timer: *mut u8) {
53//!     // Start your compilation timer here.
54//! }
55//! extern "C" fn compilation_end(timer: *mut u8) {
56//!     // End your compilation timer here.
57//! }
58//!
59//! // Similar for instantiation.
60//! extern "C" fn instantiation_start(timer: *mut u8) {
61//!     // Start your instantiation timer here.
62//! }
63//! extern "C" fn instantiation_end(timer: *mut u8) {
64//!     // End your instantiation timer here.
65//! }
66//!
67//! // Similar for execution.
68//! extern "C" fn execution_start(timer: *mut u8) {
69//!     // Start your execution timer here.
70//! }
71//! extern "C" fn execution_end(timer: *mut u8) {
72//!     // End your execution timer here.
73//! }
74//!
75//! let config = WasmBenchConfig {
76//!     working_dir_ptr: working_dir.as_ptr(),
77//!     working_dir_len: working_dir.len(),
78//!     stdout_path_ptr: stdout_path.as_ptr(),
79//!     stdout_path_len: stdout_path.len(),
80//!     stderr_path_ptr: stderr_path.as_ptr(),
81//!     stderr_path_len: stderr_path.len(),
82//!     stdin_path_ptr: ptr::null(),
83//!     stdin_path_len: 0,
84//!     compilation_timer: ptr::null_mut(),
85//!     compilation_start,
86//!     compilation_end,
87//!     instantiation_timer: ptr::null_mut(),
88//!     instantiation_start,
89//!     instantiation_end,
90//!     execution_timer: ptr::null_mut(),
91//!     execution_start,
92//!     execution_end,
93//!     execution_flags_ptr: ptr::null(),
94//!     execution_flags_len: 0,
95//! };
96//!
97//! let mut bench_api = ptr::null_mut();
98//! unsafe {
99//!     let code = wasm_bench_create(config, &mut bench_api);
100//!     assert_eq!(code, OK);
101//!     assert!(!bench_api.is_null());
102//! };
103//!
104//! let wasm = wat::parse_bytes(br#"
105//!     (module
106//!         (func $bench_start (import "bench" "start"))
107//!         (func $bench_end (import "bench" "end"))
108//!         (func $start (export "_start")
109//!             call $bench_start
110//!             i32.const 1
111//!             i32.const 2
112//!             i32.add
113//!             drop
114//!             call $bench_end
115//!         )
116//!     )
117//! "#).unwrap();
118//!
119//! // This will call the `compilation_{start,end}` timing functions on success.
120//! let code = unsafe { wasm_bench_compile(bench_api, wasm.as_ptr(), wasm.len()) };
121//! assert_eq!(code, OK);
122//!
123//! // This will call the `instantiation_{start,end}` timing functions on success.
124//! let code = unsafe { wasm_bench_instantiate(bench_api) };
125//! assert_eq!(code, OK);
126//!
127//! // This will call the `execution_{start,end}` timing functions on success.
128//! let code = unsafe { wasm_bench_execute(bench_api) };
129//! assert_eq!(code, OK);
130//!
131//! unsafe {
132//!     wasm_bench_free(bench_api);
133//! }
134//! ```
135
136mod unsafe_send_sync;
137
138use crate::unsafe_send_sync::UnsafeSendSync;
139use clap::Parser;
140use std::os::raw::{c_int, c_void};
141use std::slice;
142use std::{env, path::PathBuf};
143use wasmtime::component::ResourceTable;
144use wasmtime::{
145    CodeBuilder, CodeHint, Engine, Instance, Linker, Module, Result, Store, error::Context as _,
146    format_err,
147};
148use wasmtime_cli_flags::CommonOptions;
149use wasmtime_wasi::cli::{InputFile, OutputFile};
150use wasmtime_wasi::{DirPerms, FilePerms, I32Exit, WasiCtx, WasiCtxView, WasiView, p1::WasiP1Ctx};
151
152pub type ExitCode = c_int;
153pub const OK: ExitCode = 0;
154pub const ERR: ExitCode = -1;
155
156// Randomize the location of heap objects to avoid accidental locality being an
157// uncontrolled variable that obscures performance evaluation in our
158// experiments.
159#[cfg(feature = "shuffling-allocator")]
160#[global_allocator]
161static ALLOC: shuffling_allocator::ShufflingAllocator<std::alloc::System> =
162    shuffling_allocator::wrap!(&std::alloc::System);
163
164/// Configuration options for the benchmark.
165#[repr(C)]
166pub struct WasmBenchConfig {
167    /// The working directory where benchmarks should be executed.
168    pub working_dir_ptr: *const u8,
169    pub working_dir_len: usize,
170
171    /// The file path that should be created and used as `stdout`.
172    pub stdout_path_ptr: *const u8,
173    pub stdout_path_len: usize,
174
175    /// The file path that should be created and used as `stderr`.
176    pub stderr_path_ptr: *const u8,
177    pub stderr_path_len: usize,
178
179    /// The (optional) file path that should be opened and used as `stdin`. If
180    /// not provided, then the WASI context will not have a `stdin` initialized.
181    pub stdin_path_ptr: *const u8,
182    pub stdin_path_len: usize,
183
184    /// The functions to start and stop performance timers/counters during Wasm
185    /// compilation.
186    pub compilation_timer: *mut u8,
187    pub compilation_start: extern "C" fn(*mut u8),
188    pub compilation_end: extern "C" fn(*mut u8),
189
190    /// The functions to start and stop performance timers/counters during Wasm
191    /// instantiation.
192    pub instantiation_timer: *mut u8,
193    pub instantiation_start: extern "C" fn(*mut u8),
194    pub instantiation_end: extern "C" fn(*mut u8),
195
196    /// The functions to start and stop performance timers/counters during Wasm
197    /// execution.
198    pub execution_timer: *mut u8,
199    pub execution_start: extern "C" fn(*mut u8),
200    pub execution_end: extern "C" fn(*mut u8),
201
202    /// The (optional) flags to use when running Wasmtime. These correspond to
203    /// the flags used when running Wasmtime from the command line.
204    pub execution_flags_ptr: *const u8,
205    pub execution_flags_len: usize,
206}
207
208impl WasmBenchConfig {
209    fn working_dir(&self) -> Result<PathBuf> {
210        let working_dir =
211            unsafe { std::slice::from_raw_parts(self.working_dir_ptr, self.working_dir_len) };
212        let working_dir = std::str::from_utf8(working_dir)
213            .context("given working directory is not valid UTF-8")?;
214        Ok(working_dir.into())
215    }
216
217    fn stdout_path(&self) -> Result<PathBuf> {
218        let stdout_path =
219            unsafe { std::slice::from_raw_parts(self.stdout_path_ptr, self.stdout_path_len) };
220        let stdout_path =
221            std::str::from_utf8(stdout_path).context("given stdout path is not valid UTF-8")?;
222        Ok(stdout_path.into())
223    }
224
225    fn stderr_path(&self) -> Result<PathBuf> {
226        let stderr_path =
227            unsafe { std::slice::from_raw_parts(self.stderr_path_ptr, self.stderr_path_len) };
228        let stderr_path =
229            std::str::from_utf8(stderr_path).context("given stderr path is not valid UTF-8")?;
230        Ok(stderr_path.into())
231    }
232
233    fn stdin_path(&self) -> Result<Option<PathBuf>> {
234        if self.stdin_path_ptr.is_null() {
235            return Ok(None);
236        }
237
238        let stdin_path =
239            unsafe { std::slice::from_raw_parts(self.stdin_path_ptr, self.stdin_path_len) };
240        let stdin_path =
241            std::str::from_utf8(stdin_path).context("given stdin path is not valid UTF-8")?;
242        Ok(Some(stdin_path.into()))
243    }
244
245    fn execution_flags(&self) -> Result<CommonOptions> {
246        let flags = if self.execution_flags_ptr.is_null() {
247            ""
248        } else {
249            let execution_flags = unsafe {
250                std::slice::from_raw_parts(self.execution_flags_ptr, self.execution_flags_len)
251            };
252            std::str::from_utf8(execution_flags)
253                .context("given execution flags string is not valid UTF-8")?
254        };
255        let options = CommonOptions::try_parse_from(
256            ["wasmtime"]
257                .into_iter()
258                .chain(flags.split(' ').filter(|s| !s.is_empty())),
259        )
260        .context("failed to parse options")?;
261        Ok(options)
262    }
263}
264
265/// Exposes a C-compatible way of creating the engine from the bytes of a single
266/// Wasm module.
267///
268/// On success, the `out_bench_ptr` is initialized to a pointer to a structure
269/// that contains the engine's initialized state, and `0` is returned. On
270/// failure, a non-zero status code is returned and `out_bench_ptr` is left
271/// untouched.
272#[unsafe(no_mangle)]
273pub extern "C" fn wasm_bench_create(
274    config: WasmBenchConfig,
275    out_bench_ptr: *mut *mut c_void,
276) -> ExitCode {
277    let result = (|| -> Result<_> {
278        // Clone paths for the core Wasm WASI closure.
279        let working_dir = config.working_dir()?;
280        let stdout_path = config.stdout_path()?;
281        let stderr_path = config.stderr_path()?;
282        let stdin_path = config.stdin_path()?;
283        let options = config.execution_flags()?;
284
285        // Clone paths for the component WASI closure.
286        let working_dir2 = working_dir.clone();
287        let stdout_path2 = stdout_path.clone();
288        let stderr_path2 = stderr_path.clone();
289        let stdin_path2 = stdin_path.clone();
290
291        let state = Box::new(BenchState::new(
292            options,
293            config.compilation_timer,
294            config.compilation_start,
295            config.compilation_end,
296            config.instantiation_timer,
297            config.instantiation_start,
298            config.instantiation_end,
299            config.execution_timer,
300            config.execution_start,
301            config.execution_end,
302            move || {
303                let mut cx = WasiCtx::builder();
304
305                let stdout = std::fs::File::create(&stdout_path)
306                    .with_context(|| format!("failed to create {}", stdout_path.display()))?;
307                cx.stdout(OutputFile::new(stdout));
308
309                let stderr = std::fs::File::create(&stderr_path)
310                    .with_context(|| format!("failed to create {}", stderr_path.display()))?;
311                cx.stderr(OutputFile::new(stderr));
312
313                if let Some(stdin_path) = &stdin_path {
314                    let stdin = std::fs::File::open(stdin_path)
315                        .with_context(|| format!("failed to open {}", stdin_path.display()))?;
316                    cx.stdin(InputFile::new(stdin));
317                }
318
319                // Allow access to the working directory so that the benchmark can read
320                // its input workload(s).
321                cx.preopened_dir(working_dir.clone(), ".", DirPerms::READ, FilePerms::READ)?;
322
323                // Pass this env var along so that the benchmark program can use smaller
324                // input workload(s) if it has them and that has been requested.
325                if let Ok(val) = env::var("WASM_BENCH_USE_SMALL_WORKLOAD") {
326                    cx.env("WASM_BENCH_USE_SMALL_WORKLOAD", &val);
327                }
328
329                Ok(cx.build_p1())
330            },
331            move || {
332                let mut cx = WasiCtx::builder();
333
334                let stdout = std::fs::File::create(&stdout_path2)
335                    .with_context(|| format!("failed to create {}", stdout_path2.display()))?;
336                cx.stdout(OutputFile::new(stdout));
337
338                let stderr = std::fs::File::create(&stderr_path2)
339                    .with_context(|| format!("failed to create {}", stderr_path2.display()))?;
340                cx.stderr(OutputFile::new(stderr));
341
342                if let Some(stdin_path) = &stdin_path2 {
343                    let stdin = std::fs::File::open(stdin_path)
344                        .with_context(|| format!("failed to open {}", stdin_path.display()))?;
345                    cx.stdin(InputFile::new(stdin));
346                }
347
348                cx.preopened_dir(working_dir2.clone(), ".", DirPerms::READ, FilePerms::READ)?;
349
350                if let Ok(val) = env::var("WASM_BENCH_USE_SMALL_WORKLOAD") {
351                    cx.env("WASM_BENCH_USE_SMALL_WORKLOAD", &val);
352                }
353
354                Ok(ComponentHostState {
355                    wasi: cx.build(),
356                    table: ResourceTable::new(),
357                })
358            },
359        )?);
360        Ok(Box::into_raw(state) as _)
361    })();
362
363    if let Ok(bench_ptr) = result {
364        unsafe {
365            assert!(!out_bench_ptr.is_null());
366            *out_bench_ptr = bench_ptr;
367        }
368    }
369
370    to_exit_code(result.map(|_| ()))
371}
372
373/// Free the engine state allocated by this library.
374#[unsafe(no_mangle)]
375pub extern "C" fn wasm_bench_free(state: *mut c_void) {
376    assert!(!state.is_null());
377    unsafe {
378        drop(Box::from_raw(state as *mut BenchState));
379    }
380}
381
382/// Compile the Wasm benchmark module.
383#[unsafe(no_mangle)]
384pub extern "C" fn wasm_bench_compile(
385    state: *mut c_void,
386    wasm_bytes: *const u8,
387    wasm_bytes_length: usize,
388) -> ExitCode {
389    let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
390    let wasm_bytes = unsafe { slice::from_raw_parts(wasm_bytes, wasm_bytes_length) };
391    let result = state.compile(wasm_bytes).context("failed to compile");
392    to_exit_code(result)
393}
394
395/// Instantiate the Wasm benchmark module.
396#[unsafe(no_mangle)]
397pub extern "C" fn wasm_bench_instantiate(state: *mut c_void) -> ExitCode {
398    let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
399    let result = state.instantiate().context("failed to instantiate");
400    to_exit_code(result)
401}
402
403/// Execute the Wasm benchmark module.
404#[unsafe(no_mangle)]
405pub extern "C" fn wasm_bench_execute(state: *mut c_void) -> ExitCode {
406    let state = unsafe { (state as *mut BenchState).as_mut().unwrap() };
407    let result = state.execute().context("failed to execute");
408    to_exit_code(result)
409}
410
411/// Helper function for converting a Rust result to a C error code.
412///
413/// This will print an error indicating some information regarding the failure.
414fn to_exit_code<T>(result: impl Into<Result<T>>) -> ExitCode {
415    match result.into() {
416        Ok(_) => OK,
417        Err(error) => {
418            eprintln!("{error:?}");
419            ERR
420        }
421    }
422}
423
424/// This structure contains the actual Rust implementation of the state required
425/// to manage the Wasmtime engine between calls.
426struct BenchState {
427    linker: Linker<HostState>,
428    component_linker: wasmtime::component::Linker<ComponentHostState>,
429    compilation_timer: *mut u8,
430    compilation_start: extern "C" fn(*mut u8),
431    compilation_end: extern "C" fn(*mut u8),
432    instantiation_timer: *mut u8,
433    instantiation_start: extern "C" fn(*mut u8),
434    instantiation_end: extern "C" fn(*mut u8),
435    make_wasi_cx: Box<dyn FnMut() -> Result<WasiP1Ctx>>,
436    make_component_wasi_cx: Box<dyn FnMut() -> Result<ComponentHostState>>,
437    module: Option<Module>,
438    component: Option<wasmtime::component::Component>,
439    store_and_instance: Option<(Store<HostState>, Instance)>,
440    component_store_and_instance:
441        Option<(Store<ComponentHostState>, wasmtime::component::Instance)>,
442    epoch_interruption: bool,
443    fuel: Option<u64>,
444}
445
446struct HostState {
447    wasi: WasiP1Ctx,
448    #[cfg(feature = "wasi-nn")]
449    wasi_nn: wasmtime_wasi_nn::witx::WasiNnCtx,
450}
451
452struct ComponentHostState {
453    wasi: WasiCtx,
454    table: ResourceTable,
455}
456
457impl WasiView for ComponentHostState {
458    fn ctx(&mut self) -> WasiCtxView<'_> {
459        WasiCtxView {
460            ctx: &mut self.wasi,
461            table: &mut self.table,
462        }
463    }
464}
465
466impl BenchState {
467    fn new(
468        mut options: CommonOptions,
469        compilation_timer: *mut u8,
470        compilation_start: extern "C" fn(*mut u8),
471        compilation_end: extern "C" fn(*mut u8),
472        instantiation_timer: *mut u8,
473        instantiation_start: extern "C" fn(*mut u8),
474        instantiation_end: extern "C" fn(*mut u8),
475        execution_timer: *mut u8,
476        execution_start: extern "C" fn(*mut u8),
477        execution_end: extern "C" fn(*mut u8),
478        make_wasi_cx: impl FnMut() -> Result<WasiP1Ctx> + 'static,
479        make_component_wasi_cx: impl FnMut() -> Result<ComponentHostState> + 'static,
480    ) -> Result<Self> {
481        let mut config = options.config(None)?;
482        // NB: always disable the compilation cache.
483        config.cache(None);
484        let engine = Engine::new(&config)?;
485        let mut linker = Linker::<HostState>::new(&engine);
486        let mut component_linker = wasmtime::component::Linker::<ComponentHostState>::new(&engine);
487
488        // Define the benchmarking start/end functions.
489        let execution_timer = unsafe {
490            // Safe because this bench API's contract requires that its methods
491            // are only ever called from a single thread.
492            UnsafeSendSync::new(execution_timer)
493        };
494        linker.func_wrap("bench", "start", move || {
495            execution_start(*execution_timer.get());
496            Ok(())
497        })?;
498        linker.func_wrap("bench", "end", move || {
499            execution_end(*execution_timer.get());
500            Ok(())
501        })?;
502
503        // Define the same benchmarking functions on the component linker.
504        let mut bench_instance = component_linker.instance("bench")?;
505        bench_instance.func_wrap(
506            "start",
507            move |_: wasmtime::StoreContextMut<'_, ComponentHostState>, (): ()| {
508                execution_start(*execution_timer.get());
509                Ok(())
510            },
511        )?;
512        bench_instance.func_wrap(
513            "end",
514            move |_: wasmtime::StoreContextMut<'_, ComponentHostState>, (): ()| {
515                execution_end(*execution_timer.get());
516                Ok(())
517            },
518        )?;
519
520        let epoch_interruption = options.wasm.epoch_interruption.unwrap_or(false);
521        let fuel = options.wasm.fuel;
522
523        if options.wasi.common != Some(false) {
524            wasmtime_wasi::p1::add_to_linker_sync(&mut linker, |cx| &mut cx.wasi)?;
525            wasmtime_wasi::p2::add_to_linker_sync(&mut component_linker)?;
526        }
527
528        #[cfg(feature = "wasi-nn")]
529        if options.wasi.nn == Some(true) {
530            wasmtime_wasi_nn::witx::add_to_linker(&mut linker, |cx| &mut cx.wasi_nn)?;
531        }
532
533        Ok(Self {
534            linker,
535            component_linker,
536            compilation_timer,
537            compilation_start,
538            compilation_end,
539            instantiation_timer,
540            instantiation_start,
541            instantiation_end,
542            make_wasi_cx: Box::new(make_wasi_cx) as _,
543            make_component_wasi_cx: Box::new(make_component_wasi_cx) as _,
544            module: None,
545            component: None,
546            store_and_instance: None,
547            component_store_and_instance: None,
548            epoch_interruption,
549            fuel,
550        })
551    }
552
553    fn compile(&mut self, bytes: &[u8]) -> Result<()> {
554        self.module = None;
555        self.component = None;
556
557        let mut builder = CodeBuilder::new(self.linker.engine());
558        builder.wasm_binary(bytes, None)?;
559
560        match builder.hint() {
561            Some(CodeHint::Component) => {
562                (self.compilation_start)(self.compilation_timer);
563                let component = builder.compile_component()?;
564                (self.compilation_end)(self.compilation_timer);
565                self.component = Some(component);
566            }
567            Some(CodeHint::Module) | None => {
568                (self.compilation_start)(self.compilation_timer);
569                let module = builder.compile_module()?;
570                (self.compilation_end)(self.compilation_timer);
571                self.module = Some(module);
572            }
573        }
574
575        Ok(())
576    }
577
578    fn instantiate(&mut self) -> Result<()> {
579        self.store_and_instance = None;
580        self.component_store_and_instance = None;
581
582        if let Some(component) = &self.component {
583            let host = (self.make_component_wasi_cx)()
584                .context("failed to create a WASI context for component")?;
585
586            // NB: Start measuring instantiation time *after* we've created the
587            // WASI context, since that needs to do file I/O to setup
588            // stdin/stdout/stderr.
589            (self.instantiation_start)(self.instantiation_timer);
590
591            let mut store = Store::new(self.component_linker.engine(), host);
592            if self.epoch_interruption {
593                store.set_epoch_deadline(1);
594            }
595            if let Some(fuel) = self.fuel {
596                store.set_fuel(fuel).unwrap();
597            }
598
599            let instance = self.component_linker.instantiate(&mut store, component)?;
600
601            (self.instantiation_end)(self.instantiation_timer);
602
603            self.component_store_and_instance = Some((store, instance));
604        } else {
605            let module = self
606                .module
607                .as_ref()
608                .expect("compile the module before instantiating it");
609
610            let host = HostState {
611                wasi: (self.make_wasi_cx)().context("failed to create a WASI context")?,
612                #[cfg(feature = "wasi-nn")]
613                wasi_nn: {
614                    let (backends, registry) = wasmtime_wasi_nn::preload(&[])?;
615                    wasmtime_wasi_nn::witx::WasiNnCtx::new(backends, registry)
616                },
617            };
618
619            // NB: Start measuring instantiation time *after* we've created the
620            // WASI context, since that needs to do file I/O to setup
621            // stdin/stdout/stderr.
622            (self.instantiation_start)(self.instantiation_timer);
623
624            let mut store = Store::new(self.linker.engine(), host);
625            if self.epoch_interruption {
626                store.set_epoch_deadline(1);
627            }
628            if let Some(fuel) = self.fuel {
629                store.set_fuel(fuel).unwrap();
630            }
631
632            let instance = self.linker.instantiate(&mut store, &module)?;
633
634            (self.instantiation_end)(self.instantiation_timer);
635
636            self.store_and_instance = Some((store, instance));
637        }
638        Ok(())
639    }
640
641    fn execute(&mut self) -> Result<()> {
642        if let Some((mut store, instance)) = self.component_store_and_instance.take() {
643            let command = wasmtime_wasi::p2::bindings::sync::Command::new(&mut store, &instance)?;
644            match command.wasi_cli_run().call_run(&mut store)? {
645                Ok(()) => Ok(()),
646                Err(()) => Err(format_err!("calling `run` failed")),
647            }
648        } else {
649            let (mut store, instance) = self
650                .store_and_instance
651                .take()
652                .expect("instantiate the module before executing it");
653
654            let start_func = instance.get_typed_func::<(), ()>(&mut store, "_start")?;
655            match start_func.call(&mut store, ()) {
656                Ok(_) => Ok(()),
657                Err(trap) => {
658                    // Since _start will likely return by using the system `exit` call, we must
659                    // check the trap code to see if it actually represents a successful exit.
660                    if let Some(exit) = trap.downcast_ref::<I32Exit>() {
661                        if exit.0 == 0 {
662                            return Ok(());
663                        }
664                    }
665
666                    Err(trap)
667                }
668            }
669        }
670    }
671}