libgir/codegen/sys/
tests.rs

1use std::{
2    io::{self, prelude::*},
3    path::Path,
4};
5
6use log::info;
7
8use crate::{
9    analysis::types::IsIncomplete,
10    codegen::general,
11    config::gobjects::GStatus,
12    env::Env,
13    file_saver::save_to_file,
14    library::{self, Bitfield, Enumeration, Namespace, Type, MAIN_NAMESPACE},
15};
16
17#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
18struct CType {
19    /// Name of type, as used in C.
20    name: String,
21    /// Expression describing when type is available (when defined only
22    /// conditionally).
23    cfg_condition: Option<String>,
24}
25
26#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
27struct CConstant {
28    /// Identifier in C.
29    name: String,
30    /// Stringified value.
31    value: String,
32    status: GStatus,
33}
34
35pub fn generate(env: &Env, crate_name: &str) {
36    let ctypes = prepare_ctypes(env);
37    let cconsts = prepare_cconsts(env);
38
39    if ctypes.is_empty() && cconsts.is_empty() {
40        return;
41    }
42
43    let tests = env.config.target_path.join("tests");
44
45    let manual_h = tests.join("manual.h");
46    if !manual_h.exists() {
47        save_to_file(&manual_h, env.config.make_backup, |w| {
48            generate_manual_h(env, &manual_h, w)
49        });
50    }
51
52    let layout_c = tests.join("layout.c");
53    save_to_file(&layout_c, env.config.make_backup, |w| {
54        generate_layout_c(env, &layout_c, w, &ctypes)
55    });
56
57    let constant_c = tests.join("constant.c");
58    save_to_file(&constant_c, env.config.make_backup, |w| {
59        generate_constant_c(env, &constant_c, w, &cconsts)
60    });
61
62    let abi_rs = tests.join("abi.rs");
63    save_to_file(&abi_rs, env.config.make_backup, |w| {
64        generate_abi_rs(env, &abi_rs, w, crate_name, &ctypes, &cconsts)
65    });
66}
67
68fn prepare_ctypes(env: &Env) -> Vec<CType> {
69    let ns = env.library.namespace(MAIN_NAMESPACE);
70    let mut types: Vec<CType> = ns
71        .types
72        .iter()
73        .filter_map(Option::as_ref)
74        .filter(|t| !t.is_incomplete(&env.library))
75        .filter_map(|t| match t {
76            Type::Record(library::Record {
77                disguised: false, ..
78            }) => prepare_ctype(env, ns, t),
79            Type::Alias(_)
80            | Type::Class(_)
81            | Type::Union(_)
82            | Type::Enumeration(_)
83            | Type::Bitfield(_)
84            | Type::Interface(_) => prepare_ctype(env, ns, t),
85            _ => None,
86        })
87        .collect();
88
89    types.sort();
90    types
91}
92
93fn prepare_ctype(env: &Env, ns: &Namespace, t: &Type) -> Option<CType> {
94    let full_name = format!("{}.{}", ns.name, t.get_name());
95    if env.type_status_sys(&full_name).ignored() {
96        return None;
97    }
98    let name = t.get_glib_name()?;
99
100    if is_name_made_up(name) {
101        return None;
102    }
103    let object = env.config.objects.get(&full_name);
104    Some(CType {
105        name: name.to_owned(),
106        cfg_condition: object.and_then(|obj| obj.cfg_condition.clone()),
107    })
108}
109
110fn prepare_cconsts(env: &Env) -> Vec<CConstant> {
111    let ns = env.library.namespace(MAIN_NAMESPACE);
112    let mut constants: Vec<CConstant> = ns
113        .constants
114        .iter()
115        .filter_map(|constant| {
116            let full_name = format!("{}.{}", &ns.name, constant.name);
117            if env.type_status_sys(&full_name).ignored() {
118                return None;
119            }
120            let value = match constant {
121                c if c.c_type == "gboolean" && c.value == "true" => "1",
122                c if c.c_type == "gboolean" && c.value == "false" => "0",
123                c => &c.value,
124            };
125            Some(CConstant {
126                name: constant.c_identifier.clone(),
127                value: value.to_owned(),
128                status: GStatus::Generate, // Assume generate because we skip ignored ones above
129            })
130        })
131        .collect();
132
133    for typ in &ns.types {
134        let typ = if let Some(typ) = typ {
135            typ
136        } else {
137            continue;
138        };
139        let full_name = format!("{}.{}", &ns.name, typ.get_name());
140        if env.type_status_sys(&full_name).ignored() {
141            continue;
142        }
143        match typ {
144            Type::Bitfield(Bitfield { members, .. }) => {
145                for member in members {
146                    // GLib assumes that bitflags are unsigned integers,
147                    // see the GValue machinery around them for example
148                    constants.push(CConstant {
149                        name: format!("(guint) {}", member.c_identifier),
150                        value: member
151                            .value
152                            .parse::<i32>()
153                            .map(|i| (i as u32).to_string())
154                            .unwrap_or_else(|_| member.value.clone()),
155                        status: member.status,
156                    });
157                }
158            }
159            Type::Enumeration(Enumeration { members, .. }) => {
160                for member in members {
161                    // GLib assumes that enums are signed integers,
162                    // see the GValue machinery around them for example
163                    constants.push(CConstant {
164                        name: format!("(gint) {}", member.c_identifier),
165                        value: member.value.clone(),
166                        status: member.status,
167                    });
168                }
169            }
170            _ => {}
171        }
172    }
173
174    constants.sort_by(|a, b| {
175        fn strip_cast(x: &CConstant) -> &str {
176            if x.name.starts_with("(gint) ") {
177                &x.name[7..]
178            } else if x.name.starts_with("(guint) ") {
179                &x.name[8..]
180            } else {
181                x.name.as_str()
182            }
183        }
184
185        strip_cast(a).cmp(strip_cast(b))
186    });
187    constants
188}
189
190/// Checks if type name is unlikely to correspond to a real C type name.
191fn is_name_made_up(name: &str) -> bool {
192    // Unnamed types are assigned name during parsing, those names contain an
193    // underscore.
194    name.contains('_') && !name.ends_with("_t")
195}
196
197fn generate_manual_h(env: &Env, path: &Path, w: &mut dyn Write) -> io::Result<()> {
198    info!("Generating file {:?}", path);
199    writeln!(
200        w,
201        "// Feel free to edit this file, it won't be regenerated by gir generator unless removed."
202    )?;
203    writeln!(w)?;
204
205    let ns = env.library.namespace(MAIN_NAMESPACE);
206    for include in &ns.c_includes {
207        writeln!(w, "#include <{include}>")?;
208    }
209
210    Ok(())
211}
212
213#[allow(clippy::write_literal)]
214fn generate_layout_c(
215    env: &Env,
216    path: &Path,
217    w: &mut dyn Write,
218    ctypes: &[CType],
219) -> io::Result<()> {
220    info!("Generating file {:?}", path);
221    general::start_comments(w, &env.config)?;
222    writeln!(w)?;
223    writeln!(w, "#include \"manual.h\"")?;
224    writeln!(w, "#include <stdalign.h>")?;
225    writeln!(w, "#include <stdio.h>")?;
226    writeln!(w)?;
227    writeln!(w, "{}", r"int main() {")?;
228
229    for ctype in ctypes {
230        writeln!(
231            w,
232            "    printf(\"%s;%zu;%zu\\n\", \"{ctype}\", sizeof({ctype}), alignof({ctype}));",
233            ctype = ctype.name
234        )?;
235    }
236
237    writeln!(w, "    return 0;")?;
238    writeln!(w, "{}", r"}")
239}
240
241#[allow(clippy::write_literal)]
242fn generate_constant_c(
243    env: &Env,
244    path: &Path,
245    w: &mut dyn Write,
246    cconsts: &[CConstant],
247) -> io::Result<()> {
248    info!("Generating file {:?}", path);
249    general::start_comments(w, &env.config)?;
250    writeln!(w)?;
251    writeln!(w, "#include \"manual.h\"")?;
252    writeln!(w, "#include <stdio.h>")?;
253    writeln!(
254        w,
255        "{}",
256        r#"
257#define PRINT_CONSTANT(CONSTANT_NAME) \
258    printf("%s;", #CONSTANT_NAME); \
259    printf(_Generic((CONSTANT_NAME), \
260                    char *: "%s", \
261                    const char *: "%s", \
262                    char: "%c", \
263                    signed char: "%hhd", \
264                    unsigned char: "%hhu", \
265                    short int: "%hd", \
266                    unsigned short int: "%hu", \
267                    int: "%d", \
268                    unsigned int: "%u", \
269                    long: "%ld", \
270                    unsigned long: "%lu", \
271                    long long: "%lld", \
272                    unsigned long long: "%llu", \
273                    float: "%f", \
274                    double: "%f", \
275                    long double: "%ld"), \
276           CONSTANT_NAME); \
277    printf("\n");
278"#
279    )?;
280
281    writeln!(w, "{}", r"int main() {")?;
282
283    for cconst in cconsts {
284        if cconst.status.ignored() {
285            continue;
286        }
287        writeln!(w, "    PRINT_CONSTANT({name});", name = cconst.name,)?;
288    }
289
290    writeln!(w, "    return 0;")?;
291    writeln!(w, "{}", r"}")
292}
293
294#[allow(clippy::write_literal)]
295fn generate_abi_rs(
296    env: &Env,
297    path: &Path,
298    w: &mut dyn Write,
299    crate_name: &str,
300    ctypes: &[CType],
301    cconsts: &[CConstant],
302) -> io::Result<()> {
303    let ns = env.library.namespace(MAIN_NAMESPACE);
304    let mut package_names = ns.package_names.join("\", \"");
305    if !package_names.is_empty() {
306        package_names = format!("\"{package_names}\"");
307    }
308
309    info!("Generating file {:?}", path);
310    general::start_comments(w, &env.config)?;
311    writeln!(w)?;
312    writeln!(w, "#![cfg(unix)]")?;
313    writeln!(w)?;
314
315    if !ctypes.is_empty() {
316        writeln!(w, "use {crate_name}::*;")?;
317        writeln!(w, "use std::mem::{{align_of, size_of}};")?;
318    }
319
320    writeln!(w, "use std::env;")?;
321    writeln!(w, "use std::error::Error;")?;
322    writeln!(w, "use std::ffi::OsString;")?;
323    writeln!(w, "use std::path::Path;")?;
324    writeln!(w, "use std::process::{{Command, Stdio}};")?;
325    writeln!(w, "use std::str;")?;
326    writeln!(w, "use tempfile::Builder;")?;
327    writeln!(w)?;
328    writeln!(w, "static PACKAGES: &[&str] = &[{package_names}];")?;
329    writeln!(
330        w,
331        "{}",
332        r#"
333#[derive(Clone, Debug)]
334struct Compiler {
335    pub args: Vec<String>,
336}
337
338impl Compiler {
339    pub fn new() -> Result<Self, Box<dyn Error>> {
340        let mut args = get_var("CC", "cc")?;
341        args.push("-Wno-deprecated-declarations".to_owned());
342        // For _Generic
343        args.push("-std=c11".to_owned());
344        // For %z support in printf when using MinGW.
345        args.push("-D__USE_MINGW_ANSI_STDIO".to_owned());
346        args.extend(get_var("CFLAGS", "")?);
347        args.extend(get_var("CPPFLAGS", "")?);
348        args.extend(pkg_config_cflags(PACKAGES)?);
349        Ok(Self { args })
350    }
351
352    pub fn compile(&self, src: &Path, out: &Path) -> Result<(), Box<dyn Error>> {
353        let mut cmd = self.to_command();
354        cmd.arg(src);
355        cmd.arg("-o");
356        cmd.arg(out);
357        let status = cmd.spawn()?.wait()?;
358        if !status.success() {
359            return Err(format!("compilation command {cmd:?} failed, {status}").into());
360        }
361        Ok(())
362    }
363
364    fn to_command(&self) -> Command {
365        let mut cmd = Command::new(&self.args[0]);
366        cmd.args(&self.args[1..]);
367        cmd
368    }
369}
370
371fn get_var(name: &str, default: &str) -> Result<Vec<String>, Box<dyn Error>> {
372    match env::var(name) {
373        Ok(value) => Ok(shell_words::split(&value)?),
374        Err(env::VarError::NotPresent) => Ok(shell_words::split(default)?),
375        Err(err) => Err(format!("{name} {err}").into()),
376    }
377}
378
379fn pkg_config_cflags(packages: &[&str]) -> Result<Vec<String>, Box<dyn Error>> {
380    if packages.is_empty() {
381        return Ok(Vec::new());
382    }
383    let pkg_config = env::var_os("PKG_CONFIG")
384        .unwrap_or_else(|| OsString::from("pkg-config"));
385    let mut cmd = Command::new(pkg_config);
386    cmd.arg("--cflags");
387    cmd.args(packages);
388    cmd.stderr(Stdio::inherit());
389    let out = cmd.output()?;
390    if !out.status.success() {
391        let (status, stdout) = (out.status, String::from_utf8_lossy(&out.stdout));
392        return Err(format!("command {cmd:?} failed, {status:?}\nstdout: {stdout}").into());
393    }
394    let stdout = str::from_utf8(&out.stdout)?;
395    Ok(shell_words::split(stdout.trim())?)
396}
397
398
399#[derive(Copy, Clone, Debug, Eq, PartialEq)]
400struct Layout {
401    size: usize,
402    alignment: usize,
403}
404
405#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
406struct Results {
407    /// Number of successfully completed tests.
408    passed: usize,
409    /// Total number of failed tests (including those that failed to compile).
410    failed: usize,
411}
412
413impl Results {
414    fn record_passed(&mut self) {
415        self.passed += 1;
416    }
417    fn record_failed(&mut self) {
418        self.failed += 1;
419    }
420    fn summary(&self) -> String {
421        format!("{} passed; {} failed", self.passed, self.failed)
422    }
423    fn expect_total_success(&self) {
424        if self.failed == 0 {
425            println!("OK: {}", self.summary());
426        } else {
427            panic!("FAILED: {}", self.summary());
428        };
429    }
430}
431
432#[test]
433fn cross_validate_constants_with_c() {
434    let mut c_constants: Vec<(String, String)> = Vec::new();
435
436    for l in get_c_output("constant").unwrap().lines() {
437        let (name, value) = l.split_once(';').expect("Missing ';' separator");
438        c_constants.push((name.to_owned(), value.to_owned()));
439    }
440
441    let mut results = Results::default();
442
443    for ((rust_name, rust_value), (c_name, c_value)) in
444        RUST_CONSTANTS.iter().zip(c_constants.iter())
445    {
446        if rust_name != c_name {
447            results.record_failed();
448            eprintln!("Name mismatch:\nRust: {rust_name:?}\nC:    {c_name:?}");
449            continue;
450        }
451
452        if rust_value != c_value {
453            results.record_failed();
454            eprintln!(
455                "Constant value mismatch for {rust_name}\nRust: {rust_value:?}\nC:    {c_value:?}",
456            );
457            continue;
458        }
459
460        results.record_passed();
461    }
462
463    results.expect_total_success();
464}
465
466#[test]
467fn cross_validate_layout_with_c() {
468    let mut c_layouts = Vec::new();
469
470    for l in get_c_output("layout").unwrap().lines() {
471        let (name, value) = l.split_once(';').expect("Missing first ';' separator");
472        let (size, alignment) = value.split_once(';').expect("Missing second ';' separator");
473        let size = size.parse().expect("Failed to parse size");
474        let alignment = alignment.parse().expect("Failed to parse alignment");
475        c_layouts.push((name.to_owned(), Layout { size, alignment }));
476    }
477
478    let mut results = Results::default();
479
480    for ((rust_name, rust_layout), (c_name, c_layout)) in
481        RUST_LAYOUTS.iter().zip(c_layouts.iter())
482    {
483        if rust_name != c_name {
484            results.record_failed();
485            eprintln!("Name mismatch:\nRust: {rust_name:?}\nC:    {c_name:?}");
486            continue;
487        }
488
489        if rust_layout != c_layout {
490            results.record_failed();
491            eprintln!(
492                "Layout mismatch for {rust_name}\nRust: {rust_layout:?}\nC:    {c_layout:?}",
493            );
494            continue;
495        }
496
497        results.record_passed();
498    }
499
500    results.expect_total_success();
501}
502
503fn get_c_output(name: &str) -> Result<String, Box<dyn Error>> {
504    let tmpdir = Builder::new().prefix("abi").tempdir()?;
505    let exe = tmpdir.path().join(name);
506    let c_file = Path::new("tests").join(name).with_extension("c");
507
508    let cc = Compiler::new().expect("configured compiler");
509    cc.compile(&c_file, &exe)?;
510
511    let mut cmd = Command::new(exe);
512    cmd.stderr(Stdio::inherit());
513    let out = cmd.output()?;
514    if !out.status.success() {
515        let (status, stdout) = (out.status, String::from_utf8_lossy(&out.stdout));
516        return Err(format!("command {cmd:?} failed, {status:?}\nstdout: {stdout}").into());
517    }
518
519    Ok(String::from_utf8(out.stdout)?)
520}
521
522const RUST_LAYOUTS: &[(&str, Layout)] = &["#
523    )?;
524    for ctype in ctypes {
525        general::cfg_condition(w, ctype.cfg_condition.as_ref(), false, 1)?;
526        writeln!(w, "    (\"{ctype}\", Layout {{size: size_of::<{ctype}>(), alignment: align_of::<{ctype}>()}}),",
527                 ctype=ctype.name)?;
528    }
529    writeln!(
530        w,
531        "{}",
532        r#"];
533
534const RUST_CONSTANTS: &[(&str, &str)] = &["#
535    )?;
536    for cconst in cconsts {
537        if cconst.status.ignored() {
538            continue;
539        }
540        writeln!(
541            w,
542            "    (\"{name}\", \"{value}\"),",
543            name = cconst.name,
544            value = &general::escape_string(&cconst.value)
545        )?;
546    }
547    writeln!(
548        w,
549        "{}",
550        r#"];
551
552"#
553    )
554}