1use crate::model_description::{FmiModelDescription, ScalarVariable};
2use itertools::Itertools;
3use libfmi::{
4 fmi2Boolean, fmi2Byte, fmi2CallbackFunctions, fmi2Component, fmi2FMUstate, fmi2Integer,
5 fmi2Real, fmi2Status, fmi2Type, fmi2ValueReference, Fmi2Dll,
6};
7use std::{
8 borrow::Borrow,
9 collections::HashMap,
10 env,
11 ffi::CString,
12 fmt::Display,
13 fs, io,
14 iter::zip,
15 ops::Deref,
16 os,
17 path::PathBuf,
18 sync::atomic::{AtomicUsize, Ordering},
19};
20use thiserror::Error;
21use zip::result::ZipError;
22
23#[derive(Debug)]
25pub struct Fmu {
26 #[allow(dead_code)]
27 temp_dir: Option<tempfile::TempDir>,
29 unpacked_dir: PathBuf,
31 pub model_description: FmiModelDescription,
33}
34
35pub struct FmuLibrary {
37 fmi: Fmi2Dll,
39 simulation_type: fmi2Type,
44 pub fmu: Fmu,
47 instance_name_factory: InstanceNameFactory,
49}
50
51pub struct FmuInstance<C: Borrow<FmuLibrary>> {
53 pub lib: C,
61 instance: *mut os::raw::c_void,
63 #[allow(dead_code)]
64 callbacks: Box<fmi2CallbackFunctions>,
65}
66
67pub struct FmuState<'fmu, C: Borrow<FmuLibrary>>(fmi2FMUstate, &'fmu FmuInstance<C>);
68
69impl<'fmu, C: Borrow<FmuLibrary>> Drop for FmuState<'fmu, C> {
70 fn drop(&mut self) {
71 unsafe {
72 self.1
73 .lib
74 .borrow()
75 .fmi
76 .fmi2FreeFMUstate(self.1.instance, &mut self.0);
77 }
78 }
79}
80
81pub struct FmuGetSetStateCapability<'fmu, C: Borrow<FmuLibrary>>(&'fmu FmuInstance<C>);
82
83impl<'fmu, C: Borrow<FmuLibrary>> FmuGetSetStateCapability<'fmu, C> {
84 pub fn get_state(&self) -> Result<FmuState<'fmu, C>, FmuError> {
85 let mut fmu2state: fmi2FMUstate = std::ptr::null_mut();
86 let pfmu2state = std::ptr::addr_of_mut!(fmu2state);
87 FmuInstance::<C>::ok_or_err(unsafe {
88 self.0
89 .lib
90 .borrow()
91 .fmi
92 .fmi2GetFMUstate(self.0.instance, pfmu2state)
93 })?;
94 Ok(FmuState(fmu2state, self.0))
95 }
96
97 pub fn set_state(&self, mut state: FmuState<'fmu, C>) -> Result<(), FmuError> {
98 let pfmu2state = std::ptr::addr_of_mut!(state.0);
99 FmuInstance::<C>::ok_or_err(unsafe {
100 self.0
101 .lib
102 .borrow()
103 .fmi
104 .fmi2SetFMUstate(self.0.instance, *pfmu2state)
105 })?;
106 Ok(())
107 }
108}
109
110pub struct FmuSerializeStateCapability<'fmu, C: Borrow<FmuLibrary>>(&'fmu FmuInstance<C>);
111
112impl<'fmu, C: Borrow<FmuLibrary>> FmuSerializeStateCapability<'fmu, C> {
113 pub fn serialize_state(&self, state: &FmuState<'fmu, C>) -> Result<Vec<u8>, FmuError> {
114 let mut size: usize = 0;
115 let pfmu2state = std::ptr::addr_of!(state.0);
116 FmuInstance::<C>::ok_or_err(unsafe {
117 self.0.lib.borrow().fmi.fmi2SerializedFMUstateSize(
118 self.0.instance,
119 *pfmu2state,
120 &mut size,
121 )
122 })?;
123 let mut serialized_state = vec![0u8; size];
124 let raw_serialized_state: *mut fmi2Byte = serialized_state.as_mut_ptr() as *mut fmi2Byte;
125 FmuInstance::<C>::ok_or_err(unsafe {
126 self.0.lib.borrow().fmi.fmi2SerializeFMUstate(
127 self.0.instance,
128 *pfmu2state,
129 raw_serialized_state,
130 size,
131 )
132 })?;
133 Ok(serialized_state)
134 }
135
136 pub fn deserialize_state(
137 &self,
138 serialized_state: &[u8],
139 ) -> Result<FmuState<'fmu, C>, FmuError> {
140 let mut fmu2state: fmi2FMUstate = std::ptr::null_mut();
141 let pfmu2state = std::ptr::addr_of_mut!(fmu2state);
142 let raw_serialized_state: *const fmi2Byte = serialized_state.as_ptr() as *const fmi2Byte;
143 FmuInstance::<C>::ok_or_err(unsafe {
144 self.0.lib.borrow().fmi.fmi2DeSerializeFMUstate(
145 self.0.instance,
146 raw_serialized_state,
147 serialized_state.len(),
148 pfmu2state,
149 )
150 })?;
151 Ok(FmuState(fmu2state, self.0))
152 }
153}
154
155struct InstanceNameFactory {
157 model_identifier: String,
158 instance_counter: AtomicUsize,
162}
163
164impl Deref for FmuLibrary {
165 type Target = Fmu;
166
167 fn deref(&self) -> &Self::Target {
169 &self.fmu
170 }
171}
172
173impl InstanceNameFactory {
174 fn new(model_identifier: String) -> Self {
175 Self {
176 model_identifier,
177 instance_counter: AtomicUsize::new(0),
178 }
179 }
180
181 fn next(&self) -> String {
182 let instance_counter = self.instance_counter.fetch_add(1, Ordering::Relaxed);
183 format!("{}_{}", self.model_identifier, instance_counter)
184 }
185}
186
187impl Fmu {
188 pub fn unpack(fmu_path: impl Into<std::path::PathBuf>) -> Result<Self, FmuUnpackError> {
190 let temp_dir = tempfile::Builder::new()
191 .prefix("fmi-runner")
192 .tempdir()
193 .map_err(FmuUnpackError::NoTempdir)?;
194
195 let fmu = Self::unpack_to(fmu_path, temp_dir.path())?;
196
197 Ok(Self {
198 temp_dir: Some(temp_dir),
199 unpacked_dir: fmu.unpacked_dir,
200 model_description: fmu.model_description,
201 })
202 }
203
204 pub fn unpack_to(
206 fmu_path: impl Into<std::path::PathBuf>,
207 target_dir: impl Into<std::path::PathBuf>,
208 ) -> Result<Self, FmuUnpackError> {
209 let fmu_path = fs::canonicalize(fmu_path.into()).map_err(FmuUnpackError::InvalidFile)?;
210 let target_dir = target_dir.into();
211
212 let zipfile = std::fs::File::open(fmu_path).map_err(FmuUnpackError::InvalidFile)?;
213 let mut archive = zip::ZipArchive::new(zipfile).map_err(|e| match e {
214 ZipError::Io(e) => FmuUnpackError::InvalidFile(e),
215 e => FmuUnpackError::InvalidArchive(e),
216 })?;
217 archive.extract(&target_dir).map_err(|e| match e {
218 ZipError::Io(e) => FmuUnpackError::InvalidOutputDir(e),
219 e => FmuUnpackError::InvalidArchive(e),
220 })?;
221
222 let model_description = FmiModelDescription::new(&target_dir.join("modelDescription.xml"))?;
223
224 Ok(Self {
225 temp_dir: None,
226 unpacked_dir: target_dir,
227 model_description,
228 })
229 }
230
231 pub fn load(self, simulation_type: fmi2Type) -> Result<FmuLibrary, FmuLoadError> {
233 self.load_with_handler(simulation_type, |_| {})
234 }
235
236 pub fn load_with_handler<F>(
258 self,
259 simulation_type: fmi2Type,
260 handler: F,
261 ) -> Result<FmuLibrary, FmuLoadError>
262 where
263 F: FnOnce(&::libloading::Library),
264 {
265 let (os_type, lib_type) = match env::consts::OS {
266 "macos" => ("darwin", "dylib"),
267 "linux" => ("linux", "so"),
268 "windows" => ("win", "dll"),
269 _ => ("unknown", "so"),
270 };
271
272 let arch_type = match std::env::consts::ARCH {
273 "x86" => "32",
274 "x86_64" => "64",
275 "aarch64" => "64",
277 _ => "unknown",
278 };
279
280 let model_identifier = match simulation_type {
281 fmi2Type::fmi2ModelExchange => self
282 .model_description
283 .model_exchange
284 .as_ref()
285 .ok_or(FmuLoadError::NoModelExchangeModel)?
286 .model_identifier
287 .clone(),
288 fmi2Type::fmi2CoSimulation => self
289 .model_description
290 .co_simulation
291 .as_ref()
292 .ok_or(FmuLoadError::NoCoSimulationModel)?
293 .model_identifier
294 .clone(),
295 };
296
297 let lib_str = os_type.to_owned() + arch_type;
299
300 let mut lib_path = self
302 .unpacked_dir
303 .join("binaries")
304 .join(lib_str)
305 .join(&model_identifier);
306 lib_path.set_extension(lib_type);
307
308 let library = unsafe { ::libloading::Library::new(lib_path)? };
310
311 handler(&library);
313
314 let fmi = unsafe { Fmi2Dll::from_library(library) }?;
316
317 Ok(FmuLibrary {
318 fmi,
319 simulation_type,
320 fmu: self,
321 instance_name_factory: InstanceNameFactory::new(model_identifier),
322 })
323 }
324
325 pub fn variables(&self) -> &HashMap<String, ScalarVariable> {
326 &self.model_description.model_variables.scalar_variable
327 }
328}
329
330unsafe impl<C: Borrow<FmuLibrary>> Send for FmuInstance<C> {}
331
332impl<C: Borrow<FmuLibrary>> FmuInstance<C> {
333 pub fn instantiate(lib: C, logging_on: bool) -> Result<Self, FmuError> {
335 let fmu_guid = &lib.borrow().model_description.guid;
336
337 let callbacks = Box::<fmi2CallbackFunctions>::new(fmi2CallbackFunctions {
338 logger: Some(libfmi::logger::callback_logger_handler),
339 allocateMemory: Some(libc::calloc),
340 freeMemory: Some(libc::free),
341 stepFinished: None,
342 componentEnvironment: std::ptr::null_mut::<std::os::raw::c_void>(),
343 });
344
345 let fmu_guid = CString::new(fmu_guid.as_bytes()).expect("Error building fmu_guid CString");
346
347 let resource_location = "file://".to_owned()
348 + lib
349 .borrow()
350 .unpacked_dir
351 .join("resources")
352 .to_str()
353 .unwrap();
354 let resource_location =
355 CString::new(resource_location).expect("Error building resource_location CString");
356
357 let visible = false as fmi2Boolean;
358 let logging_on = logging_on as fmi2Boolean;
359
360 let instance_name = CString::new(lib.borrow().instance_name_factory.next())
362 .expect("Error building instance_name CString");
363
364 let instance = unsafe {
365 lib.borrow().fmi.fmi2Instantiate(
366 instance_name.as_ptr(),
367 lib.borrow().simulation_type,
368 fmu_guid.as_ptr(),
369 resource_location.as_ptr(),
370 &*callbacks,
371 visible,
372 logging_on,
373 )
374 };
375
376 if instance.is_null() {
377 return Err(FmuError::FmuInstantiateFailed);
378 }
379
380 Ok(Self {
381 lib,
382 instance,
383 callbacks,
384 })
385 }
386
387 pub fn get_set_state_capability(&self) -> Option<FmuGetSetStateCapability<C>> {
388 if let Some(description) = self.lib.borrow().model_description.co_simulation.as_ref() {
389 if description.can_get_and_set_fmustate {
390 Some(FmuGetSetStateCapability(self))
391 } else {
392 None
393 }
394 } else if let Some(description) =
395 self.lib.borrow().model_description.model_exchange.as_ref()
396 {
397 if description.can_get_and_set_fmustate {
398 Some(FmuGetSetStateCapability(self))
399 } else {
400 None
401 }
402 } else {
403 None
404 }
405 }
406
407 pub fn serialize_state_capability(&self) -> Option<FmuSerializeStateCapability<C>> {
408 if let Some(description) = self.lib.borrow().model_description.co_simulation.as_ref() {
409 if description.can_serialize_fmustate {
410 Some(FmuSerializeStateCapability(self))
411 } else {
412 None
413 }
414 } else if let Some(description) =
415 self.lib.borrow().model_description.model_exchange.as_ref()
416 {
417 if description.can_serialize_fmustate {
418 Some(FmuSerializeStateCapability(self))
419 } else {
420 None
421 }
422 } else {
423 None
424 }
425 }
426
427 pub fn get_types_platform(&self) -> &str {
428 let types_platform =
429 unsafe { std::ffi::CStr::from_ptr(self.lib.borrow().fmi.fmi2GetTypesPlatform()) }
430 .to_str()
431 .unwrap();
432 types_platform
433 }
434
435 pub fn set_debug_logging(
436 &self,
437 logging_on: bool,
438 log_categories: &[&str],
439 ) -> Result<(), FmuError> {
440 let category_cstr = log_categories
441 .iter()
442 .map(|c| CString::new(*c).unwrap())
443 .collect::<Vec<_>>();
444
445 let category_ptrs: Vec<_> = category_cstr.iter().map(|c| c.as_ptr()).collect();
446
447 Self::ok_or_err(unsafe {
448 self.lib.borrow().fmi.fmi2SetDebugLogging(
449 self.instance,
450 logging_on as fmi2Boolean,
451 category_ptrs.len(),
452 category_ptrs.as_ptr(),
453 )
454 })
455 }
456
457 pub fn setup_experiment(
458 &self,
459 start_time: f64,
460 stop_time: Option<f64>,
461 tolerance: Option<f64>,
462 ) -> Result<(), FmuError> {
463 Self::ok_or_err(unsafe {
464 self.lib.borrow().fmi.fmi2SetupExperiment(
465 self.instance,
466 tolerance.is_some() as fmi2Boolean,
467 tolerance.unwrap_or(0.0),
468 start_time,
469 stop_time.is_some() as fmi2Boolean,
470 stop_time.unwrap_or(0.0),
471 )
472 })
473 }
474
475 pub fn enter_initialization_mode(&self) -> Result<(), FmuError> {
476 Self::ok_or_err(unsafe {
477 self.lib
478 .borrow()
479 .fmi
480 .fmi2EnterInitializationMode(self.instance)
481 })
482 }
483
484 pub fn exit_initialization_mode(&self) -> Result<(), FmuError> {
485 Self::ok_or_err(unsafe {
486 self.lib
487 .borrow()
488 .fmi
489 .fmi2ExitInitializationMode(self.instance)
490 })
491 }
492
493 pub fn get_reals<'fmu>(
494 &'fmu self,
495 signals: &[&'fmu ScalarVariable],
496 ) -> Result<HashMap<&ScalarVariable, fmi2Real>, FmuError> {
497 self.get(signals, Fmi2Dll::fmi2GetReal)
498 }
499
500 pub fn get_integers<'fmu>(
501 &'fmu self,
502 signals: &[&'fmu ScalarVariable],
503 ) -> Result<HashMap<&ScalarVariable, fmi2Integer>, FmuError> {
504 self.get(signals, Fmi2Dll::fmi2GetInteger)
505 }
506
507 pub fn get_booleans<'fmu>(
508 &'fmu self,
509 signals: &[&'fmu ScalarVariable],
510 ) -> Result<HashMap<&ScalarVariable, fmi2Integer>, FmuError> {
511 self.get(signals, Fmi2Dll::fmi2GetBoolean)
512 }
513
514 pub fn set_reals(
515 &self,
516 value_map: &HashMap<&ScalarVariable, fmi2Real>,
517 ) -> Result<(), FmuError> {
518 self.set(value_map, Fmi2Dll::fmi2SetReal)
519 }
520
521 pub fn set_integers(
522 &self,
523 value_map: &HashMap<&ScalarVariable, fmi2Integer>,
524 ) -> Result<(), FmuError> {
525 self.set(value_map, Fmi2Dll::fmi2SetInteger)
526 }
527
528 pub fn set_booleans(
529 &self,
530 value_map: &HashMap<&ScalarVariable, fmi2Integer>,
531 ) -> Result<(), FmuError> {
532 self.set(value_map, Fmi2Dll::fmi2SetBoolean)
533 }
534
535 pub fn do_step(
536 &self,
537 current_communication_point: fmi2Real,
538 communication_step_size: fmi2Real,
539 no_set_fmustate_prior_to_current_point: bool,
540 ) -> Result<(), FmuError> {
541 Self::ok_or_err(unsafe {
542 self.lib.borrow().fmi.fmi2DoStep(
543 self.instance,
544 current_communication_point,
545 communication_step_size,
546 no_set_fmustate_prior_to_current_point as fmi2Boolean,
547 )
548 })
549 }
550
551 fn get<'fmu, T>(
552 &'fmu self,
553 signals: &[&'fmu ScalarVariable],
554 func: unsafe fn(
555 &Fmi2Dll,
556 fmi2Component,
557 *const fmi2ValueReference,
558 usize,
559 *mut T,
560 ) -> fmi2Status,
561 ) -> Result<HashMap<&'fmu ScalarVariable, T>, FmuError> {
562 let mut values = Vec::<T>::with_capacity(signals.len());
563 match unsafe {
564 values.set_len(signals.len());
565 func(
566 &self.lib.borrow().fmi,
567 self.instance,
568 signals
569 .iter()
570 .map(|s| s.value_reference)
571 .collect::<Vec<_>>()
572 .as_ptr(),
573 signals.len(),
574 values.as_mut_ptr(),
575 )
576 } {
577 fmi2Status::fmi2OK => Ok(zip(signals.to_owned(), values).collect()),
578 status => Err(FmuError::BadFunctionCall(status)),
579 }
580 }
581
582 fn set<T: Copy>(
583 &self,
584 value_map: &HashMap<&ScalarVariable, T>,
585 func: unsafe fn(
586 &Fmi2Dll,
587 fmi2Component,
588 *const fmi2ValueReference,
589 usize,
590 *const T,
591 ) -> fmi2Status,
592 ) -> Result<(), FmuError> {
593 let len = value_map.len();
594 let mut vrs = Vec::<fmi2ValueReference>::with_capacity(len);
595 let mut values = Vec::<T>::with_capacity(len);
596
597 for (signal, value) in value_map.iter() {
598 vrs.push(signal.value_reference);
599 values.push(*value);
600 }
601
602 Self::ok_or_err(unsafe {
603 func(
604 &self.lib.borrow().fmi,
605 self.instance,
606 vrs.as_ptr(),
607 len,
608 values.as_ptr(),
609 )
610 })
611 }
612
613 fn ok_or_err(status: fmi2Status) -> Result<(), FmuError> {
614 match status {
615 fmi2Status::fmi2OK => Ok(()),
616 status => Err(FmuError::BadFunctionCall(status)),
617 }
618 }
619}
620
621impl<C: Borrow<FmuLibrary>> Drop for FmuInstance<C> {
622 fn drop(&mut self) {
623 unsafe { self.lib.borrow().fmi.fmi2FreeInstance(self.instance) };
624 }
625}
626
627pub fn outputs_to_string<T: Display>(outputs: &HashMap<&ScalarVariable, T>) -> String {
628 let mut s = String::new();
629
630 for signal in outputs.keys().sorted_by_key(|s| &s.name) {
631 s.push_str(&format!("{}: {:.3} | ", signal.name, outputs[signal]));
632 }
633
634 s
635}
636
637#[derive(Error, Debug)]
638pub enum FmuUnpackError {
639 #[error("Failed to create tempdir")]
640 NoTempdir(#[source] io::Error),
641 #[error("Invalid FMU path")]
642 InvalidFile(#[source] io::Error),
643 #[error("Invalid FMU unzip output directory")]
644 InvalidOutputDir(#[source] io::Error),
645 #[error("Invalid FMU archive")]
646 InvalidArchive(#[from] ZipError),
647 #[error("Invalid FMU model description XML")]
648 InvalidModelDescription(#[from] quick_xml::DeError),
649}
650
651#[derive(Error, Debug)]
652pub enum FmuLoadError {
653 #[error("FMU does not contain CoSimulation model")]
654 NoCoSimulationModel,
655 #[error("FMU does not contain ModelExchange model")]
656 NoModelExchangeModel,
657 #[error("Error loading FMU dynamic library")]
658 DLOpen(#[from] libloading::Error),
659}
660
661#[derive(Error, Debug)]
662pub enum FmuError {
663 #[error("FMU bad function call: {0:?}")]
664 BadFunctionCall(fmi2Status),
665 #[error("fmi2Instantiate() call failed")]
668 FmuInstantiateFailed,
669}
670
671#[cfg(test)]
673mod tests {
674 use super::*;
675
676 fn print_err(err: impl std::error::Error) {
677 eprintln!("Display:\n{}", err);
678 eprintln!("Debug:\n{:?}", err);
679 }
680
681 #[test]
682 fn test_invalid_file() {
683 let res = Fmu::unpack("dasf:?-()");
684 assert!(matches!(res, Err(FmuUnpackError::InvalidFile { .. })));
685 print_err(res.unwrap_err());
686 }
687
688 #[test]
689 fn test_invalid_output_dir() {
690 let res = Fmu::unpack_to("./tests/fmu/free_fall.fmu", "/z.(),.dasda/dasd");
691 assert!(matches!(res, Err(FmuUnpackError::InvalidOutputDir { .. })));
692 print_err(res.unwrap_err());
693 }
694}