Skip to main content

c2rust_transpile/
lib.rs

1#![allow(clippy::too_many_arguments)]
2
3mod diagnostics;
4
5pub mod build_files;
6pub mod c_ast;
7pub mod cfg;
8mod compile_cmds;
9pub mod convert_type;
10pub mod renamer;
11pub mod rust_ast;
12pub mod translator;
13pub mod with_stmts;
14
15use std::collections::HashSet;
16use std::fs::{self, File};
17use std::io::prelude::*;
18use std::path::{Path, PathBuf};
19use std::process::Command;
20use std::{env, io};
21
22use crate::compile_cmds::CompileCmd;
23use failure::Error;
24use itertools::Itertools;
25use log::{info, warn};
26use regex::Regex;
27use serde_derive::Serialize;
28pub use tempfile::TempDir;
29
30use crate::c_ast::Printer;
31use crate::c_ast::*;
32pub use crate::diagnostics::Diagnostic;
33use c2rust_ast_exporter as ast_exporter;
34
35use crate::build_files::{emit_build_files, get_build_dir, CrateConfig};
36use crate::compile_cmds::get_compile_commands;
37use crate::convert_type::RESERVED_NAMES;
38pub use crate::translator::ReplaceMode;
39use std::prelude::v1::Vec;
40
41type PragmaVec = Vec<(&'static str, Vec<&'static str>)>;
42type PragmaSet = indexmap::IndexSet<(&'static str, &'static str)>;
43type CrateSet = indexmap::IndexSet<ExternCrate>;
44type TranspileResult = Result<(PathBuf, PragmaVec, CrateSet), ()>;
45
46#[derive(Default, Debug)]
47pub enum TranslateMacros {
48    /// Don't translate any macros.
49    None,
50
51    /// Translate the conservative subset of macros known to always work.
52    #[default]
53    Conservative,
54
55    /// Try to translate more, but this is experimental and not guaranteed to work.
56    ///
57    /// For const-like macros, this works in some cases.
58    /// For function-like macros, this doesn't really work at all yet.
59    Experimental,
60}
61
62/// Configuration settings for the translation process
63#[derive(Debug)]
64pub struct TranspilerConfig {
65    // Debug output options
66    pub dump_untyped_context: bool,
67    pub dump_typed_context: bool,
68    pub pretty_typed_context: bool,
69    pub dump_function_cfgs: bool,
70    pub json_function_cfgs: bool,
71    pub dump_cfg_liveness: bool,
72    pub dump_structures: bool,
73    pub verbose: bool,
74    pub debug_ast_exporter: bool,
75    pub emit_c_decl_map: bool,
76
77    // Options that control translation
78    pub incremental_relooper: bool,
79    pub fail_on_multiple: bool,
80    pub filter: Option<Regex>,
81    pub debug_relooper_labels: bool,
82    pub cross_checks: bool,
83    pub cross_check_backend: String,
84    pub cross_check_configs: Vec<String>,
85    pub prefix_function_names: Option<String>,
86    pub translate_asm: bool,
87    pub use_c_loop_info: bool,
88    pub use_c_multiple_info: bool,
89    pub simplify_structures: bool,
90    pub panic_on_translator_failure: bool,
91    pub emit_modules: bool,
92    pub fail_on_error: bool,
93    pub replace_unsupported_decls: ReplaceMode,
94    pub translate_valist: bool,
95    pub overwrite_existing: bool,
96    pub reduce_type_annotations: bool,
97    pub reorganize_definitions: bool,
98    pub enabled_warnings: HashSet<Diagnostic>,
99    pub emit_no_std: bool,
100    pub output_dir: Option<PathBuf>,
101    pub translate_const_macros: TranslateMacros,
102    pub translate_fn_macros: TranslateMacros,
103    pub disable_rustfmt: bool,
104    pub disable_refactoring: bool,
105    pub preserve_unused_functions: bool,
106    pub log_level: log::LevelFilter,
107
108    // Options that control build files
109    /// Emit `Cargo.toml` and `lib.rs`
110    pub emit_build_files: bool,
111
112    /// Names of translation units containing main functions that we should make
113    /// into binaries
114    pub binaries: Vec<String>,
115
116    pub c2rust_dir: Option<PathBuf>,
117}
118
119impl TranspilerConfig {
120    fn binary_name_from_path(file: &Path) -> String {
121        let file = Path::new(file.file_stem().unwrap());
122        get_module_name(file, false, false, false).unwrap()
123    }
124
125    fn is_binary(&self, file: &Path) -> bool {
126        let module_name = Self::binary_name_from_path(file);
127        self.binaries.contains(&module_name)
128    }
129
130    fn check_if_all_binaries_used(
131        &self,
132        transpiled_modules: impl IntoIterator<Item = impl AsRef<Path>>,
133    ) -> bool {
134        let module_names = transpiled_modules
135            .into_iter()
136            .map(|module| Self::binary_name_from_path(module.as_ref()))
137            .collect::<HashSet<_>>();
138        let mut ok = true;
139        for binary in &self.binaries {
140            if !module_names.contains(binary) {
141                ok = false;
142                warn!("binary not used: {binary}");
143            }
144        }
145        if !ok {
146            let module_names = module_names.iter().format(", ");
147            info!("candidate modules for binaries are: {module_names}");
148        }
149        ok
150    }
151
152    fn crate_name(&self) -> String {
153        self.output_dir
154            .as_ref()
155            .and_then(|dir| dir.file_name())
156            .map(|fname| str_to_ident_checked(fname.to_string_lossy().as_ref(), true))
157            .unwrap_or_else(|| "c2rust_out".into())
158    }
159}
160
161#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
162pub enum ExternCrate {
163    C2RustBitfields,
164    C2RustAsmCasts,
165    F128,
166    NumTraits,
167    Memoffset,
168    Libc,
169}
170
171#[derive(Serialize)]
172struct ExternCrateDetails {
173    name: &'static str,
174    ident: String,
175    macro_use: bool,
176    version: &'static str,
177    path: Option<PathBuf>,
178}
179
180impl ExternCrateDetails {
181    pub fn new(
182        name: &'static str,
183        version: &'static str,
184        macro_use: bool,
185        path: Option<PathBuf>,
186    ) -> Self {
187        Self {
188            name,
189            ident: name.replace('-', "_"),
190            macro_use,
191            version,
192            path,
193        }
194    }
195
196    /// An external (to c2rust) dependency.
197    pub fn external(name: &'static str, version: &'static str, macro_use: bool) -> Self {
198        Self::new(name, version, macro_use, None)
199    }
200
201    /// An internal (to c2rust) dependency.
202    pub fn internal(name: &'static str, macro_use: bool, c2rust_dir: Option<&Path>) -> Self {
203        Self::new(
204            name,
205            env!("CARGO_PKG_VERSION"),
206            macro_use,
207            c2rust_dir.map(|dir| dir.join(name)),
208        )
209    }
210}
211
212impl ExternCrate {
213    fn with_details(&self, c2rust_dir: Option<&Path>) -> ExternCrateDetails {
214        use ExternCrate::*;
215        match self {
216            C2RustBitfields => ExternCrateDetails::internal("c2rust-bitfields", true, c2rust_dir),
217            C2RustAsmCasts => ExternCrateDetails::internal("c2rust-asm-casts", true, c2rust_dir),
218            F128 => ExternCrateDetails::external("f128", "0.2", false),
219            NumTraits => ExternCrateDetails::external("num-traits", "0.2", true),
220            Memoffset => ExternCrateDetails::external("memoffset", "0.5", true),
221            Libc => ExternCrateDetails::external("libc", "0.2", false),
222        }
223    }
224}
225
226fn char_to_ident(c: char) -> char {
227    if c.is_alphanumeric() {
228        c
229    } else {
230        '_'
231    }
232}
233
234fn str_to_ident(s: &str) -> String {
235    s.chars().map(char_to_ident).collect()
236}
237
238/// Make sure that name:
239/// - does not contain illegal characters,
240/// - does not clash with reserved keywords.
241fn str_to_ident_checked(s: &str, check_reserved: bool) -> String {
242    let s = str_to_ident(s);
243
244    // make sure the name does not clash with keywords
245    if check_reserved && RESERVED_NAMES.contains(&s.as_str()) {
246        format!("r#{}", s)
247    } else {
248        s
249    }
250}
251
252fn get_module_name(
253    file: &Path,
254    check_reserved: bool,
255    keep_extension: bool,
256    full_path: bool,
257) -> Option<String> {
258    let is_rs = file.extension().map(|ext| ext == "rs").unwrap_or(false);
259    let fname = if is_rs {
260        file.file_stem()
261    } else {
262        file.file_name()
263    };
264    let fname = fname.unwrap().to_str().unwrap();
265    let mut name = str_to_ident_checked(fname, check_reserved);
266    if keep_extension && is_rs {
267        name.push_str(".rs");
268    }
269    let file = if full_path {
270        file.with_file_name(name)
271    } else {
272        Path::new(&name).to_path_buf()
273    };
274    file.to_str().map(String::from)
275}
276
277pub fn create_temp_compile_commands(sources: &[PathBuf]) -> (TempDir, PathBuf) {
278    // If we generate the same path here on every run, then we can't run
279    // multiple transpiles in parallel, so we need a unique path. But clang
280    // won't read this file unless it is named exactly "compile_commands.json",
281    // so we can't change the filename. Instead, create a temporary directory
282    // with a unique name, and put the file there.
283    let temp_dir = tempfile::Builder::new()
284        .prefix("c2rust-")
285        .tempdir()
286        .expect("Failed to create temporary directory for compile_commands.json");
287    let temp_path = temp_dir.path().join("compile_commands.json");
288
289    let compile_commands: Vec<CompileCmd> = sources
290        .iter()
291        .map(|source_file| {
292            let absolute_path = fs::canonicalize(source_file)
293                .unwrap_or_else(|_| panic!("Could not canonicalize {}", source_file.display()));
294
295            CompileCmd {
296                directory: PathBuf::from("."),
297                file: absolute_path.clone(),
298                arguments: vec![
299                    "clang".to_string(),
300                    absolute_path.to_str().unwrap().to_owned(),
301                ],
302                command: None,
303                output: None,
304            }
305        })
306        .collect();
307
308    let json_content = serde_json::to_string(&compile_commands).unwrap();
309    let mut file =
310        File::create(&temp_path).expect("Failed to create temporary compile_commands.json");
311    file.write_all(json_content.as_bytes())
312        .expect("Failed to write to temporary compile_commands.json");
313    (temp_dir, temp_path)
314}
315
316/// Main entry point to transpiler. Called from CLI tools with the result of
317/// clap::App::get_matches().
318pub fn transpile(tcfg: TranspilerConfig, cc_db: &Path, extra_clang_args: &[&str]) {
319    diagnostics::init(tcfg.enabled_warnings.clone(), tcfg.log_level);
320
321    let build_dir = get_build_dir(&tcfg, cc_db);
322
323    let lcmds = get_compile_commands(cc_db, &tcfg.filter).unwrap_or_else(|_| {
324        panic!(
325            "Could not parse compile commands from {}",
326            cc_db.to_string_lossy()
327        )
328    });
329
330    // Specify path to system include dir on macOS 10.14 and later. Disable the blocks extension.
331    let clang_args: Vec<String> = get_extra_args_macos();
332    let mut clang_args: Vec<&str> = clang_args.iter().map(AsRef::as_ref).collect();
333    clang_args.extend_from_slice(extra_clang_args);
334
335    let mut top_level_ccfg = None;
336    let mut workspace_members = vec![];
337    let mut num_transpiled_files = 0;
338    let mut transpiled_modules = Vec::new();
339
340    for lcmd in &lcmds {
341        let cmds = &lcmd.cmd_inputs;
342        let lcmd_name = lcmd
343            .output
344            .as_ref()
345            .map(|output| {
346                let output_path = Path::new(output);
347                output_path
348                    .file_stem()
349                    .unwrap()
350                    .to_str()
351                    .unwrap()
352                    .to_owned()
353            })
354            .unwrap_or_else(|| tcfg.crate_name());
355        let build_dir = if lcmd.top_level {
356            build_dir.to_path_buf()
357        } else {
358            build_dir.join(&lcmd_name)
359        };
360
361        // Compute the common ancestor of all input files
362        // FIXME: this is quadratic-time in the length of the ancestor path
363        let mut ancestor_path = cmds
364            .first()
365            .map(|cmd| {
366                let mut dir = cmd.abs_file();
367                dir.pop(); // discard the file part
368                dir
369            })
370            .unwrap_or_else(PathBuf::new);
371        if cmds.len() > 1 {
372            for cmd in &cmds[1..] {
373                let cmd_path = cmd.abs_file();
374                ancestor_path = ancestor_path
375                    .ancestors()
376                    .find(|a| cmd_path.starts_with(a))
377                    .map(ToOwned::to_owned)
378                    .unwrap_or_else(PathBuf::new);
379            }
380        }
381
382        let results = cmds
383            .iter()
384            .map(|cmd| {
385                transpile_single(
386                    &tcfg,
387                    &cmd.abs_file(),
388                    &ancestor_path,
389                    &build_dir,
390                    cc_db,
391                    &clang_args,
392                )
393            })
394            .collect::<Vec<TranspileResult>>();
395        let mut modules = vec![];
396        let mut modules_skipped = false;
397        let mut pragmas = PragmaSet::new();
398        let mut crates = CrateSet::new();
399        for res in results {
400            match res {
401                Ok((module, pragma_vec, crate_set)) => {
402                    modules.push(module);
403                    crates.extend(crate_set);
404
405                    num_transpiled_files += 1;
406                    for (key, vals) in pragma_vec {
407                        for val in vals {
408                            pragmas.insert((key, val));
409                        }
410                    }
411                }
412                Err(_) => {
413                    modules_skipped = true;
414                }
415            }
416        }
417        pragmas.sort();
418        crates.sort();
419
420        transpiled_modules.extend(modules.iter().cloned());
421
422        if tcfg.emit_build_files {
423            if modules_skipped {
424                // If we skipped a file, we may not have collected all required pragmas
425                warn!("Can't emit build files after incremental transpiler run; skipped.");
426                return;
427            }
428
429            let ccfg = CrateConfig {
430                crate_name: lcmd_name.clone(),
431                modules,
432                pragmas,
433                crates,
434                link_cmd: lcmd,
435            };
436            if lcmd.top_level {
437                top_level_ccfg = Some(ccfg);
438            } else {
439                let crate_file = emit_build_files(&tcfg, &build_dir, Some(ccfg), None);
440                reorganize_definitions(&tcfg, &build_dir, crate_file)
441                    .unwrap_or_else(|e| warn!("Reorganizing definitions failed: {}", e));
442                workspace_members.push(lcmd_name);
443            }
444        }
445    }
446
447    if num_transpiled_files == 0 {
448        warn!("No C files found in compile_commands.json; nothing to do.");
449        return;
450    }
451
452    if tcfg.emit_build_files {
453        let crate_file =
454            emit_build_files(&tcfg, &build_dir, top_level_ccfg, Some(workspace_members));
455        reorganize_definitions(&tcfg, &build_dir, crate_file)
456            .unwrap_or_else(|e| warn!("Reorganizing definitions failed: {}", e));
457    }
458
459    tcfg.check_if_all_binaries_used(&transpiled_modules);
460}
461
462/// Ensure that clang can locate the system headers on macOS 10.14+.
463///
464/// MacOS 10.14 does not have a `/usr/include` folder even if Xcode
465/// or the command line developer tools are installed as explained in
466/// this [thread](https://forums.developer.apple.com/thread/104296).
467/// It is possible to install a package which puts the headers in
468/// `/usr/include` but the user doesn't have to since we can find
469/// the system headers we need by running `xcrun --show-sdk-path`.
470fn get_extra_args_macos() -> Vec<String> {
471    let mut args = vec![];
472    if cfg!(target_os = "macos") {
473        let usr_incl = Path::new("/usr/include");
474        if !usr_incl.exists() {
475            let output = Command::new("xcrun")
476                .args(["--show-sdk-path"])
477                .output()
478                .expect("failed to run `xcrun` subcommand");
479            let mut sdk_path = String::from_utf8(output.stdout).unwrap();
480            let olen = sdk_path.len();
481            sdk_path.truncate(olen - 1);
482            sdk_path.push_str("/usr/include");
483
484            args.push("-isystem".to_owned());
485            args.push(sdk_path);
486        }
487
488        // disable Apple's blocks extension; see https://github.com/immunant/c2rust/issues/229
489        args.push("-fno-blocks".to_owned());
490    }
491    args
492}
493
494fn invoke_refactor(build_dir: &Path) -> Result<(), Error> {
495    // Make sure the crate builds cleanly
496    let status = Command::new("cargo")
497        .args(["check"])
498        .env("RUSTFLAGS", "-Awarnings")
499        .current_dir(build_dir)
500        .status()?;
501    if !status.success() {
502        return Err(failure::format_err!("Crate does not compile."));
503    }
504
505    // Assumes the subcommand executable is in the same directory as this program.
506    let refactor = env::current_exe()
507        .expect("Cannot get current executable path")
508        .with_file_name("c2rust-refactor");
509    let args = [
510        "--cargo",
511        "--rewrite-mode",
512        "inplace",
513        "rename_unnamed",
514        ";",
515        "reorganize_definitions",
516    ];
517    let status = Command::new(&refactor)
518        .args(args)
519        .current_dir(build_dir)
520        .status()
521        .map_err(|e| {
522            let refactor = refactor.display();
523            failure::format_err!("unable to run {refactor}: {e}\nNote that c2rust-refactor must be installed separately from c2rust and c2rust-transpile.")
524        })?;
525    if status.success() {
526        Ok(())
527    } else {
528        Err(failure::format_err!(
529            "Refactoring failed. Please fix errors above and re-run:\n    c2rust refactor {}",
530            args.join(" "),
531        ))
532    }
533}
534
535fn reorganize_definitions(
536    tcfg: &TranspilerConfig,
537    build_dir: &Path,
538    crate_file: Option<PathBuf>,
539) -> Result<(), Error> {
540    // We only run the reorganization refactoring if we emitted a fresh crate file
541    if crate_file.is_none() || tcfg.disable_refactoring || !tcfg.reorganize_definitions {
542        return Ok(());
543    }
544
545    invoke_refactor(build_dir)?;
546
547    if !tcfg.disable_rustfmt {
548        // fix the formatting of the output of `c2rust-refactor`
549        let status = Command::new("cargo")
550            .args(["fmt"])
551            .current_dir(build_dir)
552            .status()?;
553        if !status.success() {
554            warn!("cargo fmt failed, code may not be well-formatted");
555        }
556    }
557
558    Ok(())
559}
560
561fn transpile_single(
562    tcfg: &TranspilerConfig,
563    input_path: &Path,
564    ancestor_path: &Path,
565    build_dir: &Path,
566    cc_db: &Path,
567    extra_clang_args: &[&str],
568) -> TranspileResult {
569    let output_path = get_output_path(tcfg, input_path, ancestor_path, build_dir);
570    if output_path.exists() && !tcfg.overwrite_existing {
571        warn!("Skipping existing file {}", output_path.display());
572        return Err(());
573    }
574
575    let file = input_path.file_name().unwrap().to_str().unwrap();
576    if !input_path.exists() {
577        warn!(
578            "Input C file {} does not exist, skipping!",
579            input_path.display()
580        );
581        return Err(());
582    }
583
584    if tcfg.verbose {
585        println!("Additional Clang arguments: {}", extra_clang_args.join(" "));
586    }
587
588    // Extract the untyped AST from the CBOR file
589    let untyped_context = match ast_exporter::get_untyped_ast(
590        input_path,
591        cc_db,
592        extra_clang_args,
593        tcfg.debug_ast_exporter,
594    ) {
595        Err(e) => {
596            warn!(
597                "Error: {}. Skipping {}; is it well-formed C?",
598                e,
599                input_path.display()
600            );
601            return Err(());
602        }
603        Ok(cxt) => cxt,
604    };
605
606    println!("Transpiling {}", file);
607
608    if tcfg.dump_untyped_context {
609        println!("CBOR Clang AST");
610        println!("{:#?}", untyped_context);
611    }
612
613    // Convert this into a typed AST
614    let typed_context = {
615        let conv = ConversionContext::new(input_path, &untyped_context);
616        if conv.invalid_clang_ast && tcfg.fail_on_error {
617            panic!("Clang AST was invalid");
618        }
619        conv.into_typed_context()
620    };
621
622    if tcfg.dump_typed_context {
623        println!("Clang AST");
624        println!("{:#?}", typed_context);
625    }
626
627    if tcfg.pretty_typed_context {
628        println!("Pretty-printed Clang AST");
629        println!("{:#?}", Printer::new(io::stdout()).print(&typed_context));
630    }
631
632    // Perform the translation
633    let (translated_string, maybe_decl_map, pragmas, crates) =
634        translator::translate(typed_context, tcfg, input_path);
635
636    if let Some(decl_map) = maybe_decl_map {
637        let decl_map_path = output_path.with_extension("c_decls.json");
638        let file = match File::create(&decl_map_path) {
639            Ok(file) => file,
640            Err(e) => panic!(
641                "Unable to open file {} for writing: {}",
642                output_path.display(),
643                e
644            ),
645        };
646
647        match serde_json::ser::to_writer(file, &decl_map) {
648            Ok(()) => (),
649            Err(e) => panic!(
650                "Unable to write C declaration map to file {}: {}",
651                output_path.display(),
652                e
653            ),
654        };
655    }
656
657    let mut file = match File::create(&output_path) {
658        Ok(file) => file,
659        Err(e) => panic!(
660            "Unable to open file {} for writing: {}",
661            output_path.display(),
662            e
663        ),
664    };
665
666    match file.write_all(translated_string.as_bytes()) {
667        Ok(()) => (),
668        Err(e) => panic!(
669            "Unable to write translation to file {}: {}",
670            output_path.display(),
671            e
672        ),
673    };
674
675    if !tcfg.disable_rustfmt {
676        rustfmt(&output_path, build_dir);
677    }
678
679    Ok((output_path, pragmas, crates))
680}
681
682fn get_output_path(
683    tcfg: &TranspilerConfig,
684    input_path: &Path,
685    ancestor_path: &Path,
686    build_dir: &Path,
687) -> PathBuf {
688    // When an output file name is not explicitly specified, we should convert files
689    // with dashes to underscores, as they are not allowed in rust file names.
690    let file_name = input_path
691        .file_name()
692        .unwrap()
693        .to_str()
694        .unwrap()
695        .replace('-', "_");
696
697    let mut input_path = input_path.with_file_name(file_name);
698    input_path.set_extension("rs");
699
700    if tcfg.output_dir.is_some() {
701        let path_buf = input_path
702            .strip_prefix(ancestor_path)
703            .expect("Couldn't strip common ancestor path");
704
705        // Place the source files in build_dir/src/
706        let mut output_path = build_dir.to_path_buf();
707        output_path.push("src");
708        for elem in path_buf.iter() {
709            let path = Path::new(elem);
710            let name = get_module_name(path, false, true, false).unwrap();
711            output_path.push(name);
712        }
713
714        // Create the parent directory if it doesn't exist
715        let parent = output_path.parent().unwrap();
716        if !parent.exists() {
717            fs::create_dir_all(parent).unwrap_or_else(|_| {
718                panic!("couldn't create source directory: {}", parent.display())
719            });
720        }
721        output_path
722    } else {
723        input_path
724    }
725}
726
727fn rustfmt(output_path: &Path, build_dir: &Path) {
728    let edition = "2021";
729
730    let status = Command::new("rustfmt")
731        .args(["--edition", edition])
732        .arg(output_path)
733        .current_dir(build_dir)
734        .status();
735
736    if !status.map_or(false, |status| status.success()) {
737        warn!("rustfmt failed, code may not be well-formatted");
738    }
739}