diff --git a/appveyor.yml b/appveyor.yml index 4c277a63e..2d6f15f95 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,7 +14,7 @@ install: - SET LIBGIT2_SYS_USE_PKG_CONFIG=1 - rustc -Vv - cargo -Vv - - pacman --noconfirm -S mingw-w64-%ARCH%-gtk3 mingw-w64-%ARCH%-libgit2 + - pacman --noconfirm -S mingw-w64-%ARCH%-gtk3 mingw-w64-%ARCH%-libgit2 mingw-w64-%ARCH%-atk build_script: - git clone -q https://github.com/gtk-rs/gir-files tests/gir-files @@ -25,5 +25,7 @@ build_script: - cd sys_build - cargo build - cargo build --features v3_20 + - cd ../atk-sys + - cargo test test: false diff --git a/src/analysis/types.rs b/src/analysis/types.rs index 7636c02c6..bbc140879 100644 --- a/src/analysis/types.rs +++ b/src/analysis/types.rs @@ -133,11 +133,11 @@ impl IsIncomplete for Type { Type::Class(ref klass) => klass.is_incomplete(lib), Type::Record(ref record) => record.is_incomplete(lib), Type::Union(ref union) => union.is_incomplete(lib), + Type::Interface(..) => true, Type::Custom(..) | Type::Enumeration(..) | Type::Bitfield(..) | Type::Function(..) | - Type::Interface(..) | Type::Array(..) | Type::CArray(..) | Type::PtrArray(..) | @@ -189,12 +189,12 @@ impl IsExternal for Type { Type::Class(ref klass) => klass.is_external(lib), Type::Record(ref record) => record.is_external(lib), Type::Union(ref union) => union.is_external(lib), + Type::Interface(..) => true, Type::Custom(..) | Type::Fundamental(..) | Type::Enumeration(..) | Type::Bitfield(..) | Type::Function(..) | - Type::Interface(..) | Type::Array(..) | Type::CArray(..) | Type::FixedArray(..) | diff --git a/src/codegen/sys/cargo_toml.rs b/src/codegen/sys/cargo_toml.rs index e2fd792a8..ab24fe2c1 100644 --- a/src/codegen/sys/cargo_toml.rs +++ b/src/codegen/sys/cargo_toml.rs @@ -77,6 +77,11 @@ fn fill_in(root: &mut Table, env: &Env) { set_string(build_deps, "pkg-config", "0.3.7"); } + { + let dev_deps = upsert_table(root, "dev-dependencies"); + set_string(dev_deps, "shell-words", "0.1.0"); + } + { let features = upsert_table(root, "features"); features.clear(); diff --git a/src/codegen/sys/mod.rs b/src/codegen/sys/mod.rs index 7f256222e..b6c55e395 100644 --- a/src/codegen/sys/mod.rs +++ b/src/codegen/sys/mod.rs @@ -6,10 +6,12 @@ mod fields; mod functions; mod lib_; mod statics; +mod tests; pub mod ffi_type; pub fn generate(env: &Env) { lib_::generate(env); build::generate(env); cargo_toml::generate(env); + tests::generate(env); } diff --git a/src/codegen/sys/tests.rs b/src/codegen/sys/tests.rs new file mode 100644 index 000000000..3aaed9166 --- /dev/null +++ b/src/codegen/sys/tests.rs @@ -0,0 +1,297 @@ +use std::io::prelude::*; +use std::io; +use std::path::Path; + +use analysis::types::IsIncomplete; +use env::Env; +use file_saver::save_to_file; +use library::{Type, MAIN_NAMESPACE}; +use nameutil::crate_name; +use codegen::general; + +struct CType { + /// Name of type, as used in C. + name: String, + /// Expression describing when type is available (when defined only conditionally). + cfg_condition: Option, +} + +pub fn generate(env :&Env) { + let tests = env.config.target_path.join("tests"); + let abi_c = tests.join("abi.c"); + let abi_rs = tests.join("abi.rs"); + let manual_h = tests.join("manual.h"); + + let ns = env.library.namespace(MAIN_NAMESPACE); + let ctypes = ns.types + .iter() + .filter_map(|t| t.as_ref()) + .filter(|t| !t.is_incomplete(&env.library)) + .filter_map(|t| match *t { + Type::Alias(_) | + Type::Class(_) | + Type::Record(_) | + Type::Union(_) | + Type::Enumeration(_) | + Type::Bitfield(_) | + Type::Interface(_) + => { + let full_name = format!("{}.{}", &ns.name, t.get_name()); + if env.type_status_sys(&full_name).ignored() { + return None; + } + let name = match t.get_glib_name() { + None => return None, + Some(name) => name, + }; + if is_name_made_up(name) { + return None; + } + let cfg_condition = env.config.objects.get(&full_name).and_then(|obj| { + obj.cfg_condition.clone() + }); + Some(CType { + name: name.to_owned(), + cfg_condition, + }) + }, + _ => None, + }) + .collect::>(); + + if ctypes.is_empty() { + return; + } + + if !manual_h.exists() { + save_to_file(&manual_h, env.config.make_backup, |w| { + generate_manual_h(env, &manual_h, w) + }); + } + save_to_file(&abi_c, env.config.make_backup, |w| { + generate_abi_c(env, &abi_c, w) + }); + save_to_file(&abi_rs, env.config.make_backup, |w| { + generate_abi_rs(env, &abi_rs, w, &ctypes) + }); +} + +/// Checks if type name is unlikely to correspond to a real C type name. +fn is_name_made_up(name: &str) -> bool { + // Unnamed types are assigned name during parsing, those names contain an underscore. + name.contains('_') +} + +fn generate_manual_h(env: &Env, path: &Path, w: &mut Write) -> io::Result<()> { + info!("Generating file {:?}", path); + writeln!(w, "// Feel free to edit this file, it won't be regenerated by gir generator unless removed.")?; + writeln!(w, "")?; + + let ns = env.library.namespace(MAIN_NAMESPACE); + for include in &ns.c_includes { + writeln!(w, "#include <{}>", include)?; + } + + Ok(()) +} + +fn generate_abi_c(env: &Env, path: &Path, w: &mut Write) -> io::Result<()> { + info!("Generating file {:?}", path); + general::start_comments(w, &env.config)?; + writeln!(w, "")?; + writeln!(w, "/* For %z support in printf when using MinGW. */")?; + writeln!(w, "#define _POSIX_C_SOURCE 200809L")?; + writeln!(w, "#include ")?; + writeln!(w, "#include ")?; + writeln!(w, "#include \"manual.h\"")?; + + writeln!(w, "{}", r##" +int main() { + printf("%zu\n%zu\n", sizeof(ABI_TEST_TYPE), alignof(ABI_TEST_TYPE)); + return 0; +}"##) + +} + +fn generate_abi_rs(env: &Env, path: &Path, w: &mut Write, ctypes: &[CType]) -> io::Result<()> { + info!("Generating file {:?}", path); + general::start_comments(w, &env.config)?; + writeln!(w, "")?; + let name = format!("{}_sys", crate_name(&env.config.library_name)); + writeln!(w, "extern crate {};", &name)?; + writeln!(w, "extern crate shell_words;")?; + writeln!(w, "{}", r##" +use std::collections::BTreeMap; +use std::env; +use std::error::Error; +use std::mem::{align_of, size_of}; +use std::process::Command; +use std::str;"##)?; + writeln!(w, "use {}::*;", &name)?; + writeln!(w, "{}", r##" + +#[derive(Clone, Debug)] +struct Compiler { + pub args: Vec, +} + +impl Compiler { + pub fn new() -> Result> { + let mut args = get_var("CC", "cc")?; + args.extend(get_var("CFLAGS", "")?); + args.extend(get_var("CPPFLAGS", "")?); + Ok(Compiler { args }) + } + + pub fn define<'a, V: Into>>(&mut self, var: &str, val: V) { + let arg = match val.into() { + None => format!("-D{}", var), + Some(val) => format!("-D{}={}", var, val), + }; + self.args.push(arg); + } + + pub fn to_command(&self) -> Command { + let mut cmd = Command::new(&self.args[0]); + cmd.args(&self.args[1..]); + cmd + } +} + +fn get_var(name: &str, default: &str) -> Result, Box> { + match env::var(name) { + Ok(value) => Ok(shell_words::split(&value)?), + Err(env::VarError::NotPresent) => Ok(shell_words::split(default)?), + Err(err) => Err(format!("{} {}", name, err).into()), + } +} + +fn pkg_config_cflags(package: &str) -> Result, Box> { + let mut cmd = Command::new("pkg-config"); + cmd.arg("--cflags"); + cmd.arg(package); + let out = cmd.output()?; + if !out.status.success() { + return Err(format!("command {:?} returned {}", + &cmd, out.status).into()); + } + let stdout = str::from_utf8(&out.stdout)?; + Ok(shell_words::split(stdout)?) +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +struct ABI { + size: usize, + alignment: usize, +} + +impl ABI { + pub fn from_type() -> ABI { + ABI { + size: size_of::(), + alignment: align_of::(), + } + } +} + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +struct TestResults { + /// Number of successfully completed tests. + passed: usize, + /// Total number of failed tests (including those that failed to compile). + failed: usize, + /// Number of tests that failed to compile. + failed_compile: usize, +} + +#[test] +fn test_cross_validate_abi_with_c() { + // Configure compiler instance."##)?; + + let ns = env.library.namespace(MAIN_NAMESPACE); + let package_name = ns.package_name.as_ref() + .expect("Missing package name"); + + writeln!(w, "\tlet package_name = \"{}\";", package_name)?; + writeln!(w, "{}", r##" let mut cc = Compiler::new() + .expect("compiler from environment"); + let cflags = pkg_config_cflags(package_name) + .expect("cflags from pkg-config"); + cc.args.extend(cflags); + cc.args.push("-Wno-deprecated-declarations".to_owned()); + + // Sanity check that compilation works. + assert_eq!(ABI {size: 1, alignment: 1}, + get_c_abi(&cc, "char").expect("C ABI for char"), + "failed to obtain correct ABI for char type"); + + let mut results : TestResults = Default::default(); + for (name, rust_abi) in &get_rust_abi() { + match get_c_abi(&cc, name) { + Err(e) => { + results.failed += 1; + results.failed_compile += 1; + eprintln!("{}", e); + continue; + }, + Ok(ref c_abi) => { + if rust_abi == c_abi { + results.passed += 1; + } else { + results.failed += 1; + eprintln!("ABI mismatch for {}\nRust: {:?}\nC: {:?}", + name, rust_abi, c_abi); + } + } + }; + } + + if results.failed + results.failed_compile != 0 { + panic!("FAILED\nABI test results: {} passed; {} failed (compilation errors: {})", + results.passed, + results.failed, + results.failed_compile); + } +} + +fn get_c_abi(cc: &Compiler, name: &str) -> Result> { + let mut cc = cc.clone(); + cc.define("ABI_TEST_TYPE", name); + cc.args.push("tests/abi.c".to_owned()); + cc.args.push("-oabi".to_owned()); + + let mut cc_cmd = cc.to_command(); + let status = cc_cmd.spawn()?.wait()?; + if !status.success() { + return Err(format!("compilation command {:?} failed, {}", + &cc_cmd, status).into()); + } + + let mut abi_cmd = Command::new("./abi"); + let output = abi_cmd.output()?; + if !output.status.success() { + return Err(format!("command {:?} failed, {:?}", + &abi_cmd, &output).into()); + } + + let stdout = str::from_utf8(&output.stdout)?; + let mut words = stdout.split_whitespace(); + let size = words.next().unwrap().parse().unwrap(); + let alignment = words.next().unwrap().parse().unwrap(); + Ok(ABI {size, alignment}) +} + +fn get_rust_abi() -> BTreeMap { + let mut abi = BTreeMap::new();"##)?; + + for ctype in ctypes { + general::cfg_condition(w, &ctype.cfg_condition, false, 1)?; + writeln!(w, r##" abi.insert("{ctype}".to_owned(), ABI::from_type::<{ctype}>());"##, + ctype=ctype.name)?; + } + + writeln!(w, "{}", r##" abi +} +"##) + +} diff --git a/src/library.rs b/src/library.rs index f5f4e01d7..a5e1ea7da 100644 --- a/src/library.rs +++ b/src/library.rs @@ -669,6 +669,8 @@ pub struct Namespace { pub shared_library: Vec, pub identifier_prefixes: Vec, pub symbol_prefixes: Vec, + /// C headers, relative to include directories provided by pkg-config --cflags. + pub c_includes: Vec, } impl Namespace { diff --git a/src/parser.rs b/src/parser.rs index d8cb0c658..24de30676 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,3 +1,4 @@ +use std::mem::replace; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -25,13 +26,18 @@ impl Library { fn read_repository(&mut self, dir: &Path, parser: &mut XmlParser) -> Result<()> { let mut package = None; + let mut includes = Vec::new(); parser.elements(|parser, elem| match elem.name() { "include" => { - if let (Some(lib), Some(ver)) = (elem.attr("name"), elem.attr("version")) { - if self.find_namespace(lib).is_none() { - let lib = format!("{}-{}", lib, ver); - self.read_file(dir, &lib)?; - } + match (elem.attr("name"), elem.attr("version")) { + (Some(name), Some(ver)) => { + if self.find_namespace(name).is_none() { + let lib = format!("{}-{}", name, ver); + self.read_file(dir, &lib)?; + } + }, + (Some(name), None) => includes.push(name.to_owned()), + _ => {}, } Ok(()) } @@ -43,7 +49,8 @@ impl Library { } Ok(()) } - "namespace" => self.read_namespace(parser, elem, package.take()), + "namespace" => self.read_namespace(parser, elem, package.take(), + replace(&mut includes, Vec::new())), _ => Err(parser.unexpected_element(elem)), })?; Ok(()) @@ -54,19 +61,25 @@ impl Library { parser: &mut XmlParser, elem: &Element, package: Option, + c_includes: Vec, ) -> Result<()> { let ns_name = elem.attr_required("name")?; let ns_id = self.add_namespace(ns_name); - self.namespace_mut(ns_id).package_name = package; - if let Some(s) = elem.attr("shared-library") { - self.namespace_mut(ns_id).shared_library = s.split(',').map(String::from).collect(); - } - if let Some(s) = elem.attr("identifier-prefixes") { - self.namespace_mut(ns_id).identifier_prefixes = - s.split(',').map(String::from).collect(); - } - if let Some(s) = elem.attr("symbol-prefixes") { - self.namespace_mut(ns_id).symbol_prefixes = s.split(',').map(String::from).collect(); + + { + let ns = self.namespace_mut(ns_id); + ns.package_name = package; + ns.c_includes = c_includes; + if let Some(s) = elem.attr("shared-library") { + ns.shared_library = s.split(',').map(String::from).collect(); + } + if let Some(s) = elem.attr("identifier-prefixes") { + ns.identifier_prefixes = + s.split(',').map(String::from).collect(); + } + if let Some(s) = elem.attr("symbol-prefixes") { + ns.symbol_prefixes = s.split(',').map(String::from).collect(); + } } trace!(