//! FMI 2.0 Co-Simulation C ABI. //! //! Exports the standard `fmi2*` symbols expected by an FMI 2.0 Co-Sim host //! (TwinCAT, B&R, Siemens TIA, Codesys, fmpy, OpenModelica, ...). Each //! `fmi2Component` is a heap-allocated [`crate::model::FmuInstance`] handed //! back to the host as an opaque pointer. //! //! Only Co-Simulation is supported (the host drives the real-time scheduler; //! this FMU just re-solves the steady cycle on each `fmi2DoStep`). use std::collections::HashMap; use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_int, c_uint, c_void}; use std::sync::Mutex; use libc::{c_double, size_t}; use crate::embedded; use crate::model::{FmiStatus, FmuInstance}; pub type fmi2Real = c_double; pub type fmi2Integer = c_int; pub type fmi2Boolean = c_int; pub type fmi2String = *const c_char; pub type fmi2ValueReference = c_uint; pub type fmi2Time = c_double; pub type fmi2Component = *mut c_void; pub type fmi2Status = c_int; pub const FMI2_OK: fmi2Status = 0; pub const FMI2_WARNING: fmi2Status = 1; pub const FMI2_DISCARD: fmi2Status = 2; pub const FMI2_ERROR: fmi2Status = 3; pub const FMI2_FATAL: fmi2Status = 4; pub const FMI2_PENDING: fmi2Status = 5; pub const FMI2_TRUE: fmi2Boolean = 1; pub const FMI2_FALSE: fmi2Boolean = 0; /// FMI 2.0 callback functions. We accept the struct for ABI compatibility but /// do not invoke the callbacks in this skeleton (the host owns memory/logging). #[repr(C)] pub struct fmi2CallbackFunctions { pub logger: *mut c_void, pub allocate_memory: *mut c_void, pub free_memory: *mut c_void, pub step_finished: *mut c_void, pub intermediate_update: *mut c_void, } fn status_to_i32(s: FmiStatus) -> fmi2Status { s.as_i32() } /// Global registry of last error messages per instance (for host diagnostics). static LAST_ERRORS: Mutex>> = Mutex::new(None); fn record_error(ptr: usize, msg: String) { let mut guard = LAST_ERRORS.lock().unwrap(); guard.get_or_insert_with(HashMap::new).insert(ptr, msg); } /// FMI 2.0 types platform for Co-Simulation. #[no_mangle] pub unsafe extern "C" fn fmi2GetTypesPlatform() -> fmi2String { static PLATFORM: &[u8] = b"default\0"; PLATFORM.as_ptr() as *const c_char } /// FMI standard version string. #[no_mangle] pub unsafe extern "C" fn fmi2GetVersion() -> fmi2String { static VERSION: &[u8] = b"2.0\0"; VERSION.as_ptr() as *const c_char } /// Instantiate one FMU. The model JSON + IO-map JSON are loaded via /// [`crate::embedded::load_model`]: embedded at build time (codegen path) or /// from `ENTROPYK_FMU_MODEL` / `ENTROPYK_FMU_IO` env vars (dev path). #[no_mangle] pub unsafe extern "C" fn fmi2Instantiate( _instance_name: fmi2String, fmu_type: fmi2String, _callback_functions: *const fmi2CallbackFunctions, _visible: fmi2Boolean, _logging: fmi2Boolean, ) -> fmi2Component { // Skeleton supports Co-Simulation only. if !fmu_type.is_null() { if let Ok(t) = CStr::from_ptr(fmu_type).to_str() { if t != "CoSimulation" { return std::ptr::null_mut(); } } } match embedded::load_model() { Ok((model_json, io_json)) => match FmuInstance::new(&model_json, &io_json) { Ok(instance) => Box::into_raw(Box::new(instance)) as fmi2Component, Err(e) => { record_error(0, e); std::ptr::null_mut() } }, Err(e) => { record_error(0, e); std::ptr::null_mut() } } } /// Release an instance. #[no_mangle] pub unsafe extern "C" fn fmi2FreeInstance(c: fmi2Component) { if !c.is_null() { unsafe { drop(Box::from_raw(c as *mut FmuInstance)) }; } } /// Helper to access the instance behind a component handle. unsafe fn instance(c: fmi2Component) -> Option<&'static mut FmuInstance> { if c.is_null() { return None; } unsafe { Some(&mut *(c as *mut FmuInstance)) } } #[no_mangle] pub unsafe extern "C" fn fmi2SetDebugLogging( _c: fmi2Component, _logging: fmi2Boolean, _n: size_t, _categories: *const c_uint, ) -> fmi2Status { FMI2_OK } #[no_mangle] pub unsafe extern "C" fn fmi2SetupExperiment( _c: fmi2Component, _tolerance_defined: fmi2Boolean, _tolerance: fmi2Real, _start_time: fmi2Real, _stop_time_defined: fmi2Boolean, _stop_time: fmi2Real, ) -> fmi2Status { FMI2_OK } #[no_mangle] pub unsafe extern "C" fn fmi2EnterInitializationMode(c: fmi2Component) -> fmi2Status { match unsafe { instance(c) } { Some(inst) => status_to_i32(inst.enter_init()), None => FMI2_ERROR, } } #[no_mangle] pub unsafe extern "C" fn fmi2ExitInitializationMode(c: fmi2Component) -> fmi2Status { // Outputs are already populated by enter_init; nothing more to do. if c.is_null() { FMI2_ERROR } else { FMI2_OK } } #[no_mangle] pub unsafe extern "C" fn fmi2Terminate(c: fmi2Component) -> fmi2Status { if c.is_null() { FMI2_ERROR } else { FMI2_OK } } #[no_mangle] pub unsafe extern "C" fn fmi2Reset(c: fmi2Component) -> fmi2Status { // Re-run a cold solve from current inputs. match unsafe { instance(c) } { Some(inst) => status_to_i32(inst.do_step()), None => FMI2_ERROR, } } #[no_mangle] pub unsafe extern "C" fn fmi2GetReal( c: fmi2Component, vr: *const fmi2ValueReference, n: size_t, value: *mut fmi2Real, ) -> fmi2Status { let inst = match unsafe { instance(c) } { Some(i) => i, None => return FMI2_ERROR, }; if vr.is_null() || value.is_null() { return FMI2_ERROR; } let vrs = unsafe { std::slice::from_raw_parts(vr, n) }; let out = unsafe { std::slice::from_raw_parts_mut(value, n) }; let mut all_ok = true; for (k, &v) in vrs.iter().enumerate() { match inst.get_real(v) { Ok(x) => out[k] = x, Err(_) => { out[k] = f64::NAN; all_ok = false; } } } if all_ok { FMI2_OK } else { FMI2_WARNING } } #[no_mangle] pub unsafe extern "C" fn fmi2SetReal( c: fmi2Component, vr: *const fmi2ValueReference, n: size_t, value: *const fmi2Real, ) -> fmi2Status { let inst = match unsafe { instance(c) } { Some(i) => i, None => return FMI2_ERROR, }; if vr.is_null() || value.is_null() { return FMI2_ERROR; } let vrs = unsafe { std::slice::from_raw_parts(vr, n) }; let vals = unsafe { std::slice::from_raw_parts(value, n) }; let mut all_ok = true; for (k, &v) in vrs.iter().enumerate() { if status_to_i32(inst.set_real(v, vals[k])) != FMI2_OK { all_ok = false; } } if all_ok { FMI2_OK } else { FMI2_ERROR } } #[no_mangle] pub unsafe extern "C" fn fmi2GetInteger( _c: fmi2Component, _vr: *const fmi2ValueReference, _n: size_t, _value: *mut fmi2Integer, ) -> fmi2Status { FMI2_ERROR } #[no_mangle] pub unsafe extern "C" fn fmi2SetInteger( _c: fmi2Component, _vr: *const fmi2ValueReference, _n: size_t, _value: *const fmi2Integer, ) -> fmi2Status { FMI2_ERROR } #[no_mangle] pub unsafe extern "C" fn fmi2GetBoolean( _c: fmi2Component, _vr: *const fmi2ValueReference, _n: size_t, _value: *mut fmi2Boolean, ) -> fmi2Status { FMI2_ERROR } #[no_mangle] pub unsafe extern "C" fn fmi2SetBoolean( _c: fmi2Component, _vr: *const fmi2ValueReference, _n: size_t, _value: *const fmi2Boolean, ) -> fmi2Status { FMI2_ERROR } #[no_mangle] pub unsafe extern "C" fn fmi2GetString( _c: fmi2Component, _vr: *const fmi2ValueReference, _n: size_t, _value: *mut fmi2String, ) -> fmi2Status { FMI2_ERROR } #[no_mangle] pub unsafe extern "C" fn fmi2SetString( _c: fmi2Component, _vr: *const fmi2ValueReference, _n: size_t, _value: *const fmi2String, ) -> fmi2Status { FMI2_ERROR } /// Advance the model by one communication step. Entropyk is steady-state, so /// `currentCommunicationPoint` and `communicationStepSize` are accepted but /// ignored: each step re-solves the cycle at the current inputs (quasi-steady /// co-simulation). #[no_mangle] pub unsafe extern "C" fn fmi2DoStep( c: fmi2Component, _current_communication_point: fmi2Real, _communication_step_size: fmi2Real, _no_set_fmu_state_prior_to_current_point: fmi2Boolean, ) -> fmi2Status { match unsafe { instance(c) } { Some(inst) => status_to_i32(inst.do_step()), None => FMI2_ERROR, } } #[no_mangle] pub unsafe extern "C" fn fmi2CancelStep(_c: fmi2Component) -> fmi2Status { FMI2_ERROR } /// Non-standard helper for the host to retrieve the last error message as a C /// string (null-terminated). Returns null if no error. Caller must NOT free. #[no_mangle] pub unsafe extern "C" fn fmi2GetLastError() -> fmi2String { static mut BUF: Option = None; let msg = LAST_ERRORS .lock() .unwrap() .as_ref() .and_then(|m| m.values().last().cloned()); match msg { Some(s) => { // Rebuild the static buffer each call (single-threaded host query). let c = CString::new(s).unwrap_or_default(); let ptr = c.as_ptr(); unsafe { BUF = Some(c) }; // Keep the CString alive for the caller's read by leaking the old // value only after the pointer is handed out. The static retains it. ptr } None => std::ptr::null(), } }