Skip to main content

windows_rdl/
lib.rs

1#![doc = include_str!("../readme.md")]
2
3/// RDL source-emission primitives shared with the `windows-clang` scraper.
4pub mod emit;
5mod error;
6/// Helpers for formatting generated RDL source.
7pub mod formatter;
8/// Reader for COFF import libraries (the SDK `.lib` archives), used to recover
9/// the function -> DLL mapping that headers do not carry.
10pub mod implib;
11mod reader;
12mod writer;
13
14use emit::*;
15use std::collections::{BTreeMap, HashMap, HashSet};
16use std::path::{Path, PathBuf};
17use syn::spanned::Spanned;
18use windows_metadata as metadata;
19
20pub use error::Error;
21use proc_macro2::{Literal, Span, TokenStream};
22use quote::quote;
23pub use reader::Reader;
24pub use writer::Writer;
25
26/// The metadata namespace that owns the Win32 attribute vocabulary.
27pub(crate) const METADATA_NAMESPACE: &str = "Windows.Win32.Metadata";
28
29/// Short RDL attribute spelling and the metadata attribute it maps to.
30pub(crate) struct PseudoAttr {
31    pub short: &'static str,
32    pub metadata: &'static str,
33    /// Named property receiving a short-form positional argument, if any.
34    pub prop: Option<&'static str>,
35}
36
37/// Pseudo-attribute table; order is metadata-significant for parameters.
38pub(crate) const PSEUDO_ATTRS: &[PseudoAttr] = &[
39    PseudoAttr {
40        short: "retval",
41        metadata: "RetValAttribute",
42        prop: None,
43    },
44    PseudoAttr {
45        short: "iid_is",
46        metadata: "ComOutPtrAttribute",
47        prop: None,
48    },
49    PseudoAttr {
50        short: "len_param",
51        metadata: "NativeArrayInfoAttribute",
52        prop: Some("CountParamIndex"),
53    },
54    PseudoAttr {
55        short: "len_const",
56        metadata: "NativeArrayInfoAttribute",
57        prop: Some("CountConst"),
58    },
59    PseudoAttr {
60        short: "size_param",
61        metadata: "MemorySizeAttribute",
62        prop: Some("BytesParamIndex"),
63    },
64    PseudoAttr {
65        short: "reserved",
66        metadata: "ReservedAttribute",
67        prop: None,
68    },
69    PseudoAttr {
70        short: "noreturn",
71        metadata: "DoesNotReturnAttribute",
72        prop: None,
73    },
74    PseudoAttr {
75        short: "scoped",
76        metadata: "ScopedEnumAttribute",
77        prop: None,
78    },
79    PseudoAttr {
80        short: "encoding",
81        metadata: "NativeEncodingAttribute",
82        prop: None,
83    },
84];
85
86pub(crate) fn pseudo_by_short(short: &str) -> Option<&'static PseudoAttr> {
87    PSEUDO_ATTRS.iter().find(|p| p.short == short)
88}
89
90/// Finds the short spelling for a metadata attribute, using property names to distinguish shared
91/// attribute types such as `NativeArrayInfoAttribute`.
92pub(crate) fn pseudo_for_metadata(name: &str, arg_names: &[String]) -> Option<&'static PseudoAttr> {
93    let mut fallback = None;
94    for pseudo in PSEUDO_ATTRS.iter().filter(|p| p.metadata == name) {
95        match pseudo.prop {
96            Some(prop) if arg_names.len() == 1 && arg_names[0] == prop => return Some(pseudo),
97            None => fallback = Some(pseudo),
98            _ => {}
99        }
100    }
101    fallback
102}
103
104/// Creates a [`Reader`] that compiles RDL files into `.winmd` metadata.
105pub fn reader() -> Reader {
106    Reader::new()
107}
108
109/// Parses one `.rdl` file and returns the items it defines under `namespace`.
110pub fn item_names(path: impl AsRef<Path>, namespace: &str) -> Result<Vec<String>, Error> {
111    reader::item_names(path, namespace)
112}
113
114/// Creates a [`Writer`] that converts `.winmd` metadata into RDL.
115pub fn writer() -> Writer {
116    Writer::new()
117}
118
119/// One architecture's RDL directory, compiled winmd, and architecture bitmask.
120pub struct ArchInput {
121    pub rdl_dir: PathBuf,
122    pub winmd: PathBuf,
123    pub bits: i32,
124}
125
126/// Arch-merges per-architecture scrapes and restores the per-header RDL partition.
127pub fn merge_arch_rdl(
128    inputs: &[ArchInput],
129    seed: Option<&Path>,
130    output_dir: impl AsRef<Path>,
131) -> Result<(), Error> {
132    let output_dir = output_dir.as_ref();
133
134    if inputs.is_empty() {
135        return Err(writer_err!(
136            "merge_arch_rdl requires at least one arch input"
137        ));
138    }
139
140    // `Writer` clears `*.rdl`; capture the seed first so it can be restored verbatim.
141    let seed = seed
142        .map(|seed| {
143            let name = seed
144                .file_name()
145                .ok_or_else(|| writer_err!("invalid seed path `{}`", seed.display()))?
146                .to_os_string();
147            let text = std::fs::read(seed)
148                .map_err(|e| writer_err!("failed to read seed `{}`: {e}", seed.display()))?;
149            Ok::<_, Error>((name, seed.to_path_buf(), text))
150        })
151        .transpose()?;
152
153    // Unique scratch dirs avoid collisions between concurrent arch merges.
154    let temp = std::env::temp_dir().join(format!(
155        "win32-arch-merge-{}-{}",
156        std::process::id(),
157        std::time::SystemTime::now()
158            .duration_since(std::time::UNIX_EPOCH)
159            .map_or(0, |d| d.as_nanos())
160    ));
161    std::fs::create_dir_all(&temp)
162        .map_err(|e| writer_err!("failed to create temp dir `{}`: {e}", temp.display()))?;
163    let _scratch = ScratchDir(temp.clone());
164    let merged = temp.join("Windows.Win32.merged.winmd");
165    let mut merger = metadata::merge();
166    for input in inputs {
167        merger.arch_input(&input.winmd, input.bits);
168    }
169    merger
170        .output(&merged)
171        .merge()
172        .map_err(|e| writer_err!("arch-merge failed: {e}"))?;
173
174    // Recover item-name -> header-stem routing from the per-arch RDL partitions.
175    let mut map = HashMap::<String, String>::new();
176    for input in inputs {
177        for entry in std::fs::read_dir(&input.rdl_dir)
178            .map_err(|e| writer_err!("failed to read `{}`: {e}", input.rdl_dir.display()))?
179            .flatten()
180        {
181            let path = entry.path();
182            if path.extension().is_none_or(|x| x != "rdl")
183                || path.file_name() == seed.as_ref().map(|(name, _, _)| name.as_os_str())
184            {
185                continue;
186            }
187            let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
188                continue;
189            };
190            for name in reader::item_names(&path, "Windows.Win32")? {
191                map.entry(name).or_insert_with(|| stem.to_string());
192            }
193        }
194    }
195
196    writer()
197        .input(&merged)
198        .partition(map)
199        .output(output_dir)
200        .write()?;
201
202    // Restore the hand-authored seed if this metadata set has one.
203    if let Some((_, seed_path, seed_text)) = seed {
204        write_to_file(seed_path, seed_text)?;
205    }
206
207    Ok(())
208}
209
210/// Removes a scratch directory on every return path.
211struct ScratchDir(PathBuf);
212
213impl Drop for ScratchDir {
214    fn drop(&mut self) {
215        let _ = std::fs::remove_dir_all(&self.0);
216    }
217}
218
219pub fn expand_input_paths<P: AsRef<Path>>(
220    inputs: &[P],
221    ext1: &str,
222    ext2: &str,
223) -> Result<(Vec<PathBuf>, Vec<PathBuf>), Error> {
224    let mut paths1 = vec![];
225    let mut paths2 = vec![];
226
227    for input in inputs {
228        let path = input.as_ref();
229        let display = path.to_string_lossy();
230
231        if path.is_dir() {
232            let prev_total = paths1.len() + paths2.len();
233
234            for entry_path in path
235                .read_dir()
236                .map_err(|_| Error::new("failed to read directory", &display, 0, 0))?
237                .flatten()
238                .map(|entry| entry.path())
239            {
240                if entry_path.is_file() {
241                    if entry_path
242                        .extension()
243                        .is_some_and(|ext| ext.eq_ignore_ascii_case(ext1))
244                    {
245                        paths1.push(entry_path);
246                    } else if entry_path
247                        .extension()
248                        .is_some_and(|ext| ext.eq_ignore_ascii_case(ext2))
249                    {
250                        paths2.push(entry_path);
251                    }
252                }
253            }
254
255            if paths1.len() + paths2.len() == prev_total {
256                let message = if ext1 == ext2 {
257                    format!("failed to find .{ext1} files in directory")
258                } else {
259                    format!("failed to find .{ext1} or .{ext2} files in directory")
260                };
261                return Err(Error::new(&message, &display, 0, 0));
262            }
263        } else if path
264            .extension()
265            .is_some_and(|ext| ext.eq_ignore_ascii_case(ext1))
266        {
267            paths1.push(path.to_path_buf());
268        } else if path
269            .extension()
270            .is_some_and(|ext| ext.eq_ignore_ascii_case(ext2))
271        {
272            paths2.push(path.to_path_buf());
273        } else {
274            let message = if ext1 == ext2 {
275                format!("expected .{ext1} file")
276            } else {
277                format!("expected .{ext1} or .{ext2} file")
278            };
279            return Err(Error::new(&message, &display, 0, 0));
280        }
281    }
282
283    Ok((paths1, paths2))
284}
285
286/// Expands file and directory inputs containing one file type.
287pub fn expand_input_files<P: AsRef<Path>>(
288    inputs: &[P],
289    extension: &str,
290) -> Result<Vec<PathBuf>, Error> {
291    Ok(expand_input_paths(inputs, extension, extension)?.0)
292}
293
294pub fn write_to_file<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> Result<(), Error> {
295    let path = path.as_ref();
296    let display = path.to_string_lossy();
297    if let Some(parent) = path.parent() {
298        std::fs::create_dir_all(parent)
299            .map_err(|_| writer_err!("failed to create directory `{display}`"))?;
300    }
301
302    std::fs::write(path, contents).map_err(|_| writer_err!("failed to write file `{display}`"))
303}
304
305macro_rules! writer_err {
306    ($($arg:tt)*) => {
307        Error::new(&format!($($arg)*), "", 0, 0)
308    };
309}
310
311use writer_err;
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn prop_bound_pseudo_requires_sole_argument() {
319        let sole = ["CountParamIndex".to_string()];
320        let pseudo = pseudo_for_metadata("NativeArrayInfoAttribute", &sole)
321            .expect("single-property NativeArrayInfo should map to a short pseudo");
322        assert_eq!(pseudo.short, "len_param");
323
324        let extra = ["CountParamIndex".to_string(), "CountConst".to_string()];
325        assert!(
326            pseudo_for_metadata("NativeArrayInfoAttribute", &extra).is_none(),
327            "a multi-valued property-bound attribute must fall back to the fully-qualified spelling"
328        );
329    }
330
331    #[test]
332    fn property_less_pseudo_matches_by_name() {
333        let pseudo = pseudo_for_metadata("RetValAttribute", &[])
334            .expect("RetValAttribute should map to a pseudo");
335        assert_eq!(pseudo.short, "retval");
336    }
337}