Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 147 additions & 3 deletions crates/libs/metadata/src/merge/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ impl std::fmt::Display for Error {
#[derive(Default)]
pub struct Merger {
input: Vec<String>,
/// Arch-tagged inputs: `(path, arch_bits)` where arch_bits is a bitmask
/// (1=X86, 2=X64, 4=Arm64) indicating which architecture the file targets.
arch_inputs: Vec<(String, i32)>,
output: String,
}

Expand All @@ -49,6 +52,21 @@ impl Merger {
self
}

/// Adds an architecture-tagged input winmd file.
///
/// `arch` is a bitmask indicating which architecture this file was built for:
/// - `1` → X86
/// - `2` → X64
/// - `4` → Arm64
///
/// When `merge()` is called, types present in **all** arch-tagged inputs get
/// no `SupportedArchitectureAttribute`; types present only in a **subset** get
/// `SupportedArchitectureAttribute(present_arch_mask)`.
pub fn arch_input(&mut self, path: &str, arch: i32) -> &mut Self {
self.arch_inputs.push((path.to_string(), arch));
self
}

pub fn output(&mut self, output: &str) -> &mut Self {
self.output = output.to_string();
self
Expand All @@ -70,11 +88,68 @@ impl Merger {

let mut file = writer::File::new(name);

// Write plain (untagged) inputs as-is.
let mut types: Vec<reader::TypeDef<'_>> = index.types().collect();
types.sort_by(|a, b| (a.namespace(), a.name()).cmp(&(b.namespace(), b.name())));

for ty in types {
write_type(&mut file, &index, ty, None);
write_type(&mut file, &index, ty, None, None);
}

// Write arch-tagged inputs with computed SupportedArchitecture annotations.
if !self.arch_inputs.is_empty() {
// Compute the bitmask for "all arches present in this merge run".
let all_arches_mask: i32 = self.arch_inputs.iter().fold(0, |acc, (_, arch)| acc | arch);

// Load each arch-tagged file group.
let mut arch_groups: Vec<(reader::TypeIndex, i32)> =
Vec::with_capacity(self.arch_inputs.len());
for (path, arch_bits) in &self.arch_inputs {
let files = read_inputs(&[path.clone()])?;
arch_groups.push((reader::TypeIndex::new(files), *arch_bits));
}

// Compute the union of arch bits for each (namespace, name) pair.
let mut arch_presence: HashMap<(String, String), i32> = HashMap::new();
for (idx, arch_bits) in &arch_groups {
for ty in idx.types() {
*arch_presence
.entry((ty.namespace().to_string(), ty.name().to_string()))
.or_default() |= arch_bits;
}
}

// Build a flat list of (TypeIndex ref, TypeDef, arch_bits) sorted by (ns, name).
let mut all_type_refs: Vec<(&reader::TypeIndex, reader::TypeDef<'_>, i32)> = Vec::new();
for (idx, arch_bits) in &arch_groups {
for ty in idx.types() {
all_type_refs.push((idx, ty, *arch_bits));
}
}
all_type_refs
.sort_by(|a, b| (a.1.namespace(), a.1.name()).cmp(&(b.1.namespace(), b.1.name())));

// Write each type with the appropriate arch annotation.
// For types present in all arches, deduplicate to a single arch-neutral TypeDef.
// For types present in a subset, write each copy with SupportedArchitecture.
let mut deduped: HashSet<(String, String)> = HashSet::new();
for (idx, ty, _arch_bits) in &all_type_refs {
let key = (ty.namespace().to_string(), ty.name().to_string());
let present_mask = arch_presence.get(&key).copied().unwrap_or(0);

let arch_override = if present_mask == all_arches_mask {
// Present on every arch: write once without SupportedArchitecture.
if !deduped.insert(key) {
continue; // Already written.
}
Some(0) // 0 = suppress arch attribute
} else {
// Present on a subset: annotate with the union of arches that have it.
Some(present_mask)
};

write_type(&mut file, idx, *ty, None, arch_override);
}
}

let bytes = file.into_stream();
Expand Down Expand Up @@ -125,11 +200,18 @@ fn read_inputs(inputs: &[String]) -> Result<Vec<reader::File>, Error> {
Ok(result)
}

/// Writes a `TypeDef` (and its nested types) from `index` into `file`.
///
/// `arch_override` controls the `SupportedArchitectureAttribute` on the TypeDef:
/// - `None` → copy attributes as-is (plain merge, no arch logic)
/// - `Some(0)` → drop any existing `SupportedArchitectureAttribute`
/// - `Some(n)` → drop existing and write `SupportedArchitecture(n)`
fn write_type(
file: &mut writer::File,
index: &reader::TypeIndex,
def: reader::TypeDef,
outer: Option<writer::TypeDef>,
arch_override: Option<i32>,
) {
let extends = def
.extends()
Expand Down Expand Up @@ -168,7 +250,12 @@ fn write_type(
.map(|param| Type::Generic(param.name().to_string(), param.sequence()))
.collect();

write_attributes(file, writer::HasAttribute::TypeDef(type_def), def);
write_attributes_with_arch(
file,
writer::HasAttribute::TypeDef(type_def),
def,
arch_override,
);

for map in def.interface_impls() {
let interface_impl = file.InterfaceImpl(type_def, &map.interface(&generics));
Expand Down Expand Up @@ -229,19 +316,43 @@ fn write_type(
for inner_def in index.nested(def) {
debug_assert!(inner_def.namespace().is_empty());
debug_assert!(inner_def.flags().is_nested());
write_type(file, index, inner_def, Some(type_def));
write_type(file, index, inner_def, Some(type_def), arch_override);
}
}

fn write_attributes<'a, R: reader::HasAttributes<'a>>(
file: &mut writer::File,
parent: writer::HasAttribute,
row: R,
) {
write_attributes_with_arch(file, parent, row, None);
}

/// Like [`write_attributes`] but with optional architecture annotation overriding.
///
/// When `arch_override` is `Some`:
/// - Any existing `SupportedArchitectureAttribute` on `row` is **dropped**.
/// - If `arch_override` is `Some(bits)` with `bits != 0`, a new
/// `SupportedArchitectureAttribute(bits)` is written.
/// - If `arch_override` is `Some(0)`, no arch attribute is written (arch-neutral).
fn write_attributes_with_arch<'a, R: reader::HasAttributes<'a>>(
file: &mut writer::File,
parent: writer::HasAttribute,
row: R,
arch_override: Option<i32>,
) {
for attribute in row.attributes() {
let ctor = attribute.ctor();
let ty = ctor.parent();

// Skip the existing SupportedArchitectureAttribute when we're overriding arch.
if arch_override.is_some()
&& ty.namespace() == "Windows.Win32.Foundation.Metadata"
&& ty.name() == "SupportedArchitectureAttribute"
{
continue;
}

let attribute_ref =
writer::MemberRefParent::TypeRef(file.TypeRef(ty.namespace(), ty.name()));

Expand All @@ -253,4 +364,37 @@ fn write_attributes<'a, R: reader::HasAttributes<'a>>(
&attribute.value(),
);
}

// Emit the overriding arch attribute when requested (non-zero bits).
if let Some(arch_bits) = arch_override {
if arch_bits != 0 {
write_supported_architecture_attr(file, parent, arch_bits);
}
}
}

/// Writes a `SupportedArchitectureAttribute` with the given `arch_bits` bitmask.
fn write_supported_architecture_attr(
file: &mut writer::File,
parent: writer::HasAttribute,
arch_bits: i32,
) {
let ns = "Windows.Win32.Foundation.Metadata";
let name = "SupportedArchitectureAttribute";

let type_ref = writer::MemberRefParent::TypeRef(file.TypeRef(ns, name));

let sig = Signature {
flags: MethodCallAttributes::HASTHIS,
return_type: Type::Void,
types: vec![Type::I32],
};

let ctor_ref = file.MemberRef(".ctor", &sig, type_ref);

file.Attribute(
parent,
writer::AttributeType::MemberRef(ctor_ref),
&[("".to_string(), Value::I32(arch_bits))],
);
}
21 changes: 20 additions & 1 deletion crates/libs/rdl/src/clang/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ pub struct Clang {
args: Vec<String>,
library: String,
filter: Vec<String>,
target: Option<String>,
}

impl Clang {
Expand Down Expand Up @@ -400,6 +401,17 @@ impl Clang {
self
}

/// Sets the target triple used for all clang invocations, e.g.
/// `"x86_64-pc-windows-msvc"`, `"i686-pc-windows-msvc"`, or
/// `"aarch64-pc-windows-msvc"`.
///
/// This is equivalent to passing `--target=<triple>` as the first argument
/// via [`arg`][Self::arg], but is cleaner for per-arch builds.
pub fn target(&mut self, target: &str) -> &mut Self {
self.target = Some(target.to_string());
self
}

/// Returns the version string reported by the loaded libclang, e.g.
/// `"clang version 18.1.0 (...)"`. Loads libclang on first call.
pub fn version() -> Result<String, Error> {
Expand Down Expand Up @@ -429,7 +441,14 @@ impl Clang {
let _library = Library::new()?;
let index = Index::new()?;
let mut collector = Collector::new();
let args: Vec<_> = self.args.iter().map(String::as_str).collect();

// Build the effective args list: optional --target= first, then user args.
let target_arg: Option<String> = self.target.as_ref().map(|t| format!("--target={t}"));
let args: Vec<&str> = target_arg
.iter()
.map(String::as_str)
.chain(self.args.iter().map(String::as_str))
.collect();

for input in &h_paths {
let tu = index.parse(input, &args)?;
Expand Down
15 changes: 14 additions & 1 deletion crates/libs/rdl/src/reader/attribute_ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,19 @@ impl Encoder<'_> {
self.encode_named_attribute(target, &attr_ref);
}

/// Emits a `Windows.Win32.Foundation.Metadata.SupportedArchitectureAttribute` on `target`
/// with the given architecture bitmask value (1=X86, 2=X64, 4=Arm64, combinable with `|`).
pub fn emit_arch_attribute(&mut self, target: metadata::writer::HasAttribute, arch_bits: i32) {
let attr_ref = AttributeRef {
type_name: metadata::TypeName::named(
"Windows.Win32.Foundation.Metadata",
"SupportedArchitectureAttribute",
),
args: vec![("".to_string(), metadata::Value::I32(arch_bits))],
};
self.encode_named_attribute(target, &attr_ref);
}

pub fn is_guid_attribute(&self, attr: &syn::Attribute) -> bool {
self.find_attribute_type(attr.path())
.map(|info| &info.type_name == ("Windows.Foundation.Metadata", "GuidAttribute"))
Expand Down Expand Up @@ -550,7 +563,7 @@ impl Encoder<'_> {
for attr in attrs {
let path = attr.path();

if path.is_ident("win32") || path.is_ident("winrt") {
if path.is_ident("win32") || path.is_ident("winrt") || path.is_ident("arch") {
continue;
}

Expand Down
7 changes: 7 additions & 0 deletions crates/libs/rdl/src/reader/fn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ impl Encoder<'_> {
self.output
.ImplMap(method_def, flags, &name, library.value().as_str());

if let Some(arch_bits) = self.read_arch(&item.attrs)? {
self.emit_arch_attribute(
metadata::writer::HasAttribute::MethodDef(method_def),
arch_bits,
);
}

self.encode_attrs(
metadata::writer::HasAttribute::MethodDef(method_def),
&item.attrs,
Expand Down
74 changes: 74 additions & 0 deletions crates/libs/rdl/src/reader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,36 @@ impl Encoder<'_> {
Ok(None)
}

/// Parse an optional `#[arch(...)]` attribute from `attrs`. Returns `Some(bits)` where
/// `bits` is the architecture bitmask (1=X86, 2=X64, 4=Arm64, combinable with `|`) if the
/// attribute is present, `None` if absent, or an error if the attribute is malformed or uses
/// an unknown architecture name.
fn read_arch(&self, attrs: &[syn::Attribute]) -> Result<Option<i32>, Error> {
for attr in attrs {
if !attr.path().is_ident("arch") {
continue;
}

let expr = attr.parse_args::<syn::Expr>().map_err(|_| {
self.error(
attr,
"`arch` attribute requires architecture arguments (e.g. `#[arch(X86)]`)",
)
})?;

let bits = parse_arch_bitmask(&expr).ok_or_else(|| {
self.error(
attr,
"invalid `arch` value; expected `X86`, `X64`, `Arm64`, or a `|`-combination",
)
})?;

return Ok(Some(bits));
}

Ok(None)
}

fn encode_type(&self, ty: &syn::Type) -> Result<metadata::Type, Error> {
match ty {
syn::Type::Path(ty) => self.encode_type_path(ty),
Expand Down Expand Up @@ -886,6 +916,50 @@ impl Encoder<'_> {
}
}

/// Parses a `#[arch(...)]` expression into an architecture bitmask.
///
/// Accepts:
/// - A single identifier: `X86` → 1, `X64` → 2, `Arm64` → 4.
/// - A bitwise-OR combination: `X86 | X64` → 3, `X64 | Arm64` → 6, etc.
///
/// Returns `None` when the expression contains an unknown architecture name or an
/// unsupported expression form.
pub(crate) fn parse_arch_bitmask(expr: &syn::Expr) -> Option<i32> {
match expr {
syn::Expr::Path(p)
if p.qself.is_none()
&& p.path.leading_colon.is_none()
&& p.path.segments.len() == 1 =>
{
arch_name_to_bits(&p.path.segments[0].ident.to_string())
}
syn::Expr::Binary(syn::ExprBinary {
left,
op: syn::BinOp::BitOr(_),
right,
..
}) => {
let l = parse_arch_bitmask(left)?;
let r = parse_arch_bitmask(right)?;
Some(l | r)
}
_ => None,
}
}

/// Maps a well-known architecture name to its bitmask bit.
/// `X86` → 1
/// `X64` → 2
/// `Arm64` → 4
fn arch_name_to_bits(name: &str) -> Option<i32> {
match name {
"X86" => Some(1),
"X64" => Some(2),
"Arm64" => Some(4),
_ => None,
}
}

/// Builds a `syn::Signature` with all the optional fields (`constness`, `asyncness`,
/// `unsafety`, `abi`) set to `None`. Shared by `Callback`, `Fn`, `Delegate`, and `Method`.
pub(crate) fn make_sig(
Expand Down
Loading
Loading