inline_c/
run.rs

1use crate::assert::Assert;
2use lazy_static::lazy_static;
3use regex::Regex;
4use std::{
5    borrow::Cow, collections::HashMap, env, error::Error, ffi::OsString, io::prelude::*,
6    path::PathBuf, process::Command,
7};
8
9#[doc(hidden)]
10pub enum Language {
11    C,
12    Cxx,
13}
14
15impl ToString for Language {
16    fn to_string(&self) -> String {
17        match self {
18            Self::C => String::from("c"),
19            Self::Cxx => String::from("cpp"),
20        }
21    }
22}
23
24#[doc(hidden)]
25pub fn run(language: Language, program: &str) -> Result<Assert, Box<dyn Error>> {
26    let (program, variables) = collect_environment_variables(program);
27
28    let mut program_file = tempfile::Builder::new()
29        .prefix("inline-c-rs-")
30        .suffix(&format!(".{}", language.to_string()))
31        .tempfile()?;
32    program_file.write_all(program.as_bytes())?;
33
34    let host = target_lexicon::HOST.to_string();
35    let target = &host;
36
37    let msvc = target.contains("msvc");
38
39    let (_, input_path) = program_file.keep()?;
40    let mut output_temp = tempfile::Builder::new();
41    let output_temp = output_temp.prefix("inline-c-rs-");
42
43    if msvc {
44        output_temp.suffix(".exe");
45    }
46
47    let (_, output_path) = output_temp.tempfile()?.keep()?;
48
49    let mut build = cc::Build::new();
50    let mut build = build
51        .cargo_metadata(false)
52        .warnings(true)
53        .extra_warnings(true)
54        .warnings_into_errors(true)
55        .debug(false)
56        .host(&host)
57        .target(target)
58        .opt_level(1);
59
60    if let Language::Cxx = language {
61        build = build.cpp(true);
62    }
63
64    // Usually, `cc-rs` is used to produce libraries. In our case, we
65    // want to produce an (executable) object file. The following code
66    // is kind of a hack around `cc-rs`. It avoids the addition of the
67    // `-c` argument on the compiler, and manually adds other
68    // arguments.
69
70    let compiler = build.try_get_compiler()?;
71    let mut command;
72
73    if msvc {
74        command = compiler.to_command();
75
76        command_add_compiler_flags(&mut command, &variables, msvc);
77        command_add_output_file(&mut command, &output_path, msvc, compiler.is_like_clang());
78        command.arg(input_path.clone());
79        command_add_linker_flags_msvc(&mut command, &variables);
80    } else {
81        command = Command::new(compiler.path());
82
83        command.arg(input_path.clone()); // the input must come first
84        command.args(compiler.args());
85        command_add_compiler_flags(&mut command, &variables, msvc);
86        command_add_output_file(&mut command, &output_path, msvc, compiler.is_like_clang());
87    }
88
89    command.envs(variables.clone());
90
91    let mut files_to_remove = vec![input_path, output_path.clone()];
92    if msvc {
93        let mut intermediate_path = output_path.clone();
94        intermediate_path.set_extension("obj");
95        files_to_remove.push(intermediate_path);
96    }
97
98    let clang_output = command.output()?;
99
100    if !clang_output.status.success() {
101        return Ok(Assert::new(command, Some(files_to_remove)));
102    }
103
104    let mut command = Command::new(output_path);
105    command.envs(variables);
106
107    Ok(Assert::new(command, Some(files_to_remove)))
108}
109
110fn collect_environment_variables<'p>(program: &'p str) -> (Cow<'p, str>, HashMap<String, String>) {
111    const ENV_VAR_PREFIX: &str = "INLINE_C_RS_";
112
113    lazy_static! {
114        static ref REGEX: Regex = Regex::new(
115            r#"#inline_c_rs (?P<variable_name>[^:]+):\s*"(?P<variable_value>[^"]+)"\r?\n"#
116        )
117        .unwrap();
118    }
119
120    let mut variables = HashMap::new();
121
122    for (variable_name, variable_value) in env::vars().filter_map(|(mut name, value)| {
123        if name.starts_with(ENV_VAR_PREFIX) {
124            Some((name.split_off(ENV_VAR_PREFIX.len()), value))
125        } else {
126            None
127        }
128    }) {
129        variables.insert(variable_name, variable_value);
130    }
131
132    for captures in REGEX.captures_iter(program) {
133        variables.insert(
134            captures["variable_name"].trim().to_string(),
135            captures["variable_value"].to_string(),
136        );
137    }
138
139    let program = REGEX.replace_all(program, "");
140
141    (program, variables)
142}
143
144// This is copy-pasted and edited from `cc-rs`.
145fn command_add_output_file(command: &mut Command, output_path: &PathBuf, msvc: bool, clang: bool) {
146    if msvc && !clang {
147        let mut intermediate_path = output_path.clone();
148        intermediate_path.set_extension("obj");
149
150        let mut fo_arg = OsString::from("-Fo");
151        fo_arg.push(intermediate_path);
152        command.arg(fo_arg);
153
154        let mut fe_arg = OsString::from("-Fe");
155        fe_arg.push(output_path);
156        command.arg(fe_arg);
157    } else {
158        command.arg("-o").arg(output_path);
159    }
160}
161
162fn get_env_flags(variables: &HashMap<String, String>, env_name: &str) -> Vec<String> {
163    variables
164        .get(env_name)
165        .map(|e| e.to_string())
166        .ok_or_else(|| env::var(env_name))
167        .unwrap_or_default()
168        .split_ascii_whitespace()
169        .map(|slice| slice.to_string())
170        .collect()
171}
172
173fn command_add_compiler_flags(
174    command: &mut Command,
175    variables: &HashMap<String, String>,
176    msvc: bool,
177) {
178    command.args(get_env_flags(variables, "CFLAGS"));
179    command.args(get_env_flags(variables, "CPPFLAGS"));
180    command.args(get_env_flags(variables, "CXXFLAGS"));
181
182    if !msvc {
183        for linker_argument in get_env_flags(variables, "LDFLAGS") {
184            command.arg(format!("-Wl,{}", linker_argument));
185        }
186    }
187}
188
189fn command_add_linker_flags_msvc(command: &mut Command, variables: &HashMap<String, String>) {
190    let linker_flags = get_env_flags(variables, "LDFLAGS");
191    if !linker_flags.is_empty() {
192        command.arg("/link");
193        for linker_argument in linker_flags {
194            command.arg(linker_argument);
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::predicates::*;
203
204    #[test]
205    fn test_run_c() {
206        run(
207            Language::C,
208            r#"
209                #include <stdio.h>
210
211                int main() {
212                    printf("Hello, World!\n");
213
214                    return 0;
215                }
216            "#,
217        )
218        .unwrap()
219        .success()
220        .stdout(predicate::eq("Hello, World!\n").normalize());
221    }
222
223    #[cfg(not(target_os = "macos"))] // macOS doesn't support `--defsym`
224    #[test]
225    fn test_run_c_ldflags() {
226        use std::env::{remove_var, set_var};
227
228        let host = target_lexicon::HOST.to_string();
229        let msvc = host.contains("msvc");
230        if msvc {
231            // Use a linker flag to set the stack size and run a program that
232            // outputs its stack size. If the linker flags aren't set
233            // properly, the program will output the wrong value.
234
235            set_var("INLINE_C_RS_LDFLAGS", "/STACK:0x40000");
236            run(
237                Language::C,
238                r#"
239                #include <Windows.h>
240
241                #include <stdio.h>
242
243                int main() {
244                    ULONG_PTR stack_low_limit, stack_high_limit;
245                    GetCurrentThreadStackLimits(&stack_low_limit, &stack_high_limit);
246                    printf("%#llx", stack_high_limit-stack_low_limit);
247
248                    return 0;
249                }
250            "#,
251            )
252            .unwrap()
253            .success()
254            .stdout(predicate::eq("0x40000").normalize());
255
256            remove_var("INLINE_C_RS_LDFLAGS");
257        } else {
258            // Introduces a symbol using linker flags and builds a program that
259            // ODR uses that symbol. If the linker flags aren't set properly,
260            // there will be a linker error.
261
262            set_var("INLINE_C_RS_LDFLAGS", "--defsym MY_SYMBOL=0x0");
263            run(
264                Language::C,
265                r#"
266                #include <stdio.h>
267
268                extern void* MY_SYMBOL;
269
270                int main() {
271                    printf("%p", MY_SYMBOL);
272                    return 0;
273                }
274            "#,
275            )
276            .unwrap()
277            .success();
278
279            remove_var("INLINE_C_RS_LDFLAGS");
280        }
281    }
282
283    #[test]
284    fn test_run_cxx() {
285        run(
286            Language::Cxx,
287            r#"
288                #include <stdio.h>
289
290                int main() {
291                    printf("Hello, World!\n");
292
293                    return 0;
294                }
295            "#,
296        )
297        .unwrap()
298        .success()
299        .stdout(predicate::eq("Hello, World!\n").normalize());
300    }
301}