simulink_rs/
lib.rs

1//! # Simulink C Rust wrapper and binder
2//!
3//! A Rust library to import generated C code from Simulink in Rust
4//!
5//! # Example
6//! ```ignore
7//! let sys = Sys::new(Some("MySimulinkController"));
8//! sys.compile().generate_module();
9//! ```
10
11use regex::Regex;
12use std::{
13    env,
14    fmt::{Debug, Display},
15    fs::{self, File},
16    io::{BufRead, BufReader},
17    path::{Path, PathBuf},
18};
19
20mod model;
21use model::{Model, Simulink};
22
23/// Simulink control system C source and header files parser and builder
24///
25/// # Example
26/// ```ignore
27/// let sys = Sys::new(Some("MySimulinkController"));
28/// sys.compile().generate_module();
29/// ```
30#[derive(Debug, Default, Clone)]
31pub struct Sys {
32    controller: Option<String>,
33    sources: Vec<PathBuf>,
34    headers: Vec<PathBuf>,
35}
36
37pub struct Builder {
38    controller_type: Option<String>,
39    sys_folder: String,
40}
41impl Default for Builder {
42    fn default() -> Self {
43        Self {
44            controller_type: Default::default(),
45            sys_folder: "sys".into(),
46        }
47    }
48}
49impl Builder {
50    /// Sets the name of the Rust structure that acts as a wrapper for the Simulink C code
51    ///
52    /// If not set, the structure is given the same name than the Simulink control model
53    pub fn name<S: Into<String>>(mut self, rs_type: S) -> Self {
54        self.controller_type = Some(rs_type.into());
55        self
56    }
57    /// Sets the name of the folder with the C header and source file
58    ///
59    /// If not set, expect the folder to be named "sys"
60    pub fn folder<S: Into<String>>(mut self, folder: S) -> Self {
61        self.sys_folder = folder.into();
62        self
63    }
64    /// Builds a new Simulink C to Rust wrapper
65    pub fn build(self) -> Sys {
66        let sys = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).join(self.sys_folder);
67
68        let mut sources = vec![];
69        let mut headers = vec![];
70
71        if let Ok(entries) = fs::read_dir(&sys) {
72            for entry in entries {
73                if let Ok(entry) = entry {
74                    let file_name = entry.path();
75                    if let Some(extension) = file_name.extension() {
76                        match extension.to_str() {
77                            Some("c") => {
78                                sources.push(file_name);
79                            }
80                            Some("h") => {
81                                headers.push(file_name);
82                            }
83                            _ => (),
84                        }
85                    }
86                }
87            }
88        }
89
90        Sys {
91            controller: self.controller_type,
92            sources,
93            headers,
94        }
95    }
96}
97
98impl Sys {
99    /// Create a new Simulink FFI
100    ///
101    /// The Simulink controlller will be given the type `rs_type` if present
102    pub fn new<S: Into<String>>(rs_type: Option<S>) -> Self {
103        let sys = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).join("sys");
104
105        let mut sources = vec![];
106        let mut headers = vec![];
107
108        if let Ok(entries) = fs::read_dir(&sys) {
109            for entry in entries {
110                if let Ok(entry) = entry {
111                    let file_name = entry.path();
112                    if let Some(extension) = file_name.extension() {
113                        match extension.to_str() {
114                            Some("c") => {
115                                sources.push(file_name);
116                            }
117                            Some("h") => {
118                                headers.push(file_name);
119                            }
120                            _ => (),
121                        }
122                    }
123                }
124            }
125        }
126
127        Self {
128            controller: rs_type.map(|x| x.into()),
129            sources,
130            headers,
131        }
132    }
133    /// Creates a builder for the Simulink C to Rust wrapper
134    pub fn builder() -> Builder {
135        Default::default()
136    }
137    /// Returns the main header file
138    fn header(&self) -> Option<&str> {
139        self.headers.iter().find_map(|header| {
140            header.to_str().filter(|f| {
141                !(f.ends_with("rtwtypes.h")
142                    || f.ends_with("rt_defines.h")
143                    || f.ends_with("_private.h")
144                    || f.ends_with("_types.h"))
145            })
146        })
147    }
148    /// Parses the main header file into [Model]
149    ///
150    /// Extract the model name and the lists of inputs, outputs and states variables
151    /// and creates a [Model]
152    fn parse_header(&self) -> Model {
153        let Some(header) = self.header() else {
154            panic!("cannot find error in sys")
155        };
156        let file = File::open(header).expect(&format!("file {:?} not found", header));
157        let reader = BufReader::new(file);
158        let mut lines = reader.lines();
159
160        let mut model = Model::default();
161        model.name = loop {
162            if let Some(Ok(line)) = lines.next() {
163                if line.contains("File:") {
164                    let regex = Regex::new(r"File:\s*(\w+)\.h").unwrap();
165                    if let Some(captures) = regex.captures(&line) {
166                        let name = captures.get(1).unwrap().as_str();
167                        break name.to_string();
168                    }
169                }
170            }
171        };
172        while let Some(data) = Simulink::parse_io(&mut lines) {
173            model.simulink.push(data);
174        }
175        model
176    }
177    /// Compiles the Simulink C model
178    pub fn compile(&self) -> &Self {
179        let mut cc_builder = cc::Build::new();
180        self.sources
181            .iter()
182            .fold(&mut cc_builder, |cc_builder, source| {
183                cc_builder.file(source)
184            });
185        let bindings_builder = self
186            .headers
187            .iter()
188            .fold(bindgen::builder(), |bindings, header| {
189                println!("cargo:rerun-if-changed={:}", header.to_str().unwrap());
190                bindings.header(
191                    header
192                        .to_str()
193                        .expect(&format!("{:?} conversion to str failed", header)),
194                )
195            });
196
197        let lib = env::var("CARGO_PKG_NAME").unwrap();
198        println!("cargo:rustc-link-search=native=lib{}", lib);
199        println!("cargo:rustc-link-lib={}", lib);
200
201        cc_builder.compile(lib.as_str());
202        let bindings = bindings_builder
203            .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
204            .generate()
205            .expect("Unable to generate bindings");
206        let out_path = PathBuf::from(std::env::var("OUT_DIR").unwrap());
207        bindings
208            .write_to_file(out_path.join("bindings.rs"))
209            .expect("Couldn't write bindings!");
210        self
211    }
212    /// Generates the controller.rs module
213    pub fn generate_module(&self) {
214        let out_dir = env::var_os("OUT_DIR").unwrap();
215        let dest_path = Path::new(&out_dir).join("controller.rs");
216        fs::write(&dest_path, format!("{}", self)).unwrap();
217    }
218}
219
220impl Display for Sys {
221    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222        let model = self.parse_header();
223        if let Some(controller) = self.controller.as_ref() {
224            writeln!(f, "/// Rust binder to Simulink C controller wrapper")?;
225            writeln!(f, "#[allow(dead_code)]")?;
226            writeln!(f, "pub type {} = {};", controller, model.name)?;
227        }
228        <Model as Display>::fmt(&model, f)
229    }
230}