diff --git a/crates/libs/bindgen/readme.md b/crates/libs/bindgen/readme.md index 79d734a01a1..98632753505 100644 --- a/crates/libs/bindgen/readme.md +++ b/crates/libs/bindgen/readme.md @@ -31,6 +31,9 @@ let args = [ windows_bindgen::bindgen(args); ``` +Use `windows_bindgen::bindgen(["--etc", "bindings.txt"])` when the commands live in a text file. +For a filter-only file, use `Bindgen::filter_file`/`filter_files` or `--filter-file`. + Variadic native exports are emitted only by `--sys`, where the generated declaration retains the literal `...` tail. Default and minimal bindings omit them rather than exposing a callable fixed-prefix wrapper. diff --git a/crates/libs/bindgen/src/cli.rs b/crates/libs/bindgen/src/cli.rs index 75ed4bb087b..0492a1da6fe 100644 --- a/crates/libs/bindgen/src/cli.rs +++ b/crates/libs/bindgen/src/cli.rs @@ -26,7 +26,8 @@ use super::*; /// - `--minimal`: Omits class wrappers, inherited forwarders, and handle wrappers. /// - `--implement`: Emits implementation traits for selected WinRT interfaces. /// - `--dead-code`: Emits `pub(crate)` items for dead-code analysis. -/// - `--etc`: Reads arguments from response files. +/// - `--etc`: Reads arguments from command files. +/// - `--filter-file`: Reads filters from text files. /// # `--out` /// /// Exactly one `--out` argument is required. @@ -47,10 +48,10 @@ use super::*; /// !Windows.Win32.Storage.FileSystem.WIN32_FIND_DATAW /// ``` /// -/// Use a response file for longer filter lists: +/// Use a filter file for longer filter lists: /// /// ```text -/// --etc path/to/file.txt +/// --filter-file path/to/filter.txt /// ``` /// /// The in-repo crates use this convention; see the filter `.txt` files in @@ -81,8 +82,9 @@ where let mut builder = Bindgen::new(); let mut kind = ArgKind::None; let mut has_output = false; + let mut implement = None::>; - for arg in &args { + for arg in args { if arg.starts_with('-') { kind = ArgKind::None; } @@ -92,6 +94,7 @@ where "--in" => kind = ArgKind::Input, "--out" => kind = ArgKind::Output, "--filter" => kind = ArgKind::Filter, + "--filter-file" => kind = ArgKind::FilterFile, "--rustfmt" => kind = ArgKind::Rustfmt, "--derive" => kind = ArgKind::Derive, "--flat" => { @@ -113,7 +116,7 @@ where builder.extern_fns(); } "--implement" => { - builder.implement.get_or_insert_with(Vec::new); + implement.get_or_insert_with(Vec::new); kind = ArgKind::Implement; } _ => panic!("invalid option `{arg}`"), @@ -124,26 +127,38 @@ where has_output = true; } ArgKind::Input => { - builder.input(arg); + if arg == "default" { + builder.input_default(); + } else { + builder.input(arg); + } } ArgKind::Filter => { - builder.filter(arg); + builder.filter(&arg); + } + ArgKind::FilterFile => { + builder.filter_file(&arg); } ArgKind::Derive => { - builder.derive(arg); + builder.derive(&arg); } ArgKind::Implement => { - builder - .implement - .get_or_insert_with(Vec::new) - .push(arg.clone()); + implement.as_mut().unwrap().push(arg.clone()); } ArgKind::Rustfmt => { - builder.rustfmt(arg); + builder.rustfmt(&arg); } } } + if let Some(implement) = implement { + if implement.is_empty() { + builder.implement_all(); + } else { + builder.implements(implement); + } + } + builder.write(); } @@ -152,6 +167,7 @@ enum ArgKind { Input, Output, Filter, + FilterFile, Rustfmt, Derive, Implement, @@ -163,16 +179,49 @@ where I: IntoIterator, S: AsRef, { - // This function is needed to avoid a recursion limit in the Rust compiler. #[track_caller] - fn from_string(result: &mut Vec, value: &str) { + fn expand(result: &mut Vec, args: I) + where + I: IntoIterator, + S: AsRef, + { + let mut command_files = false; + + for arg in args.into_iter().map(|arg| arg.as_ref().to_string()) { + if arg.starts_with('-') { + command_files = false; + } + + if command_files { + expand(result, read_tokens(arg)); + } else if arg == "--etc" { + command_files = true; + } else { + result.push(arg); + } + } + } + + let mut result = Vec::new(); + expand(&mut result, args); + result +} + +#[track_caller] +pub(super) fn read_tokens(input: impl AsRef) -> Vec { + let mut result = Vec::new(); + + for line in read_file_lines(input) { + if line.trim_start().starts_with("//") { + continue; + } + // Split on whitespace but keep `{...}` groups together so that - // `Type::{a, b}` is not split across multiple args. - let mut args = Vec::new(); + // `Type::{a, b}` is not split across multiple filters. let mut current = String::new(); let mut brace_depth = 0u32; - for ch in value.chars() { + for ch in line.chars() { if ch == '{' { brace_depth += 1; current.push(ch); @@ -181,46 +230,16 @@ where current.push(ch); } else if ch.is_whitespace() && brace_depth == 0 { if !current.is_empty() { - args.push(std::mem::take(&mut current)); + result.push(std::mem::take(&mut current)); } } else { current.push(ch); } } if !current.is_empty() { - args.push(current); - } - - expand_args(result, args); - } - - #[track_caller] - fn expand_args(result: &mut Vec, args: I) - where - I: IntoIterator, - S: AsRef, - { - let mut expand = false; - - for arg in args.into_iter().map(|arg| arg.as_ref().to_string()) { - if arg.starts_with('-') { - expand = false; - } - if expand { - for args in read_file_lines(&arg) { - if !args.starts_with("//") { - from_string(result, &args); - } - } - } else if arg == "--etc" { - expand = true; - } else { - result.push(arg); - } + result.push(current); } } - let mut result = vec![]; - expand_args(&mut result, args); result } diff --git a/crates/libs/bindgen/src/io.rs b/crates/libs/bindgen/src/io.rs index 56f00e8f12b..ddc0833748e 100644 --- a/crates/libs/bindgen/src/io.rs +++ b/crates/libs/bindgen/src/io.rs @@ -1,9 +1,11 @@ use std::io::BufRead; +use std::path::Path; #[track_caller] -pub fn read_file_lines(path: &str) -> Vec { +pub fn read_file_lines(path: impl AsRef) -> Vec { + let path = path.as_ref(); let Ok(file) = std::fs::File::open(path) else { - panic!("failed to open file `{path}`") + panic!("failed to open file `{}`", path.display()) }; let file = std::io::BufReader::new(file); @@ -11,7 +13,7 @@ pub fn read_file_lines(path: &str) -> Vec { for line in file.lines() { let Ok(line) = line else { - panic!("failed to read file lines `{path}`"); + panic!("failed to read file lines `{}`", path.display()); }; lines.push(line); @@ -21,22 +23,25 @@ pub fn read_file_lines(path: &str) -> Vec { } #[track_caller] -pub fn write_to_file>(path: &str, contents: C) -> bool { +pub fn write_to_file, C: AsRef<[u8]>>(path: P, contents: C) -> bool { + let path = path.as_ref(); let contents = contents.as_ref(); if std::fs::read(path).is_ok_and(|existing| existing == contents) { return false; } - if let Some(parent) = std::path::Path::new(path).parent() { + if let Some(parent) = path.parent() { assert!( std::fs::create_dir_all(parent).is_ok(), - "failed to create directory `{path}`" + "failed to create directory `{}`", + path.display() ); } assert!( std::fs::write(path, contents).is_ok(), - "failed to write file `{path}`" + "failed to write file `{}`", + path.display() ); true } @@ -55,13 +60,11 @@ mod tests { .unwrap() .as_nanos() )); - let path = path.to_str().unwrap(); + assert!(write_to_file(&path, b"one")); + assert!(!write_to_file(&path, b"one")); + assert!(write_to_file(&path, b"two")); + assert_eq!(std::fs::read(&path).unwrap(), b"two"); - assert!(write_to_file(path, b"one")); - assert!(!write_to_file(path, b"one")); - assert!(write_to_file(path, b"two")); - assert_eq!(std::fs::read(path).unwrap(), b"two"); - - std::fs::remove_file(path).unwrap(); + std::fs::remove_file(&path).unwrap(); } } diff --git a/crates/libs/bindgen/src/lib.rs b/crates/libs/bindgen/src/lib.rs index 04f56c12484..84000d76d23 100644 --- a/crates/libs/bindgen/src/lib.rs +++ b/crates/libs/bindgen/src/lib.rs @@ -38,6 +38,7 @@ use signature::*; use std::cmp::Ordering; use std::collections::*; use std::fmt::Write; +use std::path::{Path, PathBuf}; use tables::*; use tokens::*; use type_map::*; @@ -52,10 +53,11 @@ mod type_closure; use method_names::*; use type_closure::*; -fn report_timing(output: &str, phase: &str, elapsed: std::time::Duration) { +fn report_timing(output: &Path, phase: &str, elapsed: std::time::Duration) { if std::env::var_os("WINDOWS_BINDGEN_TIMINGS").is_some() { eprintln!( - "windows-bindgen timing `{output}` {phase}: {:.3} ms", + "windows-bindgen timing `{}` {phase}: {:.3} ms", + output.display(), elapsed.as_secs_f64() * 1_000.0 ); } @@ -81,8 +83,9 @@ pub fn builder() -> Bindgen { #[derive(Default)] pub struct Bindgen { input: Vec, + input_default: bool, filter: Vec, - output: String, + output: PathBuf, derive: Vec, implement: Option>, rustfmt: Option, @@ -92,8 +95,7 @@ pub struct Bindgen { } enum Input { - Default, - Path(String), + Path(PathBuf), Bytes(Vec), } @@ -192,19 +194,15 @@ impl Bindgen { Self::default() } - /// Adds a `.winmd` file or directory. `"default"` selects the bundled metadata. - pub fn input(&mut self, input: &str) -> &mut Self { - if input == "default" { - self.input_default() - } else { - self.input.push(Input::Path(input.to_string())); - self - } + /// Adds a `.winmd` file or directory. + pub fn input(&mut self, input: impl AsRef) -> &mut Self { + self.input.push(Input::Path(input.as_ref().to_path_buf())); + self } /// Adds the default Windows metadata. pub fn input_default(&mut self) -> &mut Self { - self.input.push(Input::Default); + self.input_default = true; self } @@ -214,21 +212,33 @@ impl Bindgen { self } + /// Adds `.winmd` files from memory. + pub fn input_byte_sets(&mut self, inputs: I) -> &mut Self + where + I: IntoIterator, + B: AsRef<[u8]>, + { + for input in inputs { + self.input_bytes(input.as_ref()); + } + self + } + /// Adds `.winmd` files or directories. pub fn inputs(&mut self, inputs: I) -> &mut Self where I: IntoIterator, - S: AsRef, + S: AsRef, { for input in inputs { - self.input(input.as_ref()); + self.input(input); } self } /// Sets the generated Rust file. - pub fn output(&mut self, output: &str) -> &mut Self { - self.output = output.to_string(); + pub fn output(&mut self, output: impl AsRef) -> &mut Self { + self.output = output.as_ref().to_path_buf(); self } @@ -242,6 +252,25 @@ impl Bindgen { self.filters(std::iter::once(filter)) } + /// Adds filter rules from a text file. + #[track_caller] + pub fn filter_file(&mut self, input: impl AsRef) -> &mut Self { + self.filters(cli::read_tokens(input)) + } + + /// Adds filter rules from text files. + #[track_caller] + pub fn filter_files(&mut self, inputs: I) -> &mut Self + where + I: IntoIterator, + S: AsRef, + { + for input in inputs { + self.filter_file(input); + } + self + } + /// Add multiple filter rules to include or exclude APIs. /// /// Filter rules may be a function or type name, a namespace prefix, a fully-qualified name, @@ -306,25 +335,41 @@ impl Bindgen { self } - /// Include implementation traits for WinRT interfaces. + /// Includes implementation traits for every WinRT interface in scope. + #[track_caller] + pub fn implement_all(&mut self) -> &mut Self { + match &self.implement { + None => self.implement = Some(vec![]), + Some(names) if names.is_empty() => {} + Some(_) => panic!("cannot combine `implement_all` with selected implementations"), + } + self + } + + /// Includes implementation traits for a WinRT interface or namespace prefix. /// - /// Each entry may be a fully-qualified type name (`Namespace.Name`) or a - /// namespace prefix that matches every type defined under it. When called - /// with no patterns (an empty iterator), `_Impl` scaffolding is emitted for - /// every WinRT interface in scope. When called with one or more patterns, - /// `_Impl` scaffolding is emitted only for types matching the patterns, - /// rather than for every interface/class in the filter set. The latter is - /// a finer-grained alternative to the broad form and can significantly - /// reduce build time when only a handful of interfaces need to be - /// implemented. - pub fn implement(&mut self, names: I) -> &mut Self + /// The name may be a fully-qualified type name (`Namespace.Name`) or a namespace prefix that + /// matches every type defined under it. + #[track_caller] + pub fn implement(&mut self, name: &str) -> &mut Self { + assert!( + !self.implement.as_ref().is_some_and(Vec::is_empty), + "cannot combine selected implementations with `implement_all`" + ); + self.implement + .get_or_insert_with(Vec::new) + .push(name.to_string()); + self + } + + /// Includes implementation traits for multiple WinRT interfaces or namespace prefixes. + pub fn implements(&mut self, names: I) -> &mut Self where I: IntoIterator, S: AsRef, { - let list = self.implement.get_or_insert_with(Vec::new); for name in names { - list.push(name.as_ref().to_string()); + self.implement(name.as_ref()); } self } @@ -384,7 +429,7 @@ impl Bindgen { // Validate before setting up reader and reference state. assert!( - !self.output.is_empty(), + !self.output.as_os_str().is_empty(), "output is required (call `.output()` or pass `--out`)" ); @@ -406,15 +451,10 @@ impl Bindgen { let phase = std::time::Instant::now(); let reader_storage; - let reader = if self.input.is_empty() - || self - .input - .iter() - .all(|input| matches!(input, Input::Default)) - { + let reader = if self.input.is_empty() { default_reader() } else { - reader_storage = Reader::new(expand_input(&self.input)); + reader_storage = Reader::new(expand_input(&self.input, self.input_default)); &reader_storage }; report_timing(&self.output, "metadata", phase.elapsed()); @@ -587,17 +627,15 @@ fn default_input() -> Vec { .collect() } -fn expand_input(input: &[Input]) -> Vec { +fn expand_input(input: &[Input], input_default: bool) -> Vec { #[track_caller] - fn expand_path(result: &mut Vec, input: &str) { - let path = std::path::Path::new(input); - + fn expand_path(result: &mut Vec, path: &Path) { if path.is_dir() { let mut paths = vec![]; for path in path .read_dir() - .unwrap_or_else(|_| panic!("failed to read directory `{input}`")) + .unwrap_or_else(|_| panic!("failed to read directory `{}`", path.display())) .flatten() .map(|entry| entry.path()) { @@ -612,7 +650,8 @@ fn expand_input(input: &[Input]) -> Vec { assert!( !paths.is_empty(), - "failed to find .winmd files in directory `{input}`" + "failed to find .winmd files in directory `{}`", + path.display() ); for path in paths { @@ -624,16 +663,16 @@ fn expand_input(input: &[Input]) -> Vec { } } else { let Ok(bytes) = std::fs::read(path) else { - panic!("failed to read binary file `{input}`"); + panic!("failed to read binary file `{}`", path.display()); }; let Some(file) = File::new(bytes) else { - panic!("failed to read .winmd format `{input}`"); + panic!("failed to read .winmd format `{}`", path.display()); }; result.push(file); } } - let mut result = if input.iter().any(|input| matches!(input, Input::Default)) { + let mut result = if input_default { default_input() } else { vec![] @@ -641,7 +680,6 @@ fn expand_input(input: &[Input]) -> Vec { for input in input { match input { - Input::Default => {} Input::Path(path) => expand_path(&mut result, path), Input::Bytes(bytes) => result.push( File::new(bytes.clone()) @@ -757,4 +795,40 @@ mod tests { fn default_metadata_reader_is_reused() { assert!(std::ptr::eq(default_reader(), default_reader())); } + + #[test] + fn implementation_selection() { + let mut builder = Bindgen::new(); + builder + .implement("Test.IFirst") + .implements(["Test.ISecond", "Other"]); + assert_eq!( + builder.implement, + Some(vec![ + "Test.IFirst".to_string(), + "Test.ISecond".to_string(), + "Other".to_string() + ]) + ); + + let mut builder = Bindgen::new(); + builder.implement_all().implement_all(); + assert_eq!(builder.implement, Some(vec![])); + + let mut builder = Bindgen::new(); + builder.implements(std::iter::empty::<&str>()); + assert_eq!(builder.implement, None); + } + + #[test] + #[should_panic(expected = "cannot combine selected implementations with `implement_all`")] + fn implementation_after_all_panics() { + Bindgen::new().implement_all().implement("Test.IFirst"); + } + + #[test] + #[should_panic(expected = "cannot combine `implement_all` with selected implementations")] + fn implementation_all_after_selection_panics() { + Bindgen::new().implement("Test.IFirst").implement_all(); + } } diff --git a/crates/libs/bindgen/src/package_writer.rs b/crates/libs/bindgen/src/package_writer.rs index 042325d9d4e..95711f93a66 100644 --- a/crates/libs/bindgen/src/package_writer.rs +++ b/crates/libs/bindgen/src/package_writer.rs @@ -101,7 +101,7 @@ impl Config<'_> { pub(crate) fn write_package(&self, tree: &TypeTree) { let output = &self.bindgen.output; for name in tree.nested.keys() { - _ = std::fs::remove_dir_all(format!("{output}/src/{name}")); + _ = std::fs::remove_dir_all(output.join("src").join(name)); } let trees = tree.flatten_trees(); @@ -119,7 +119,7 @@ impl Config<'_> { return; } - let directory = format!("{output}/src/{}", tree.namespace.replace('.', "/")); + let directory = output.join("src").join(tree.namespace.replace('.', "/")); // Flat Win32/WDK umbrellas glob-reexport private per-header child modules. let flatten_children = is_flat_container(tree.namespace); @@ -200,11 +200,11 @@ impl Config<'_> { tokens.combine(ty.write(&config)); } - let path = format!("{directory}/mod.rs"); + let path = directory.join("mod.rs"); write_to_file(&path, self.format(&tokens.into_string())); }); - let toml_path = format!("{output}/Cargo.toml"); + let toml_path = output.join("Cargo.toml"); let mut toml = String::new(); for line in read_file_lines(&toml_path) { diff --git a/crates/libs/clang/readme.md b/crates/libs/clang/readme.md index 2f3f7733f35..2b66f29d93a 100644 --- a/crates/libs/clang/readme.md +++ b/crates/libs/clang/readme.md @@ -20,8 +20,13 @@ Point it at one or more headers and write the resulting per-header RDL, then fee ```rust,no_run windows_clang::clang() - .input_str("#include ") - .output("example.rdl") - .write() + .input("Example.h") + .output("rdl") + .namespace("Example") + .write_by_header() .unwrap(); ``` + +Use `.reference("dependency.winmd")` when the headers refer to types defined by another metadata +file. Use `.input_text(source)` or `.input_texts(sources)` for C/C++ source already in memory. Use +`.reference_default()` for the standard Windows metadata. diff --git a/crates/libs/clang/src/const.rs b/crates/libs/clang/src/const.rs index 068309ffe56..226aae27ce0 100644 --- a/crates/libs/clang/src/const.rs +++ b/crates/libs/clang/src/const.rs @@ -195,7 +195,7 @@ impl Const { } // Put the synthetic file beside the header; include by basename so relative includes work. - let input_basename = std::path::Path::new(input) + let input_basename = Path::new(input) .file_name() .and_then(|n| n.to_str()) .unwrap_or(input); @@ -219,7 +219,7 @@ impl Const { // There is no on-disk directory context, so relative includes may not resolve. let prefix = format!("{content}\n{NARG_PROLOGUE}"); - const SYNTHETIC: &str = "__rdl_input_str_eval__.cpp"; + const SYNTHETIC: &str = "__rdl_input_text_eval__.cpp"; Self::evaluate_names(&prefix, SYNTHETIC, names, index, args) } diff --git a/crates/libs/clang/src/lib.rs b/crates/libs/clang/src/lib.rs index 802167c5dfb..6029a61c314 100644 --- a/crates/libs/clang/src/lib.rs +++ b/crates/libs/clang/src/lib.rs @@ -2,13 +2,14 @@ #![doc = include_str!("../readme.md")] use std::collections::{BTreeMap, HashMap, HashSet}; +use std::path::{Path, PathBuf}; use windows_metadata as metadata; use proc_macro2::{Literal, Span, TokenStream}; use quote::quote; use windows_rdl::emit::{uuid_to_u128_literal, write_ident, write_typed_value}; -use windows_rdl::{Error, expand_input_paths, formatter, implib, write_to_file}; +use windows_rdl::{Error, expand_input_files, formatter, implib, write_to_file}; mod cx; use cx::*; @@ -499,9 +500,10 @@ impl<'a> Parser<'a> { #[derive(Default, Clone)] /// Builder that generates RDL from C/C++ headers using libclang. pub struct Clang { - input: Vec, - input_str: Vec, - output: String, + input: Vec, + input_text: Vec, + reference: Vec, + output: PathBuf, namespace: String, args: Vec, library: String, @@ -520,8 +522,8 @@ pub struct Clang { /// Drops functions with no resolved import library; off for fixtures without `.lib` inputs. drop_lib_less: bool, /// Winmds used only to classify `ABI::Windows::*` projection declarations. - resolution_input: Vec, - input_default: bool, + resolution_input: Vec, + reference_default: bool, resolution_default: bool, reference_bytes: Vec>, resolution_bytes: Vec>, @@ -532,8 +534,6 @@ pub struct Clang { struct HeaderPass<'a> { /// Flat namespace root every partition emits into (`Windows.Win32`). root: &'a str, - /// Defining-header stems whose partitions are written (empty writes all). - allow: &'a HashSet<&'a str>, /// Resolution-winmd type-name membership for `ABI::Windows::*` declarations. winrt_types: &'a HashSet, } @@ -544,32 +544,57 @@ impl Clang { Self::default() } - /// Adds an input header (`.h`) or `.winmd` file or directory. `"default"` selects the default - /// Windows metadata references. - pub fn input(&mut self, input: &str) -> &mut Self { - if input == "default" { - self.input_default() - } else { - self.input.push(input.to_string()); - self - } + /// Adds an input header (`.h`) file or directory. + pub fn input(&mut self, input: impl AsRef) -> &mut Self { + self.input.push(input.as_ref().to_path_buf()); + self } - /// Adds input headers or `.winmd` files. + /// Adds input headers. pub fn inputs(&mut self, inputs: I) -> &mut Self where I: IntoIterator, - S: AsRef, + S: AsRef, { for input in inputs { - self.input(input.as_ref()); + self.input(input); } self } /// Adds inline source text to compile instead of a file on disk. - pub fn input_str(&mut self, input: &str) -> &mut Self { - self.input_str.push(input.to_string()); + pub fn input_text(&mut self, input: &str) -> &mut Self { + self.input_text.push(input.to_string()); + self + } + + /// Adds inline source texts to compile instead of files on disk. + pub fn input_texts(&mut self, inputs: I) -> &mut Self + where + I: IntoIterator, + S: AsRef, + { + for input in inputs { + self.input_text(input.as_ref()); + } + self + } + + /// Adds a reference winmd file or directory. + pub fn reference(&mut self, input: impl AsRef) -> &mut Self { + self.reference.push(input.as_ref().to_path_buf()); + self + } + + /// Adds multiple reference winmd files or directories. + pub fn references(&mut self, inputs: I) -> &mut Self + where + I: IntoIterator, + S: AsRef, + { + for input in inputs { + self.reference(input); + } self } @@ -579,15 +604,27 @@ impl Clang { self } + /// Adds reference winmds from memory. + pub fn reference_byte_sets(&mut self, inputs: I) -> &mut Self + where + I: IntoIterator, + B: AsRef<[u8]>, + { + for input in inputs { + self.reference_bytes(input.as_ref()); + } + self + } + /// Adds the default Windows metadata as references. - pub fn input_default(&mut self) -> &mut Self { - self.input_default = true; + pub fn reference_default(&mut self) -> &mut Self { + self.reference_default = true; self } /// Sets the output `.rdl` file path. - pub fn output(&mut self, output: &str) -> &mut Self { - self.output = output.to_string(); + pub fn output(&mut self, output: impl AsRef) -> &mut Self { + self.output = output.as_ref().to_path_buf(); self } @@ -604,20 +641,27 @@ impl Clang { } /// Drops functions with no resolved import library; leave off without `.lib` inputs. - pub fn drop_lib_less(&mut self, drop_lib_less: bool) -> &mut Self { - self.drop_lib_less = drop_lib_less; + pub fn drop_lib_less(&mut self) -> &mut Self { + self.drop_lib_less = true; self } - /// Adds a winmd used only to classify `ABI::Windows::*` projection declarations. `"default"` - /// selects the default Windows Runtime metadata. - pub fn resolution_input(&mut self, input: &str) -> &mut Self { - if input == "default" { - self.resolution_default() - } else { - self.resolution_input.push(input.to_string()); - self + /// Adds a winmd used only to classify `ABI::Windows::*` projection declarations. + pub fn resolution_input(&mut self, input: impl AsRef) -> &mut Self { + self.resolution_input.push(input.as_ref().to_path_buf()); + self + } + + /// Adds winmds used only to classify `ABI::Windows::*` projection declarations. + pub fn resolution_inputs(&mut self, inputs: I) -> &mut Self + where + I: IntoIterator, + S: AsRef, + { + for input in inputs { + self.resolution_input(input); } + self } /// Adds a resolution-only winmd from memory. @@ -626,6 +670,18 @@ impl Clang { self } + /// Adds resolution-only winmds from memory. + pub fn resolution_byte_sets(&mut self, inputs: I) -> &mut Self + where + I: IntoIterator, + B: AsRef<[u8]>, + { + for input in inputs { + self.resolution_bytes(input.as_ref()); + } + self + } + /// Adds the default Windows Runtime metadata as a resolution-only input. pub fn resolution_default(&mut self) -> &mut Self { self.resolution_default = true; @@ -650,8 +706,8 @@ impl Clang { } /// Reads a COFF import library and adds its symbol -> DLL mappings. - pub fn import_library(&mut self, path: &str) -> Result<&mut Self, Error> { - extend_libraries(&mut self.libraries, path)?; + pub fn import_library(&mut self, path: impl AsRef) -> Result<&mut Self, Error> { + extend_libraries(&mut self.libraries, path.as_ref())?; Ok(self) } @@ -697,48 +753,72 @@ impl Clang { self } - /// Sets header directory segments that act as roots for the reachability sweep. - pub fn scope(&mut self, scope: I) -> &mut Self + /// Adds a header directory segment that acts as a root for the reachability sweep. + pub fn scope(&mut self, scope: &str) -> &mut Self { + self.scope.push(scope.to_string()); + self + } + + /// Adds multiple header directory segments as roots for the reachability sweep. + pub fn scopes(&mut self, scopes: I) -> &mut Self where I: IntoIterator, S: AsRef, { - for seg in scope { - self.scope.push(seg.as_ref().to_string()); + for scope in scopes { + self.scope(scope.as_ref()); } self } - /// Marks specific headers as sweep roots regardless of SDK directory. + /// Marks a header as a sweep root regardless of SDK directory. + pub fn scope_header(&mut self, header: &str) -> &mut Self { + let stem = header_stem_to_namespace(header); + if !stem.is_empty() { + self.scope_headers.insert(stem); + } + self + } + + /// Marks multiple headers as sweep roots regardless of SDK directory. pub fn scope_headers(&mut self, headers: I) -> &mut Self where I: IntoIterator, S: AsRef, { for header in headers { - let stem = header_stem_to_namespace(header.as_ref()); - if !stem.is_empty() { - self.scope_headers.insert(stem); - } + self.scope_header(header.as_ref()); } self } - /// Drops named header partitions before the reachability sweep. + /// Drops a named header partition before the reachability sweep. + pub fn exclude_header(&mut self, header: &str) -> &mut Self { + let stem = header_stem_to_namespace(header); + if !stem.is_empty() { + self.exclude_headers.insert(stem); + } + self + } + + /// Drops multiple named header partitions before the reachability sweep. pub fn exclude_headers(&mut self, headers: I) -> &mut Self where I: IntoIterator, S: AsRef, { for header in headers { - let stem = header_stem_to_namespace(header.as_ref()); - if !stem.is_empty() { - self.exclude_headers.insert(stem); - } + self.exclude_header(header.as_ref()); } self } + /// Restricts root emission to a named function symbol. + pub fn symbol(&mut self, symbol: &str) -> &mut Self { + self.symbols.insert(symbol.to_string()); + self + } + /// Restricts root emission to the named function symbols. pub fn symbols(&mut self, symbols: I) -> &mut Self where @@ -746,7 +826,7 @@ impl Clang { S: AsRef, { for symbol in symbols { - self.symbols.insert(symbol.as_ref().to_string()); + self.symbol(symbol.as_ref()); } self } @@ -759,6 +839,7 @@ impl Clang { /// Generates the RDL and writes it to the configured output. pub fn write(&self) -> Result<(), Error> { + self.validate_output()?; let reference = self.load_reference()?; let spec = NamespaceSpec { namespace: &self.namespace, @@ -773,28 +854,31 @@ impl Clang { } /// Writes one flat-root RDL file per defining header. - /// - /// `partitions` limits which files are written, not which references resolve. Winmd - /// inputs act as exclusion references for additive scrapes. - pub fn write_by_header( - &self, - root: &str, - partitions: &[&str], - output_dir: &str, - ) -> Result<(), Error> { - let allow: HashSet<&str> = partitions.iter().copied().collect(); - let outputs = self.parse_and_emit_by_header(root, &allow)?; + pub fn write_by_header(&self) -> Result<(), Error> { + self.validate_output()?; + let outputs = self.parse_and_emit_by_header(&self.namespace)?; for (stem, rdl) in outputs { // File names are lowercased defining-header stems. let leaf = stem.to_lowercase(); - write_to_file(&format!("{output_dir}/{leaf}.rdl"), formatter::format(&rdl))?; + write_to_file( + self.output.join(format!("{leaf}.rdl")), + formatter::format(&rdl), + )?; } Ok(()) } + fn validate_output(&self) -> Result<(), Error> { + if self.output.as_os_str().is_empty() { + Err(Error::new("output is required", "", 0, 0)) + } else { + Ok(()) + } + } + /// Parses inputs once and returns the libclang state that keeps the TUs valid. fn parse_inputs(&self) -> Result { - let (h_paths, _) = expand_input_paths(&self.input, "h", "winmd")?; + let h_paths = expand_input_files(&self.input, "h")?; let library = Library::new()?; let index = Index::new()?; @@ -810,10 +894,18 @@ impl Clang { let mut h_tus = vec![]; for input in &h_paths { - h_tus.push((input.clone(), index.parse(input, &arg_refs)?)); + let source = input.to_str().ok_or_else(|| { + Error::new( + "input path is not valid UTF-8", + &input.to_string_lossy(), + 0, + 0, + ) + })?; + h_tus.push((source.replace('\\', "/"), index.parse(source, &arg_refs)?)); } let mut str_tus = vec![]; - for content in &self.input_str { + for content in &self.input_text { str_tus.push(( content.clone(), index.parse_unsaved( @@ -835,11 +927,7 @@ impl Clang { } /// Emits one flat-root RDL string per defining-header stem. - fn parse_and_emit_by_header( - &self, - root: &str, - allow: &HashSet<&str>, - ) -> Result, Error> { + fn parse_and_emit_by_header(&self, root: &str) -> Result, Error> { // Additive scrapes skip entities already defined by input winmds. Split type and // value names because functions/constants live on `Apis`, not in `iter()`. let reference = self.load_reference()?; @@ -883,7 +971,6 @@ impl Clang { let pass = HeaderPass { root, - allow, winrt_types: &winrt_types, }; @@ -1050,11 +1137,7 @@ impl Clang { scope_in: &mut BTreeMap, eval: MacroEval<'_>, ) -> Result<(), Error> { - let HeaderPass { - root, - allow, - winrt_types, - } = *pass; + let HeaderPass { root, winrt_types } = *pass; // Abort on diagnostics in emitted headers; tolerate transitive-only include errors // so interop headers can survive broken C++/WinRT projection includes. for diag in tu.diagnostics() { @@ -1126,13 +1209,9 @@ impl Clang { } } - // The allowlist limits written files, not cross-header resolution. let mut buckets: BTreeMap> = BTreeMap::new(); for (_, (child, extern_c)) in chosen { let stem = header_stem_of(&child).expect("filtered above"); - if !allow.is_empty() && !allow.contains(stem.as_str()) { - continue; - } // Keep a partition in-scope if any contributing cursor is in-scope. if !self.scope.is_empty() { let in_scope = self.scope_headers.contains(&stem) @@ -1220,16 +1299,17 @@ impl Clang { /// Loads `.winmd` reference inputs for cross-namespace resolution. fn load_reference(&self) -> Result { - let (_, winmd_paths) = expand_input_paths(&self.input, "h", "winmd")?; + let winmd_paths = expand_input_files(&self.reference, "winmd")?; let mut winmd_files = vec![]; for file_name in &winmd_paths { + let source = file_name.to_string_lossy(); winmd_files.push( metadata::reader::File::read(file_name) - .ok_or_else(|| Error::new("invalid input", file_name, 0, 0))?, + .ok_or_else(|| Error::new("invalid reference", &source, 0, 0))?, ); } - if self.input_default { + if self.reference_default { winmd_files.extend( [windows_default::WINRT, windows_default::WIN32] .into_iter() @@ -1239,7 +1319,7 @@ impl Clang { for bytes in &self.reference_bytes { winmd_files.push( metadata::reader::File::new(bytes.to_vec()) - .ok_or_else(|| Error::new("invalid input", "", 0, 0))?, + .ok_or_else(|| Error::new("invalid reference", "", 0, 0))?, ); } @@ -1250,9 +1330,10 @@ impl Clang { fn load_winrt_types(&self) -> Result, Error> { let mut winmd_files = vec![]; for file_name in &self.resolution_input { + let source = file_name.to_string_lossy(); winmd_files.push( metadata::reader::File::read(file_name) - .ok_or_else(|| Error::new("invalid input", file_name, 0, 0))?, + .ok_or_else(|| Error::new("invalid resolution input", &source, 0, 0))?, ); } if self.resolution_default { @@ -1261,7 +1342,7 @@ impl Clang { for bytes in &self.resolution_bytes { winmd_files.push( metadata::reader::File::new(bytes.to_vec()) - .ok_or_else(|| Error::new("invalid input", "", 0, 0))?, + .ok_or_else(|| Error::new("invalid resolution input", "", 0, 0))?, ); } let index = metadata::reader::Index::new(winmd_files); diff --git a/crates/libs/clang/src/scope.rs b/crates/libs/clang/src/scope.rs index 25117204776..63098f953b9 100644 --- a/crates/libs/clang/src/scope.rs +++ b/crates/libs/clang/src/scope.rs @@ -1,8 +1,12 @@ use super::*; /// Add symbol -> DLL entries from an import library without overwriting existing ones. -pub(crate) fn extend_libraries(map: &mut HashMap, path: &str) -> Result<(), Error> { - let bytes = std::fs::read(path).map_err(|_| Error::new("invalid input", path, 0, 0))?; +pub(crate) fn extend_libraries( + map: &mut HashMap, + path: &Path, +) -> Result<(), Error> { + let source = path.to_string_lossy(); + let bytes = std::fs::read(path).map_err(|_| Error::new("invalid input", &source, 0, 0))?; for import in implib::read(&bytes)? { map.entry(import.symbol).or_insert(import.dll); } diff --git a/crates/libs/clang/src/scrape.rs b/crates/libs/clang/src/scrape.rs index 59238bd9c32..ab6d818c382 100644 --- a/crates/libs/clang/src/scrape.rs +++ b/crates/libs/clang/src/scrape.rs @@ -4,6 +4,7 @@ //! orchestration state: arches, outputs, seed metadata, and reference winmds. use crate::{Clang, clang_resource_dir}; +use std::path::{Path, PathBuf}; use windows_rdl::{ArchInput, merge_arch_rdl, reader}; /// Target architecture settings that differ between scrape passes. @@ -52,19 +53,19 @@ pub struct ScrapePlan { /// Root namespace; each defining header becomes `.`. pub root: String, /// Committed per-header RDL directory. - pub rdl_dir: String, + pub rdl_dir: PathBuf, /// Scratch directory for per-arch throwaway RDL dirs and winmds. - pub out_dir: String, + pub out_dir: PathBuf, /// Committed unified winmd output path. - pub winmd: String, + pub winmd: PathBuf, /// `archs[0]` writes `rdl_dir`; extras are folded in by arch-merge. pub archs: Vec, /// Exclusion/reference winmds needed by both clang and RDL reader passes. - pub reference_winmds: Vec, + pub reference_winmds: Vec, /// Resolution-only winmds: they qualify external references but never exclude entities. - pub resolution_winmds: Vec, + pub resolution_winmds: Vec, /// Optional hand-authored seed RDL, preserved across generated output clears. - pub seed: Option, + pub seed: Option, /// Scrape architectures concurrently. pub parallel: bool, } @@ -106,15 +107,15 @@ impl std::fmt::Display for Summary { /// Find `name` in `dirs`, returning a forward-slashed path. pub fn find_in_dirs(name: &str, dirs: &[String]) -> Option { dirs.iter() - .map(|dir| std::path::Path::new(dir).join(name)) + .map(|dir| Path::new(dir).join(name)) .find(|path| path.is_file()) .map(|path| path.to_string_lossy().replace('\\', "/")) } struct Job<'a> { arch: &'a Arch, - rdl_dir: String, - winmd: String, + rdl_dir: PathBuf, + winmd: PathBuf, } impl Clang { @@ -126,17 +127,20 @@ impl Clang { ); std::fs::create_dir_all(&plan.out_dir) - .unwrap_or_else(|e| panic!("failed to create `{}`: {e}", plan.out_dir)); + .unwrap_or_else(|e| panic!("failed to create `{}`: {e}", plan.out_dir.display())); std::fs::create_dir_all(&plan.rdl_dir) - .unwrap_or_else(|e| panic!("failed to create `{}`: {e}", plan.rdl_dir)); + .unwrap_or_else(|e| panic!("failed to create `{}`: {e}", plan.rdl_dir.display())); let canonical = &plan.archs[0]; - let winmd_file = std::path::Path::new(&plan.winmd) + let winmd_file = plan + .winmd .file_name() - .and_then(|n| n.to_str()) - .unwrap_or_else(|| panic!("`plan.winmd` has no file name: `{}`", plan.winmd)); - let stem = winmd_file.strip_suffix(".winmd").unwrap_or(winmd_file); - let canonical_winmd = format!("{}/{winmd_file}", plan.out_dir); + .unwrap_or_else(|| panic!("`plan.winmd` has no file name: `{}`", plan.winmd.display())); + let stem = plan + .winmd + .file_stem() + .unwrap_or_else(|| panic!("`plan.winmd` has no file stem: `{}`", plan.winmd.display())); + let canonical_winmd = plan.out_dir.join(winmd_file); // The canonical arch writes committed RDL; extras write throwaway dirs. let mut jobs = vec![Job { @@ -145,10 +149,12 @@ impl Clang { winmd: canonical_winmd.clone(), }]; for arch in &plan.archs[1..] { + let mut winmd_file = stem.to_os_string(); + winmd_file.push(format!(".{}.winmd", arch.name)); jobs.push(Job { arch, - rdl_dir: format!("{}/{}", plan.out_dir, arch.name), - winmd: format!("{}/{stem}.{}.winmd", plan.out_dir, arch.name), + rdl_dir: plan.out_dir.join(&arch.name), + winmd: plan.out_dir.join(winmd_file), }); } let multi_arch = jobs.len() > 1; @@ -203,20 +209,23 @@ impl Clang { reader.input(seed); } for reference in &plan.reference_winmds { - reader.input(reference); + reader.reference(reference); } for resolution in &plan.resolution_winmds { - reader.input(resolution); + reader.reference(resolution); } - reader - .output(&plan.winmd) - .write() - .unwrap_or_else(|e| panic!("failed to compile merged winmd `{}`: {e}", plan.winmd)); + reader.output(&plan.winmd).write().unwrap_or_else(|e| { + panic!( + "failed to compile merged winmd `{}`: {e}", + plan.winmd.display() + ) + }); winmd_wall = w.elapsed().as_secs_f32(); } else { // Single arch: publish the canonical job's winmd. - std::fs::copy(&canonical_winmd, &plan.winmd) - .unwrap_or_else(|e| panic!("failed to publish winmd to `{}`: {e}", plan.winmd)); + std::fs::copy(&canonical_winmd, &plan.winmd).unwrap_or_else(|e| { + panic!("failed to publish winmd to `{}`: {e}", plan.winmd.display()) + }); } let mut arch_timings = timings.into_inner().unwrap(); @@ -242,8 +251,8 @@ impl Clang { &self, plan: &ScrapePlan, arch: &Arch, - rdl_dir: &str, - winmd: &str, + rdl_dir: &Path, + winmd: &Path, resource_dir: Option<&str>, ) { clear_rdl_dir(rdl_dir, plan.seed.as_deref()); @@ -256,15 +265,22 @@ impl Clang { clang.args(["-resource-dir", dir]); } for reference in &plan.reference_winmds { - clang.input(reference); + clang.reference(reference); } for resolution in &plan.resolution_winmds { clang.resolution_input(resolution); } clang - .write_by_header(&plan.root, &[], rdl_dir) - .unwrap_or_else(|e| panic!("failed to generate partitions in `{rdl_dir}`: {e}")); + .namespace(&plan.root) + .output(rdl_dir) + .write_by_header() + .unwrap_or_else(|e| { + panic!( + "failed to generate partitions in `{}`: {e}", + rdl_dir.display() + ) + }); let mut rdl_paths = collect_rdl_paths(rdl_dir); if let Some(seed) = &plan.seed @@ -276,25 +292,28 @@ impl Clang { let mut reader = reader(); reader.inputs(&rdl_paths); for reference in &plan.reference_winmds { - reader.input(reference); + reader.reference(reference); } for resolution in &plan.resolution_winmds { - reader.input(resolution); + reader.reference(resolution); } - reader - .output(winmd) - .write() - .unwrap_or_else(|e| panic!("failed to compile `{rdl_dir}` into `{winmd}`: {e}")); + reader.output(winmd).write().unwrap_or_else(|e| { + panic!( + "failed to compile `{}` into `{}`: {e}", + rdl_dir.display(), + winmd.display() + ) + }); } } /// Remove stale generated `.rdl` partitions, preserving the seed file by name. -fn clear_rdl_dir(rdl_dir: &str, seed: Option<&str>) { +fn clear_rdl_dir(rdl_dir: &Path, seed: Option<&Path>) { std::fs::create_dir_all(rdl_dir) - .unwrap_or_else(|e| panic!("failed to create `{rdl_dir}`: {e}")); - let seed_name = seed.and_then(|s| std::path::Path::new(s).file_name()); - for entry in - std::fs::read_dir(rdl_dir).unwrap_or_else(|e| panic!("failed to read `{rdl_dir}`: {e}")) + .unwrap_or_else(|e| panic!("failed to create `{}`: {e}", rdl_dir.display())); + let seed_name = seed.and_then(Path::file_name); + for entry in std::fs::read_dir(rdl_dir) + .unwrap_or_else(|e| panic!("failed to read `{}`: {e}", rdl_dir.display())) { let path = entry.unwrap().path(); let is_seed = path.file_name() == seed_name; @@ -305,22 +324,21 @@ fn clear_rdl_dir(rdl_dir: &str, seed: Option<&str>) { } } -/// Sorted, forward-slashed `.rdl` file paths in a directory. -fn collect_rdl_paths(rdl_dir: &str) -> Vec { - let mut paths: Vec = std::fs::read_dir(rdl_dir) - .unwrap_or_else(|e| panic!("failed to read `{rdl_dir}`: {e}")) +/// Sorted `.rdl` file paths in a directory. +fn collect_rdl_paths(rdl_dir: &Path) -> Vec { + let mut paths: Vec = std::fs::read_dir(rdl_dir) + .unwrap_or_else(|e| panic!("failed to read `{}`: {e}", rdl_dir.display())) .filter_map(|entry| entry.ok()) .map(|entry| entry.path()) .filter(|path| path.extension().is_some_and(|x| x == "rdl")) - .map(|path| path.to_string_lossy().replace('\\', "/")) .collect(); paths.sort(); paths } /// Count committed partition files, excluding the seed. -fn count_partitions(rdl_dir: &str, seed: Option<&str>) -> usize { - let seed_name = seed.and_then(|s| std::path::Path::new(s).file_name()); +fn count_partitions(rdl_dir: &Path, seed: Option<&Path>) -> usize { + let seed_name = seed.and_then(Path::file_name); std::fs::read_dir(rdl_dir).map_or(0, |rd| { rd.filter_map(|e| e.ok()) .filter(|e| { diff --git a/crates/libs/csharp/tests/generate.rs b/crates/libs/csharp/tests/generate.rs index eb533d09b9b..5977452feb4 100644 --- a/crates/libs/csharp/tests/generate.rs +++ b/crates/libs/csharp/tests/generate.rs @@ -31,7 +31,7 @@ fn generate() -> String { windows_rdl::reader() .input(rdl.to_str().unwrap()) - .input_default() + .reference_default() .output(winmd.to_str().unwrap()) .write() .unwrap(); diff --git a/crates/libs/default/readme.md b/crates/libs/default/readme.md index 5065f68e5df..542a5aaafb2 100644 --- a/crates/libs/default/readme.md +++ b/crates/libs/default/readme.md @@ -4,9 +4,9 @@ The [windows-default](https://crates.io/crates/windows-default) crate provides t for Windows APIs as embedded byte slices. Build tools can use [`WINRT`] and [`WIN32`] without locating or distributing separate `.winmd` files. -Most callers should use `.input_default()` on `windows-bindgen`, `windows-rdl`, `windows-clang`, or -`windows-csharp`. Use this crate directly when implementing another tool that accepts metadata -bytes. +Most callers should use `.input_default()` on `windows-bindgen` or `windows-csharp`, and +`.reference_default()` on `windows-rdl` or `windows-clang`. Use this crate directly when +implementing another tool that accepts metadata bytes. Programs that link this crate include both metadata files in their binary. diff --git a/crates/libs/metadata/src/merge/mod.rs b/crates/libs/metadata/src/merge/mod.rs index 3a5c741fc71..f74c7200611 100644 --- a/crates/libs/metadata/src/merge/mod.rs +++ b/crates/libs/metadata/src/merge/mod.rs @@ -1,4 +1,5 @@ use super::*; +use std::path::{Path, PathBuf}; mod remap; pub use remap::Remapper; @@ -27,10 +28,10 @@ impl std::fmt::Display for Error { #[derive(Default)] pub struct Merger { - input: Vec, + input: Vec, /// `(path, arch_bits)` where bits are 1=X86, 2=X64, 4=Arm64. - arch_inputs: Vec<(String, i32)>, - output: String, + arch_inputs: Vec<(PathBuf, i32)>, + output: PathBuf, union_enums: bool, } @@ -39,25 +40,25 @@ impl Merger { Self::default() } - pub fn input(&mut self, input: &str) -> &mut Self { - self.input.push(input.to_string()); + pub fn input(&mut self, input: impl AsRef) -> &mut Self { + self.input.push(input.as_ref().to_path_buf()); self } pub fn inputs(&mut self, inputs: I) -> &mut Self where I: IntoIterator, - S: AsRef, + S: AsRef, { for input in inputs { - self.input.push(input.as_ref().to_string()); + self.input(input); } self } /// Adds an architecture-tagged input winmd file. - pub fn arch_input(&mut self, path: &str, arch: i32) -> &mut Self { - self.arch_inputs.push((path.to_string(), arch)); + pub fn arch_input(&mut self, path: impl AsRef, arch: i32) -> &mut Self { + self.arch_inputs.push((path.as_ref().to_path_buf(), arch)); self } @@ -67,26 +68,28 @@ impl Merger { /// produce two `TypeDef` rows. `tool_win32` uses this to reconcile a value type an `um` /// header truncates (for example `FILE_INFORMATION_CLASS`) with the complete `km` /// definition, yielding one enum carrying every member. - pub fn union_enums(&mut self, union_enums: bool) -> &mut Self { - self.union_enums = union_enums; + pub fn union_enums(&mut self) -> &mut Self { + self.union_enums = true; self } - pub fn output(&mut self, output: &str) -> &mut Self { - self.output = output.to_string(); + pub fn output(&mut self, output: impl AsRef) -> &mut Self { + self.output = output.as_ref().to_path_buf(); self } pub fn merge(&self) -> Result<(), Error> { - if self.output.is_empty() { + if self.output.as_os_str().is_empty() { return Err(Error::new("output is required")); } - let output_path = std::path::Path::new(&self.output); - let name = output_path + let name = self + .output .file_stem() .and_then(|s| s.to_str()) - .ok_or_else(|| Error::new(format!("invalid output path `{}`", self.output)))?; + .ok_or_else(|| { + Error::new(format!("invalid output path `{}`", self.output.display())) + })?; let files = read_inputs(&self.input)?; let index = reader::Index::new(files); @@ -208,21 +211,23 @@ impl Merger { let bytes = file.into_stream(); std::fs::write(&self.output, bytes) - .map_err(|e| Error::new(format!("failed to write `{}`: {e}", self.output))) + .map_err(|e| Error::new(format!("failed to write `{}`: {e}", self.output.display()))) } } -fn read_inputs(inputs: &[String]) -> Result, Error> { +fn read_inputs(inputs: &[PathBuf]) -> Result, Error> { let mut result = vec![]; for input in inputs { - let path = std::path::Path::new(input); - - if path.is_dir() { + if input.is_dir() { let prev_len = result.len(); - let entries = std::fs::read_dir(path) - .map_err(|e| Error::new(format!("failed to read directory `{input}`: {e}")))?; + let entries = std::fs::read_dir(input).map_err(|e| { + Error::new(format!( + "failed to read directory `{}`: {e}", + input.display() + )) + })?; for entry in entries.flatten() { let entry_path = entry.path(); @@ -232,21 +237,22 @@ fn read_inputs(inputs: &[String]) -> Result, Error> { .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("winmd")) { - let path_str = entry_path.to_string_lossy().to_string(); - let file = reader::File::read(&entry_path) - .ok_or_else(|| Error::new(format!("failed to read `{path_str}`")))?; + let file = reader::File::read(&entry_path).ok_or_else(|| { + Error::new(format!("failed to read `{}`", entry_path.display())) + })?; result.push(file); } } if result.len() == prev_len { return Err(Error::new(format!( - "no .winmd files found in directory `{input}`" + "no .winmd files found in directory `{}`", + input.display() ))); } } else { - let file = reader::File::read(path) - .ok_or_else(|| Error::new(format!("failed to read `{input}`")))?; + let file = reader::File::read(input) + .ok_or_else(|| Error::new(format!("failed to read `{}`", input.display())))?; result.push(file); } } diff --git a/crates/libs/metadata/src/merge/remap.rs b/crates/libs/metadata/src/merge/remap.rs index 40df627ad79..71797f70d43 100644 --- a/crates/libs/metadata/src/merge/remap.rs +++ b/crates/libs/metadata/src/merge/remap.rs @@ -3,8 +3,8 @@ use super::*; /// Rewrites a flat winmd into header-based namespaces for package generation. #[derive(Default)] pub struct Remapper { - input: Vec, - output: String, + input: Vec, + output: PathBuf, routes: HashMap, sources: Vec, fallback: String, @@ -15,8 +15,19 @@ impl Remapper { Self::default() } - pub fn input(&mut self, input: &str) -> &mut Self { - self.input.push(input.to_string()); + pub fn input(&mut self, input: impl AsRef) -> &mut Self { + self.input.push(input.as_ref().to_path_buf()); + self + } + + pub fn inputs(&mut self, inputs: I) -> &mut Self + where + I: IntoIterator, + S: AsRef, + { + for input in inputs { + self.input(input); + } self } @@ -26,11 +37,28 @@ impl Remapper { self } + /// Registers namespaces whose members are remapped. + pub fn sources(&mut self, namespaces: I) -> &mut Self + where + I: IntoIterator, + S: AsRef, + { + for namespace in namespaces { + self.source(namespace.as_ref()); + } + self + } + pub fn fallback(&mut self, namespace: &str) -> &mut Self { self.fallback = namespace.to_string(); self } + pub fn route(&mut self, name: impl Into, namespace: impl Into) -> &mut Self { + self.routes.insert(name.into(), namespace.into()); + self + } + pub fn routes(&mut self, routes: I) -> &mut Self where I: IntoIterator, @@ -38,26 +66,28 @@ impl Remapper { V: Into, { for (name, namespace) in routes { - self.routes.insert(name.into(), namespace.into()); + self.route(name, namespace); } self } - pub fn output(&mut self, output: &str) -> &mut Self { - self.output = output.to_string(); + pub fn output(&mut self, output: impl AsRef) -> &mut Self { + self.output = output.as_ref().to_path_buf(); self } pub fn remap(&self) -> Result<(), Error> { - if self.output.is_empty() { + if self.output.as_os_str().is_empty() { return Err(Error::new("output is required")); } - let output_path = std::path::Path::new(&self.output); - let name = output_path + let name = self + .output .file_stem() .and_then(|s| s.to_str()) - .ok_or_else(|| Error::new(format!("invalid output path `{}`", self.output)))?; + .ok_or_else(|| { + Error::new(format!("invalid output path `{}`", self.output.display())) + })?; let files = read_inputs(&self.input)?; let index = reader::Index::new(files); @@ -80,7 +110,7 @@ impl Remapper { let bytes = file.into_stream(); std::fs::write(&self.output, bytes) - .map_err(|e| Error::new(format!("failed to write `{}`: {e}", self.output))) + .map_err(|e| Error::new(format!("failed to write `{}`: {e}", self.output.display()))) } fn is_source_apis(&self, ty: reader::TypeDef) -> bool { diff --git a/crates/libs/rdl/readme.md b/crates/libs/rdl/readme.md index 825c0c35a0d..112dc7153e4 100644 --- a/crates/libs/rdl/readme.md +++ b/crates/libs/rdl/readme.md @@ -31,6 +31,10 @@ windows_rdl::writer() .unwrap(); ``` +Use `.reference("dependency.winmd")` when the RDL refers to types defined by another metadata file. +Use `.input_text(source)` or `.input_texts(sources)` for RDL already in memory. Use +`.reference_default()` for the standard Windows metadata. + The winmd writer matches `Param` rows by ECMA-335 `Param.Sequence`, not table order. Sparse methods still emit every signature parameter, using `pN` and the reader's type-based default direction when a row is absent. Sequence 0 return attributes are emitted on the return type. Duplicate and diff --git a/crates/libs/rdl/src/lib.rs b/crates/libs/rdl/src/lib.rs index 53bfc1a507f..b8e96e3d8da 100644 --- a/crates/libs/rdl/src/lib.rs +++ b/crates/libs/rdl/src/lib.rs @@ -13,6 +13,7 @@ mod writer; use emit::*; use std::collections::{BTreeMap, HashMap, HashSet}; +use std::path::{Path, PathBuf}; use syn::spanned::Spanned; use windows_metadata as metadata; @@ -106,7 +107,7 @@ pub fn reader() -> Reader { } /// Parses one `.rdl` file and returns the items it defines under `namespace`. -pub fn item_names(path: &str, namespace: &str) -> Result, Error> { +pub fn item_names(path: impl AsRef, namespace: &str) -> Result, Error> { reader::item_names(path, namespace) } @@ -117,17 +118,19 @@ pub fn writer() -> Writer { /// One architecture's RDL directory, compiled winmd, and architecture bitmask. pub struct ArchInput { - pub rdl_dir: String, - pub winmd: String, + pub rdl_dir: PathBuf, + pub winmd: PathBuf, pub bits: i32, } /// Arch-merges per-architecture scrapes and restores the per-header RDL partition. pub fn merge_arch_rdl( inputs: &[ArchInput], - seed: Option<&str>, - output_dir: &str, + seed: Option<&Path>, + output_dir: impl AsRef, ) -> Result<(), Error> { + let output_dir = output_dir.as_ref(); + if inputs.is_empty() { return Err(writer_err!( "merge_arch_rdl requires at least one arch input" @@ -137,14 +140,13 @@ pub fn merge_arch_rdl( // `Writer` clears `*.rdl`; capture the seed first so it can be restored verbatim. let seed = seed .map(|seed| { - let name = std::path::Path::new(seed) + let name = seed .file_name() - .and_then(|n| n.to_str()) - .ok_or_else(|| writer_err!("invalid seed path `{seed}`"))? - .to_string(); + .ok_or_else(|| writer_err!("invalid seed path `{}`", seed.display()))? + .to_os_string(); let text = std::fs::read(seed) - .map_err(|e| writer_err!("failed to read seed `{seed}`: {e}"))?; - Ok::<_, Error>((name, seed.to_string(), text)) + .map_err(|e| writer_err!("failed to read seed `{}`: {e}", seed.display()))?; + Ok::<_, Error>((name, seed.to_path_buf(), text)) }) .transpose()?; @@ -160,7 +162,6 @@ pub fn merge_arch_rdl( .map_err(|e| writer_err!("failed to create temp dir `{}`: {e}", temp.display()))?; let _scratch = ScratchDir(temp.clone()); let merged = temp.join("Windows.Win32.merged.winmd"); - let merged = merged.to_string_lossy().to_string(); let mut merger = metadata::merge(); for input in inputs { merger.arch_input(&input.winmd, input.bits); @@ -174,21 +175,19 @@ pub fn merge_arch_rdl( let mut map = HashMap::::new(); for input in inputs { for entry in std::fs::read_dir(&input.rdl_dir) - .map_err(|e| writer_err!("failed to read `{}`: {e}", input.rdl_dir))? + .map_err(|e| writer_err!("failed to read `{}`: {e}", input.rdl_dir.display()))? .flatten() { let path = entry.path(); if path.extension().is_none_or(|x| x != "rdl") - || path.file_name().and_then(|n| n.to_str()) - == seed.as_ref().map(|(name, _, _)| name.as_str()) + || path.file_name() == seed.as_ref().map(|(name, _, _)| name.as_os_str()) { continue; } let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else { continue; }; - let rdl_path = path.to_string_lossy().to_string(); - for name in reader::item_names(&rdl_path, "Windows.Win32")? { + for name in reader::item_names(&path, "Windows.Win32")? { map.entry(name).or_insert_with(|| stem.to_string()); } } @@ -202,14 +201,14 @@ pub fn merge_arch_rdl( // Restore the hand-authored seed if this metadata set has one. if let Some((_, seed_path, seed_text)) = seed { - write_to_file(&seed_path, seed_text)?; + write_to_file(seed_path, seed_text)?; } Ok(()) } /// Removes a scratch directory on every return path. -struct ScratchDir(std::path::PathBuf); +struct ScratchDir(PathBuf); impl Drop for ScratchDir { fn drop(&mut self) { @@ -217,23 +216,24 @@ impl Drop for ScratchDir { } } -pub fn expand_input_paths( - inputs: &[String], +pub fn expand_input_paths>( + inputs: &[P], ext1: &str, ext2: &str, -) -> Result<(Vec, Vec), Error> { +) -> Result<(Vec, Vec), Error> { let mut paths1 = vec![]; let mut paths2 = vec![]; for input in inputs { - let path = std::path::Path::new(input); + let path = input.as_ref(); + let display = path.to_string_lossy(); if path.is_dir() { let prev_total = paths1.len() + paths2.len(); for entry_path in path .read_dir() - .map_err(|_| Error::new("failed to read directory", input, 0, 0))? + .map_err(|_| Error::new("failed to read directory", &display, 0, 0))? .flatten() .map(|entry| entry.path()) { @@ -242,54 +242,64 @@ pub fn expand_input_paths( .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case(ext1)) { - paths1.push(entry_path.to_string_lossy().replace('\\', "/")); + paths1.push(entry_path); } else if entry_path .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case(ext2)) { - paths2.push(entry_path.to_string_lossy().replace('\\', "/")); + paths2.push(entry_path); } } } if paths1.len() + paths2.len() == prev_total { - return Err(Error::new( - &format!("failed to find .{ext1} or .{ext2} files in directory"), - input, - 0, - 0, - )); + let message = if ext1 == ext2 { + format!("failed to find .{ext1} files in directory") + } else { + format!("failed to find .{ext1} or .{ext2} files in directory") + }; + return Err(Error::new(&message, &display, 0, 0)); } } else if path .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case(ext1)) { - paths1.push(input.clone()); + paths1.push(path.to_path_buf()); } else if path .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case(ext2)) { - paths2.push(input.clone()); + paths2.push(path.to_path_buf()); } else { - return Err(Error::new( - &format!("expected .{ext1} or .{ext2} file"), - input, - 0, - 0, - )); + let message = if ext1 == ext2 { + format!("expected .{ext1} file") + } else { + format!("expected .{ext1} or .{ext2} file") + }; + return Err(Error::new(&message, &display, 0, 0)); } } Ok((paths1, paths2)) } -pub fn write_to_file>(path: &str, contents: C) -> Result<(), Error> { - if let Some(parent) = std::path::Path::new(path).parent() { +/// Expands file and directory inputs containing one file type. +pub fn expand_input_files>( + inputs: &[P], + extension: &str, +) -> Result, Error> { + Ok(expand_input_paths(inputs, extension, extension)?.0) +} + +pub fn write_to_file, C: AsRef<[u8]>>(path: P, contents: C) -> Result<(), Error> { + let path = path.as_ref(); + let display = path.to_string_lossy(); + if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) - .map_err(|_| writer_err!("failed to create directory `{path}`"))?; + .map_err(|_| writer_err!("failed to create directory `{display}`"))?; } - std::fs::write(path, contents).map_err(|_| writer_err!("failed to write file `{path}`")) + std::fs::write(path, contents).map_err(|_| writer_err!("failed to write file `{display}`")) } macro_rules! writer_err { diff --git a/crates/libs/rdl/src/reader/mod.rs b/crates/libs/rdl/src/reader/mod.rs index 7563678eff7..8a19437cab9 100644 --- a/crates/libs/rdl/src/reader/mod.rs +++ b/crates/libs/rdl/src/reader/mod.rs @@ -54,11 +54,12 @@ fn fixed_unsigned_value(value: u64) -> metadata::Value { #[derive(Default)] /// Builder that compiles RDL files into `.winmd` metadata. pub struct Reader { - input: Vec, - input_str: Vec, - input_default: bool, + input: Vec, + input_text: Vec, + reference: Vec, + reference_default: bool, reference_bytes: Vec>, - output: String, + output: PathBuf, } impl Reader { @@ -67,20 +68,45 @@ impl Reader { Self::default() } - /// Adds an input `.rdl` file (or `.winmd` reference) or directory. `"default"` selects the - /// default Windows metadata references. - pub fn input(&mut self, input: &str) -> &mut Self { - if input == "default" { - self.input_default() - } else { - self.input.push(input.to_string()); - self - } + /// Adds an input `.rdl` file or directory. + pub fn input(&mut self, input: impl AsRef) -> &mut Self { + self.input.push(input.as_ref().to_path_buf()); + self } /// Adds inline RDL source text to compile instead of a file on disk. - pub fn input_str(&mut self, input: &str) -> &mut Self { - self.input_str.push(input.to_string()); + pub fn input_text(&mut self, input: &str) -> &mut Self { + self.input_text.push(input.to_string()); + self + } + + /// Adds inline RDL source texts to compile instead of files on disk. + pub fn input_texts(&mut self, inputs: I) -> &mut Self + where + I: IntoIterator, + S: AsRef, + { + for input in inputs { + self.input_text(input.as_ref()); + } + self + } + + /// Adds a `.winmd` reference file or directory. + pub fn reference(&mut self, input: impl AsRef) -> &mut Self { + self.reference.push(input.as_ref().to_path_buf()); + self + } + + /// Adds multiple `.winmd` reference files or directories. + pub fn references(&mut self, inputs: I) -> &mut Self + where + I: IntoIterator, + S: AsRef, + { + for input in inputs { + self.reference(input); + } self } @@ -90,39 +116,52 @@ impl Reader { self } + /// Adds `.winmd` references from memory. + pub fn reference_byte_sets(&mut self, inputs: I) -> &mut Self + where + I: IntoIterator, + B: AsRef<[u8]>, + { + for input in inputs { + self.reference_bytes(input.as_ref()); + } + self + } + /// Adds the default Windows metadata references. - pub fn input_default(&mut self) -> &mut Self { - self.input_default = true; + pub fn reference_default(&mut self) -> &mut Self { + self.reference_default = true; self } - /// Adds multiple input `.rdl` files or `.winmd` references. + /// Adds multiple input `.rdl` files or directories. pub fn inputs(&mut self, inputs: I) -> &mut Self where I: IntoIterator, - S: AsRef, + S: AsRef, { for input in inputs { - self.input(input.as_ref()); + self.input(input); } self } /// Sets the output `.winmd` file path. - pub fn output(&mut self, output: &str) -> &mut Self { - self.output = output.to_string(); + pub fn output(&mut self, output: impl AsRef) -> &mut Self { + self.output = output.as_ref().to_path_buf(); self } /// Compiles the inputs and writes the `.winmd` to the configured output. pub fn write(&self) -> Result<(), Error> { - if self.output.is_empty() { + if self.output.as_os_str().is_empty() { return Err(Error::new("output is required", "", 0, 0)); } - let (rdl_paths, reference_paths) = expand_input_paths(&self.input, "rdl", "winmd")?; + let rdl_paths = expand_input_files(&self.input, "rdl")?; + let reference_paths = expand_input_files(&self.reference, "winmd")?; - let input = expand_rdl_files(&rdl_paths, &self.input_str)?; + let input = expand_rdl_files(&rdl_paths, &self.input_text)?; let mut index = Index::new(); @@ -135,13 +174,14 @@ impl Reader { let mut reference = vec![]; for file_name in &reference_paths { + let source = file_name.to_string_lossy(); reference.push( metadata::reader::File::read(file_name) - .ok_or_else(|| Error::new("invalid reference", file_name, 0, 0))?, + .ok_or_else(|| Error::new("invalid reference", &source, 0, 0))?, ); } - if self.input_default { + if self.reference_default { reference.extend( [windows_default::WINRT, windows_default::WIN32] .into_iter() @@ -159,10 +199,11 @@ impl Reader { let reference = metadata::reader::Index::new(reference); validate_use_declarations(&input, &index, &reference)?; - let assembly_name = std::path::Path::new(&self.output) + let assembly_name = self + .output .file_stem() .and_then(|file_name| file_name.to_str()) - .ok_or_else(|| Error::new("invalid output", &self.output, 0, 0))?; + .ok_or_else(|| Error::new("invalid output", &self.output.to_string_lossy(), 0, 0))?; let mut output = metadata::writer::File::new(assembly_name); output.set_reference(reference); @@ -246,13 +287,14 @@ impl Reader { } std::fs::write(&self.output, output.into_stream()) - .map_err(|error| Error::new(&error.to_string(), &self.output, 0, 0)) + .map_err(|error| Error::new(&error.to_string(), &self.output.to_string_lossy(), 0, 0)) } } /// Parses one `.rdl` file and returns the items it defines under `namespace`. -pub(crate) fn item_names(path: &str, namespace: &str) -> Result, Error> { - let input = expand_rdl_files(std::slice::from_ref(&path.to_string()), &[])?; +pub(crate) fn item_names(path: impl AsRef, namespace: &str) -> Result, Error> { + let path = path.as_ref().to_path_buf(); + let input = expand_rdl_files(std::slice::from_ref(&path), &[])?; let mut index = Index::new(); for file in &input { for item in &file.items { @@ -285,25 +327,26 @@ fn preprocess_rdl(contents: &str) -> std::borrow::Cow<'_, str> { std::borrow::Cow::Owned(result) } -fn expand_rdl_files(paths: &[String], input_str: &[String]) -> Result, Error> { +fn expand_rdl_files(paths: &[PathBuf], input_text: &[String]) -> Result, Error> { let mut input = vec![]; for path in paths { + let source = path.to_string_lossy(); let Ok(contents) = std::fs::read_to_string(path) else { - return Err(Error::new("failed to read binary file", path, 0, 0)); + return Err(Error::new("failed to read binary file", &source, 0, 0)); }; let contents = preprocess_rdl(&contents); let mut file = syn::parse_str::(&contents).map_err(|error| { let start = error.span().start(); - Error::new(&error.to_string(), path, start.line, start.column) + Error::new(&error.to_string(), &source, start.line, start.column) })?; - file.source.clone_from(path); + file.source = source.replace('\\', "/"); input.push(file); } - for contents in input_str { + for contents in input_text { let contents = preprocess_rdl(contents); let mut file = syn::parse_str::(&contents).map_err(|error| { let start = error.span().start(); @@ -1305,7 +1348,7 @@ fn use_glob_resolves_type() { let output = std::env::temp_dir().join("windows_rdl_use_glob_resolves_type.winmd"); reader() - .input_str( + .input_text( r#" use Other::*; @@ -1325,7 +1368,7 @@ mod Other { } "#, ) - .output(&output.to_string_lossy()) + .output(&output) .write() .unwrap(); } diff --git a/crates/libs/rdl/src/writer/mod.rs b/crates/libs/rdl/src/writer/mod.rs index bfd6c9f3c4e..b171840410a 100644 --- a/crates/libs/rdl/src/writer/mod.rs +++ b/crates/libs/rdl/src/writer/mod.rs @@ -24,11 +24,11 @@ use r#struct::*; #[derive(Default)] /// Builder that converts `.winmd` metadata into RDL. pub struct Writer { - input: Vec, + input: Vec, input_default: bool, input_bytes: Vec>, filter: Vec, - output: String, + output: PathBuf, split: bool, partition: Option>, } @@ -39,14 +39,10 @@ impl Writer { Self::default() } - /// Adds an input `.winmd` file or directory. `"default"` selects the default Windows metadata. - pub fn input(&mut self, input: &str) -> &mut Self { - if input == "default" { - self.input_default() - } else { - self.input.push(input.to_string()); - self - } + /// Adds an input `.winmd` file or directory. + pub fn input(&mut self, input: impl AsRef) -> &mut Self { + self.input.push(input.as_ref().to_path_buf()); + self } /// Adds a `.winmd` file from memory. @@ -55,6 +51,18 @@ impl Writer { self } + /// Adds `.winmd` files from memory. + pub fn input_byte_sets(&mut self, inputs: I) -> &mut Self + where + I: IntoIterator, + B: AsRef<[u8]>, + { + for input in inputs { + self.input_bytes(input.as_ref()); + } + self + } + /// Adds the default Windows metadata inputs. pub fn input_default(&mut self) -> &mut Self { self.input_default = true; @@ -62,8 +70,8 @@ impl Writer { } /// Sets the output `.rdl` file or directory path. - pub fn output(&mut self, output: &str) -> &mut Self { - self.output = output.to_string(); + pub fn output(&mut self, output: impl AsRef) -> &mut Self { + self.output = output.as_ref().to_path_buf(); self } @@ -71,10 +79,10 @@ impl Writer { pub fn inputs(&mut self, inputs: I) -> &mut Self where I: IntoIterator, - S: AsRef, + S: AsRef, { for input in inputs { - self.input(input.as_ref()); + self.input(input); } self } @@ -98,9 +106,9 @@ impl Writer { self } - /// Writes each namespace to a separate file when `true`. - pub fn split(&mut self, split: bool) -> &mut Self { - self.split = split; + /// Writes each namespace to a separate file. + pub fn split(&mut self) -> &mut Self { + self.split = true; self } @@ -112,12 +120,17 @@ impl Writer { /// Converts the inputs and writes the RDL to the configured output. pub fn write(&self) -> Result<(), Error> { + if self.output.as_os_str().is_empty() { + return Err(Error::new("output is required", "", 0, 0)); + } + let mut files = vec![]; - for file_name in &expand_input_paths(&self.input, "winmd", ".")?.0 { + for file_name in &expand_input_files(&self.input, "winmd")? { + let source = file_name.to_string_lossy(); files.push( metadata::reader::File::read(file_name) - .ok_or_else(|| Error::new("invalid input", file_name, 0, 0))?, + .ok_or_else(|| Error::new("invalid input", &source, 0, 0))?, ); } @@ -177,14 +190,11 @@ impl Writer { continue; } - let mut path = std::path::PathBuf::new(); + let mut path = PathBuf::new(); path.push(&self.output); path.push(format!("{stem}.rdl")); - let path_str = path - .to_str() - .ok_or_else(|| writer_err!("output path contains non-UTF-8 characters"))?; - write_to_file(path_str, formatter::format(&output))?; + write_to_file(path, formatter::format(&output))?; } return Ok(()); @@ -225,14 +235,11 @@ impl Writer { continue; } - let mut path = std::path::PathBuf::new(); + let mut path = PathBuf::new(); path.push(&self.output); path.push(format!("{namespace}.rdl")); - let path_str = path - .to_str() - .ok_or_else(|| writer_err!("output path contains non-UTF-8 characters"))?; - write_to_file(path_str, formatter::format(&output))?; + write_to_file(path, formatter::format(&output))?; } } else { let mut layout = Layout::new(); diff --git a/crates/samples/robot/component/build.rs b/crates/samples/robot/component/build.rs index 5324a99ed2e..ead9a94a8ba 100644 --- a/crates/samples/robot/component/build.rs +++ b/crates/samples/robot/component/build.rs @@ -3,7 +3,7 @@ fn main() { windows_rdl::reader() .input("src/robot.rdl") - .input_default() + .reference_default() .output("robot.winmd") .write() .unwrap(); @@ -14,6 +14,6 @@ fn main() { .output("src/bindings.rs") .filter("Robotics") .flat() - .implement(std::iter::empty::<&str>()) + .implement_all() .write(); } diff --git a/crates/samples/robot/component_cpp/build.rs b/crates/samples/robot/component_cpp/build.rs index 7ca6a34547d..cfb3441d68c 100644 --- a/crates/samples/robot/component_cpp/build.rs +++ b/crates/samples/robot/component_cpp/build.rs @@ -18,7 +18,7 @@ fn msvc_main() { windows_rdl::reader() .input("src/robot.rdl") - .input_default() + .reference_default() .output("robot.winmd") .write() .unwrap(); diff --git a/crates/samples/test_bench/component/build.rs b/crates/samples/test_bench/component/build.rs index 57906ff4b02..142f9736096 100644 --- a/crates/samples/test_bench/component/build.rs +++ b/crates/samples/test_bench/component/build.rs @@ -3,7 +3,7 @@ fn main() { windows_rdl::reader() .input("src/bench.rdl") - .input_default() + .reference_default() .output("bench.winmd") .write() .unwrap(); @@ -14,6 +14,6 @@ fn main() { .output("src/bindings.rs") .filter("Bench") .flat() - .implement(std::iter::empty::<&str>()) + .implement_all() .write(); } diff --git a/crates/tests/libs/bindgen/tests/bytes.rs b/crates/tests/libs/bindgen/tests/bytes.rs index 3ad1eeebe35..f6a10f6d18e 100644 --- a/crates/tests/libs/bindgen/tests/bytes.rs +++ b/crates/tests/libs/bindgen/tests/bytes.rs @@ -3,26 +3,26 @@ fn bindgen_accepts_metadata_bytes() { let temp = std::env::temp_dir(); let winmd = temp.join("windows_bindgen_bytes.winmd"); let output = temp.join("windows_bindgen_bytes.rs"); + let filters = temp.join("windows_bindgen_bytes.txt"); windows_rdl::reader() - .input_str( - r#" + .input_texts([r#" #[win32] mod Test { #[library("test.dll")] extern fn Function() -> u32; } -"#, - ) - .output(&winmd.to_string_lossy()) +"#]) + .output(&winmd) .write() .unwrap(); let bytes = std::fs::read(winmd).unwrap(); + std::fs::write(&filters, " // comment\nTest\n").unwrap(); windows_bindgen::builder() - .input_bytes(&bytes) - .output(&output.to_string_lossy()) - .filter("Test") + .input_byte_sets([bytes]) + .output(&output) + .filter_files([filters]) .flat() .write(); @@ -32,3 +32,93 @@ mod Test { .contains("fn Function") ); } + +#[test] +fn bindgen_accepts_command_files() { + let temp = std::env::temp_dir(); + let winmd = temp.join("windows_bindgen_commands.winmd"); + let output = temp.join("windows_bindgen_commands.rs"); + let commands = temp.join("windows_bindgen_commands.txt"); + let filters = temp.join("windows_bindgen_commands_filters.txt"); + + windows_rdl::reader() + .input_text( + r#" +#[win32] +mod Test { + #[library("test.dll")] + extern fn Function() -> u32; +} +"#, + ) + .output(&winmd) + .write() + .unwrap(); + + std::fs::write(&filters, "Test\n").unwrap(); + std::fs::write( + &commands, + format!( + "// commands\n--in {}\n--out {}\n--flat\n--filter-file {}\n", + winmd.display(), + output.display(), + filters.display() + ), + ) + .unwrap(); + + let commands = commands.to_string_lossy(); + windows_bindgen::bindgen(["--etc", commands.as_ref()]); + + assert!( + std::fs::read_to_string(output) + .unwrap() + .contains("fn Function") + ); +} + +#[test] +fn command_files_can_be_nested_and_combined() { + let temp = std::env::temp_dir(); + let winmd = temp.join("windows_bindgen_nested_commands.winmd"); + let output = temp.join("windows_bindgen_nested_commands.rs"); + let input_commands = temp.join("windows_bindgen_input_commands.txt"); + let output_commands = temp.join("windows_bindgen_output_commands.txt"); + let nested_commands = temp.join("windows_bindgen_nested_commands.txt"); + + windows_rdl::reader() + .input_text( + r#" +#[win32] +mod Test { + #[library("test.dll")] + extern fn Function() -> u32; +} +"#, + ) + .output(&winmd) + .write() + .unwrap(); + + std::fs::write(&input_commands, format!("--in {}\n", winmd.display())).unwrap(); + std::fs::write( + &nested_commands, + format!("--out {}\n--flat\n--filter Test\n", output.display()), + ) + .unwrap(); + std::fs::write( + &output_commands, + format!("--etc {}\n", nested_commands.display()), + ) + .unwrap(); + + let input_commands = input_commands.to_string_lossy(); + let output_commands = output_commands.to_string_lossy(); + windows_bindgen::bindgen(["--etc", input_commands.as_ref(), output_commands.as_ref()]); + + assert!( + std::fs::read_to_string(output) + .unwrap() + .contains("fn Function") + ); +} diff --git a/crates/tests/libs/bindgen/tests/errors.rs b/crates/tests/libs/bindgen/tests/errors.rs index 9341f5b6c7d..541add3f943 100644 --- a/crates/tests/libs/bindgen/tests/errors.rs +++ b/crates/tests/libs/bindgen/tests/errors.rs @@ -1,10 +1,10 @@ // Negative tests for windows-bindgen. The golden harness only feeds valid // input, so this exercises the panic path in `src/io.rs::read_file_lines`, -// reached when an `--etc` response file cannot be opened. +// reached when a command file cannot be opened. #[test] #[should_panic(expected = "failed to open file")] -fn etc_missing_response_file_panics() { +fn missing_command_file_panics() { let missing = std::env::temp_dir() .join("test_bindgen_missing_response_file.rsp") .to_string_lossy() @@ -13,13 +13,45 @@ fn etc_missing_response_file_panics() { windows_bindgen::bindgen(["--etc", &missing]); } +#[test] +#[should_panic(expected = "failed to open file")] +fn missing_filter_file_panics() { + let missing = std::env::temp_dir().join("test_bindgen_missing_filter_file.txt"); + windows_bindgen::builder().filter_file(missing); +} + +#[test] +#[should_panic(expected = "invalid option `--unknown`")] +fn invalid_option_panics() { + windows_bindgen::bindgen(["--unknown"]); +} + +#[test] +#[should_panic(expected = "output is required")] +fn missing_output_panics() { + windows_bindgen::bindgen(["--filter", "GetTickCount"]); +} + +#[test] +#[should_panic(expected = "cannot combine `--sys` and `--minimal`")] +fn conflicting_styles_panic() { + windows_bindgen::bindgen([ + "--out", + "unused.rs", + "--filter", + "GetTickCount", + "--sys", + "--minimal", + ]); +} + fn author_variadic(name: &str) -> (String, String) { let scratch = std::path::Path::new(env!("OUT_DIR")).join(name); std::fs::create_dir_all(&scratch).unwrap(); let winmd = scratch.join("out.winmd"); windows_rdl::reader() .input("input/variadic_fn_sys.rdl") - .output(winmd.to_str().unwrap()) + .output(&winmd) .write() .unwrap(); ( diff --git a/crates/tests/libs/clang/tests/clang.rs b/crates/tests/libs/clang/tests/clang.rs index 8691073aa5e..fb89fb209cc 100644 --- a/crates/tests/libs/clang/tests/clang.rs +++ b/crates/tests/libs/clang/tests/clang.rs @@ -2,6 +2,44 @@ include!(concat!(env!("OUT_DIR"), "/generated_tests.rs")); +#[test] +fn reference_rejects_non_winmd_input() { + let error = windows_clang::clang() + .reference("reference.rdl") + .output("unused.rdl") + .write() + .unwrap_err(); + assert_eq!(error.message, "expected .winmd file"); +} + +#[test] +fn terminals_require_output() { + let write = windows_clang::clang().write().unwrap_err(); + assert_eq!(write.message, "output is required"); + + let partition = windows_clang::clang().write_by_header().unwrap_err(); + assert_eq!(partition.message, "output is required"); +} + +#[test] +fn malformed_metadata_reports_its_role() { + let reference = windows_clang::clang() + .reference_bytes(b"not metadata") + .output("unused.rdl") + .write() + .unwrap_err(); + assert_eq!(reference.message, "invalid reference"); + assert_eq!(reference.file_name, ""); + + let resolution = windows_clang::clang() + .resolution_bytes(b"not metadata") + .output("unused") + .write_by_header() + .unwrap_err(); + assert_eq!(resolution.message, "invalid resolution input"); + assert_eq!(resolution.file_name, ""); +} + fn run(name: &str) { let input_path = format!("input/{name}.h"); let expected_path = format!("expected/{name}.rdl"); @@ -99,9 +137,7 @@ fn run(name: &str) { clang.resolution_default(); - for bytes in &reference_winmds { - clang.reference_bytes(bytes); - } + clang.reference_byte_sets(&reference_winmds); if !library.is_empty() { clang.library(&library); @@ -125,10 +161,14 @@ fn run(name: &str) { if flat { // Source-based per-header (flat) scrape, as `tool_win32`: one flat root namespace, - // `header_root.is_some()`. Emits every defining header in the parse (empty - // partition allowlist) into `scratch`; a self-contained fixture yields a single - // `.rdl` (the lowercased header stem, which matches `rdl_out`). - clang.write_by_header(&namespace, &[], &scratch).unwrap(); + // `header_root.is_some()`. Emits every defining header in the parse into `scratch`; + // a self-contained fixture yields a single `.rdl` (the lowercased header stem, + // which matches `rdl_out`). + clang + .namespace(&namespace) + .output(&scratch) + .write_by_header() + .unwrap(); } else { // Namespaced scrape, as `tool_webview`: `header_root.is_none()`, resolves external // types via the reference winmds. diff --git a/crates/tests/libs/clang/tests/header_partition.rs b/crates/tests/libs/clang/tests/header_partition.rs index 7e6639ea9d3..a9c9075e981 100644 --- a/crates/tests/libs/clang/tests/header_partition.rs +++ b/crates/tests/libs/clang/tests/header_partition.rs @@ -22,7 +22,11 @@ fn partition_by_defining_header() { .input("partition_input/a.h") .input("partition_input/b.h"); - clang.write_by_header("Test", &[], &scratch).unwrap(); + clang + .namespace("Test") + .output(&scratch) + .write_by_header() + .unwrap(); let shared = read(&scratch, "shared"); let a = read(&scratch, "a"); @@ -82,7 +86,11 @@ fn duplicate_typedef_prefers_direct_alias() { .input("partition_input/typedef_a.h") .input("partition_input/typedef_b.h"); - clang.write_by_header("Test", &[], &scratch).unwrap(); + clang + .namespace("Test") + .output(&scratch) + .write_by_header() + .unwrap(); let a = read(&scratch, "typedef_a"); let b = read(&scratch, "typedef_b"); @@ -110,11 +118,15 @@ fn duplicate_typedef_ignores_excluded_owner() { "--target=x86_64-pc-windows-msvc", "-fms-extensions", ]) - .exclude_headers(["duplicate_a.h"]) + .exclude_header("duplicate_a.h") .input("partition_input/duplicate_a.h") .input("partition_input/duplicate_b.h"); - clang.write_by_header("Test", &[], &scratch).unwrap(); + clang + .namespace("Test") + .output(&scratch) + .write_by_header() + .unwrap(); let b = read(&scratch, "duplicate_b"); assert!(b.contains("type DUPLICATE = i32"), "duplicate_b.rdl:\n{b}"); @@ -140,11 +152,15 @@ fn exclude_headers_drops_partition() { "-fms-extensions", ]) .library("test.dll") - .exclude_headers(["a.h"]) + .exclude_header("a.h") .input("partition_input/a.h") .input("partition_input/b.h"); - clang.write_by_header("Test", &[], &scratch).unwrap(); + clang + .namespace("Test") + .output(&scratch) + .write_by_header() + .unwrap(); assert!( !std::path::Path::new(&format!("{scratch}/a.rdl")).exists(), @@ -176,10 +192,14 @@ fn scope_sweeps_unreferenced_out_of_scope() { "-fms-extensions", ]) .library("test.dll") - .scope(["scope_api"]) + .scope("scope_api") .input("partition_input/scope_api/api.h"); - clang.write_by_header("Test", &[], &scratch).unwrap(); + clang + .namespace("Test") + .output(&scratch) + .write_by_header() + .unwrap(); let api = read(&scratch, "api"); let crt = read(&scratch, "crt"); @@ -209,11 +229,15 @@ fn preferred_duplicate_typedef_keeps_pointee_through_scope_sweep() { "--target=x86_64-pc-windows-msvc", "-fms-extensions", ]) - .scope(["scope_api"]) + .scope("scope_api") .input("partition_input/scope_api/z_api.h") .input("partition_input/scope_crt/a_crt.h"); - clang.write_by_header("Test", &[], &scratch).unwrap(); + clang + .namespace("Test") + .output(&scratch) + .write_by_header() + .unwrap(); let crt = read(&scratch, "a_crt"); assert!(crt.contains("type PFOO = *mut FOO"), "a_crt.rdl:\n{crt}"); @@ -238,7 +262,11 @@ fn dotted_header_flattens_to_single_partition() { .library("test.dll") .input("partition_input/Dotted.Name.Interop.h"); - clang.write_by_header("Test", &[], &scratch).unwrap(); + clang + .namespace("Test") + .output(&scratch) + .write_by_header() + .unwrap(); let dotted = read(&scratch, "dottednameinterop"); assert!( @@ -266,10 +294,14 @@ fn abi_projection_type_maps_and_sweeps() { "-fms-extensions", ]) .library("test.dll") - .scope(["abi_interop"]) + .scope("abi_interop") .input("partition_input/abi_interop/interop.h"); - clang.write_by_header("Test", &[], &scratch).unwrap(); + clang + .namespace("Test") + .output(&scratch) + .write_by_header() + .unwrap(); let interop = read(&scratch, "interop"); assert!( diff --git a/crates/tests/libs/csharp/tests/csharp.rs b/crates/tests/libs/csharp/tests/csharp.rs index 7b287bd7578..60bfa814362 100644 --- a/crates/tests/libs/csharp/tests/csharp.rs +++ b/crates/tests/libs/csharp/tests/csharp.rs @@ -47,7 +47,7 @@ fn author(name: &str, scratch: &Path) -> PathBuf { let winmd = scratch.join(format!("{name}.winmd")); windows_rdl::reader() .input(format!("input/{name}.rdl").as_str()) - .input_default() + .reference_default() .output(winmd.to_str().unwrap()) .write() .unwrap(); diff --git a/crates/tests/libs/csharp/tests/scale.rs b/crates/tests/libs/csharp/tests/scale.rs index 615e75ffe1d..0a9330f4f85 100644 --- a/crates/tests/libs/csharp/tests/scale.rs +++ b/crates/tests/libs/csharp/tests/scale.rs @@ -23,7 +23,7 @@ fn broad_maps_use_static_function_specialization() { let winmd = scratch.join("scale.winmd"); windows_rdl::reader() .input(rdl.to_str().unwrap()) - .input_default() + .reference_default() .output(winmd.to_str().unwrap()) .write() .unwrap(); @@ -81,7 +81,7 @@ fn measure(breadth: usize) { let winmd = scratch.join("scale.winmd"); windows_rdl::reader() .input(rdl.to_str().unwrap()) - .input_default() + .reference_default() .output(winmd.to_str().unwrap()) .write() .unwrap(); diff --git a/crates/tests/libs/metadata/tests/arch_roundtrip.rs b/crates/tests/libs/metadata/tests/arch_roundtrip.rs index b439aa5180d..05ea66c0150 100644 --- a/crates/tests/libs/metadata/tests/arch_roundtrip.rs +++ b/crates/tests/libs/metadata/tests/arch_roundtrip.rs @@ -5,8 +5,8 @@ fn winmd(dir: &std::path::Path, name: &str, rdl: &str) -> String { std::fs::write(&rdl_path, rdl).unwrap(); let out = dir.join(format!("{name}.winmd")); windows_rdl::reader() - .input(rdl_path.to_string_lossy().as_ref()) - .output(out.to_string_lossy().as_ref()) + .input(&rdl_path) + .output(&out) .write() .unwrap(); out.to_string_lossy().into_owned() @@ -52,16 +52,16 @@ fn arch_survives_winmd_rdl_roundtrip() { merge() .arch_input(&x64, 2) .arch_input(&arm, 4) - .output(merged.to_string_lossy().as_ref()) + .output(&merged) .merge() .unwrap(); let rdl_dir = dir.join("rdl"); std::fs::create_dir_all(&rdl_dir).unwrap(); windows_rdl::writer() - .input(merged.to_string_lossy().as_ref()) - .output(rdl_dir.to_string_lossy().as_ref()) - .split(true) + .input(&merged) + .output(&rdl_dir) + .split() .write() .unwrap(); @@ -82,8 +82,8 @@ fn arch_survives_winmd_rdl_roundtrip() { let out = dir.join("roundtrip.winmd"); windows_rdl::reader() - .input(rdl_dir.to_string_lossy().as_ref()) - .output(out.to_string_lossy().as_ref()) + .input(&rdl_dir) + .output(&out) .write() .unwrap(); let index = reader::Index::read(out.to_string_lossy().as_ref()).unwrap(); @@ -164,7 +164,7 @@ fn arch_divergent_nested_type_hoists_arch_to_enclosing() { .arch_input(&x64, 2) .arch_input(&arm, 4) .arch_input(&x86, 1) - .output(merged.to_string_lossy().as_ref()) + .output(&merged) .merge() .unwrap(); let index = reader::Index::read(merged.to_string_lossy().as_ref()).unwrap(); @@ -214,7 +214,7 @@ fn arch_divergent_forced_alignment_splits() { merge() .arch_input(&x64, 2) .arch_input(&arm, 4) - .output(merged.to_string_lossy().as_ref()) + .output(&merged) .merge() .unwrap(); let index = reader::Index::read(merged.to_string_lossy().as_ref()).unwrap(); @@ -251,7 +251,7 @@ fn subset_present_divergent_type_splits() { .arch_input(&x64, 2) .arch_input(&arm, 4) .arch_input(&x86, 1) - .output(merged.to_string_lossy().as_ref()) + .output(&merged) .merge() .unwrap(); let index = reader::Index::read(merged.to_string_lossy().as_ref()).unwrap(); @@ -290,7 +290,7 @@ fn arch_divergent_enum_constant_values_split() { merge() .arch_input(&x64, 2) .arch_input(&arm, 4) - .output(merged.to_string_lossy().as_ref()) + .output(&merged) .merge() .unwrap(); let index = reader::Index::read(merged.to_string_lossy().as_ref()).unwrap(); @@ -324,7 +324,7 @@ fn structurally_identical_arch_copies_coalesce() { .arch_input(&x64, 2) .arch_input(&x86, 1) .arch_input(&arm, 4) - .output(merged.to_string_lossy().as_ref()) + .output(&merged) .merge() .unwrap(); let index = reader::Index::read(merged.to_string_lossy().as_ref()).unwrap(); diff --git a/crates/tests/libs/metadata/tests/merge.rs b/crates/tests/libs/metadata/tests/merge.rs index ffcf4693ccf..5fab1e541f5 100644 --- a/crates/tests/libs/metadata/tests/merge.rs +++ b/crates/tests/libs/metadata/tests/merge.rs @@ -6,8 +6,8 @@ fn winmd(dir: &std::path::Path, name: &str, rdl: &str) -> String { std::fs::write(&rdl_path, rdl).unwrap(); let out = dir.join(format!("{name}.winmd")); windows_rdl::reader() - .input(rdl_path.to_string_lossy().as_ref()) - .output(out.to_string_lossy().as_ref()) + .input(&rdl_path) + .output(&out) .write() .unwrap(); out.to_string_lossy().into_owned() @@ -49,7 +49,7 @@ fn arch_merge_constants() { merge() .arch_input(&x64, 2) .arch_input(&arm, 4) - .output(merged.to_string_lossy().as_ref()) + .output(&merged) .merge() .unwrap(); @@ -97,8 +97,8 @@ fn union_enums_merges_members() { merge() .input(&um) .input(&km) - .union_enums(true) - .output(merged.to_string_lossy().as_ref()) + .union_enums() + .output(&merged) .merge() .unwrap(); @@ -135,8 +135,8 @@ fn union_enums_rejects_conflicting_values() { let result = merge() .input(&a) .input(&b) - .union_enums(true) - .output(merged.to_string_lossy().as_ref()) + .union_enums() + .output(&merged) .merge(); assert!(result.is_err(), "conflicting member values must error"); @@ -165,8 +165,8 @@ fn union_enums_rejects_conflicting_non_sentinel_max() { let result = merge() .input(&a) .input(&b) - .union_enums(true) - .output(merged.to_string_lossy().as_ref()) + .union_enums() + .output(&merged) .merge(); assert!( @@ -198,8 +198,8 @@ fn union_enums_merges_partial_copies() { merge() .input(&um) .input(&km) - .union_enums(true) - .output(merged.to_string_lossy().as_ref()) + .union_enums() + .output(&merged) .merge() .unwrap(); @@ -251,7 +251,7 @@ fn arch_merge_divergent_struct() { merge() .arch_input(&x64, 2) .arch_input(&arm, 4) - .output(merged.to_string_lossy().as_ref()) + .output(&merged) .merge() .unwrap(); @@ -296,7 +296,7 @@ fn arch_merge_normalizes_native_sized_callback_signature() { .arch_input(&x64, 2) .arch_input(&arm, 4) .arch_input(&x86, 1) - .output(merged.to_string_lossy().as_ref()) + .output(&merged) .merge() .unwrap(); @@ -329,7 +329,7 @@ fn arch_merge_does_not_infer_native_size_without_native_evidence() { .arch_input(&x64, 2) .arch_input(&arm, 4) .arch_input(&x86, 1) - .output(merged.to_string_lossy().as_ref()) + .output(&merged) .merge() .unwrap(); @@ -361,7 +361,7 @@ fn arch_merge_rejects_fixed_integer_with_wrong_pointer_width() { .arch_input(&x64, 2) .arch_input(&arm, 4) .arch_input(&x86, 1) - .output(merged.to_string_lossy().as_ref()) + .output(&merged) .merge() .unwrap(); @@ -391,7 +391,7 @@ fn arch_merge_rejects_callback_attribute_mismatch() { merge() .arch_input(&x64, 2) .arch_input(&x86, 1) - .output(merged.to_string_lossy().as_ref()) + .output(&merged) .merge() .unwrap(); diff --git a/crates/tests/libs/metadata/tests/nested_roundtrip.rs b/crates/tests/libs/metadata/tests/nested_roundtrip.rs index 5396e4bd28d..d74032dbff2 100644 --- a/crates/tests/libs/metadata/tests/nested_roundtrip.rs +++ b/crates/tests/libs/metadata/tests/nested_roundtrip.rs @@ -6,8 +6,8 @@ fn winmd(dir: &std::path::Path, name: &str, rdl: &str) -> String { std::fs::write(&rdl_path, rdl).unwrap(); let out = dir.join(format!("{name}.winmd")); windows_rdl::reader() - .input(rdl_path.to_string_lossy().as_ref()) - .output(out.to_string_lossy().as_ref()) + .input(&rdl_path) + .output(&out) .write() .unwrap(); out.to_string_lossy().into_owned() @@ -84,11 +84,7 @@ fn nested_types_survive_rdl_merge_and_writer() { // 2. merge() preserves the nested structure. let merged = dir.join("merged.winmd"); - merge() - .input(&winmd_path) - .output(merged.to_string_lossy().as_ref()) - .merge() - .unwrap(); + merge().input(&winmd_path).output(&merged).merge().unwrap(); let merged_index = reader::Index::read(merged.to_string_lossy().as_ref()).unwrap(); assert_nested(&merged_index); @@ -97,8 +93,8 @@ fn nested_types_survive_rdl_merge_and_writer() { std::fs::create_dir_all(&rdl_dir).unwrap(); windows_rdl::writer() .input(&winmd_path) - .output(rdl_dir.to_string_lossy().as_ref()) - .split(true) + .output(&rdl_dir) + .split() .write() .unwrap(); let rdl_text = std::fs::read_to_string(rdl_dir.join("Test.rdl")).unwrap(); @@ -114,8 +110,8 @@ fn nested_types_survive_rdl_merge_and_writer() { // 4. Reading that RDL back reproduces the nested structure. let roundtrip = dir.join("roundtrip.winmd"); windows_rdl::reader() - .input(rdl_dir.to_string_lossy().as_ref()) - .output(roundtrip.to_string_lossy().as_ref()) + .input(&rdl_dir) + .output(&roundtrip) .write() .unwrap(); let roundtrip_index = reader::Index::read(roundtrip.to_string_lossy().as_ref()).unwrap(); @@ -177,8 +173,8 @@ fn nested_type_inherits_parent_arch() { std::fs::create_dir_all(&rdl_dir).unwrap(); windows_rdl::writer() .input(&winmd_path) - .output(rdl_dir.to_string_lossy().as_ref()) - .split(true) + .output(&rdl_dir) + .split() .write() .unwrap(); let rdl_text = std::fs::read_to_string(rdl_dir.join("Test.rdl")).unwrap(); diff --git a/crates/tests/libs/metadata/tests/remap.rs b/crates/tests/libs/metadata/tests/remap.rs new file mode 100644 index 00000000000..44888932913 --- /dev/null +++ b/crates/tests/libs/metadata/tests/remap.rs @@ -0,0 +1,73 @@ +use windows_metadata::*; + +fn test_dir(name: &str) -> std::path::PathBuf { + std::env::temp_dir().join(format!("windows_metadata_{name}")) +} + +fn input_winmd(dir: &std::path::Path) -> std::path::PathBuf { + let input = dir.join("input.winmd"); + windows_rdl::reader() + .input_text( + "#[win32] mod Flat { \ + struct Routed { value: u32 } \ + struct RoutedMany { value: u32 } \ + struct Other { value: u32 } \ + }", + ) + .output(&input) + .write() + .unwrap(); + input +} + +#[test] +fn remapper_routes_types_and_uses_fallback() { + let dir = test_dir("remap_routes"); + std::fs::create_dir_all(&dir).unwrap(); + let input = input_winmd(&dir); + let output = dir.join("output.winmd"); + + remap() + .inputs([input]) + .sources(["Flat"]) + .route("Routed", "Flat.Routed") + .routes([("RoutedMany", "Flat.RoutedMany")]) + .fallback("Flat.Fallback") + .output(&output) + .remap() + .unwrap(); + + let index = reader::Index::read(output.to_string_lossy().as_ref()).unwrap(); + assert!(index.get("Flat.Routed", "Routed").next().is_some()); + assert!(index.get("Flat.RoutedMany", "RoutedMany").next().is_some()); + assert!(index.get("Flat.Fallback", "Other").next().is_some()); +} + +#[test] +fn remapper_reports_configuration_and_input_errors() { + let missing_output = remap().remap().unwrap_err().to_string(); + assert!(missing_output.contains("error: output is required")); + + let dir = test_dir("remap_errors"); + std::fs::create_dir_all(&dir).unwrap(); + let missing = dir.join("missing.winmd"); + let output = dir.join("output.winmd"); + let invalid_input = remap() + .input(&missing) + .output(output) + .remap() + .unwrap_err() + .to_string(); + assert!(invalid_input.contains(&format!("failed to read `{}`", missing.display()))); + + let missing_merge_output = merge().merge().unwrap_err().to_string(); + assert!(missing_merge_output.contains("error: output is required")); + + let invalid_merge_input = merge() + .input(&missing) + .output(dir.join("merged.winmd")) + .merge() + .unwrap_err() + .to_string(); + assert!(invalid_merge_input.contains(&format!("failed to read `{}`", missing.display()))); +} diff --git a/crates/tests/libs/rdl/tests/bytes.rs b/crates/tests/libs/rdl/tests/bytes.rs index bf53ed2a698..0251acb898a 100644 --- a/crates/tests/libs/rdl/tests/bytes.rs +++ b/crates/tests/libs/rdl/tests/bytes.rs @@ -8,7 +8,7 @@ fn temp_path(name: &str, extension: &str) -> String { #[test] fn default_input_resolves_default_metadata() { windows_rdl::reader() - .input_str( + .input_text( r#" use Windows::Foundation::*; @@ -20,8 +20,8 @@ mod Test { } "#, ) - .input("default") - .output(&temp_path("default_input", "winmd")) + .reference_default() + .output(temp_path("default_input", "winmd")) .write() .unwrap(); } @@ -31,7 +31,7 @@ fn reference_bytes_resolve_metadata() { let reference = temp_path("reference_bytes_reference", "winmd"); windows_rdl::reader() - .input_str( + .input_text( r#" #[winrt] mod Other { @@ -48,7 +48,7 @@ mod Other { let bytes = std::fs::read(reference).unwrap(); windows_rdl::reader() - .input_str( + .input_text( r#" use Other::*; @@ -60,8 +60,47 @@ mod Test { } "#, ) - .reference_bytes(&bytes) - .output(&temp_path("reference_bytes", "winmd")) + .reference_byte_sets([bytes]) + .output(temp_path("reference_bytes", "winmd")) + .write() + .unwrap(); +} + +#[test] +fn reference_path_resolves_metadata() { + let reference = temp_path("reference_path_reference", "winmd"); + + windows_rdl::reader() + .input_text( + r#" +#[winrt] +mod Other { + struct Point { + x: i32, + y: i32, + } +} +"#, + ) + .output(&reference) + .write() + .unwrap(); + + windows_rdl::reader() + .input_text( + r#" +use Other::*; + +#[winrt] +mod Test { + struct Wrapper { + value: Point, + } +} +"#, + ) + .reference(&reference) + .output(temp_path("reference_path", "winmd")) .write() .unwrap(); } @@ -72,7 +111,7 @@ fn writer_accepts_metadata_bytes() { let rdl = temp_path("writer_bytes_output", "rdl"); windows_rdl::reader() - .input_str( + .input_text( r#" #[win32] mod Test { @@ -88,7 +127,7 @@ mod Test { let bytes = std::fs::read(&winmd).unwrap(); windows_rdl::writer() - .input_bytes(&bytes) + .input_byte_sets([bytes]) .output(&rdl) .write() .unwrap(); diff --git a/crates/tests/libs/rdl/tests/errors.rs b/crates/tests/libs/rdl/tests/errors.rs index ad9bd8899f4..9cbc029c2a6 100644 --- a/crates/tests/libs/rdl/tests/errors.rs +++ b/crates/tests/libs/rdl/tests/errors.rs @@ -4,11 +4,8 @@ // `Debug` output, which defers to `Display`, so `should_panic` exercises all // three `Display` branches in `src/error.rs`. -fn out_path(name: &str) -> String { - std::path::Path::new(env!("OUT_DIR")) - .join(format!("test_rdl_err_{name}.winmd")) - .to_string_lossy() - .into_owned() +fn out_path(name: &str) -> std::path::PathBuf { + std::path::Path::new(env!("OUT_DIR")).join(format!("test_rdl_err_{name}.winmd")) } #[test] @@ -16,8 +13,8 @@ fn out_path(name: &str) -> String { fn syntax_error_reports_line_and_column() { // `Display` branch 1: a parse error carries a `file:line:column` location. windows_rdl::reader() - .input_str("#[winrt] mod Test { this is not valid rdl }") - .output(&out_path("syntax")) + .input_text("#[winrt] mod Test { this is not valid rdl }") + .output(out_path("syntax")) .write() .unwrap(); } @@ -27,18 +24,55 @@ fn syntax_error_reports_line_and_column() { fn missing_output_is_rejected() { // `Display` branch 2: empty file name yields a bare message with no `-->`. windows_rdl::reader() - .input_str("#[winrt] mod Test {}") + .input_text("#[winrt] mod Test {}") .write() .unwrap(); } #[test] -#[should_panic(expected = "expected .rdl or .winmd")] +fn writer_missing_output_is_rejected() { + let error = windows_rdl::writer().split().write().unwrap_err(); + assert_eq!(error.message, "output is required"); +} + +#[test] +fn malformed_metadata_reports_its_role() { + let reference_error = windows_rdl::reader() + .input_text("#[winrt] mod Test {}") + .reference_bytes(b"not metadata") + .output(out_path("invalid_reference")) + .write() + .unwrap_err(); + assert_eq!(reference_error.message, "invalid reference"); + assert_eq!(reference_error.file_name, ""); + + let input_error = windows_rdl::writer() + .input_bytes(b"not metadata") + .output(out_path("invalid_input").with_extension("rdl")) + .write() + .unwrap_err(); + assert_eq!(input_error.message, "invalid input"); + assert_eq!(input_error.file_name, ""); +} + +#[test] +fn writer_rejects_non_winmd_input() { + let error = windows_rdl::writer() + .input("input.rdl") + .output(out_path("writer_extension").with_extension("rdl")) + .write() + .unwrap_err(); + assert_eq!(error.message, "expected .winmd file"); + assert_eq!(error.file_name, "input.rdl"); +} + +#[test] +#[should_panic(expected = "expected .rdl file")] fn unsupported_input_extension_is_rejected() { // `Display` branch 3: a file name but no source location (line/column 0). windows_rdl::reader() .input("definitely_not_here.txt") - .output(&out_path("ext")) + .output(out_path("ext")) .write() .unwrap(); } @@ -55,7 +89,7 @@ fn integer_constants_reinterpret_bits_across_partitions() { // * `(LPCSTR)2` MAKEINTRESOURCE pointer constant // * a constant typed by an enum, encoded against its `#[repr]` integer windows_rdl::reader() - .input_str( + .input_text( "#[win32] mod Test {\n\ mod Win {\n\ type WORD = u16;\n\ @@ -74,7 +108,7 @@ fn integer_constants_reinterpret_bits_across_partitions() { }\n\ }", ) - .output(&out_path("const_reinterpret")) + .output(out_path("const_reinterpret")) .write() .unwrap(); } @@ -92,8 +126,8 @@ fn mixed_pointer_constness_is_rejected() { "#[win32]\nmod Test {{\n #[library(\"test.dll\")]\n extern fn Mixed(value: {ty});\n}}\n" ); let error = windows_rdl::reader() - .input_str(&source) - .output(&out_path(name)) + .input_text(&source) + .output(out_path(name)) .write() .unwrap_err(); diff --git a/crates/tests/libs/rdl/tests/method_params.rs b/crates/tests/libs/rdl/tests/method_params.rs index 48815519c83..f3a5c0a8e59 100644 --- a/crates/tests/libs/rdl/tests/method_params.rs +++ b/crates/tests/libs/rdl/tests/method_params.rs @@ -234,8 +234,8 @@ fn sparse_out_of_order_params_round_trip_with_flags_and_pseudos() { ); windows_rdl::writer() - .input(input.to_str().unwrap()) - .output(rdl.to_str().unwrap()) + .input(&input) + .output(&rdl) .write() .unwrap(); @@ -248,8 +248,8 @@ fn sparse_out_of_order_params_round_trip_with_flags_and_pseudos() { assert!(method.contains("-> #[noreturn] i32")); windows_rdl::reader() - .input(rdl.to_str().unwrap()) - .output(roundtrip.to_str().unwrap()) + .input(&rdl) + .output(&roundtrip) .write() .unwrap(); @@ -397,8 +397,8 @@ fn all_supported_param_attributes_and_directions_round_trip() { write_attribute_definitions(&attributes); windows_rdl::writer() - .input(input.to_str().unwrap()) - .output(rdl.to_str().unwrap()) + .input(&input) + .output(&rdl) .write() .unwrap(); @@ -424,9 +424,9 @@ fn all_supported_param_attributes_and_directions_round_trip() { assert!(method.contains("-> #[encoding(\"ansi\")] #[noreturn] i32")); windows_rdl::reader() - .input(rdl.to_str().unwrap()) - .input(attributes.to_str().unwrap()) - .output(roundtrip.to_str().unwrap()) + .input(&rdl) + .reference(&attributes) + .output(&roundtrip) .write() .unwrap(); @@ -558,8 +558,8 @@ fn malformed_param_sequence_is_reported() { ); let error = windows_rdl::writer() - .input(input.to_str().unwrap()) - .output(rdl.to_str().unwrap()) + .input(&input) + .output(&rdl) .write() .unwrap_err(); diff --git a/crates/tests/libs/rdl/tests/roundtrip.rs b/crates/tests/libs/rdl/tests/roundtrip.rs index e20145fcca0..6f12e5435a5 100644 --- a/crates/tests/libs/rdl/tests/roundtrip.rs +++ b/crates/tests/libs/rdl/tests/roundtrip.rs @@ -19,25 +19,23 @@ fn run(name: &str) { std::fs::create_dir_all(out_dir).unwrap(); let winmd_path = out_dir.join(format!("{name}.winmd")); - let winmd_str = winmd_path.to_str().unwrap(); windows_rdl::reader() .input(&input_path) - .output(winmd_str) + .output(&winmd_path) .write() .unwrap_or_else(|e| panic!("{name}: reader failed: {e}")); let rdl_out_path = out_dir.join(format!("{name}.rdl")); - let rdl_out_str = rdl_out_path.to_str().unwrap(); windows_rdl::writer() - .input(winmd_str) - .output(rdl_out_str) + .input(&winmd_path) + .output(&rdl_out_path) .write() .unwrap_or_else(|e| panic!("{name}: writer failed: {e}")); let actual = std::fs::read_to_string(&rdl_out_path) - .unwrap_or_else(|e| panic!("failed to read {rdl_out_str}: {e}")); + .unwrap_or_else(|e| panic!("failed to read {}: {e}", rdl_out_path.display())); if actual != original { // Overwrite input with canonical form so the user can review the diff. diff --git a/crates/tests/libs/win32_metadata/tests/win32.rs b/crates/tests/libs/win32_metadata/tests/win32.rs index 501425178ce..e30cdb14cbb 100644 --- a/crates/tests/libs/win32_metadata/tests/win32.rs +++ b/crates/tests/libs/win32_metadata/tests/win32.rs @@ -36,7 +36,7 @@ fn slice() { windows_rdl::reader() .input(&rdl_dir) .input(&seed) - .input(&winrt) + .reference(&winrt) .output(&winmd) .write() .unwrap(); diff --git a/crates/tests/winrt/composable_aggregation/build.rs b/crates/tests/winrt/composable_aggregation/build.rs index 97ab77b493b..df70e3deac6 100644 --- a/crates/tests/winrt/composable_aggregation/build.rs +++ b/crates/tests/winrt/composable_aggregation/build.rs @@ -4,7 +4,7 @@ fn main() { windows_rdl::reader() .output("metadata.winmd") .input("src/metadata.rdl") - .input_default() + .reference_default() .write() .unwrap(); diff --git a/crates/tools/composition/src/main.rs b/crates/tools/composition/src/main.rs index a4deb2e30ed..b095b32dda1 100644 --- a/crates/tools/composition/src/main.rs +++ b/crates/tools/composition/src/main.rs @@ -1,5 +1,5 @@ use std::io::Write; -use windows_bindgen::bindgen; +use windows_bindgen::builder; const FILTER: &str = "crates/tools/composition/src/composition.txt"; @@ -10,18 +10,14 @@ fn main() { // supplies ICompositorDesktopInterop and the HWND/BOOL types used to host a visual // tree in a plain window. Flat + minimal keeps the crate's own surface small and // namespace-free (see docs/crates/windows-composition.md). - bindgen([ - "--in", - "default", - "--out", - "crates/libs/composition/src/bindings.rs", - "--minimal", - "--dead-code", - "--flat", - "--filter", - "--etc", - FILTER, - ]); + builder() + .input_default() + .output("crates/libs/composition/src/bindings.rs") + .minimal() + .dead_code() + .flat() + .filter_file(FILTER) + .write(); // Lifted stack: Microsoft.UI.Composition (Microsoft.UI.winmd) mirrors the system // API, so the same wrapper source compiles against it. The filter is derived from @@ -29,19 +25,17 @@ fn main() { // single filter stays the source of truth for both stacks. Windows.winmd resolves // the shared foundation types (Color, TimeSpan, IVector, numerics). let lifted_filter = write_lifted_filter(); - bindgen([ - "--in", - "crates/tools/reactor/winmd/Microsoft.UI.winmd", - "crates/libs/default/Windows.winmd", - "--out", - "crates/libs/composition/src/bindings_lifted.rs", - "--minimal", - "--dead-code", - "--flat", - "--filter", - "--etc", - lifted_filter.to_str().expect("utf-8 temp path"), - ]); + builder() + .inputs([ + "crates/tools/reactor/winmd/Microsoft.UI.winmd", + "crates/libs/default/Windows.winmd", + ]) + .output("crates/libs/composition/src/bindings_lifted.rs") + .minimal() + .dead_code() + .flat() + .filter_file(lifted_filter) + .write(); println!( "tool_composition: generated system + lifted bindings in {:.2}s", diff --git a/crates/tools/reactor/src/main.rs b/crates/tools/reactor/src/main.rs index 47e5e2aad54..b53f441dec3 100644 --- a/crates/tools/reactor/src/main.rs +++ b/crates/tools/reactor/src/main.rs @@ -303,41 +303,36 @@ fn generate_reactor_bindings() { .write() .unwrap(); - let reactor_args = [ - "--in", - "crates/tools/reactor/winmd", - "default", - "--out", - "crates/libs/reactor/src/bindings.rs", - "--implement", - "Microsoft.UI.Xaml.IApplicationOverrides", - "Microsoft.UI.Xaml.Markup.IXamlMetadataProvider", - "--minimal", - "--dead-code", - "--flat", - "--filter", - "--etc", - "crates/tools/reactor/src/base.txt", - "crates/tools/reactor/src/generated.txt", - ]; - windows_bindgen::bindgen(reactor_args); - - let test_args = [ - "--in", - "crates/tools/reactor/winmd", - "default", - "--out", - "crates/tests/libs/reactor_selftest/src/bindings.rs", - "--minimal", - "--dead-code", - "--flat", - "--filter", - "--etc", - "crates/tools/reactor/src/base.txt", - "crates/tools/reactor/src/generated.txt", - "crates/tools/reactor/src/test.txt", - ]; - windows_bindgen::bindgen(test_args); + windows_bindgen::builder() + .input("crates/tools/reactor/winmd") + .input_default() + .output("crates/libs/reactor/src/bindings.rs") + .implements([ + "Microsoft.UI.Xaml.IApplicationOverrides", + "Microsoft.UI.Xaml.Markup.IXamlMetadataProvider", + ]) + .minimal() + .dead_code() + .flat() + .filter_files([ + "crates/tools/reactor/src/base.txt", + "crates/tools/reactor/src/generated.txt", + ]) + .write(); + + windows_bindgen::builder() + .input("crates/tools/reactor/winmd") + .input_default() + .output("crates/tests/libs/reactor_selftest/src/bindings.rs") + .minimal() + .dead_code() + .flat() + .filter_files([ + "crates/tools/reactor/src/base.txt", + "crates/tools/reactor/src/generated.txt", + "crates/tools/reactor/src/test.txt", + ]) + .write(); } /// Write `content` to `path` if changed. Runs `rustfmt` when `format` is true. diff --git a/crates/tools/roundtrip/src/main.rs b/crates/tools/roundtrip/src/main.rs index 715198a009e..5e7e51c03e8 100644 --- a/crates/tools/roundtrip/src/main.rs +++ b/crates/tools/roundtrip/src/main.rs @@ -37,7 +37,8 @@ fn main() { // Win32: compile the committed RDL (+ seed + WinRT resolution) to a um winmd, then // decompile it back under the committed header layout. compile( - &[WIN32_RDL, WIN32_SEED, WINRT_RESOLUTION], + &[WIN32_RDL, WIN32_SEED], + &[WINRT_RESOLUTION], WIN32_UM_WINMD, "Win32", ); @@ -46,7 +47,7 @@ fn main() { // WDK: compile the committed km RDL against the um winmd (its Win32 dependencies resolve // there), then decompile it back. Only WDK-defined types are emitted, so the round-trip // reproduces `metadata/wdk`. - compile(&[WDK_RDL, WIN32_UM_WINMD], WDK_KM_WINMD, "WDK"); + compile(&[WDK_RDL], &[WIN32_UM_WINMD], WDK_KM_WINMD, "WDK"); partitioned("WDK", WDK_KM_WINMD, WDK_RDL, None); println!( @@ -55,10 +56,11 @@ fn main() { ); } -/// Compiles RDL and winmd references into a single winmd. -fn compile(inputs: &[&str], output: &str, label: &str) { +/// Compiles RDL sources and winmd references into a single winmd. +fn compile(inputs: &[&str], references: &[&str], output: &str, label: &str) { reader() .inputs(inputs) + .references(references) .output(output) .write() .unwrap_or_else(|e| panic!("{label} winmd compile failed: {e}")); @@ -75,7 +77,7 @@ fn winrt() { writer() .input(WINRT_WINMD) .filters(["Windows", "!Windows.Win32"]) - .split(true) + .split() .output(WINRT_RDL) .write() .unwrap_or_else(|e| panic!("WinRT roundtrip failed: {e}")); diff --git a/crates/tools/webview/src/main.rs b/crates/tools/webview/src/main.rs index e815adaf048..e0c19239b3a 100644 --- a/crates/tools/webview/src/main.rs +++ b/crates/tools/webview/src/main.rs @@ -42,9 +42,9 @@ fn main() { "-fms-extensions", &include_arg, ]) - .input(include.join("WebView2.h").to_str().unwrap()) - .input(include_winrt.join("WebView2Interop.h").to_str().unwrap()) - .input_default() + .input(include.join("WebView2.h")) + .input(include_winrt.join("WebView2Interop.h")) + .reference_default() .output("target/webview/WebView2.rdl") .namespace("WebView2") .library("WebView2Loader.dll") @@ -53,7 +53,7 @@ fn main() { reader() .input("target/webview/WebView2.rdl") - .input_default() + .reference_default() .output("target/webview/WebView2.winmd") .write() .unwrap(); diff --git a/crates/tools/win32/src/km.rs b/crates/tools/win32/src/km.rs index 4f905b827cf..65dc37e98ff 100644 --- a/crates/tools/win32/src/km.rs +++ b/crates/tools/win32/src/km.rs @@ -110,10 +110,10 @@ pub fn scrape() -> Summary { .args(["-include", crate::SAL_SHIM]) .args(["-include", OFFREG_PRELUDE]) .args(include_args) - .drop_lib_less(true) - .scope(SCOPE.iter().copied()) + .drop_lib_less() + .scopes(SCOPE.iter().copied()) .scope_headers(SOURCE_HEADERS.iter().copied()); - clang.input_str(&source); + clang.input_text(&source); for lib in &import_libs { clang .import_library(lib) @@ -122,11 +122,11 @@ pub fn scrape() -> Summary { let summary = clang.scrape(&ScrapePlan { root: crate::ROOT.to_string(), - rdl_dir: RDL_DIR.to_string(), - out_dir: OUT_DIR.to_string(), - winmd: crate::KM_WINMD.to_string(), + rdl_dir: RDL_DIR.into(), + out_dir: OUT_DIR.into(), + winmd: crate::KM_WINMD.into(), archs, - reference_winmds: vec![REFERENCE_WINMD.to_string()], + reference_winmds: vec![REFERENCE_WINMD.into()], resolution_winmds: Vec::new(), seed: None, parallel: true, diff --git a/crates/tools/win32/src/main.rs b/crates/tools/win32/src/main.rs index 1c50f193c08..08f340c53e2 100644 --- a/crates/tools/win32/src/main.rs +++ b/crates/tools/win32/src/main.rs @@ -790,7 +790,7 @@ fn main() { windows_metadata::merge() .input(UM_WINMD) .input(KM_WINMD) - .union_enums(true) + .union_enums() .output(MERGED_WINMD) .merge() .unwrap_or_else(|e| panic!("failed to merge um + km winmds into `{MERGED_WINMD}`: {e}")); @@ -866,13 +866,11 @@ fn scrape_um() -> Summary { .args(CLANG_ARGS) .args(["-include", SAL_SHIM]) .args(include_args) - .drop_lib_less(true) - .scope(SCOPE.iter().copied()) + .drop_lib_less() + .scopes(SCOPE.iter().copied()) .scope_headers(scope_headers.iter().copied()) .exclude_headers(EXCLUDE_HEADERS.iter().copied()); - for source in &sources { - clang.input_str(source); - } + clang.input_texts(&sources); for lib in &import_libs { clang .import_library(lib) @@ -881,13 +879,13 @@ fn scrape_um() -> Summary { let summary = clang.scrape(&ScrapePlan { root: ROOT.to_string(), - rdl_dir: RDL_DIR.to_string(), - out_dir: OUT_DIR.to_string(), - winmd: UM_WINMD.to_string(), + rdl_dir: RDL_DIR.into(), + out_dir: OUT_DIR.into(), + winmd: UM_WINMD.into(), archs, reference_winmds: Vec::new(), - resolution_winmds: RESOLUTION_WINMDS.iter().map(|s| s.to_string()).collect(), - seed: Some(METADATA_SEED.to_string()), + resolution_winmds: RESOLUTION_WINMDS.iter().map(Into::into).collect(), + seed: Some(METADATA_SEED.into()), parallel: true, }); diff --git a/crates/tools/winrt/src/main.rs b/crates/tools/winrt/src/main.rs index 4142e73d8d4..beca52fc537 100644 --- a/crates/tools/winrt/src/main.rs +++ b/crates/tools/winrt/src/main.rs @@ -78,7 +78,7 @@ fn main() { windows_rdl::writer() .input(&merged) .filters(["Windows", "!Windows.Win32"]) - .split(true) + .split() .output(RDL_DIR) .write() .unwrap_or_else(|e| panic!("failed to write WinRT RDL to `{RDL_DIR}`: {e}")); diff --git a/docs/crates/windows-bindgen.md b/docs/crates/windows-bindgen.md index 739b41493f3..6515e42c578 100644 --- a/docs/crates/windows-bindgen.md +++ b/docs/crates/windows-bindgen.md @@ -29,7 +29,7 @@ windows-link = "0.100" windows-bindgen = "0.100" ``` -Generate bindings from `build.rs` with either command-line-style arguments or the builder: +Generate bindings from `build.rs` with command-line-style arguments, a command file, or the builder: ```rust,no_run windows_bindgen::bindgen([ @@ -72,8 +72,8 @@ to select a smaller surface. Prefix a rule with `!` to exclude it. A selected type also pulls in the types that its signatures require. Those dependency types are emitted as shells. -For more than a few names, keep the arguments in a response file. Pass it with `--etc`. Lines that -start with `//` are comments: +For a complete command file, use `--etc`. Blank lines and lines whose first non-whitespace +characters are `//` are ignored: ```text --out crates/libs/version/src/bindings.rs @@ -89,7 +89,10 @@ start with `//` are comments: windows_bindgen::bindgen(["--etc", "bindings.txt"]); ``` -The in-repo crates use this pattern. `tool_bindings` runs +When only the filter list is large, keep it in a filter-only file and use +`Bindgen::filter_file`/`filter_files` or the textual `--filter-file` option. + +The in-repo crates use both patterns. `tool_bindings` runs `bindgen(["--etc", "crates/tools/bindings/src/.txt"])` for each library. ## Choosing the output shape @@ -175,11 +178,15 @@ and `remove_X`. ### Other useful options -- `--in`, `.input(..)`, and `.inputs(..)` add `.winmd` files or directories. - `.input_default()` or the literal `"default"` includes the standard metadata. The builder uses - it implicitly when no input is supplied. +- `--in`, `.input(..)`, and `.inputs(..)` add `.winmd` files or directories. The builder uses the + standard metadata implicitly when no input is supplied. Builder inputs accept strings, `Path`, or + `PathBuf`. Use `.input_default()` to combine the bundled metadata with custom inputs; the textual + `--in default` form provides the same behavior. +- `.output(..)` accepts a string, `Path`, or `PathBuf`. - `--derive` and `.derive(..)` add derives to generated types. -- `--implement` and `.implement(..)` emit `_Impl` scaffolding for WinRT interface implementations. +- Bare `--implement` and `.implement_all()` emit `_Impl` scaffolding for every WinRT interface in + scope. Use `.implement(name)` or `.implements(names)` to limit scaffolding to type names or + namespace prefixes. - `--rustfmt` and `.rustfmt(..)` set the formatter for the output. - `--dead-code` and `.dead_code()` emit `pub(crate)` for callable items. This lets the compiler flag unused generated callables. @@ -420,6 +427,49 @@ pins valid counted-buffer output. `method_params` pins In+Out mutable projection `method_return` covers explicit and heuristic retval selection with In+Out, optional, reserved, and counted exclusions plus explicit void-pointer and large-pointee returns. +## Build-tool API harmonization + +The major-version API pass covers `windows-default`, `windows-bindgen`, `windows-rdl`, +`windows-clang`, and the merge/remap surface in `windows-metadata`. Compatibility with the removed +spellings is not required. + +| Surface | Current contract | +| --- | --- | +| Default metadata | `windows-default` exposes the bundled `WINRT` and `WIN32` byte slices. | +| Bindgen metadata | `input`/`inputs`, `input_bytes`/`input_byte_sets`, and `input_default`. | +| RDL source | `input`/`inputs` for paths and `input_text`/`input_texts` for source in memory. | +| RDL references | `reference(s)`, byte-set variants, and `reference_default`. | +| Clang source | `input`/`inputs` for headers and `input_text`/`input_texts` for source in memory. | +| Clang metadata | Reference and resolution metadata support every input form. | +| Paths | Builders and merge plans retain `PathBuf`; strings remain valid inputs. | +| Boolean options | `split`, `drop_lib_less`, `dead_code`, and `union_enums` are enabling methods. | +| Implementations | `implement_all`, `implement`, and `implements` select the mode. | +| Command files | `bindgen --etc` reads commands; `filter_file(s)` read filters. | +| Terminals | Builder state owns configuration; terminals do not repeat it. | + +Bindgen keeps one intentional default difference. A builder with no metadata inputs uses the +bundled metadata, preserving its small-build-script workflow. Once a caller supplies inputs, +`input_default()` adds the bundled metadata explicitly. The textual adapter expresses the same +choice as `--in default`; the programmatic builders do not recognize `"default"` as a path +sentinel. + +Command files are for complete textual configurations with large filters. `--etc` keeps the +established `bindgen` response-file contract, including nested and multiple command files. +`--filter-file` includes a filter-only file from textual arguments or another command file. +Programmatic callers should use the builder and `filter_file`/`filter_files`, which retain typed +paths. + +### Audit result + +The API audit is complete. Public methods, rustdoc, crate readmes, crate pages, samples, tools, and +tests use the current names. Remaining `"default"` arguments use bindgen's textual `--in` adapter; +programmatic builders use explicit default methods. `windows-csharp` remains outside this API pass +because it is a removal candidate. + +The affected tests and clippy targets pass. `tool_bindings`, `tool_composition`, `tool_reactor`, +`tool_webview`, `tool_package`, `tool_winrt`, `tool_roundtrip`, and `tool_win32` regenerate without +unexpected tracked output. This closes the API harmonization phase. + ## Investigation: lessons from windows-csharp The windows-csharp generator starts from the same metadata but builds a different public language diff --git a/docs/crates/windows-clang.md b/docs/crates/windows-clang.md index b6340d44b6a..a1890bc8a0d 100644 --- a/docs/crates/windows-clang.md +++ b/docs/crates/windows-clang.md @@ -55,26 +55,32 @@ cross-header type references resolve: windows_clang::clang() .args(["-x", "c++", "--target=x86_64-pc-windows-msvc"]) .input("Example.h") - .input_default() - .output("example.rdl") + .reference_default() + .output("rdl") .namespace("Example") .library("Example.dll") - .write() + .write_by_header() .unwrap(); ``` `clang_version()` returns the loaded libclang's version string; the tooling pins a specific libclang release so the scrape is deterministic (see `tool_win32`). +Use `.input_text(..)` and `.input_texts(..)` when the C/C++ source is already in memory. + ### Default metadata -`.input_default()` adds the standard WinRT and Win32 metadata as references for declarations used by -the scraped headers. `.reference_bytes(..)` adds custom reference metadata already in memory. +`.reference_default()` adds the standard WinRT and Win32 metadata as references for declarations +used by the scraped headers. `.reference(..)` and `.references(..)` add reference metadata from +files or directories. `.reference_bytes(..)` and `.reference_byte_sets(..)` accept metadata already +in memory. Input, reference, resolution, import library, and output paths accept strings, `Path`, or +`PathBuf`. `.resolution_default()` is different: it adds only the WinRT metadata to the resolution set used to classify `ABI::Windows::*` declarations. This lets `tool_win32` distinguish real WinRT types from Win32 COM interop types without treating the Win32 metadata being generated as an existing -definition. `.resolution_bytes(..)` provides the same role for custom metadata. +definition. `.resolution_input(..)` and `.resolution_inputs(..)` add custom metadata from paths; +`.resolution_bytes(..)` and `.resolution_byte_sets(..)` add it from memory. ## Consumers @@ -134,13 +140,14 @@ Everything generic to *any* header scrape lives in `windows-clang`: - **Provisioning** - `ensure_libclang` / `assert_libclang_version` (the pinned `LIBCLANG_VERSION` wheel, fetched + cached on first use), `clang_resource_dir`, and `nuget_package` (restore a pinned NuGet package into the global cache). -- **Parse + emit** - the `clang()` builder (target, args, `input`/`input_str`, `scope`, - `scope_headers`, `exclude_headers`, `import_library`, `drop_lib_less`), header partitioning - (`write_by_header`), and the per-kind cursor->RDL modules. +- **Parse + emit** - the `clang()` builder (target, args, `input`/`input_text`/`input_texts`, + `scope`/`scopes`, `scope_header`/`scope_headers`, `exclude_header`/`exclude_headers`, + `import_library`, `drop_lib_less`), header partitioning (`write_by_header`), and the per-kind + cursor->RDL modules. - **Multi-arch orchestration** - the `Clang::scrape` terminal, `Arch` (clang triple + `SupportedArchitecture` bits + per-target defines), `ScrapePlan` (the orchestration-only state: - output paths, arches, reference winmds, seed - *not* a mirror of the builder), and `Summary`. This - is pure driver: nothing in it is win32- or wdk-specific. + `PathBuf` outputs, arches, reference winmds, seed - *not* a mirror of the builder), and `Summary`. + This is pure driver: nothing in it is win32- or wdk-specific. Only what is *genuinely per-scraper* stays in each tool: the NuGet package IDs and pinned versions, the SDK/WDK include+lib directory layout, the translation-unit source assembly (the `windows.h` diff --git a/docs/crates/windows-default.md b/docs/crates/windows-default.md index 274b414dbe4..04683329819 100644 --- a/docs/crates/windows-default.md +++ b/docs/crates/windows-default.md @@ -51,12 +51,13 @@ Callers therefore do not need a separate dependency or a path into the Windows S | Crate | Default behavior | | --- | --- | | [`bindgen`](windows-bindgen.md) | Implicit if no input; explicit with `.input_default()`. | -| [`rdl`](windows-rdl.md) | `.input_default()` adds both files. | -| [`clang`](windows-clang.md) | Standard references plus WinRT-only resolution metadata. | +| [`rdl`](windows-rdl.md) | `.reference_default()` adds both files as references. | +| [`clang`](windows-clang.md) | `.reference_default()` plus WinRT-only `.resolution_default()`. | | [`csharp`](windows-csharp.md) | `.input_default()` adds both files. | -Path-style input APIs also accept the literal `"default"` for compatibility. Byte-input APIs -remain available for custom metadata that is already in memory. +The `windows-bindgen` textual adapter accepts `--in default`. The bindgen, RDL, and Clang builders +use explicit default methods instead; their path-style input methods treat `"default"` as an +ordinary path. Byte-input APIs remain available for custom metadata that is already in memory. Programs that link one of these build crates include both metadata payloads in the binary. These crates are intended for build tools rather than runtime dependencies. diff --git a/docs/crates/windows-metadata.md b/docs/crates/windows-metadata.md index 3b615104cd0..2c21b046110 100644 --- a/docs/crates/windows-metadata.md +++ b/docs/crates/windows-metadata.md @@ -65,6 +65,10 @@ qualify: the explicit native-sized spelling is required as semantic evidence. The merge is deterministic: it stages through `BTreeMap`s and insertion-ordered `Vec`s, with no `HashMap` reaching the output. +The merger and namespace remapper accept strings, `Path`, or `PathBuf` for input and output paths +and retain them as `PathBuf`. The remapper provides singular/plural `input`/`inputs`, +`source`/`sources`, and `route`/`routes` methods. + ### Method parameter association ECMA-335 `Param` rows are not positional. `Param.Sequence == 0` describes the return value, and a @@ -113,4 +117,5 @@ alignment, enum constant values, subset-present divergence) and `merge.rs` (nati reconciliation). `method_params.rs` authors metadata directly with `writer::File` and covers dense, absent, return, sparse, out-of-order, duplicate, and out-of-range parameter rows. It also covers all four raw directions and verifies that optional, reserved, retval, and count attributes remain -independent facts. +independent facts. `remap.rs` covers explicit and fallback namespace routing, singular/plural +builder methods, missing outputs, and invalid inputs. diff --git a/docs/crates/windows-rdl.md b/docs/crates/windows-rdl.md index 069ac85ae19..9dbd72368e8 100644 --- a/docs/crates/windows-rdl.md +++ b/docs/crates/windows-rdl.md @@ -30,6 +30,10 @@ The crate exposes two builders: - `reader()` compiles RDL source to `.winmd` metadata. - `writer()` writes canonical RDL source from `.winmd` metadata. +Input, reference, and output paths accept strings, `Path`, or `PathBuf`, so build scripts can pass +paths without converting them to UTF-8 strings. `.input_text(..)` and `.input_texts(..)` compile RDL +source already in memory. + ### RDL to winmd, and back Use `reader` to compile `.rdl` into `.winmd`. Use `writer` to regenerate canonical `.rdl` from @@ -57,15 +61,16 @@ RDL can reference types it does not define. Examples include `HRESULT` and ```rust,no_run windows_rdl::reader() .input("example.rdl") - .input_default() + .reference_default() .output("example.winmd") .write() .unwrap(); ``` -The reader treats the default metadata as references while compiling the input RDL. The writer -treats it as metadata to render. Both builders also accept metadata already in memory through their -byte-input APIs. +The reader treats the default metadata as references while compiling the input RDL. Add other +reference metadata with `.reference(path)`, `.references(paths)`, `.reference_bytes(bytes)`, or +`.reference_byte_sets(byte_sets)`. The writer has the corresponding `.input`, `.inputs`, +`.input_bytes`, and `.input_byte_sets` methods and treats default metadata as input to render. ### C/C++ headers to RDL @@ -80,7 +85,7 @@ separate input. windows_clang::clang() .args(["-x", "c++", "--target=x86_64-pc-windows-msvc"]) .input("Example.h") - .input_default() + .reference_default() .output("example.rdl") .namespace("Example") .library("Example.dll") @@ -193,7 +198,7 @@ Two in-repo tools show both uses: SDK metadata. The tool compiles them with the standard Win32 winmd into `extras.winmd`. Then it feeds that winmd to `windows_bindgen::bindgen` for [`windows-reactor`](windows-reactor.md). -In both tools, `reader` also gets the standard metadata as input. That lets RDL references resolve +In both tools, `reader` also gets the standard metadata as references. That lets RDL names resolve against the standard definitions. --- @@ -261,7 +266,7 @@ differs by architecture is split into per-architecture copies tagged `#[arch(X86 The merge compares type structure through [`windows-metadata`](windows-metadata.md). `merge_arch_rdl` handles orchestration. It reads each architecture's RDL, runs the merge, and writes -the combined output. +the combined output. `ArchInput` stores its RDL directory and winmd as `PathBuf`. ### Published crates and namespace remap @@ -289,7 +294,7 @@ changes tracked files. `tool_roundtrip` validates the reverse direction: -- WinRT uses `writer(Windows.winmd).split(true)` to write `metadata/winrt`. +- WinRT uses `writer(Windows.winmd).split()` to write `metadata/winrt`. - Win32 and WDK cannot recover header files from flat winmd alone. The tool reads the committed RDL layout to map type names back to header stems. Then it writes `metadata/win32` or `metadata/wdk` with `writer(winmd).partition(map)`.