Files
Entropyk/bindings/fmi/src/fmi2.rs

354 lines
9.4 KiB
Rust

//! 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<Option<HashMap<usize, String>>> = 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<CString> = 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(),
}
}