diff options
| author | Mason Reed <mason@vector35.com> | 2026-02-11 18:04:07 -0800 |
|---|---|---|
| committer | Mason Reed <35282038+emesare@users.noreply.github.com> | 2026-02-23 00:09:44 -0800 |
| commit | 37008b7fa16837d04c1658868646cad681cbe035 (patch) | |
| tree | 577c2b62ee47c78a5d31d11f2aa610e441f5c808 /plugins/bntl_utils/src/winmd/translate.rs | |
| parent | 837f8590be80b7c98162e70e4f0c1814b83e9d7b (diff) | |
Add BNTL utility plugin
Allow users to easily create, diff, dump and validate type libraries
Supports the following formats:
- C header files (via core type parsers)
- Binary files (collects exported and imported functions)
- WinMD files (via `windows-metadata` crate)
- Existing type library files (for easy fixups)
- Apiset files (to resolve through forwarded windows dlls)
Can be invoked as a regular plugin via UI commands or via CLI.
Processing of type libraries inherently requires external linking, processing will automatically merge and deduplicate colliding type libraries so prefer to use inside a project or a directory and process all information (for a given platform) at once, rather than smaller invocations.
Diffstat (limited to 'plugins/bntl_utils/src/winmd/translate.rs')
| -rw-r--r-- | plugins/bntl_utils/src/winmd/translate.rs | 654 |
1 files changed, 654 insertions, 0 deletions
diff --git a/plugins/bntl_utils/src/winmd/translate.rs b/plugins/bntl_utils/src/winmd/translate.rs new file mode 100644 index 00000000..01ea54a5 --- /dev/null +++ b/plugins/bntl_utils/src/winmd/translate.rs @@ -0,0 +1,654 @@ +//! Translate windows metadata into a self-contained structure, for later use. + +use super::info::{ + MetadataConstantInfo, MetadataFieldInfo, MetadataFunctionInfo, MetadataImportInfo, + MetadataImportMethod, MetadataInfo, MetadataModuleInfo, MetadataParameterInfo, + MetadataTypeInfo, MetadataTypeKind, +}; +use std::collections::{HashMap, HashSet}; +use thiserror::Error; +use windows_metadata::reader::TypeCategory; +use windows_metadata::{ + AsRow, FieldAttributes, HasAttributes, MethodCallAttributes, Type, TypeAttributes, Value, +}; + +pub const BITFIELD_ATTR: &str = "NativeBitfieldAttribute"; +pub const CONST_ATTR: &str = "ConstAttribute"; +pub const FNPTR_ATTR: &str = "UnmanagedFunctionPointerAttribute"; +pub const _STRUCT_SIZE_ATTR: &str = "StructSizeFieldAttribute"; +pub const API_CONTRACT_ATTR: &str = "ApiContractAttribute"; + +#[derive(Error, Debug)] +pub enum TranslationError { + #[error("no files were provided")] + NoFiles, + #[error("the type name '{0}' is not handled")] + UnhandledType(String), + #[error("the attribute '{0}' is not supported")] + UnsupportedAttribute(String), +} + +pub struct WindowsMetadataTranslator { + // TODO: Allow this to be customized by user. + /// Replace references to a given name with a different one. + /// + /// This allows you to move types to a different namespace or rename them and be certain all + /// references to that type are updated. + remapped_references: HashMap<(&'static str, &'static str), (&'static str, &'static str)>, +} + +impl WindowsMetadataTranslator { + pub fn new() -> Self { + // TODO: Move this to a static array. + let mut remapped_references = HashMap::new(); + remapped_references.insert(("System", "Guid"), ("Windows.Win32.Foundation", "Guid")); + Self { + remapped_references, + } + } + + pub fn translate( + &self, + files: Vec<windows_metadata::reader::File>, + ) -> Result<MetadataInfo, TranslationError> { + if files.is_empty() { + return Err(TranslationError::NoFiles); + } + let index = windows_metadata::reader::TypeIndex::new(files); + self.translate_index(&index) + } + + pub fn translate_index( + &self, + index: &windows_metadata::reader::TypeIndex, + ) -> Result<MetadataInfo, TranslationError> { + let mut functions = Vec::new(); + let mut types = Vec::new(); + let mut constants = Vec::new(); + + // TODO: Move this somewhere else? + // Add synthetic types here. + types.extend([ + MetadataTypeInfo { + name: "Guid".to_string(), + kind: MetadataTypeKind::Struct { + fields: vec![ + MetadataFieldInfo { + name: "Data1".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(4), + is_signed: false, + }, + is_const: false, + bitfield: None, + }, + MetadataFieldInfo { + name: "Data2".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(2), + is_signed: false, + }, + is_const: false, + bitfield: None, + }, + MetadataFieldInfo { + name: "Data3".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(2), + is_signed: false, + }, + is_const: false, + bitfield: None, + }, + MetadataFieldInfo { + name: "Data4".to_string(), + ty: MetadataTypeKind::Array { + element: Box::new(MetadataTypeKind::Integer { + size: Some(1), + is_signed: false, + }), + count: 8, + }, + is_const: false, + bitfield: None, + }, + ], + is_packed: false, + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "HANDLE".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Void), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "HINSTANCE".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Void), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "HMODULE".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Void), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "PCSTR".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: true, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Character { size: 1 }), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "PCWSTR".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: true, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Character { size: 2 }), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "PSTR".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Character { size: 1 }), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "PWSTR".to_string(), + kind: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Character { size: 2 }), + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "UNICODE_STRING".to_string(), + kind: MetadataTypeKind::Struct { + fields: vec![ + MetadataFieldInfo { + name: "Length".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(2), + is_signed: false, + }, + is_const: false, + bitfield: None, + }, + MetadataFieldInfo { + name: "MaximumLength".to_string(), + ty: MetadataTypeKind::Integer { + size: Some(2), + is_signed: false, + }, + is_const: false, + bitfield: None, + }, + MetadataFieldInfo { + name: "Buffer".to_string(), + ty: MetadataTypeKind::Reference { + namespace: "Windows.Win32.Foundation".to_string(), + name: "PWSTR".to_string(), + }, + is_const: false, + bitfield: None, + }, + ], + is_packed: false, + }, + namespace: "Windows.Win32.Foundation".to_string(), + }, + MetadataTypeInfo { + name: "BOOLEAN".to_string(), + kind: MetadataTypeKind::Bool { size: Some(1) }, + namespace: "Windows.Win32.Security".to_string(), + }, + MetadataTypeInfo { + name: "BOOL".to_string(), + // BOOL is integer sized, not char sized like a typical bool value. + kind: MetadataTypeKind::Bool { size: None }, + namespace: "Windows.Win32.Security".to_string(), + }, + ]); + + for entry in index.types() { + match entry.category() { + TypeCategory::Interface => { + let (interface_ty, interface_vtable_ty) = self.translate_interface(&entry)?; + types.push(interface_ty); + types.push(interface_vtable_ty); + } + TypeCategory::Class => { + let (cls_functions, cls_constants) = self.translate_class(&entry)?; + functions.extend(cls_functions); + constants.extend(cls_constants); + } + TypeCategory::Enum => { + types.push(self.translate_enum(&entry)?); + } + TypeCategory::Struct => { + // Skip marker type structures. + if entry.has_attribute(API_CONTRACT_ATTR) { + continue; + } + types.push(self.translate_struct(&entry)?); + } + TypeCategory::Delegate => { + types.push(self.translate_delegate(&entry)?); + } + TypeCategory::Attribute => { + // We will pull attributes directly from the other entries. + } + } + } + + // Remove duplicate types within the same namespace, the first one wins. This is what allows + // us to override types by placing the overrides in the type list before traversing the index. + let mut tracked_names = HashSet::<(String, String)>::new(); + types.retain(|ty| { + let ty_name = (ty.namespace.clone(), ty.name.clone()); + tracked_names.insert(ty_name) + }); + + Ok(MetadataInfo { + types, + functions, + constants, + }) + } + + pub fn translate_struct( + &self, + structure: &windows_metadata::reader::TypeDef, + ) -> Result<MetadataTypeInfo, TranslationError> { + let mut fields = Vec::new(); + + let nested: Result<HashMap<String, _>, _> = structure + .index() + .nested(structure.clone()) + .map(|n| { + // TODO: Are all nested fields a struct? + let nested_ty = self.translate_struct(&n)?; + Ok((n.name().to_string(), nested_ty)) + }) + .collect(); + let nested = nested?; + + for field in structure.fields() { + let mut field_ty = self.translate_type(&field.ty())?; + // TODO: This is kinda ugly. + // Handle nested structures by unwrapping the reference. + let mut nested_ty = None; + field_ty.visit_references(&mut |_, name| { + nested_ty = nested.get(name).cloned().map(|n| n.kind); + }); + field_ty = nested_ty.unwrap_or(field_ty); + + // Bitfields are special, they are a "fake" field that we need to look at the attributes of + // to unwrap the real fields that are contained within the storage type. + if field.has_attribute(BITFIELD_ATTR) { + for bitfield in field.attributes() { + let bitfield_values = bitfield.value(); + let mut values = bitfield_values.iter(); + let Some((_, Value::Utf8(bitfield_name))) = values.next() else { + continue; + }; + let Some((_, Value::I64(bitfield_pos))) = values.next() else { + continue; + }; + let Some((_, Value::I64(bitfield_width))) = values.next() else { + continue; + }; + // is_private, is_public, is_virtual + fields.push(MetadataFieldInfo { + name: bitfield_name.clone(), + ty: field_ty.clone(), + is_const: field.has_attribute(CONST_ATTR), + bitfield: Some((*bitfield_pos as u8, *bitfield_width as u8)), + }); + } + } else { + fields.push(MetadataFieldInfo { + name: field.name().to_string(), + ty: field_ty, + is_const: field.has_attribute(CONST_ATTR), + bitfield: None, + }); + } + } + + let mut is_packed = false; + if let Some(_layout) = structure.class_layout() { + is_packed = _layout.packing_size() == 1; + } + + // ExplicitLayout seems to denote a union layout. + let kind = if structure.flags().contains(TypeAttributes::ExplicitLayout) { + MetadataTypeKind::Union { fields } + } else { + MetadataTypeKind::Struct { fields, is_packed } + }; + + Ok(MetadataTypeInfo { + name: structure.name().to_string(), + kind, + namespace: structure.namespace().to_string(), + }) + } + + pub fn translate_class( + &self, + class: &windows_metadata::reader::TypeDef, + ) -> Result<(Vec<MetadataFunctionInfo>, Vec<MetadataConstantInfo>), TranslationError> { + let namespace = class.namespace().to_string(); + let mut functions = Vec::new(); + for method in class.methods() { + match self.translate_method(&method) { + Ok(mut func) => { + func.namespace = namespace.clone(); + functions.push(func); + } + Err(e) => tracing::warn!("Failed to translate method {}: {}", method.name(), e), + } + } + + let mut constants = Vec::new(); + for field in class.fields() { + if let Some(constant) = field + .constant() + .map(|c| self.value_to_u64(&c.value())) + .flatten() + { + constants.push(MetadataConstantInfo { + name: field.name().to_string(), + namespace: namespace.clone(), + ty: self.translate_type(&field.ty())?, + value: constant, + }); + } else { + tracing::debug!("Field {} is not a constant, skipping...", field.name()); + } + } + + Ok((functions, constants)) + } + + pub fn translate_method( + &self, + method: &windows_metadata::reader::MethodDef, + ) -> Result<MetadataFunctionInfo, TranslationError> { + // TODO: Pass generics here? generic_params seems always empty? Even windows-rs doesn't use it. + let signature = method.signature(&[]); + let func_params: Result<Vec<MetadataParameterInfo>, TranslationError> = method + .params() + .filter(|p| !p.name().is_empty()) + .zip(signature.types) + .map(|(param, param_ty)| { + Ok(MetadataParameterInfo { + name: param.name().to_string(), + ty: self.translate_type(¶m_ty)?, + }) + }) + .collect(); + let func_ty = MetadataTypeKind::Function { + params: func_params?, + return_type: Box::new(self.translate_type(&signature.return_type)?), + is_vararg: signature.flags.contains(MethodCallAttributes::VARARG), + }; + + let import_info = method + .impl_map() + .map(|impl_map| self.import_info_from_map(&impl_map)); + + Ok(MetadataFunctionInfo { + name: method.name().to_string(), + ty: func_ty, + // NOTE: This will be set by the associated class entry once returned. + namespace: "".to_string(), + import_info, + }) + } + + pub fn translate_delegate( + &self, + delegate: &windows_metadata::reader::TypeDef, + ) -> Result<MetadataTypeInfo, TranslationError> { + if !delegate.has_attribute(FNPTR_ATTR) { + return Err(TranslationError::UnsupportedAttribute( + FNPTR_ATTR.to_string(), + )); + } + let invoke_method = delegate + .methods() + .find(|m| m.name() == "Invoke") + .expect("Invoke method not found"); + let translated_invoke_method = self.translate_method(&invoke_method)?; + Ok(MetadataTypeInfo { + name: delegate.name().to_string(), + kind: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(translated_invoke_method.ty), + }, + namespace: delegate.namespace().to_string(), + }) + } + + pub fn translate_interface( + &self, + interface: &windows_metadata::reader::TypeDef, + ) -> Result<(MetadataTypeInfo, MetadataTypeInfo), TranslationError> { + let mut vtable_fields = Vec::new(); + for meth in interface.methods() { + let meth_ty = self.translate_method(&meth)?; + vtable_fields.push(MetadataFieldInfo { + name: meth.name().to_string(), + ty: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(meth_ty.ty), + }, + is_const: false, + bitfield: None, + }) + } + + let interface_ns = interface.namespace(); + let interface_ty = MetadataTypeInfo { + name: interface.name().to_string(), + kind: MetadataTypeKind::Struct { + fields: vec![MetadataFieldInfo { + name: "vtable".to_string(), + ty: MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(MetadataTypeKind::Reference { + namespace: interface_ns.to_string(), + name: format!("{}VTable", interface.name()), + }), + }, + is_const: false, + bitfield: None, + }], + is_packed: false, + }, + namespace: interface_ns.to_string(), + }; + let interface_vtable_ty = MetadataTypeInfo { + name: format!("{}VTable", interface.name()), + kind: MetadataTypeKind::Struct { + fields: Vec::new(), + is_packed: false, + }, + namespace: interface_ns.to_string(), + }; + Ok((interface_ty, interface_vtable_ty)) + } + + pub fn translate_enum( + &self, + _enum: &windows_metadata::reader::TypeDef, + ) -> Result<MetadataTypeInfo, TranslationError> { + let mut variants = Vec::new(); + let mut last_constant = 0; + let mut enum_ty = MetadataTypeKind::Integer { + size: None, + is_signed: true, + }; + for variant in _enum.fields() { + if variant.flags().contains(FieldAttributes::RTSpecialName) { + // Skip the hidden "value__" field. + continue; + } + // Pull the enums type from the constant if it exists. + // Otherwise, we will fall back to void and use a default type when importing. + if let Some(constant) = variant.constant() { + enum_ty = self.translate_type(&constant.ty())?; + } + let variant_constant = variant + .constant() + .map(|c| self.value_to_u64(&c.value())) + .flatten() + .unwrap_or(last_constant); + let variant_name = variant.name().to_string(); + variants.push((variant_name, variant_constant)); + last_constant = variant_constant; + } + Ok(MetadataTypeInfo { + name: _enum.name().to_string(), + kind: MetadataTypeKind::Enum { + ty: Box::new(enum_ty), + variants, + }, + namespace: _enum.namespace().to_string(), + }) + } + + pub fn translate_type(&self, ty: &Type) -> Result<MetadataTypeKind, TranslationError> { + match ty { + Type::Void => Ok(MetadataTypeKind::Void), + Type::Bool => Ok(MetadataTypeKind::Bool { size: Some(1) }), + Type::Char => Ok(MetadataTypeKind::Character { size: 1 }), + Type::I8 => Ok(MetadataTypeKind::Integer { + size: Some(1), + is_signed: true, + }), + Type::U8 => Ok(MetadataTypeKind::Integer { + size: Some(1), + is_signed: false, + }), + Type::I16 => Ok(MetadataTypeKind::Integer { + size: Some(2), + is_signed: true, + }), + Type::U16 => Ok(MetadataTypeKind::Integer { + size: Some(2), + is_signed: false, + }), + Type::I32 => Ok(MetadataTypeKind::Integer { + size: Some(4), + is_signed: true, + }), + Type::U32 => Ok(MetadataTypeKind::Integer { + size: Some(4), + is_signed: false, + }), + Type::I64 => Ok(MetadataTypeKind::Integer { + size: Some(8), + is_signed: true, + }), + Type::U64 => Ok(MetadataTypeKind::Integer { + size: Some(8), + is_signed: false, + }), + Type::F32 => Ok(MetadataTypeKind::Float { size: 4 }), + Type::F64 => Ok(MetadataTypeKind::Float { size: 8 }), + Type::ISize => Ok(MetadataTypeKind::Integer { + size: None, + is_signed: true, + }), + Type::USize => Ok(MetadataTypeKind::Integer { + size: None, + is_signed: false, + }), + Type::Name(name) => { + if let Some((remapped_ns, remapped_name)) = + self.remapped_references.get(&(&name.namespace, &name.name)) + { + Ok(MetadataTypeKind::Reference { + namespace: remapped_ns.to_string(), + name: remapped_name.to_string(), + }) + } else { + Ok(MetadataTypeKind::Reference { + namespace: name.namespace.clone(), + name: name.name.clone(), + }) + } + } + Type::PtrMut(target, _) => Ok(MetadataTypeKind::Pointer { + is_const: false, + is_pointee_const: false, + target: Box::new(self.translate_type(target)?), + }), + Type::PtrConst(target, _) => { + Ok(MetadataTypeKind::Pointer { + is_const: false, + // TODO: I think this might be pointee const? + is_pointee_const: true, + target: Box::new(self.translate_type(target)?), + }) + } + Type::ArrayFixed(elem_ty, count) => Ok(MetadataTypeKind::Array { + element: Box::new(self.translate_type(elem_ty)?), + count: *count, + }), + other => Err(TranslationError::UnhandledType(format!("{:?}", other))), + } + } + + pub fn import_info_from_map( + &self, + map: &windows_metadata::reader::ImplMap, + ) -> MetadataImportInfo { + MetadataImportInfo { + method: MetadataImportMethod::ByName(map.import_name().to_string()), + module: MetadataModuleInfo { + name: map.import_scope().name().to_string(), + }, + } + } + + pub fn value_to_u64(&self, value: &Value) -> Option<u64> { + match value { + Value::Bool(b) => Some(*b as u64), + Value::U8(i) => Some(*i as u64), + Value::I8(i) => Some(*i as u64), + Value::U16(i) => Some(*i as u64), + Value::I16(i) => Some(*i as u64), + Value::U32(i) => Some(*i as u64), + Value::I32(i) => Some(*i as u64), + Value::U64(i) => Some(*i), + Value::I64(i) => Some(*i as u64), + _ => None, + } + } +} |
