synth_backend/
w2c2_wrapper.rs1use std::path::{Path, PathBuf};
6use std::process::Command;
7use synth_core::backend::{
8 Backend, BackendCapabilities, BackendError, CompilationResult, CompileConfig, CompiledFunction,
9};
10use synth_core::target::TargetSpec;
11use synth_core::wasm_decoder::DecodedModule;
12use synth_core::wasm_op::WasmOp;
13use synth_core::{Error, Result};
14
15pub struct W2C2Transpiler {
17 w2c2_path: PathBuf,
19}
20
21impl W2C2Transpiler {
22 pub fn new<P: AsRef<Path>>(w2c2_path: P) -> Self {
27 Self {
28 w2c2_path: w2c2_path.as_ref().to_path_buf(),
29 }
30 }
31
32 pub fn from_path() -> Result<Self> {
34 let paths = vec!["w2c2", "./w2c2", "../w2c2/build/w2c2"];
36
37 for path in paths {
38 if Path::new(path).exists() {
39 return Ok(Self::new(path));
40 }
41 }
42
43 Err(Error::Other(
44 "w2c2 not found in PATH. Please install w2c2 from https://github.com/turbolent/w2c2"
45 .to_string(),
46 ))
47 }
48
49 pub fn transpile<P: AsRef<Path>>(
56 &self,
57 wasm_path: P,
58 output_path: P,
59 options: &TranspileOptions,
60 ) -> Result<TranspileResult> {
61 let wasm_path = wasm_path.as_ref();
62 let output_path = output_path.as_ref();
63
64 if !wasm_path.exists() {
66 return Err(Error::Other(format!(
67 "Input WASM file not found: {}",
68 wasm_path.display()
69 )));
70 }
71
72 let mut cmd = Command::new(&self.w2c2_path);
74 cmd.arg(wasm_path);
75 cmd.arg(output_path);
76
77 if let Some(funcs_per_file) = options.functions_per_file {
79 cmd.arg("-f");
80 cmd.arg(funcs_per_file.to_string());
81 }
82
83 if let Some(threads) = options.threads {
84 cmd.arg("-t");
85 cmd.arg(threads.to_string());
86 }
87
88 if options.debug {
89 cmd.arg("-d");
90 }
91
92 let output = cmd.output().map_err(|e| {
94 Error::Other(format!(
95 "Failed to execute w2c2: {}. Make sure w2c2 is installed and accessible.",
96 e
97 ))
98 })?;
99
100 if !output.status.success() {
101 let stderr = String::from_utf8_lossy(&output.stderr);
102 return Err(Error::Other(format!(
103 "w2c2 transpilation failed: {}",
104 stderr
105 )));
106 }
107
108 let c_file = output_path.to_path_buf();
110 let h_file = output_path.with_extension("h");
111
112 if !c_file.exists() {
114 return Err(Error::Other(format!(
115 "Expected output file not created: {}",
116 c_file.display()
117 )));
118 }
119
120 Ok(TranspileResult {
121 c_file,
122 h_file: if h_file.exists() { Some(h_file) } else { None },
123 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
124 })
125 }
126}
127
128#[derive(Debug, Clone)]
130pub struct TranspileOptions {
131 pub functions_per_file: Option<usize>,
133
134 pub threads: Option<usize>,
136
137 pub debug: bool,
139}
140
141impl Default for TranspileOptions {
142 fn default() -> Self {
143 Self {
144 functions_per_file: None,
145 threads: Some(1), debug: false,
147 }
148 }
149}
150
151#[derive(Debug, Clone)]
153pub struct TranspileResult {
154 pub c_file: PathBuf,
156
157 pub h_file: Option<PathBuf>,
159
160 pub stdout: String,
162}
163
164pub struct W2C2Backend {
168 w2c2_path: Option<String>,
170 gcc_path: Option<String>,
172}
173
174impl W2C2Backend {
175 pub fn new() -> Self {
176 Self {
177 w2c2_path: None,
178 gcc_path: None,
179 }
180 }
181
182 pub fn with_paths(w2c2: impl Into<String>, gcc: impl Into<String>) -> Self {
183 Self {
184 w2c2_path: Some(w2c2.into()),
185 gcc_path: Some(gcc.into()),
186 }
187 }
188
189 fn find_w2c2(&self) -> Option<String> {
190 if let Some(ref path) = self.w2c2_path
191 && Path::new(path).exists()
192 {
193 return Some(path.clone());
194 }
195 W2C2Transpiler::from_path()
196 .ok()
197 .map(|t| t.w2c2_path.to_string_lossy().to_string())
198 }
199
200 fn find_gcc(&self) -> Option<String> {
201 if let Some(ref path) = self.gcc_path
202 && Path::new(path).exists()
203 {
204 return Some(path.clone());
205 }
206 Command::new("which")
207 .arg("arm-none-eabi-gcc")
208 .output()
209 .ok()
210 .filter(|o| o.status.success())
211 .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
212 }
213}
214
215impl Default for W2C2Backend {
216 fn default() -> Self {
217 Self::new()
218 }
219}
220
221impl Backend for W2C2Backend {
222 fn name(&self) -> &str {
223 "w2c2"
224 }
225
226 fn capabilities(&self) -> BackendCapabilities {
227 BackendCapabilities {
228 produces_elf: true,
229 supports_rule_verification: false,
230 supports_binary_verification: true,
231 is_external: true,
232 }
233 }
234
235 fn supported_targets(&self) -> Vec<TargetSpec> {
236 vec![TargetSpec::cortex_m4(), TargetSpec::cortex_m7()]
237 }
238
239 fn compile_module(
240 &self,
241 _module: &DecodedModule,
242 _config: &CompileConfig,
243 ) -> std::result::Result<CompilationResult, BackendError> {
244 let _w2c2 = self.find_w2c2().ok_or_else(|| {
245 BackendError::NotAvailable(
246 "w2c2 not found. Install from https://github.com/turbolent/w2c2".to_string(),
247 )
248 })?;
249 let _gcc = self.find_gcc().ok_or_else(|| {
250 BackendError::NotAvailable(
251 "arm-none-eabi-gcc not found. Install ARM GNU toolchain.".to_string(),
252 )
253 })?;
254
255 Err(BackendError::CompilationFailed(
257 "w2c2 module compilation not yet implemented".to_string(),
258 ))
259 }
260
261 fn compile_function(
262 &self,
263 _name: &str,
264 _ops: &[WasmOp],
265 _config: &CompileConfig,
266 ) -> std::result::Result<CompiledFunction, BackendError> {
267 Err(BackendError::UnsupportedConfig(
268 "w2c2 only supports whole-module compilation (use compile_module)".to_string(),
269 ))
270 }
271
272 fn is_available(&self) -> bool {
273 self.find_w2c2().is_some() && self.find_gcc().is_some()
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use std::fs;
281
282 #[test]
283 fn test_transpile_options_default() {
284 let options = TranspileOptions::default();
285 assert_eq!(options.functions_per_file, None);
286 assert_eq!(options.threads, Some(1));
287 assert!(!options.debug);
288 }
289
290 #[test]
291 #[ignore] fn test_transpile_simple_module() {
293 let wasm_bytes = vec![
295 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, ];
298
299 let temp_dir = std::env::temp_dir();
300 let wasm_path = temp_dir.join("test_module.wasm");
301 let output_path = temp_dir.join("test_module.c");
302
303 fs::write(&wasm_path, wasm_bytes).unwrap();
305
306 if let Ok(transpiler) = W2C2Transpiler::from_path() {
308 let options = TranspileOptions::default();
309 let result = transpiler.transpile(&wasm_path, &output_path, &options);
310
311 match result {
312 Ok(res) => {
313 assert!(res.c_file.exists());
314 println!("Successfully transpiled to: {}", res.c_file.display());
315 }
316 Err(e) => {
317 println!(
318 "Transpilation error (expected if w2c2 not installed): {}",
319 e
320 );
321 }
322 }
323 } else {
324 println!("w2c2 not found in PATH - skipping transpilation test");
325 }
326
327 let _ = fs::remove_file(&wasm_path);
329 let _ = fs::remove_file(&output_path);
330 let _ = fs::remove_file(output_path.with_extension("h"));
331 }
332
333 #[test]
334 fn test_w2c2_backend_properties() {
335 let backend = W2C2Backend::new();
336 assert_eq!(backend.name(), "w2c2");
337 assert!(backend.capabilities().produces_elf);
338 assert!(backend.capabilities().is_external);
339 assert!(!backend.capabilities().supports_rule_verification);
340 }
341
342 #[test]
343 fn test_w2c2_function_compilation_unsupported() {
344 let backend = W2C2Backend::new();
345 let config = CompileConfig::default();
346 let result = backend.compile_function("test", &[WasmOp::I32Add], &config);
347 assert!(result.is_err());
348 }
349}