From a06c129848ffc7b7f129e75f76c8bc61ec963d31 Mon Sep 17 00:00:00 2001 From: Sean Marshallsay Date: Thu, 7 Jan 2016 23:06:10 +0000 Subject: [PATCH] Brand spanking new API. Compartmentalises the compilation. `cheddar::parse` is no longer responsible for include-guards, includes, or extern declarations. --- examples/pointer.rs | 2 +- examples/stack.rs | 3 +- src/bin/cheddar.rs | 16 +--- src/lib.rs | 175 ++++++++++++++++++++++++++------------------ src/parse.rs | 25 ++++--- src/types.rs | 8 +- tests/lib.rs | 10 +-- 7 files changed, 134 insertions(+), 105 deletions(-) diff --git a/examples/pointer.rs b/examples/pointer.rs index 1714ee4..f010124 100644 --- a/examples/pointer.rs +++ b/examples/pointer.rs @@ -50,7 +50,7 @@ pub extern fn datalib_data_f64_append(data: *mut datalib_data_f64, x: f64, y: f6 fn main() { let header = cheddar::Cheddar::new().expect("failed to read cargo manifest") .source_string(RUST) - .compile_to_string() + .compile("SOME_HEADER_NAME") .expect("header could not be compiled"); println!("RUST SOURCE FILE:\n{}\n", RUST); diff --git a/examples/stack.rs b/examples/stack.rs index 30ca3b1..2332201 100644 --- a/examples/stack.rs +++ b/examples/stack.rs @@ -1,3 +1,4 @@ +//! Demonstrates a C API based on the stack. extern crate cheddar; const RUST: &'static str = r#" @@ -33,7 +34,7 @@ pub extern fn Student_change_grade(student: Student, changer: extern fn(f64) -> fn main() { let header = cheddar::Cheddar::new().expect("failed to read cargo manifest") .source_string(RUST) - .compile_to_string() + .compile("SOME_HEADER_NAME") .expect("header could not be compiled"); println!("RUST SOURCE FILE:\n{}\n", RUST); diff --git a/src/bin/cheddar.rs b/src/bin/cheddar.rs index c5dbb4e..90a81d0 100644 --- a/src/bin/cheddar.rs +++ b/src/bin/cheddar.rs @@ -41,16 +41,8 @@ fn main() { } if let Some(output) = matches.value_of("OUTPUT") { - let path = std::path::Path::new(&output); - - if let Some(dir) = path.parent() { - cheddar.directory(dir); - } - - if let Some(file) = path.file_name() { - cheddar.file(file); - } - } - - cheddar.compile(); + cheddar.write(&output); + } else { + cheddar.write("cheddar.h"); + }; } diff --git a/src/lib.rs b/src/lib.rs index 04ab5ff..92f5af2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,27 +10,29 @@ //! `build-dependencies` with `dependencies`): //! //! ```toml +//! # Cargo.toml +//! //! [build-dependencies] -//! rusty-cheddar = "0.3" +//! rusty-cheddar = "0.3.0" //! ``` //! //! Then create the following `build.rs`: //! //! ```no_run +//! // build.rs +//! //! extern crate cheddar; //! //! fn main() { //! cheddar::Cheddar::new().expect("could not read manifest") -//! .file("my_header.h") -//! .compile(); +//! .write("include/my_header.h"); //! } //! ``` //! //! This should work as is providing you've set up your project correctly. **Don't forget to add a //! `build = ...` to your `[package]` section, see [the cargo docs] for more info.** //! -//! rusty-cheddar will then create a `my_header.h` file in in `$OUT_DIR` where `$OUT_DIR` is set by -//! `cargo` (usually `target/debug/build/{your crate}_{some hash}/out`). Note that rusty-cheddar +//! rusty-cheddar will then create a `my_header.h` file in `include/`. Note that rusty-cheddar //! emits very few warnings, it is up to the programmer to write a library which can be correctly //! called from C. //! @@ -50,9 +52,8 @@ //! //! fn main() { //! cheddar::Cheddar::new().expect("could not read manifest") -//! .file("my_header.h") //! .module("c_api") -//! .compile(); +//! .write("target/include/rusty.h"); //! } //! ``` //! @@ -66,7 +67,7 @@ //! } //! ``` //! -//! There is also the `.compile_to_string()` method for finer control. +//! There is also the `.compile()` and `.compile_code()` methods for finer control. //! //! # Conversions //! @@ -254,16 +255,11 @@ //! // Some more boilerplate omitted. //! ``` //! -//! ## Type Conversions +//! ## Paths //! -//! rusty-cheddar currently does not handle type paths (e.g. `mymod::MyType`), instead they must be `use`ed -//! first: -//! -//! ```ignore -//! // pub type MyCType = mymod::MyType; // This will put `typedef mymod::MyType MyCType;` into the header. -//! use mymod::MyType; -//! pub type MyCType = MyType; -//! ``` +//! You must not put types defined in other modules in an exported type signature without hiding it +//! behind an opaque struct. This is because the C compiler must know the layout of the type and +//! rusty-cheddar can not yet search other modules. //! //! The very important exception to this rule is `libc`, types used from `libc` _must_ be qualified //! (e.g. `libc::c_void`) so that they can be converted properly. @@ -297,6 +293,8 @@ mod parse; pub use syntax::errors::Level; /// Describes an error encountered by the compiler. +/// +/// These can be printed nicely using the `Cheddar::print_err` method. #[derive(Debug)] pub struct Error { pub level: Level, @@ -361,16 +359,8 @@ enum Source { pub struct Cheddar { /// The root source file of the crate. input: Source, - /// The directory in which to place the header file. - /// - /// Default is the environment variable `OUT_DIR` when available, otherwise it is the current - /// directory. - outdir: path::PathBuf, - /// The file name of the header file. - /// - /// Default is `cheddar.h`. - outfile: path::PathBuf, // TODO: store this as a syntax::ast::Path when allowing arbitrary modules. + // TODO: this should be part of a ParseOpts struct /// The module which contains the C API. module: Option, /// The current parser session. @@ -388,13 +378,8 @@ impl Cheddar { let source_path = try!(source_file_from_cargo()); let input = Source::File(path::PathBuf::from(source_path)); - let outdir = std::env::var_os("OUT_DIR") - .map_or_else(path::PathBuf::new, path::PathBuf::from); - Ok(Cheddar { input: input, - outdir: outdir, - outfile: path::PathBuf::from("cheddar.h"), module: None, session: syntax::parse::ParseSess::new(), }) @@ -418,28 +403,6 @@ impl Cheddar { self } - /// Set the output directory. - /// - /// Default is [`OUT_DIR`] when available, otherwise it is the current directory. - /// - /// [`OUT_DIR`]: http://doc.crates.io/environment-variables.html#environment-variables-cargo-sets-for-build-scripts - pub fn directory(&mut self, path: T) -> &mut Cheddar - where path::PathBuf: convert::From, - { - self.outdir = path::PathBuf::from(path); - self - } - - /// Set the name for the created header file. - /// - /// Default is `cheddar.h`. - pub fn file(&mut self, path: T) -> &mut Cheddar - where path::PathBuf: convert::From, - { - self.outfile = path::PathBuf::from(path); - self - } - /// Set the module which contains the header file. /// /// The module should be described using Rust's path syntax, i.e. in the same way that you @@ -450,18 +413,19 @@ impl Cheddar { self } - /// Compile the header into a string. + /// Compile just the code into header declarations. /// - /// Returns a vector of errors which can be printed using `Cheddar::print_error`. - pub fn compile_to_string(&self) -> Result> { + /// This does not add any include-guards, includes, or extern declarations. It is mainly + /// intended for internal use, but may be of interest to people who wish to embed + /// rusty-cheddar's generated code in another file. + pub fn compile_code(&self) -> Result> { let sess = &self.session; - let file_name = self.outfile.file_stem().and_then(|p| p.to_str()).unwrap_or("default"); - let krate = match self.input { Source::File(ref path) => syntax::parse::parse_crate_from_file(path, vec![], sess), Source::String(ref source) => syntax::parse::parse_crate_from_source_str( "cheddar_source".to_owned(), // TODO: this clone could be quite costly, maybe rethink this design? + // or just use a slice. source.clone(), vec![], sess, @@ -469,12 +433,53 @@ impl Cheddar { }; if let Some(ref module) = self.module { - parse::parse_crate(&krate, module, &file_name) + parse::parse_crate(&krate, module) } else { - parse::parse_mod(&krate.module, &file_name) + parse::parse_mod(&krate.module) } } + /// Compile the header declarations then add the needed `#include`s. + /// + /// Currently includes: + /// + /// - `stdint.h` + /// - `stdbool.h` + fn compile_with_includes(&self) -> Result> { + let code = try!(self.compile_code()); + + Ok(format!("#include \n#include \n\n{}", code)) + } + + /// Compile a header while conforming to C89 (or ANSI C). + /// + /// This does not include `stdint.h` or `stdbool.h` and also wraps single line comments with + /// `/*` and `*/`. + /// + /// `id` is used to help generate the include guard and may be empty. + /// + /// # TODO + /// + /// This is intended to be a public API, but currently comments are not handled correctly so it + /// is being kept private. + /// + /// The parser should warn on uses of `bool` or fixed-width integers (`i16`, `u32`, etc.). + #[allow(dead_code)] + fn compile_c89(&self, id: &str) -> Result> { + let code = try!(self.compile_code()); + + Ok(wrap_guard(&wrap_extern(&code), id)) + } + + /// Compile a header. + /// + /// `id` is used to help generate the include guard and may be empty. + pub fn compile(&self, id: &str) -> Result> { + let code = try!(self.compile_with_includes()); + + Ok(wrap_guard(&wrap_extern(&code), id)) + } + /// Write the header to a file. /// /// This is a convenience method for use in build scripts. If errors occur during compilation @@ -483,20 +488,24 @@ impl Cheddar { /// # Panics /// /// Panics on any compilation error so that the build script exits. - pub fn compile(&self) { + pub fn write>(&self, file: P) { + let file = file.as_ref(); let sess = &self.session; - if let Err(error) = std::fs::create_dir_all(&self.outdir) { - sess.span_diagnostic.err(&format!( - "could not create directories in '{}': {}", - self.outdir.display(), - error, - )); + if let Some(dir) = file.parent() { + if let Err(error) = std::fs::create_dir_all(dir) { + sess.span_diagnostic.err(&format!( + "could not create directories in '{}': {}", + dir.display(), + error, + )); - panic!("errors compiling header file"); + panic!("errors compiling header file"); + } } - let header = match self.compile_to_string() { + let file_name = file.file_name().map(|os| os.to_string_lossy()).unwrap_or("default".into()); + let header = match self.compile(&file_name) { Ok(header) => header, Err(errors) => { for error in errors { @@ -508,7 +517,6 @@ impl Cheddar { }; - let file = self.outdir.join(&self.outfile); let bytes_buf = header.into_bytes(); if let Err(error) = std::fs::File::create(&file).and_then(|mut f| f.write_all(&bytes_buf)) { sess.span_diagnostic.err(&format!("could not write to '{}': {}", file.display(), error)); @@ -562,3 +570,30 @@ fn source_file_from_cargo() -> std::result::Result { .unwrap_or(default) .into()) } + +/// Wrap a block of code with an extern declaration. +fn wrap_extern(code: &str) -> String { + format!(r#" +#ifdef __cplusplus +extern "C" {{ +#endif + +{} + +#ifdef __cplusplus +}} +#endif +"#, code) +} + +/// Wrap a block of code with an include-guard. +fn wrap_guard(code: &str, id: &str) -> String { + format!(r" +#ifndef cheddar_generated_{0}_h +#define cheddar_generated_{0}_h + +{1} + +#endif +", id, code) +} diff --git a/src/parse.rs b/src/parse.rs index 5f7ee33..399ec01 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -7,12 +7,21 @@ use types; use Error; use Level; +// TODO: we should use our own parse state which tracks what types are callable from C +// - then we can give decent errors for ones that aren't +// - will require multiple passes of the module +// - each time a struct, enum, or typedef changes do another pass +// - only store errors on the final pass +// - maybe there will be some errors which will need to be stored before then +// - search inside struct as well for whitelisted types +// - possibly also search other crates when encountering a path + /// The main entry point when looking for a specific module. /// /// Determines which module to parse, ensures it is `pub use`ed then hands off to -/// `Cheddar::parse_mod`. -pub fn parse_crate(krate: &ast::Crate, module: &str, file_name: &str) -> Result> { +/// `cheddar::parse::parse_mod`. +pub fn parse_crate(krate: &ast::Crate, module: &str) -> Result> { let mut mod_item = None; let mut pub_used = false; @@ -43,7 +52,7 @@ pub fn parse_crate(krate: &ast::Crate, module: &str, file_name: &str) -> Result< if let Some(mod_item) = mod_item { if pub_used { - parse_mod(&mod_item, file_name) + parse_mod(&mod_item) } else { Err(vec![Error { level: Level::Error, @@ -64,11 +73,8 @@ pub fn parse_crate(krate: &ast::Crate, module: &str, file_name: &str) -> Result< /// /// Iterates through all items in the module and dispatches to correct methods, then pulls all /// the results together into a header. -pub fn parse_mod(module: &ast::Mod, file_name: &str) -> Result> { - let mut buffer = format!("#ifndef cheddar_gen_{0}_h\n#define cheddar_gen_{0}_h\n\n", file_name); - buffer.push_str("#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n"); - buffer.push_str("#include \n#include \n\n"); - +pub fn parse_mod(module: &ast::Mod) -> Result> { + let mut buffer = String::new(); let mut errors = vec![]; for item in &module.items { // If it's not visible it can't be called from C. @@ -95,9 +101,6 @@ pub fn parse_mod(module: &ast::Mod, file_name: &str) -> Result { +// Named(&'t ast::Ty, &'n name), +// Unnamed(&'t ast::Ty), +// } /// Turn a Rust (type, name) pair into a C (type, name) pair. /// /// If name is `None` then there is no name associated with that type. diff --git a/tests/lib.rs b/tests/lib.rs index b1a94f6..628c76e 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -6,11 +6,7 @@ macro_rules! inner_cheddar_cmp_test { ($name:ident, $compile:expr, $header:expr) => { #[test] fn $name() { - let expected = concat!( - // Due to the way CppHeaderParser works we only need to add the #define. - "#define cheddar_gen_cheddar_h\n", - $header, - ); + let expected = $header; let actual = match $compile { Ok(actual) => actual, @@ -84,7 +80,7 @@ macro_rules! cheddar_cmp_test { ($name:ident, $api:expr, $header:expr, $rust:expr) => { inner_cheddar_cmp_test! { $name, - cheddar::Cheddar::new().unwrap().source_string($rust).module($api).compile_to_string(), + cheddar::Cheddar::new().unwrap().source_string($rust).module($api).compile_code(), $header } }; @@ -92,7 +88,7 @@ macro_rules! cheddar_cmp_test { ($name:ident, $header:expr, $rust:expr) => { inner_cheddar_cmp_test! { $name, - cheddar::Cheddar::new().unwrap().source_string($rust).compile_to_string(), + cheddar::Cheddar::new().unwrap().source_string($rust).compile_code(), $header } };