From b3fac6e794465baa3ba314448a1bc365d9404375 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Sun, 4 Sep 2022 19:52:30 +0200 Subject: [PATCH] Add header translation #264 See full history in https://github.com/madsmtm/objc2/pull/264/commits/1c4c8754b71045a39f8eb6c5ba67f559a219bb94. Add initial header translation Closer to usable Mostly works with NSCursor Mostly works with NSAlert.h Refactor a bit AppKit is now parse-able handle reserved keywords Handle protocols somewhat Handle the few remaining entity kinds Works with Foundation Cleanup Refactor Refactor Method to (almost) be PartialEq Parse more things Parse NSConsumed Verify memory management More work Fix reserved keywords Refactor statements Add initial availability Prepare RustType Split RustType up in parse and ToToken part Add icrate Add config files Temporarily disable protocol generation Generate files Add initial generated files for Foundation Skip "index" header Add basic imports Allow skipping methods Properly emit `unsafe` Make classes public Rename objc feature flag Improve imports somewhat Further import improvements Handle basic typedefs Improve generics handling Improve pointers to objects Refactor RustType::TypeDef Mostly support generics Refactor config setup Small fixes Support nested generics Comment out a bit of debug logging Emit all files Parse sized integer types Parse typedefs that map to other typedefs Appease clippy Add `const`-parsing for RustType::Id Parse Objective-C in/out/inout/bycopy/byref/oneway qualifiers Fix `id` being emitted when it actually specifies a protocol Make AppKit work again Parse all qualifiers, in particular lifetime qualifiers More consistent ObjCObjectPointer parsing Validate some lifetime attributes Fix out parameters (except for NSError) Assuming we find a good solution to https://github.com/madsmtm/objc2/pull/277 Refactor Stmt objc declaration parsing Clean up how return types work Refactor property parsing Fixes their order to be the same as in the source file Add support for functions taking NSError as an out parameter Assuming we do https://github.com/madsmtm/objc2/pull/276 Change icrate directory layout Refactor slightly Refactor file handling to allow for multiple frameworks simultaneously Put method output inside an extern_methods! call We'll want this no matter what, since it'll allow us to extend things with availability attributes in the future. Use extern_methods! functionality To cut down on the amount of code, which should make things easier to review and understand. This uses features which are not actually yet done, see https://github.com/madsmtm/objc2/pull/244. Not happy with the formatting either, but not sure how to fix that? Manually fix the formatting of attribute macros in extern_methods! Add AppKit bindings Speed things up by optionally formatting at the end instead Prepare for parsing more than one SDK Specify a minimum deployment target Document SDK situation Parse headers on iOS as well Refactor stmt parsing a bit Remove Stmt::FileImport and Stmt::ItemImport These are not nearly enough to make imports work well anyhow, so I'll rip it out and find a better solution Do preprocessing step explicitly as the first thing Refactor so that file writing is done using plain Display Allows us to vastly improve the speed, as well as allowing us to make the output much prettier wrt. newlines and such in the future (proc_macro2 / quote output is not really meant to be consumed by human eyes) Improve whitespace in generated files and add warning header Don't crash on unions Add initial enum parsing Add initial enum expr parsing Add very simple enum output Fix duplicate enum check Improve enum expr parsing This should make it easier for things to work on 32-bit platforms Add static variable parsing Add a bit of WIP code Add function parsing Fix generic struct generation Make &Class as return type static Trim unnecessary parentheses Fix generics default parameter Remove methods that are both instance and class methods For now, until we can solve this more generally Skip protocols that are also classes Improve imports setups Bump recursion limit Add MacTypes.h type translation Fix int64_t type translation Make statics public Fix init methods Make __inner_extern_class allowing trailing comma in generics Attempt to improve Rust's parsing speed of icrate Custom NSObject TMP import Remove NSProxy Temporarily remove out parameter setup Add struct support Add partial support for "related result types" Refactor typedef parsing a bit Output remaining typedefs Fix Option and *mut bool Fix almost all remaining type errors in Foundation Skip statics whoose value we cannot find Fix anonymous enum types Fix AppKit duplicate methods Add CoreData Properly fix imports Add `abstract` keyword Put enum and static declarations behind a macro Add proper IncompleteArray parsing Refactor type parsing Make NSError** handling happen in all the places that it does with Swift Refactor Ty a bit more Make Display for RustType always sound Add support for function pointers Add support for block pointers Add extern functions Emit protocol information We can't parse it yet though, see https://github.com/madsmtm/objc2/pull/250 Make CoreData compile Make AppKit compile Add support for the AuthenticationServices framework Do clang < v13 workarounds without modifying sources Refactor Foundation fixes --- crates/header-translator/Cargo.toml | 14 + crates/header-translator/README.md | 11 + crates/header-translator/framework-includes.h | 17 + crates/header-translator/src/availability.rs | 15 + crates/header-translator/src/config.rs | 108 ++ crates/header-translator/src/expr.rs | 130 ++ crates/header-translator/src/lib.rs | 125 ++ crates/header-translator/src/main.rs | 312 +++++ crates/header-translator/src/method.rs | 421 ++++++ crates/header-translator/src/objc2_utils.rs | 40 + crates/header-translator/src/property.rs | 183 +++ crates/header-translator/src/rust_type.rs | 1219 +++++++++++++++++ crates/header-translator/src/stmt.rs | 898 ++++++++++++ .../header-translator/src/unexposed_macro.rs | 56 + .../icrate/src/AppKit/translation-config.toml | 177 +++ .../src/AuthenticationServices/fixes/mod.rs | 10 + .../translation-config.toml | 20 + .../src/CoreData/translation-config.toml | 34 + .../icrate/src/Foundation/fixes/NSDecimal.rs | 13 + .../icrate/src/Foundation/fixes/NSObject.rs | 47 + crates/icrate/src/Foundation/fixes/NSProxy.rs | 53 + crates/icrate/src/Foundation/fixes/mod.rs | 6 + crates/icrate/src/Foundation/mod.rs | 2 - .../src/Foundation/translation-config.toml | 213 +++ crates/icrate/src/generated | 2 +- crates/objc2/src/macros.rs | 33 + crates/objc2/src/macros/extern_class.rs | 2 +- crates/objc2/src/macros/extern_methods.rs | 12 +- 28 files changed, 4163 insertions(+), 10 deletions(-) create mode 100644 crates/header-translator/Cargo.toml create mode 100644 crates/header-translator/README.md create mode 100644 crates/header-translator/framework-includes.h create mode 100644 crates/header-translator/src/availability.rs create mode 100644 crates/header-translator/src/config.rs create mode 100644 crates/header-translator/src/expr.rs create mode 100644 crates/header-translator/src/lib.rs create mode 100644 crates/header-translator/src/main.rs create mode 100644 crates/header-translator/src/method.rs create mode 100644 crates/header-translator/src/objc2_utils.rs create mode 100644 crates/header-translator/src/property.rs create mode 100644 crates/header-translator/src/rust_type.rs create mode 100644 crates/header-translator/src/stmt.rs create mode 100644 crates/header-translator/src/unexposed_macro.rs create mode 100644 crates/icrate/src/AppKit/translation-config.toml create mode 100644 crates/icrate/src/AuthenticationServices/translation-config.toml create mode 100644 crates/icrate/src/CoreData/translation-config.toml create mode 100644 crates/icrate/src/Foundation/fixes/NSDecimal.rs create mode 100644 crates/icrate/src/Foundation/fixes/NSObject.rs create mode 100644 crates/icrate/src/Foundation/fixes/NSProxy.rs create mode 100644 crates/icrate/src/Foundation/translation-config.toml diff --git a/crates/header-translator/Cargo.toml b/crates/header-translator/Cargo.toml new file mode 100644 index 000000000..e768f3e54 --- /dev/null +++ b/crates/header-translator/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "header-translator" +version = "0.1.0" +edition = "2021" +publish = false + +repository = "https://github.com/madsmtm/objc2" +license = "Zlib OR Apache-2.0 OR MIT" + +[dependencies] +clang = { version = "2.0", features = ["runtime", "clang_10_0"] } +toml = "0.5.9" +serde = { version = "1.0.144", features = ["derive"] } +apple-sdk = { version = "0.2.0", default-features = false } diff --git a/crates/header-translator/README.md b/crates/header-translator/README.md new file mode 100644 index 000000000..2bc84d6e5 --- /dev/null +++ b/crates/header-translator/README.md @@ -0,0 +1,11 @@ +# Objective-C header translator + +For use in making `icrate`. + +```console +cargo run --bin header-translator -- /Applications/Xcode.app/Contents/Developer +``` + +## SDKs + +We do not redistribute the relevant SDKs, to hopefully avoid a license violation. You can download the SDKs yourself (they're bundled in XCode) from [Apple's website](https://developer.apple.com/download/all/?q=xcode) (requires an Apple ID). diff --git a/crates/header-translator/framework-includes.h b/crates/header-translator/framework-includes.h new file mode 100644 index 000000000..eb29fad17 --- /dev/null +++ b/crates/header-translator/framework-includes.h @@ -0,0 +1,17 @@ +// Workaround for clang < 13, only used in NSBundle.h +#define NS_FORMAT_ARGUMENT(A) + +// Workaround for clang < 13 +#define _Nullable_result _Nullable + +#include + +#import + +#import + +#if TARGET_OS_OSX +#import +#endif + +#import diff --git a/crates/header-translator/src/availability.rs b/crates/header-translator/src/availability.rs new file mode 100644 index 000000000..d86af499b --- /dev/null +++ b/crates/header-translator/src/availability.rs @@ -0,0 +1,15 @@ +use clang::PlatformAvailability; + +#[derive(Debug, Clone)] +pub struct Availability { + #[allow(dead_code)] + inner: Vec, +} + +impl Availability { + pub fn parse(availability: Vec) -> Self { + Self { + inner: availability, + } + } +} diff --git a/crates/header-translator/src/config.rs b/crates/header-translator/src/config.rs new file mode 100644 index 000000000..d03ddfa2f --- /dev/null +++ b/crates/header-translator/src/config.rs @@ -0,0 +1,108 @@ +use std::collections::HashMap; +use std::fs; +use std::io::Result; +use std::path::Path; + +use serde::Deserialize; + +#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct Config { + #[serde(rename = "class")] + #[serde(default)] + pub class_data: HashMap, + #[serde(rename = "protocol")] + #[serde(default)] + pub protocol_data: HashMap, + #[serde(rename = "struct")] + #[serde(default)] + pub struct_data: HashMap, + #[serde(rename = "enum")] + #[serde(default)] + pub enum_data: HashMap, + #[serde(rename = "fn")] + #[serde(default)] + pub fns: HashMap, + #[serde(rename = "static")] + #[serde(default)] + pub statics: HashMap, + #[serde(rename = "typedef")] + #[serde(default)] + pub typedef_data: HashMap, + #[serde(default)] + pub imports: Vec, +} + +#[derive(Deserialize, Debug, Default, Clone, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct ClassData { + #[serde(default)] + pub skipped: bool, + #[serde(rename = "superclass-name")] + #[serde(default)] + pub new_superclass_name: Option, + #[serde(default)] + pub methods: HashMap, + #[serde(default)] + pub properties: HashMap, +} + +#[derive(Deserialize, Debug, Default, Clone, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct StructData { + #[serde(default)] + pub skipped: bool, +} + +#[derive(Deserialize, Debug, Default, Clone, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct EnumData { + #[serde(default)] + pub skipped: bool, + #[serde(rename = "use-value")] + #[serde(default)] + pub use_value: bool, + #[serde(default)] + pub constants: HashMap, +} + +#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct MethodData { + #[serde(rename = "unsafe")] + #[serde(default = "unsafe_default")] + pub unsafe_: bool, + #[serde(default = "skipped_default")] + pub skipped: bool, + // TODO: mutating +} + +// TODO +pub type FnData = StructData; +pub type StaticData = StructData; +pub type TypedefData = StructData; + +fn unsafe_default() -> bool { + true +} + +fn skipped_default() -> bool { + false +} + +impl Default for MethodData { + fn default() -> Self { + Self { + unsafe_: unsafe_default(), + skipped: skipped_default(), + } + } +} + +impl Config { + pub fn from_file(file: &Path) -> Result { + let s = fs::read_to_string(file)?; + + Ok(toml::from_str(&s)?) + } +} diff --git a/crates/header-translator/src/expr.rs b/crates/header-translator/src/expr.rs new file mode 100644 index 000000000..f24be0e78 --- /dev/null +++ b/crates/header-translator/src/expr.rs @@ -0,0 +1,130 @@ +use std::fmt; +use std::fmt::Write; + +use clang::token::TokenKind; +use clang::{Entity, EntityKind, EntityVisitResult}; + +use crate::unexposed_macro::UnexposedMacro; + +#[derive(Clone, Debug, PartialEq)] +pub struct Expr { + s: String, +} + +impl Expr { + pub fn from_val((signed, unsigned): (i64, u64), is_signed: bool) -> Self { + let s = if is_signed { + format!("{}", signed) + } else { + format!("{}", unsigned) + }; + Expr { s } + } + + pub fn parse_enum_constant(entity: &Entity<'_>) -> Option { + let mut declaration_references = Vec::new(); + + entity.visit_children(|entity, _parent| { + if let EntityKind::DeclRefExpr = entity.get_kind() { + let name = entity.get_name().expect("expr decl ref name"); + declaration_references.push(name); + } + EntityVisitResult::Recurse + }); + + let mut res = None; + + entity.visit_children(|entity, _parent| { + match entity.get_kind() { + EntityKind::UnexposedAttr => { + if let Some(macro_) = UnexposedMacro::parse(&entity) { + panic!("parsed macro in expr: {macro_:?}, {entity:?}"); + } + } + _ => { + if res.is_none() { + res = Self::parse(&entity, &declaration_references); + } else { + panic!("found multiple expressions where one was expected"); + } + } + } + EntityVisitResult::Continue + }); + + res + } + + pub fn parse_var(entity: &Entity<'_>) -> Option { + Self::parse(entity, &[]) + } + + fn parse(entity: &Entity<'_>, declaration_references: &[String]) -> Option { + let range = entity.get_range().expect("expr range"); + let tokens = range.tokenize(); + + if tokens.is_empty() { + // TODO: Find a better way to parse macros + return None; + } + + let mut s = String::new(); + + for token in &tokens { + match (token.get_kind(), token.get_spelling()) { + (TokenKind::Identifier, ident) => { + if declaration_references.contains(&ident) { + // TODO: Handle these specially when we need to + } + write!(s, "{}", ident).unwrap(); + } + (TokenKind::Literal, lit) => { + let lit = lit + .trim_end_matches("UL") + .trim_end_matches("L") + .trim_end_matches("u") + .trim_end_matches("U"); + let lit = lit.replace("0X", "0x"); + write!(s, "{}", lit).unwrap(); + } + (TokenKind::Punctuation, punct) => { + match &*punct { + // These have the same semantics in C and Rust + "(" | ")" | "<<" | "-" | "+" | "|" | "&" | "^" => { + write!(s, "{}", punct).unwrap() + } + // Bitwise not + "~" => write!(s, "!").unwrap(), + punct => panic!("unknown expr punctuation {punct}"), + } + } + (kind, spelling) => panic!("unknown expr token {kind:?}/{spelling}"), + } + } + + // Trim casts + s = s + .trim_start_matches("(NSBoxType)") + .trim_start_matches("(NSBezelStyle)") + .trim_start_matches("(NSEventSubtype)") + .trim_start_matches("(NSWindowButton)") + .trim_start_matches("(NSExpressionType)") + .to_string(); + + // Trim unnecessary parentheses + if s.starts_with('(') + && s.ends_with(')') + && s.chars().filter(|&c| c == '(' || c == ')').count() == 2 + { + s = s.trim_start_matches("(").trim_end_matches(")").to_string(); + } + + Some(Self { s }) + } +} + +impl fmt::Display for Expr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.s) + } +} diff --git a/crates/header-translator/src/lib.rs b/crates/header-translator/src/lib.rs new file mode 100644 index 000000000..c4cabf92b --- /dev/null +++ b/crates/header-translator/src/lib.rs @@ -0,0 +1,125 @@ +use std::collections::HashSet; +use std::fmt::{Display, Write}; +use std::path::Path; +use std::process::{Command, Stdio}; + +mod availability; +mod config; +mod expr; +mod method; +mod objc2_utils; +mod property; +mod rust_type; +mod stmt; +mod unexposed_macro; + +pub use self::config::Config; +pub use self::stmt::Stmt; + +#[derive(Debug)] +pub struct RustFile { + declared_types: HashSet, + stmts: Vec, +} + +const INITIAL_IMPORTS: &str = r#"//! This file has been automatically generated by `objc2`'s `header-translator`. +//! DO NOT EDIT +use crate::common::*;"#; + +impl RustFile { + pub fn new() -> Self { + Self { + declared_types: HashSet::new(), + stmts: Vec::new(), + } + } + + pub fn add_stmt(&mut self, stmt: Stmt) { + match &stmt { + Stmt::ClassDecl { name, .. } => { + self.declared_types.insert(name.clone()); + } + Stmt::CategoryDecl { .. } => {} + Stmt::ProtocolDecl { name, .. } => { + self.declared_types.insert(name.clone()); + } + Stmt::StructDecl { name, .. } => { + self.declared_types.insert(name.clone()); + } + Stmt::EnumDecl { name, variants, .. } => { + if let Some(name) = name { + self.declared_types.insert(name.clone()); + } + for (name, _) in variants { + self.declared_types.insert(name.clone()); + } + } + Stmt::VarDecl { name, .. } => { + self.declared_types.insert(name.clone()); + } + Stmt::FnDecl { name, body, .. } => { + if body.is_none() { + self.declared_types.insert(name.clone()); + } else { + // TODO + } + } + Stmt::AliasDecl { name, .. } => { + self.declared_types.insert(name.clone()); + } + } + self.stmts.push(stmt); + } + + pub fn finish(self, config: &Config) -> (HashSet, String) { + let mut tokens = String::new(); + writeln!(tokens, "{}", INITIAL_IMPORTS).unwrap(); + + for import in &config.imports { + writeln!(tokens, "use crate::{import}::*;").unwrap(); + } + + writeln!(tokens, "").unwrap(); + + for stmt in self.stmts { + writeln!(tokens, "{}", stmt).unwrap(); + } + + (self.declared_types, tokens) + } +} + +pub fn run_cargo_fmt(package: &str) { + let status = Command::new("cargo") + .args(["fmt", "--package", package]) + .current_dir(Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap()) + .status() + .expect("failed running cargo fmt"); + + assert!( + status.success(), + "failed running cargo fmt with exit code {status}" + ); +} + +pub fn run_rustfmt(data: impl Display) -> Vec { + use std::io::Write; + + let mut child = Command::new("rustfmt") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .expect("failed running rustfmt"); + + let mut stdin = child.stdin.take().expect("failed to open stdin"); + write!(stdin, "{}", data).expect("failed writing"); + drop(stdin); + + let output = child.wait_with_output().expect("failed formatting"); + + if !output.status.success() { + panic!("failed running rustfmt with exit code {}", output.status) + } + + output.stdout +} diff --git a/crates/header-translator/src/main.rs b/crates/header-translator/src/main.rs new file mode 100644 index 000000000..7f1c8f1fe --- /dev/null +++ b/crates/header-translator/src/main.rs @@ -0,0 +1,312 @@ +use std::collections::BTreeMap; +use std::fmt::{self, Write}; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; + +use apple_sdk::{AppleSdk, DeveloperDirectory, Platform, SdkPath, SimpleSdk}; +use clang::{Clang, Entity, EntityKind, EntityVisitResult, Index}; + +use header_translator::{run_cargo_fmt, run_rustfmt, Config, RustFile, Stmt}; + +const FORMAT_INCREMENTALLY: bool = false; + +fn main() { + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let workspace_dir = manifest_dir.parent().unwrap(); + let crate_src = workspace_dir.join("icrate/src"); + + println!("status: loading configs..."); + let configs = load_configs(&crate_src); + println!("status: loaded {} configs", configs.len()); + + let clang = Clang::new().unwrap(); + let index = Index::new(&clang, true, true); + + let developer_dir = DeveloperDirectory::from(PathBuf::from( + std::env::args_os() + .skip(1) + .next() + .expect("must specify developer directory as first argument"), + )); + + let sdks = developer_dir + .platforms() + .expect("developer dir platforms") + .into_iter() + .filter_map(|platform| { + matches!(&*platform, Platform::MacOsX | Platform::IPhoneOs).then(|| { + let sdks: Vec<_> = platform + .find_sdks::() + .expect("platform sdks") + .into_iter() + .filter(|sdk| !sdk.is_symlink() && sdk.platform() == &*platform) + .collect(); + if sdks.len() != 1 { + panic!("found multiple sdks {sdks:?} in {:?}", &*platform); + } + sdks[0].sdk_path() + }) + }); + + let mut result: BTreeMap> = configs + .iter() + .map(|(library, _)| (library.clone(), BTreeMap::new())) + .collect(); + + // TODO: Compare SDKs + for sdk in sdks { + println!("status: parsing {:?}...", sdk.platform); + + let mut preprocessing = true; + + parse_and_visit_stmts(&index, &sdk, |library, file_name, entity| { + if let Some(config) = configs.get(library) { + let files = result.get_mut(library).expect("files"); + match entity.get_kind() { + EntityKind::InclusionDirective if preprocessing => { + // println!("{library}/{file_name}.h: {entity:?}"); + // If umbrella header + let name = entity.get_name().expect("inclusion name"); + let mut iter = name.split('/'); + let framework = iter.next().expect("inclusion name has framework"); + if framework == library { + let included = iter + .next() + .expect("inclusion name has file") + .strip_suffix(".h") + .expect("inclusion name file is header") + .to_string(); + if iter.count() != 0 { + panic!("invalid inclusion of {name:?}"); + } + + // If inclusion is not umbrella header + if included != library { + // The file is often included twice, even + // within the same file, so insertion can fail + files.entry(included).or_insert_with(RustFile::new); + } + } + } + EntityKind::MacroExpansion if preprocessing => {} + EntityKind::MacroDefinition if preprocessing => { + // let name = entity.get_name().expect("macro def name"); + // entity.is_function_like_macro(); + // println!("macrodef in {library}/{file_name}.h: {}", name); + } + _ => { + if preprocessing { + println!("status: preprocessed {:?}...", sdk.platform); + } + preprocessing = false; + // No more includes / macro expansions after this line + let file = files.get_mut(file_name).expect("file"); + if let Some(stmt) = Stmt::parse(&entity, &config) { + if sdk.platform == Platform::MacOsX { + file.add_stmt(stmt); + } + } + } + } + } else { + // println!("library not found {library}"); + } + }); + println!("status: done parsing {:?}", sdk.platform); + } + + for (library, files) in result { + println!("status: writing framework {library}..."); + let output_path = crate_src.join("generated").join(&library); + let config = configs.get(&library).expect("configs get library"); + output_files(&output_path, files, FORMAT_INCREMENTALLY, config).unwrap(); + println!("status: written framework {library}"); + } + + if !FORMAT_INCREMENTALLY { + println!("status: formatting"); + run_cargo_fmt("icrate"); + } +} + +fn load_configs(crate_src: &Path) -> BTreeMap { + crate_src + .read_dir() + .expect("read_dir") + .filter_map(|dir| { + let dir = dir.expect("dir"); + if !dir.file_type().expect("file type").is_dir() { + return None; + } + let path = dir.path(); + let file = path.join("translation-config.toml"); + match Config::from_file(&file) { + Ok(config) => Some(( + path.file_name() + .expect("framework name") + .to_string_lossy() + .to_string(), + config, + )), + Err(err) if err.kind() == io::ErrorKind::NotFound => None, + Err(err) => panic!("{file:?}: {err}"), + } + }) + .collect() +} + +fn parse_and_visit_stmts( + index: &Index<'_>, + sdk: &SdkPath, + mut f: impl FnMut(&str, &str, Entity<'_>), +) { + let (target, version_min) = match &sdk.platform { + Platform::MacOsX => ("--target=x86_64-apple-macos", "-mmacosx-version-min=10.7"), + Platform::IPhoneOs => ("--target=arm64-apple-ios", "-miphoneos-version-min=7.0"), + _ => todo!(), + }; + + let tu = index + .parser(&Path::new(env!("CARGO_MANIFEST_DIR")).join("framework-includes.h")) + .detailed_preprocessing_record(true) + .incomplete(true) + .skip_function_bodies(true) + .keep_going(true) + // .single_file_parse(true) + .include_attributed_types(true) + .visit_implicit_attributes(true) + // .ignore_non_errors_from_included_files(true) + .retain_excluded_conditional_blocks(true) + .arguments(&[ + "-x", + "objective-c", + target, + "-Wall", + "-Wextra", + "-fobjc-arc", + "-fobjc-arc-exceptions", + "-fobjc-abi-version=2", // 3?? + // "-fparse-all-comments", + "-fapinotes", + version_min, + "-isysroot", + sdk.path.to_str().unwrap(), + ]) + .parse() + .unwrap(); + + println!("status: initialized translation unit {:?}", sdk.platform); + + dbg!(&tu); + dbg!(tu.get_target()); + dbg!(tu.get_memory_usage()); + dbg!(tu.get_diagnostics()); + + // let dbg_file = |file: File<'_>| { + // dbg!( + // &file, + // file.get_module(), + // file.get_skipped_ranges(), + // file.is_include_guarded(), + // // file.get_includes(), + // // file.get_references(), + // ); + // }; + // + // dbg_file(tu.get_file(&header).unwrap()); + // dbg_file(tu.get_file(&dir.join("NSAccessibility.h")).unwrap()); + // let cursor_file = tu.get_file(&dir.join("NSCursor.h")).unwrap(); + // dbg_file(cursor_file); + + let entity = tu.get_entity(); + + dbg!(&entity); + dbg!(entity.get_availability()); + + let framework_dir = sdk.path.join("System/Library/Frameworks"); + entity.visit_children(|entity, _parent| { + if let Some(location) = entity.get_location() { + if let Some(file) = location.get_file_location().file { + let path = file.get_path(); + if let Ok(path) = path.strip_prefix(&framework_dir) { + let mut components = path.components(); + let library = components + .next() + .expect("components next") + .as_os_str() + .to_str() + .expect("component to_str") + .strip_suffix(".framework") + .expect("framework fileending"); + + let path = components.as_path(); + let name = path + .file_stem() + .expect("path file stem") + .to_string_lossy() + .to_owned(); + + f(&library, &name, entity); + } + } + } + EntityVisitResult::Continue + }); +} + +fn output_files( + output_path: &Path, + files: impl IntoIterator, + format_incrementally: bool, + config: &Config, +) -> fmt::Result { + let declared: Vec<_> = files + .into_iter() + .map(|(name, file)| { + let (declared_types, tokens) = file.finish(config); + + let mut path = output_path.join(&name); + path.set_extension("rs"); + + let output = if format_incrementally { + run_rustfmt(tokens) + } else { + tokens.into() + }; + + fs::write(&path, output).unwrap(); + + (name, declared_types) + }) + .collect(); + + let mut tokens = String::new(); + + for (name, _) in &declared { + writeln!(tokens, "#[path = \"{name}.rs\"]")?; + writeln!(tokens, "mod __{name};")?; + } + writeln!(tokens, "")?; + for (name, declared_types) in declared { + if !declared_types.is_empty() { + let declared_types: Vec<_> = declared_types.into_iter().collect(); + writeln!( + tokens, + "pub use self::__{name}::{{{}}};", + declared_types.join(",") + )?; + } + } + + let output = if format_incrementally { + run_rustfmt(tokens) + } else { + tokens.into() + }; + + // truncate if the file exists + fs::write(output_path.join("mod.rs"), output).unwrap(); + + Ok(()) +} diff --git a/crates/header-translator/src/method.rs b/crates/header-translator/src/method.rs new file mode 100644 index 000000000..d8199b880 --- /dev/null +++ b/crates/header-translator/src/method.rs @@ -0,0 +1,421 @@ +use std::fmt; + +use clang::{Entity, EntityKind, EntityVisitResult, ObjCQualifiers}; + +use crate::availability::Availability; +use crate::config::MethodData; +use crate::objc2_utils::in_selector_family; +use crate::rust_type::Ty; +use crate::unexposed_macro::UnexposedMacro; + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub enum Qualifier { + In, + Inout, + Out, + Bycopy, + Byref, + Oneway, +} + +impl Qualifier { + pub fn parse(qualifiers: ObjCQualifiers) -> Self { + match qualifiers { + ObjCQualifiers { + in_: true, + inout: false, + out: false, + bycopy: false, + byref: false, + oneway: false, + } => Self::In, + ObjCQualifiers { + in_: false, + inout: true, + out: false, + bycopy: false, + byref: false, + oneway: false, + } => Self::Inout, + ObjCQualifiers { + in_: false, + inout: false, + out: true, + bycopy: false, + byref: false, + oneway: false, + } => Self::Out, + ObjCQualifiers { + in_: false, + inout: false, + out: false, + bycopy: true, + byref: false, + oneway: false, + } => Self::Bycopy, + ObjCQualifiers { + in_: false, + inout: false, + out: false, + bycopy: false, + byref: true, + oneway: false, + } => Self::Byref, + ObjCQualifiers { + in_: false, + inout: false, + out: false, + bycopy: false, + byref: false, + oneway: true, + } => Self::Oneway, + _ => unreachable!("invalid qualifiers: {:?}", qualifiers), + } + } +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub enum MemoryManagement { + /// Consumes self and returns retained pointer + Init, + ReturnsRetained, + ReturnsInnerPointer, + Normal, +} + +impl MemoryManagement { + /// Verifies that the selector and the memory management rules match up + /// in a way that we can just use `msg_send_id!`. + fn verify_sel(self, sel: &str) { + let bytes = sel.as_bytes(); + if in_selector_family(bytes, b"init") { + assert!(self == Self::Init, "{:?} did not match {}", self, sel); + } else if in_selector_family(bytes, b"new") + || in_selector_family(bytes, b"alloc") + || in_selector_family(bytes, b"copy") + || in_selector_family(bytes, b"mutableCopy") + { + assert!( + self == Self::ReturnsRetained, + "{:?} did not match {}", + self, + sel + ); + } else { + if self == Self::ReturnsInnerPointer { + return; + } + assert!(self == Self::Normal, "{:?} did not match {}", self, sel); + } + } + + /// Matches `objc2::__macro_helpers::retain_semantics`. + fn get_memory_management_name(sel: &str) -> &'static str { + let bytes = sel.as_bytes(); + match ( + in_selector_family(bytes, b"new"), + in_selector_family(bytes, b"alloc"), + in_selector_family(bytes, b"init"), + in_selector_family(bytes, b"copy"), + in_selector_family(bytes, b"mutableCopy"), + ) { + (true, false, false, false, false) => "New", + (false, true, false, false, false) => "Alloc", + (false, false, true, false, false) => "Init", + (false, false, false, true, false) => "CopyOrMutCopy", + (false, false, false, false, true) => "CopyOrMutCopy", + (false, false, false, false, false) => "Other", + _ => unreachable!(), + } + } + + pub fn is_init(sel: &str) -> bool { + in_selector_family(sel.as_bytes(), b"init") + } + + pub fn is_alloc(sel: &str) -> bool { + in_selector_family(sel.as_bytes(), b"alloc") + } + + pub fn is_new(sel: &str) -> bool { + in_selector_family(sel.as_bytes(), b"new") + } +} + +#[allow(dead_code)] +#[derive(Debug, Clone)] +pub struct Method { + pub selector: String, + pub fn_name: String, + pub availability: Availability, + pub is_class: bool, + pub is_optional_protocol: bool, + pub memory_management: MemoryManagement, + pub designated_initializer: bool, + pub arguments: Vec<(String, Option, Ty)>, + pub result_type: Ty, + pub safe: bool, +} + +impl Method { + /// Takes one of `EntityKind::ObjCInstanceMethodDecl` or + /// `EntityKind::ObjCClassMethodDecl`. + pub fn partial(entity: Entity<'_>) -> PartialMethod<'_> { + let selector = entity.get_name().expect("method selector"); + let fn_name = selector.trim_end_matches(|c| c == ':').replace(':', "_"); + + let is_class = match entity.get_kind() { + EntityKind::ObjCInstanceMethodDecl => false, + EntityKind::ObjCClassMethodDecl => true, + _ => unreachable!("unknown method kind"), + }; + + PartialMethod { + entity, + selector, + is_class, + fn_name, + } + } +} + +#[derive(Debug, Clone)] +pub struct PartialMethod<'tu> { + entity: Entity<'tu>, + selector: String, + pub is_class: bool, + pub fn_name: String, +} + +impl<'tu> PartialMethod<'tu> { + pub fn parse(self, data: MethodData) -> Option { + let Self { + entity, + selector, + is_class, + fn_name, + } = self; + + // println!("Method {:?}", selector); + if data.skipped { + return None; + } + + if entity.is_variadic() { + println!("Can't handle variadic method {}", selector); + return None; + } + + let availability = Availability::parse( + entity + .get_platform_availability() + .expect("method availability"), + ); + + let mut arguments: Vec<_> = entity + .get_arguments() + .expect("method arguments") + .into_iter() + .map(|entity| { + let name = entity.get_name().expect("arg display name"); + let qualifier = entity.get_objc_qualifiers().map(Qualifier::parse); + let mut is_consumed = false; + + entity.visit_children(|entity, _parent| { + match entity.get_kind() { + EntityKind::ObjCClassRef + | EntityKind::ObjCProtocolRef + | EntityKind::TypeRef + | EntityKind::ParmDecl => { + // Ignore + } + EntityKind::NSConsumed => { + if is_consumed { + panic!("got NSConsumed twice"); + } + is_consumed = true; + } + EntityKind::UnexposedAttr => { + if let Some(macro_) = UnexposedMacro::parse(&entity) { + println!("method argument {fn_name}/{name}: {macro_:?}"); + } + } + // For some reason we recurse into array types + EntityKind::IntegerLiteral => {} + _ => panic!("Unknown method argument child: {:?}, {:?}", entity, _parent), + }; + EntityVisitResult::Continue + }); + + let ty = entity.get_type().expect("argument type"); + let ty = Ty::parse_method_argument(ty, is_consumed); + + (name, qualifier, ty) + }) + .collect(); + + let is_error = if let Some((_, _, ty)) = arguments.last() { + ty.argument_is_error_out() + } else { + false + }; + + // TODO: Strip these from function name? + // selector.ends_with("error:") + // || selector.ends_with("AndReturnError:") + // || selector.ends_with("WithError:") + + if is_error { + arguments.pop(); + } + + if let Some(qualifiers) = entity.get_objc_qualifiers() { + let qualifier = Qualifier::parse(qualifiers); + panic!( + "unexpected qualifier `{:?}` on return type: {:?}", + qualifier, entity + ); + } + + let result_type = entity.get_result_type().expect("method return type"); + let mut result_type = Ty::parse_method_return(result_type); + + result_type.fix_related_result_type(is_class, &selector); + + if is_class && MemoryManagement::is_alloc(&selector) { + result_type.set_is_alloc(); + } + + if is_error { + result_type.set_is_error(); + } + + let mut designated_initializer = false; + let mut consumes_self = false; + let mut memory_management = MemoryManagement::Normal; + + entity.visit_children(|entity, _parent| { + match entity.get_kind() { + EntityKind::ObjCClassRef + | EntityKind::ObjCProtocolRef + | EntityKind::TypeRef + | EntityKind::ParmDecl => { + // Ignore + } + EntityKind::ObjCDesignatedInitializer => { + if designated_initializer { + panic!("encountered ObjCDesignatedInitializer twice"); + } + designated_initializer = true; + } + EntityKind::NSConsumesSelf => { + consumes_self = true; + } + EntityKind::NSReturnsRetained => { + if memory_management != MemoryManagement::Normal { + panic!("got unexpected NSReturnsRetained") + } + memory_management = MemoryManagement::ReturnsRetained; + } + EntityKind::ObjCReturnsInnerPointer => { + if memory_management != MemoryManagement::Normal { + panic!("got unexpected ObjCReturnsInnerPointer") + } + memory_management = MemoryManagement::ReturnsInnerPointer; + } + EntityKind::NSConsumed => { + // Handled inside arguments + } + EntityKind::IbActionAttr => { + // TODO: What is this? + } + EntityKind::ObjCRequiresSuper => { + // TODO: Can we use this for something? + // + } + EntityKind::UnexposedAttr => { + if let Some(macro_) = UnexposedMacro::parse(&entity) { + println!("method {fn_name}: {macro_:?}"); + } + } + _ => panic!("Unknown method child: {:?}, {:?}", entity, _parent), + }; + // TODO: Verify that Continue is good enough + EntityVisitResult::Continue + }); + + if consumes_self { + if memory_management != MemoryManagement::ReturnsRetained { + panic!("got NSConsumesSelf without NSReturnsRetained"); + } + memory_management = MemoryManagement::Init; + } + + // Verify that memory management is as expected + if result_type.is_id() { + memory_management.verify_sel(&selector); + } + + Some(Method { + selector, + fn_name, + availability, + is_class, + is_optional_protocol: entity.is_objc_optional(), + memory_management, + designated_initializer, + arguments, + result_type, + safe: !data.unsafe_, + }) + } +} + +impl fmt::Display for Method { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.is_optional_protocol { + writeln!(f, " #[optional]")?; + } + + if self.result_type.is_id() { + writeln!( + f, + " #[method_id(@__retain_semantics {} {})]", + MemoryManagement::get_memory_management_name(&self.selector), + self.selector + )?; + } else { + writeln!(f, " #[method({})]", self.selector)?; + }; + + write!(f, " pub ")?; + if !self.safe { + write!(f, "unsafe ")?; + } + write!(f, "fn {}(", handle_reserved(&self.fn_name))?; + if !self.is_class { + if MemoryManagement::is_init(&self.selector) { + write!(f, "this: Option>, ")?; + } else { + write!(f, "&self, ")?; + } + } + for (param, _qualifier, arg_ty) in &self.arguments { + write!(f, "{}: {arg_ty},", handle_reserved(param))?; + } + write!(f, ")")?; + + writeln!(f, "{};", self.result_type)?; + + Ok(()) + } +} + +pub(crate) fn handle_reserved(s: &str) -> &str { + match s { + "type" => "type_", + "trait" => "trait_", + "abstract" => "abstract_", + s => s, + } +} diff --git a/crates/header-translator/src/objc2_utils.rs b/crates/header-translator/src/objc2_utils.rs new file mode 100644 index 000000000..779da0986 --- /dev/null +++ b/crates/header-translator/src/objc2_utils.rs @@ -0,0 +1,40 @@ +//! Utilities copied from `objc2` + +pub const fn in_selector_family(mut selector: &[u8], mut family: &[u8]) -> bool { + // Skip leading underscores from selector + loop { + selector = match selector { + [b'_', rest @ ..] => rest, + _ => break, + } + } + + // Compare each character + loop { + (selector, family) = match (selector, family) { + // Remaining items + ([s, selector @ ..], [f, family @ ..]) => { + if *s == *f { + // Next iteration + (selector, family) + } else { + // Family does not begin with selector + return false; + } + } + // Equal + ([], []) => { + return true; + } + // Selector can't be part of familiy if smaller than it + ([], _) => { + return false; + } + // Remaining items in selector + // -> ensure next character is not lowercase + ([s, ..], []) => { + return !s.is_ascii_lowercase(); + } + } + } +} diff --git a/crates/header-translator/src/property.rs b/crates/header-translator/src/property.rs new file mode 100644 index 000000000..2f7640ef8 --- /dev/null +++ b/crates/header-translator/src/property.rs @@ -0,0 +1,183 @@ +use std::fmt; + +use clang::{Entity, EntityKind, EntityVisitResult, Nullability, ObjCAttributes}; + +use crate::availability::Availability; +use crate::config::MethodData; +use crate::method::{MemoryManagement, Method, Qualifier}; +use crate::rust_type::Ty; +use crate::unexposed_macro::UnexposedMacro; + +#[allow(dead_code)] +#[derive(Debug, Clone)] +pub struct Property { + name: String, + getter_name: String, + setter_name: Option, + availability: Availability, + is_class: bool, + is_optional_protocol: bool, + type_in: Ty, + type_out: Ty, + safe: bool, +} + +impl Property { + /// Takes `EntityKind::ObjCPropertyDecl`. + pub fn partial(entity: Entity<'_>) -> PartialProperty<'_> { + let attributes = entity.get_objc_attributes(); + let has_setter = attributes.map(|a| !a.readonly).unwrap_or(true); + + PartialProperty { + entity, + name: entity.get_display_name().expect("property getter name"), + getter_name: entity.get_objc_getter_name().expect("property getter name"), + setter_name: has_setter.then(|| { + entity + .get_objc_setter_name() + .expect("property setter name") + .trim_end_matches(|c| c == ':') + .to_string() + }), + is_class: attributes.map(|a| a.class).unwrap_or(false), + attributes: entity.get_objc_attributes(), + } + } +} + +#[derive(Debug, Clone)] +pub struct PartialProperty<'tu> { + entity: Entity<'tu>, + pub name: String, + pub getter_name: String, + pub setter_name: Option, + pub is_class: bool, + attributes: Option, +} + +impl PartialProperty<'_> { + pub fn parse(self, data: MethodData) -> Option { + let Self { + entity, + name, + getter_name, + setter_name, + is_class, + attributes, + } = self; + + if data.skipped { + return None; + } + + // println!("Property {getter_name:?}/{setter_name:?}: {attributes:?}"); + + let availability = Availability::parse( + entity + .get_platform_availability() + .expect("method availability"), + ); + + // `@property(copy)` for some reason returns nonnull? + // + // Swift signifies that they use forced unwrapping here, perhaps + // because they know that it can fail (e.g. in OOM situations), but + // is very unlikely to? + let default_nullability = if attributes.map(|a| a.copy).unwrap_or(false) { + Nullability::NonNull + } else { + Nullability::Unspecified + }; + + let type_in = Ty::parse_property( + entity.get_type().expect("property type"), + Nullability::Unspecified, + ); + let type_out = Ty::parse_property_return( + entity.get_type().expect("property type"), + default_nullability, + ); + + let mut memory_management = MemoryManagement::Normal; + + entity.visit_children(|entity, _parent| { + match entity.get_kind() { + EntityKind::ObjCClassRef + | EntityKind::ObjCProtocolRef + | EntityKind::TypeRef + | EntityKind::ParmDecl => { + // Ignore + } + EntityKind::ObjCReturnsInnerPointer => { + if memory_management != MemoryManagement::Normal { + panic!("got unexpected ObjCReturnsInnerPointer") + } + memory_management = MemoryManagement::ReturnsInnerPointer; + } + EntityKind::ObjCInstanceMethodDecl => { + println!("method in property: {entity:?}"); + } + EntityKind::IbOutletAttr => { + // TODO: What is this? + } + EntityKind::UnexposedAttr => { + if let Some(macro_) = UnexposedMacro::parse(&entity) { + println!("property {name}: {macro_:?}"); + } + } + _ => panic!("Unknown property child: {:?}, {:?}", entity, _parent), + }; + EntityVisitResult::Continue + }); + + let qualifier = entity.get_objc_qualifiers().map(Qualifier::parse); + assert_eq!(qualifier, None, "properties do not support qualifiers"); + + Some(Property { + name, + getter_name, + setter_name, + availability, + is_class, + is_optional_protocol: entity.is_objc_optional(), + type_in, + type_out, + safe: !data.unsafe_, + }) + } +} + +impl fmt::Display for Property { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let method = Method { + selector: self.getter_name.clone(), + fn_name: self.getter_name.clone(), + availability: self.availability.clone(), + is_class: self.is_class, + is_optional_protocol: self.is_optional_protocol, + memory_management: MemoryManagement::Normal, + designated_initializer: false, + arguments: Vec::new(), + result_type: self.type_out.clone(), + safe: self.safe, + }; + write!(f, "{method}")?; + if let Some(setter_name) = &self.setter_name { + writeln!(f, "")?; + let method = Method { + selector: setter_name.clone() + ":", + fn_name: setter_name.clone(), + availability: self.availability.clone(), + is_class: self.is_class, + is_optional_protocol: self.is_optional_protocol, + memory_management: MemoryManagement::Normal, + designated_initializer: false, + arguments: Vec::from([(self.name.clone(), None, self.type_in.clone())]), + result_type: Ty::VOID_RESULT, + safe: self.safe, + }; + write!(f, "{method}")?; + } + Ok(()) + } +} diff --git a/crates/header-translator/src/rust_type.rs b/crates/header-translator/src/rust_type.rs new file mode 100644 index 000000000..a1990e7b3 --- /dev/null +++ b/crates/header-translator/src/rust_type.rs @@ -0,0 +1,1219 @@ +use std::fmt; + +use clang::{CallingConvention, Nullability, Type, TypeKind}; + +use crate::method::MemoryManagement; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct GenericType { + pub name: String, + pub generics: Vec, +} + +impl GenericType { + pub fn parse_objc_pointer(ty: Type<'_>) -> Self { + match ty.get_kind() { + TypeKind::ObjCInterface => { + let generics = ty.get_objc_type_arguments(); + if !generics.is_empty() { + panic!("generics not empty: {ty:?}, {generics:?}"); + } + let protocols = ty.get_objc_protocol_declarations(); + if !protocols.is_empty() { + panic!("protocols not empty: {ty:?}, {protocols:?}"); + } + let name = ty.get_display_name(); + Self { + name, + generics: Vec::new(), + } + } + TypeKind::ObjCObject => { + let base_ty = ty + .get_objc_object_base_type() + .expect("object to have base type"); + let mut name = base_ty.get_display_name(); + + let generics: Vec<_> = ty + .get_objc_type_arguments() + .into_iter() + .map(|param| { + match RustType::parse(param, false, Nullability::Unspecified, false) { + RustType::Id { + type_, + is_const: _, + lifetime: _, + nullability: _, + } => type_, + // TODO: Handle this better + RustType::Class { nullability: _ } => Self { + name: "TodoClass".to_string(), + generics: Vec::new(), + }, + param => { + panic!("invalid generic parameter {:?} in {:?}", param, ty) + } + } + }) + .collect(); + + let protocols: Vec<_> = ty + .get_objc_protocol_declarations() + .into_iter() + .map(|entity| entity.get_display_name().expect("protocol name")) + .collect(); + if !protocols.is_empty() { + if name == "id" && generics.is_empty() && protocols.len() == 1 { + name = protocols[0].clone(); + } else { + name = "TodoProtocols".to_string(); + } + } + + Self { name, generics } + } + _ => panic!("pointee was neither objcinterface nor objcobject: {ty:?}"), + } + } +} + +impl fmt::Display for GenericType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name)?; + if !self.generics.is_empty() { + write!(f, "<")?; + for generic in &self.generics { + write!(f, "{generic},")?; + } + write!(f, ">")?; + } + Ok(()) + } +} + +/// ObjCLifetime +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub enum Lifetime { + Unspecified, + /// OCL_ExplicitNone + Unretained, + /// OCL_Strong + Strong, + /// OCL_Weak + Weak, + /// OCL_Autoreleasing + Autoreleasing, +} + +/// Parse attributes. +/// +/// This is _very_ ugly, but required because libclang doesn't expose most +/// of these. +fn parse_attributed<'a>( + ty: Type<'a>, + nullability: &mut Nullability, + lifetime: &mut Lifetime, + kindof: &mut bool, + inside_partial_array: bool, +) -> Type<'a> { + let mut modified_ty = ty.clone(); + while modified_ty.get_kind() == TypeKind::Attributed { + // println!("{ty:?}, {modified_ty:?}"); + modified_ty = modified_ty + .get_modified_type() + .expect("attributed type to have modified type"); + } + + if modified_ty == ty { + return ty; + } + + let mut name = &*ty.get_display_name(); + let mut modified_name = &*modified_ty.get_display_name(); + + fn get_inner_fn(name: &str) -> &str { + let (_, name) = name.split_once('(').expect("fn to have begin parenthesis"); + let (name, _) = name.split_once(')').expect("fn to have end parenthesis"); + name.trim() + } + + match modified_ty.get_kind() { + TypeKind::ConstantArray => { + let (res, _) = name.split_once("[").expect("array to end with ["); + name = res.trim(); + let (res, _) = modified_name.split_once("[").expect("array to end with ["); + modified_name = res.trim(); + } + TypeKind::IncompleteArray => { + name = name + .strip_suffix("[]") + .expect("array to end with []") + .trim(); + modified_name = modified_name + .strip_suffix("[]") + .expect("array to end with []") + .trim(); + } + TypeKind::BlockPointer => { + name = get_inner_fn(name); + modified_name = get_inner_fn(modified_name); + } + TypeKind::Pointer => { + if modified_ty + .get_pointee_type() + .expect("pointer to have pointee") + .get_kind() + == TypeKind::FunctionPrototype + { + name = get_inner_fn(name); + modified_name = get_inner_fn(modified_name); + } + } + _ => {} + } + + if ty.is_const_qualified() { + if let Some(rest) = name.strip_suffix("const") { + name = rest.trim(); + } + if !modified_ty.is_const_qualified() { + // TODO: Fix this + println!("unnecessarily stripped const"); + } + } + + if inside_partial_array { + if let Some(rest) = name.strip_prefix("__unsafe_unretained") { + *lifetime = Lifetime::Unretained; + name = rest.trim(); + } + } + + if let Some(rest) = name.strip_suffix("__unsafe_unretained") { + *lifetime = Lifetime::Unretained; + name = rest.trim(); + } else if let Some(rest) = name.strip_suffix("__strong") { + *lifetime = Lifetime::Strong; + name = rest.trim(); + } else if let Some(rest) = name.strip_suffix("__weak") { + *lifetime = Lifetime::Weak; + name = rest.trim(); + } else if let Some(rest) = name.strip_suffix("__autoreleasing") { + *lifetime = Lifetime::Autoreleasing; + name = rest.trim(); + } + + if let Some(rest) = name.strip_suffix("_Nullable") { + assert_eq!( + ty.get_nullability(), + Some(Nullability::Nullable), + "nullable" + ); + *nullability = Nullability::Nullable; + name = rest.trim(); + } else if let Some(rest) = name.strip_suffix("_Nonnull") { + assert_eq!(ty.get_nullability(), Some(Nullability::NonNull), "nonnull"); + *nullability = match nullability { + Nullability::Nullable => Nullability::Nullable, + _ => Nullability::NonNull, + }; + name = rest.trim(); + } else if let Some(rest) = name.strip_suffix("_Null_unspecified") { + assert_eq!( + ty.get_nullability(), + Some(Nullability::Unspecified), + "unspecified" + ); + // Do nothing + name = rest.trim(); + } else { + assert_eq!( + ty.get_nullability(), + None, + "expected no nullability attribute on {name:?}" + ); + } + + if name != modified_name { + if let Some(rest) = name.strip_prefix("__kindof") { + name = rest.trim(); + *kindof = true; + } + + if name != modified_name { + let original_name = ty.get_display_name(); + println!("attributes: {original_name:?} -> {name:?} != {modified_name:?}"); + panic!( + "could not extract all attributes from attributed type. Inner: {ty:?}, {modified_ty:?}" + ); + } + } + + modified_ty +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum RustType { + // Primitives + Void, + C99Bool, + Char, + SChar, + UChar, + Short, + UShort, + Int, + UInt, + Long, + ULong, + LongLong, + ULongLong, + Float, + Double, + F32, + F64, + I8, + U8, + I16, + U16, + I32, + U32, + I64, + U64, + + // Objective-C + Id { + type_: GenericType, + is_const: bool, + lifetime: Lifetime, + nullability: Nullability, + }, + Class { + nullability: Nullability, + }, + Sel { + nullability: Nullability, + }, + ObjcBool, + + // Others + Pointer { + nullability: Nullability, + is_const: bool, + pointee: Box, + }, + IncompleteArray { + nullability: Nullability, + is_const: bool, + pointee: Box, + }, + Array { + element_type: Box, + num_elements: usize, + }, + Enum { + name: String, + }, + Struct { + name: String, + }, + Fn { + is_variadic: bool, + arguments: Vec, + result_type: Box, + }, + Block { + arguments: Vec, + result_type: Box, + }, + + TypeDef { + name: String, + }, +} + +impl RustType { + fn parse( + ty: Type<'_>, + is_consumed: bool, + mut nullability: Nullability, + inside_partial_array: bool, + ) -> Self { + use TypeKind::*; + + // println!("{:?}, {:?}", ty, ty.get_class_type()); + + let mut kindof = false; + let mut lifetime = Lifetime::Unspecified; + let ty = parse_attributed( + ty, + &mut nullability, + &mut lifetime, + &mut kindof, + inside_partial_array, + ); + + // println!("{:?}: {:?}", ty.get_kind(), ty.get_display_name()); + + match ty.get_kind() { + Void => Self::Void, + Bool => Self::C99Bool, + CharS | CharU => Self::Char, + SChar => Self::SChar, + UChar => Self::UChar, + Short => Self::Short, + UShort => Self::UShort, + Int => Self::Int, + UInt => Self::UInt, + Long => Self::Long, + ULong => Self::ULong, + LongLong => Self::LongLong, + ULongLong => Self::ULongLong, + Float => Self::Float, + Double => Self::Double, + ObjCId => Self::Id { + type_: GenericType { + name: "Object".to_string(), + generics: Vec::new(), + }, + is_const: ty.is_const_qualified(), + lifetime, + nullability, + }, + ObjCClass => Self::Class { nullability }, + ObjCSel => Self::Sel { nullability }, + Pointer => { + let is_const = ty.is_const_qualified(); + let ty = ty.get_pointee_type().expect("pointer type to have pointee"); + let pointee = Self::parse(ty, is_consumed, Nullability::Unspecified, false); + Self::Pointer { + nullability, + is_const, + pointee: Box::new(pointee), + } + } + BlockPointer => { + let is_const = ty.is_const_qualified(); + let ty = ty.get_pointee_type().expect("pointer type to have pointee"); + match Self::parse(ty, is_consumed, Nullability::Unspecified, false) { + Self::Fn { + is_variadic: false, + mut arguments, + mut result_type, + } => { + for arg in &mut arguments { + arg.set_block(); + } + result_type.set_block(); + Self::Pointer { + nullability, + is_const, + pointee: Box::new(Self::Block { + arguments, + result_type, + }), + } + } + pointee => panic!("unexpected pointee in block: {pointee:?}"), + } + } + ObjCObjectPointer => { + let ty = ty.get_pointee_type().expect("pointer type to have pointee"); + let mut kindof = false; + let ty = parse_attributed(ty, &mut nullability, &mut lifetime, &mut kindof, false); + let type_ = GenericType::parse_objc_pointer(ty); + + Self::Id { + type_, + is_const: ty.is_const_qualified(), + lifetime, + nullability, + } + } + Typedef => { + let typedef_name = ty.get_typedef_name().expect("typedef has name"); + match &*typedef_name { + "BOOL" => Self::ObjcBool, + + "int8_t" => Self::I8, + "uint8_t" => Self::U8, + "int16_t" => Self::I16, + "uint16_t" => Self::U16, + "int32_t" => Self::I32, + "uint32_t" => Self::U32, + "int64_t" => Self::I64, + "uint64_t" => Self::U64, + + // MacTypes.h + "UInt8" => Self::U8, + "UInt16" => Self::U16, + "UInt32" => Self::U32, + "UInt64" => Self::U64, + "SInt8" => Self::I8, + "SInt16" => Self::I16, + "SInt32" => Self::I32, + "SInt64" => Self::I64, + "Float32" => Self::F32, + "Float64" => Self::F64, + "Float80" => panic!("can't handle 80 bit MacOS float"), + "Float96" => panic!("can't handle 96 bit 68881 float"), + + "instancetype" => Self::Id { + type_: GenericType { + name: "Self".to_string(), + generics: Vec::new(), + }, + is_const: ty.is_const_qualified(), + lifetime, + nullability, + }, + _ => { + let ty = ty.get_canonical_type(); + match ty.get_kind() { + ObjCObjectPointer => { + let ty = + ty.get_pointee_type().expect("pointer type to have pointee"); + let type_ = GenericType::parse_objc_pointer(ty); + if !type_.generics.is_empty() { + panic!("typedef generics not empty"); + } + + Self::Id { + type_: GenericType { + name: typedef_name, + generics: Vec::new(), + }, + is_const: ty.is_const_qualified(), + lifetime, + nullability, + } + } + _ => Self::TypeDef { name: typedef_name }, + } + } + } + } + Elaborated => { + let ty = ty.get_elaborated_type().expect("elaborated"); + match ty.get_kind() { + TypeKind::Record => { + let name = ty + .get_display_name() + .trim_start_matches("struct ") + .to_string(); + Self::Struct { name } + } + TypeKind::Enum => { + let name = ty + .get_display_name() + .trim_start_matches("enum ") + .to_string(); + Self::Enum { name } + } + _ => panic!("unknown elaborated type {ty:?}"), + } + } + FunctionPrototype => { + let call_conv = ty.get_calling_convention().expect("fn calling convention"); + assert_eq!( + call_conv, + CallingConvention::Cdecl, + "fn calling convention is C" + ); + + let arguments = ty + .get_argument_types() + .expect("fn type to have argument types") + .into_iter() + .map(Ty::parse_fn_argument) + .collect(); + + let result_type = ty.get_result_type().expect("fn type to have result type"); + let result_type = Ty::parse_fn_result(result_type); + + Self::Fn { + is_variadic: ty.is_variadic(), + arguments, + result_type: Box::new(result_type), + } + } + IncompleteArray => { + let is_const = ty.is_const_qualified(); + let ty = ty + .get_element_type() + .expect("incomplete array to have element type"); + let pointee = Self::parse(ty, is_consumed, Nullability::Unspecified, true); + Self::IncompleteArray { + nullability, + is_const, + pointee: Box::new(pointee), + } + } + ConstantArray => { + let element_type = Self::parse( + ty.get_element_type().expect("array to have element type"), + is_consumed, + Nullability::Unspecified, + false, + ); + let num_elements = ty + .get_size() + .expect("constant array to have element length"); + Self::Array { + element_type: Box::new(element_type), + num_elements, + } + } + _ => { + panic!("Unsupported type: {:?}", ty) + } + } + } + + fn visit_lifetime(&self, mut f: impl FnMut(Lifetime)) { + match self { + Self::Id { lifetime, .. } => f(*lifetime), + Self::Pointer { pointee, .. } => pointee.visit_lifetime(f), + Self::IncompleteArray { pointee, .. } => pointee.visit_lifetime(f), + Self::Array { element_type, .. } => element_type.visit_lifetime(f), + _ => {} + } + } +} + +/// This is sound to output in (almost, c_void is not a valid return type) any +/// context. `Ty` is then used to change these types into something nicer when +/// requires. +impl fmt::Display for RustType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use RustType::*; + match self { + // Primitives + Void => write!(f, "c_void"), + C99Bool => panic!("C99's bool is unsupported"), // write!(f, "bool") + Char => write!(f, "c_char"), + SChar => write!(f, "c_schar"), + UChar => write!(f, "c_uchar"), + Short => write!(f, "c_short"), + UShort => write!(f, "c_ushort"), + Int => write!(f, "c_int"), + UInt => write!(f, "c_uint"), + Long => write!(f, "c_long"), + ULong => write!(f, "c_ulong"), + LongLong => write!(f, "c_longlong"), + ULongLong => write!(f, "c_ulonglong"), + Float => write!(f, "c_float"), + Double => write!(f, "c_double"), + F32 => write!(f, "f32"), + F64 => write!(f, "f64"), + I8 => write!(f, "i8"), + U8 => write!(f, "u8"), + I16 => write!(f, "i16"), + U16 => write!(f, "u16"), + I32 => write!(f, "i32"), + U32 => write!(f, "u32"), + I64 => write!(f, "i64"), + U64 => write!(f, "u64"), + + // Objective-C + Id { + type_: ty, + is_const, + // Ignore + lifetime: _, + nullability, + } => { + if *nullability == Nullability::NonNull { + write!(f, "NonNull<{ty}>") + } else if *is_const { + write!(f, "*const {ty}") + } else { + write!(f, "*mut {ty}") + } + } + Class { nullability } => { + if *nullability == Nullability::NonNull { + write!(f, "NonNull") + } else { + write!(f, "*const Class") + } + } + Sel { nullability } => { + if *nullability == Nullability::NonNull { + write!(f, "Sel") + } else { + write!(f, "OptionSel") + } + } + ObjcBool => write!(f, "Bool"), + + // Others + Pointer { + nullability, + is_const, + pointee, + } => match &**pointee { + Self::Fn { + is_variadic, + arguments, + result_type, + } => { + if *nullability != Nullability::NonNull { + write!(f, "Option<")?; + } + write!(f, "unsafe extern \"C\" fn(")?; + for arg in arguments { + write!(f, "{arg},")?; + } + if *is_variadic { + write!(f, "...")?; + } + write!(f, "){result_type}")?; + + if *nullability != Nullability::NonNull { + write!(f, ">")?; + } + Ok(()) + } + pointee => { + if *nullability == Nullability::NonNull { + write!(f, "NonNull<{pointee}>") + } else if *is_const { + write!(f, "*const {pointee}") + } else { + write!(f, "*mut {pointee}") + } + } + }, + IncompleteArray { + nullability, + is_const, + pointee, + } => { + if *nullability == Nullability::NonNull { + write!(f, "NonNull<{pointee}>") + } else if *is_const { + write!(f, "*const {pointee}") + } else { + write!(f, "*mut {pointee}") + } + } + Array { + element_type, + num_elements, + } => write!(f, "[{element_type}; {num_elements}]"), + Enum { name } | Struct { name } | TypeDef { name } => write!(f, "{name}"), + Self::Fn { .. } => write!(f, "TodoFunction"), + Block { + arguments, + result_type, + } => { + write!(f, "Block<(")?; + for arg in arguments { + write!(f, "{arg}, ")?; + } + write!(f, "), {result_type}>") + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum TyKind { + InMethodReturn, + InFnDeclReturn, + InMethodReturnWithError, + InStatic, + InTypedef, + InMethodArgument, + InFnDeclArgument, + InStructEnum, + InFnArgument, + InFnReturn, + InBlockArgument, + InBlockReturn, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Ty { + ty: RustType, + kind: TyKind, +} + +impl Ty { + pub const VOID_RESULT: Self = Self { + ty: RustType::Void, + kind: TyKind::InMethodReturn, + }; + + pub fn parse_method_argument(ty: Type<'_>, is_consumed: bool) -> Self { + let ty = RustType::parse(ty, is_consumed, Nullability::Unspecified, false); + + match &ty { + RustType::Pointer { pointee, .. } => pointee.visit_lifetime(|lifetime| { + if lifetime != Lifetime::Autoreleasing && lifetime != Lifetime::Unspecified { + panic!("unexpected lifetime {lifetime:?} in pointer argument {ty:?}"); + } + }), + RustType::IncompleteArray { pointee, .. } => pointee.visit_lifetime(|lifetime| { + if lifetime != Lifetime::Unretained && lifetime != Lifetime::Unspecified { + panic!("unexpected lifetime {lifetime:?} in incomplete array argument {ty:?}"); + } + }), + _ => ty.visit_lifetime(|lifetime| { + if lifetime != Lifetime::Strong && lifetime != Lifetime::Unspecified { + panic!("unexpected lifetime {lifetime:?} in argument {ty:?}"); + } + }), + } + + Self { + ty, + kind: TyKind::InMethodArgument, + } + } + + pub fn parse_method_return(ty: Type<'_>) -> Self { + let ty = RustType::parse(ty, false, Nullability::Unspecified, false); + + ty.visit_lifetime(|lifetime| { + if lifetime != Lifetime::Unspecified { + panic!("unexpected lifetime in return {ty:?}"); + } + }); + + Self { + ty, + kind: TyKind::InMethodReturn, + } + } + + pub fn parse_function_argument(ty: Type<'_>) -> Self { + let mut this = Self::parse_method_argument(ty, false); + this.kind = TyKind::InFnDeclArgument; + this + } + + pub fn parse_function_return(ty: Type<'_>) -> Self { + let mut this = Self::parse_method_return(ty); + this.kind = TyKind::InFnDeclReturn; + this + } + + pub fn parse_typedef(ty: Type<'_>) -> Option { + let mut ty = RustType::parse(ty, false, Nullability::Unspecified, false); + + ty.visit_lifetime(|lifetime| { + if lifetime != Lifetime::Unspecified { + panic!("unexpected lifetime in typedef {ty:?}"); + } + }); + + match &mut ty { + // Handled by Stmt::EnumDecl + RustType::Enum { .. } => None, + // Handled above and in Stmt::StructDecl + // The rest is only `NSZone` + RustType::Struct { name } => { + assert_eq!(name, "_NSZone", "invalid struct in typedef"); + None + } + // Opaque structs + RustType::Pointer { pointee, .. } if matches!(&**pointee, RustType::Struct { .. }) => { + **pointee = RustType::Void; + Some(Self { + ty, + kind: TyKind::InTypedef, + }) + } + RustType::IncompleteArray { .. } => { + unimplemented!("incomplete array in struct") + } + _ => Some(Self { + ty, + kind: TyKind::InTypedef, + }), + } + } + + pub fn parse_property(ty: Type<'_>, default_nullability: Nullability) -> Self { + let ty = RustType::parse(ty, false, default_nullability, false); + + ty.visit_lifetime(|lifetime| { + if lifetime != Lifetime::Unspecified { + panic!("unexpected lifetime in property {ty:?}"); + } + }); + + Self { + ty, + kind: TyKind::InMethodArgument, + } + } + + pub fn parse_property_return(ty: Type<'_>, default_nullability: Nullability) -> Self { + let ty = RustType::parse(ty, false, default_nullability, false); + + ty.visit_lifetime(|lifetime| { + if lifetime != Lifetime::Unspecified { + panic!("unexpected lifetime in property {ty:?}"); + } + }); + + Self { + ty, + kind: TyKind::InMethodReturn, + } + } + + pub fn parse_struct_field(ty: Type<'_>) -> Self { + let ty = RustType::parse(ty, false, Nullability::Unspecified, false); + + ty.visit_lifetime(|lifetime| { + if lifetime != Lifetime::Unspecified { + panic!("unexpected lifetime in struct field {ty:?}"); + } + }); + + Self { + ty, + kind: TyKind::InStructEnum, + } + } + + pub fn parse_enum(ty: Type<'_>) -> Self { + let ty = RustType::parse(ty, false, Nullability::Unspecified, false); + + ty.visit_lifetime(|_lifetime| { + panic!("unexpected lifetime in enum {ty:?}"); + }); + + Self { + ty, + kind: TyKind::InStructEnum, + } + } + + pub fn parse_static(ty: Type<'_>) -> Self { + let ty = RustType::parse(ty, false, Nullability::Unspecified, false); + + ty.visit_lifetime(|lifetime| { + if lifetime != Lifetime::Strong && lifetime != Lifetime::Unspecified { + panic!("unexpected lifetime in var {ty:?}"); + } + }); + + Self { + ty, + kind: TyKind::InStatic, + } + } + + fn parse_fn_argument(ty: Type<'_>) -> Self { + let ty = RustType::parse(ty, false, Nullability::Unspecified, false); + + ty.visit_lifetime(|lifetime| { + if lifetime != Lifetime::Strong { + panic!("unexpected lifetime {lifetime:?} in fn argument {ty:?}"); + } + }); + + Self { + ty, + kind: TyKind::InFnArgument, + } + } + + fn parse_fn_result(ty: Type<'_>) -> Self { + let ty = RustType::parse(ty, false, Nullability::Unspecified, false); + + ty.visit_lifetime(|lifetime| { + if lifetime != Lifetime::Unspecified { + panic!("unexpected lifetime {lifetime:?} in fn result {ty:?}"); + } + }); + + Self { + ty, + kind: TyKind::InFnReturn, + } + } + + fn set_block(&mut self) { + self.kind = match self.kind { + TyKind::InFnArgument => TyKind::InBlockArgument, + TyKind::InFnReturn => TyKind::InBlockReturn, + _ => unreachable!("set block kind"), + } + } +} + +impl Ty { + pub fn argument_is_error_out(&self) -> bool { + if let RustType::Pointer { + nullability, + is_const, + pointee, + } = &self.ty + { + if let RustType::Id { + type_: ty, + is_const: id_is_const, + lifetime, + nullability: id_nullability, + } = &**pointee + { + if ty.name != "NSError" { + return false; + } + assert_eq!( + *nullability, + Nullability::Nullable, + "invalid error nullability {self:?}" + ); + assert!(!is_const, "expected error not const {self:?}"); + + assert!( + ty.generics.is_empty(), + "expected error generics to be empty {self:?}" + ); + assert_eq!( + *id_nullability, + Nullability::Nullable, + "invalid inner error nullability {self:?}" + ); + assert!(!id_is_const, "expected inner error not const {self:?}"); + assert_eq!( + *lifetime, + Lifetime::Unspecified, + "invalid error lifetime {self:?}" + ); + return true; + } + } + false + } + + pub fn is_id(&self) -> bool { + matches!(self.ty, RustType::Id { .. }) + } + + pub fn set_is_alloc(&mut self) { + match &mut self.ty { + RustType::Id { + type_: ty, + lifetime: Lifetime::Unspecified, + is_const: false, + nullability: Nullability::NonNull, + } if ty.name == "Self" && ty.generics.is_empty() => { + ty.name = "Allocated".into(); + ty.generics = vec![GenericType { + name: "Self".into(), + generics: vec![], + }]; + } + _ => panic!("invalid alloc return type {self:?}"), + } + } + + pub fn set_is_error(&mut self) { + assert_eq!(self.kind, TyKind::InMethodReturn); + self.kind = TyKind::InMethodReturnWithError; + } + + /// Related result types + /// https://clang.llvm.org/docs/AutomaticReferenceCounting.html#related-result-types + pub fn fix_related_result_type(&mut self, is_class: bool, selector: &str) { + if let RustType::Id { type_, .. } = &mut self.ty { + if type_.name == "Object" { + assert!(type_.generics.is_empty(), "Object return generics empty"); + if (is_class && MemoryManagement::is_new(&selector)) + || (is_class && MemoryManagement::is_alloc(&selector)) + || (!is_class && MemoryManagement::is_init(&selector)) + || (!is_class && selector == "self") + { + type_.name = "Self".into(); + } + } + } + } +} + +impl fmt::Display for Ty { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.kind { + TyKind::InMethodReturn => { + if let RustType::Void = &self.ty { + // Don't output anything + return Ok(()); + } + + write!(f, " -> ")?; + + match &self.ty { + RustType::Id { + type_: ty, + // Ignore + is_const: _, + // Ignore + lifetime: _, + nullability, + } => { + if *nullability == Nullability::NonNull { + write!(f, "Id<{ty}, Shared>") + } else { + write!(f, "Option>") + } + } + RustType::Class { nullability } => { + if *nullability == Nullability::NonNull { + write!(f, "&'static Class") + } else { + write!(f, "Option<&'static Class>") + } + } + RustType::ObjcBool => write!(f, "bool"), + ty => write!(f, "{ty}"), + } + } + TyKind::InMethodReturnWithError => match &self.ty { + RustType::Id { + type_: ty, + lifetime: Lifetime::Unspecified, + is_const: false, + nullability: Nullability::Nullable, + } => { + // NULL -> error + write!(f, " -> Result, Id>") + } + RustType::ObjcBool => { + // NO -> error + write!(f, " -> Result<(), Id>") + } + _ => panic!("unknown error result type {self:?}"), + }, + TyKind::InStatic => match &self.ty { + RustType::Id { + type_: ty, + is_const: false, + lifetime: Lifetime::Strong | Lifetime::Unspecified, + nullability, + } => { + if *nullability == Nullability::NonNull { + write!(f, "&'static {ty}") + } else { + write!(f, "Option<&'static {ty}>") + } + } + ty @ RustType::Id { .. } => panic!("invalid static {ty:?}"), + ty => write!(f, "{ty}"), + }, + TyKind::InTypedef => match &self.ty { + // When we encounter a typedef declaration like this: + // typedef NSString* NSAbc; + // + // We parse it as one of: + // type NSAbc = NSString; + // struct NSAbc(NSString); + // + // Instead of: + // type NSAbc = *const NSString; + // + // Because that means we can use ordinary Id elsewhere. + RustType::Id { + type_: ty, + is_const: _, + lifetime: _, + nullability, + } => { + match &*ty.name { + "NSString" => {} + "NSUnit" => {} // TODO: Handle this differently + "TodoProtocols" => {} // TODO + _ => panic!("typedef declaration was not NSString: {ty:?}"), + } + + if !ty.generics.is_empty() { + panic!("typedef declaration generics not empty"); + } + + assert_ne!(*nullability, Nullability::NonNull); + write!(f, "{ty}") + } + ty => write!(f, "{ty}"), + }, + TyKind::InMethodArgument | TyKind::InFnDeclArgument => match &self.ty { + RustType::Id { + type_: ty, + is_const: false, + lifetime: Lifetime::Unspecified | Lifetime::Strong, + nullability, + } => { + if *nullability == Nullability::NonNull { + write!(f, "&{ty}") + } else { + write!(f, "Option<&{ty}>") + } + } + RustType::Class { nullability } => { + if *nullability == Nullability::NonNull { + write!(f, "&Class") + } else { + write!(f, "Option<&Class>") + } + } + RustType::ObjcBool if self.kind == TyKind::InMethodArgument => write!(f, "bool"), + ty @ RustType::Pointer { + nullability, + is_const: false, + pointee, + } => match &**pointee { + // TODO: Re-enable once we can support it + // RustType::Id { + // type_: ty, + // is_const: false, + // lifetime: Lifetime::Autoreleasing, + // nullability: inner_nullability, + // } if self.kind == TyKind::InMethodArgument => { + // let tokens = if *inner_nullability == Nullability::NonNull { + // format!("Id<{ty}, Shared>") + // } else { + // format!("Option>") + // }; + // if *nullability == Nullability::NonNull { + // write!(f, "&mut {tokens}") + // } else { + // write!(f, "Option<&mut {tokens}>") + // } + // } + // RustType::Id { .. } => { + // unreachable!("there should be no id with other values: {self:?}") + // } + block @ RustType::Block { .. } => { + if *nullability == Nullability::NonNull { + write!(f, "&{block}") + } else { + write!(f, "Option<&{block}>") + } + } + _ => write!(f, "{ty}"), + }, + ty => write!(f, "{ty}"), + }, + TyKind::InStructEnum => write!(f, "{}", self.ty), + TyKind::InFnArgument | TyKind::InBlockArgument => write!(f, "{}", self.ty), + TyKind::InFnDeclReturn | TyKind::InFnReturn => { + if let RustType::Void = &self.ty { + // Don't output anything + return Ok(()); + } + + write!(f, " -> {}", self.ty) + } + TyKind::InBlockReturn => match &self.ty { + RustType::Void => write!(f, "()"), + ty => write!(f, "{ty}"), + }, + } + } +} diff --git a/crates/header-translator/src/stmt.rs b/crates/header-translator/src/stmt.rs new file mode 100644 index 000000000..2f0b6f3cd --- /dev/null +++ b/crates/header-translator/src/stmt.rs @@ -0,0 +1,898 @@ +use std::collections::HashSet; +use std::fmt; + +use clang::{Entity, EntityKind, EntityVisitResult}; + +use crate::availability::Availability; +use crate::config::{ClassData, Config}; +use crate::expr::Expr; +use crate::method::{handle_reserved, Method}; +use crate::property::Property; +use crate::rust_type::Ty; +use crate::unexposed_macro::UnexposedMacro; + +#[derive(Debug, Clone)] +pub enum MethodOrProperty { + Method(Method), + Property(Property), +} + +impl fmt::Display for MethodOrProperty { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Method(method) => write!(f, "{method}"), + Self::Property(property) => write!(f, "{property}"), + } + } +} + +/// Takes one of: +/// - `EntityKind::ObjCInterfaceDecl` +/// - `EntityKind::ObjCProtocolDecl` +/// - `EntityKind::ObjCCategoryDecl` +fn parse_objc_decl( + entity: &Entity<'_>, + mut superclass: Option<&mut Option>>, + mut generics: Option<&mut Vec>, + data: Option<&ClassData>, +) -> (Vec, Vec) { + let mut protocols = Vec::new(); + let mut methods = Vec::new(); + + // Track seen properties, so that when methods are autogenerated by the + // compiler from them, we can skip them + let mut properties = HashSet::new(); + + entity.visit_children(|entity, _parent| { + match entity.get_kind() { + EntityKind::ObjCExplicitProtocolImpl if generics.is_none() && superclass.is_none() => { + // TODO NS_PROTOCOL_REQUIRES_EXPLICIT_IMPLEMENTATION + } + EntityKind::ObjCIvarDecl if superclass.is_some() => { + // Explicitly ignored + } + EntityKind::ObjCSuperClassRef => { + if let Some(superclass) = &mut superclass { + **superclass = Some(Some(entity.get_name().expect("superclass name"))); + } else { + panic!("unsupported superclass {entity:?}"); + } + } + EntityKind::ObjCRootClass => { + if let Some(superclass) = &mut superclass { + // TODO: Maybe just skip root classes entirely? + **superclass = Some(None); + } else { + panic!("unsupported root class {entity:?}"); + } + } + EntityKind::ObjCClassRef if generics.is_some() => { + // println!("ObjCClassRef: {:?}", entity.get_display_name()); + } + EntityKind::TemplateTypeParameter => { + if let Some(generics) = &mut generics { + // TODO: Generics with bounds (like NSMeasurement) + // let ty = entity.get_type().expect("template type"); + let name = entity.get_display_name().expect("template name"); + generics.push(name); + } else { + panic!("unsupported generics {entity:?}"); + } + } + EntityKind::ObjCProtocolRef => { + protocols.push(entity.get_name().expect("protocolref to have name")); + } + EntityKind::ObjCInstanceMethodDecl | EntityKind::ObjCClassMethodDecl => { + let partial = Method::partial(entity); + + if !properties.remove(&(partial.is_class, partial.fn_name.clone())) { + let data = data + .map(|data| { + data.methods + .get(&partial.fn_name) + .copied() + .unwrap_or_default() + }) + .unwrap_or_default(); + if let Some(method) = partial.parse(data) { + methods.push(MethodOrProperty::Method(method)); + } + } + } + EntityKind::ObjCPropertyDecl => { + let partial = Property::partial(entity); + let data = data + .map(|data| { + data.properties + .get(&partial.name) + .copied() + .unwrap_or_default() + }) + .unwrap_or_default(); + + assert!( + properties.insert((partial.is_class, partial.getter_name.clone())), + "already exisiting property" + ); + if let Some(setter_name) = partial.setter_name.clone() { + assert!( + properties.insert((partial.is_class, setter_name)), + "already exisiting property" + ); + } + if let Some(property) = partial.parse(data) { + methods.push(MethodOrProperty::Property(property)); + } + } + EntityKind::VisibilityAttr => { + // Already exposed as entity.get_visibility() + } + EntityKind::TypeRef if superclass.is_some() => { + // TODO + } + EntityKind::ObjCException if superclass.is_some() => { + // Maybe useful for knowing when to implement `Error` for the type + } + EntityKind::UnexposedAttr => { + if let Some(macro_) = UnexposedMacro::parse(&entity) { + println!("objc decl {entity:?}: {macro_:?}"); + } + } + _ => panic!("unknown objc decl child {entity:?}"), + }; + EntityVisitResult::Continue + }); + + if !properties.is_empty() { + if properties == HashSet::from([(false, "setDisplayName".to_owned())]) { + // TODO + } else { + panic!("did not properly add methods to properties:\n{methods:?}\n{properties:?}"); + } + } + + (protocols, methods) +} + +#[derive(Debug, Clone)] +pub enum Stmt { + /// @interface name: superclass + ClassDecl { + name: String, + availability: Availability, + // TODO: Generics + superclass: Option, + generics: Vec, + protocols: Vec, + methods: Vec, + }, + /// @interface class_name (name) + CategoryDecl { + class_name: String, + availability: Availability, + /// Some categories don't have a name. Example: NSClipView + name: Option, + generics: Vec, + /// I don't quite know what this means? + protocols: Vec, + methods: Vec, + }, + /// @protocol name + ProtocolDecl { + name: String, + availability: Availability, + protocols: Vec, + methods: Vec, + }, + /// struct name { + /// fields* + /// }; + /// + /// typedef struct { + /// fields* + /// } name; + /// + /// typedef struct _name { + /// fields* + /// } name; + StructDecl { + name: String, + boxable: bool, + fields: Vec<(String, Ty)>, + }, + /// typedef NS_OPTIONS(type, name) { + /// variants* + /// }; + /// + /// typedef NS_ENUM(type, name) { + /// variants* + /// }; + /// + /// enum name { + /// variants* + /// }; + /// + /// enum { + /// variants* + /// }; + EnumDecl { + name: Option, + ty: Ty, + kind: Option, + variants: Vec<(String, Expr)>, + }, + /// static const ty name = expr; + /// extern const ty name; + VarDecl { + name: String, + ty: Ty, + value: Option, + }, + /// extern ret name(args*); + /// + /// static inline ret name(args*) { + /// body + /// } + FnDecl { + name: String, + arguments: Vec<(String, Ty)>, + result_type: Ty, + // Some -> inline function. + body: Option<()>, + }, + /// typedef Type TypedefName; + AliasDecl { name: String, ty: Ty }, +} + +fn parse_struct(entity: &Entity<'_>, name: String) -> Stmt { + let mut boxable = false; + let mut fields = Vec::new(); + + entity.visit_children(|entity, _parent| { + match entity.get_kind() { + EntityKind::UnexposedAttr => { + if let Some(macro_) = UnexposedMacro::parse(&entity) { + panic!("unexpected attribute: {macro_:?}"); + } + } + EntityKind::FieldDecl => { + let name = entity.get_name().expect("struct field name"); + let ty = entity.get_type().expect("struct field type"); + let ty = Ty::parse_struct_field(ty); + + if entity.is_bit_field() { + println!("[UNSOUND] struct bitfield {name}: {entity:?}"); + } + + fields.push((name, ty)) + } + EntityKind::ObjCBoxable => { + boxable = true; + } + _ => panic!("unknown struct field {entity:?}"), + } + EntityVisitResult::Continue + }); + + Stmt::StructDecl { + name, + boxable, + fields, + } +} + +impl Stmt { + pub fn parse(entity: &Entity<'_>, config: &Config) -> Option { + match entity.get_kind() { + // These are inconsequential for us, since we resolve imports differently + EntityKind::ObjCClassRef | EntityKind::ObjCProtocolRef => None, + EntityKind::ObjCInterfaceDecl => { + // entity.get_mangled_objc_names() + let name = entity.get_name().expect("class name"); + let class_data = config.class_data.get(&name); + + if class_data.map(|data| data.skipped).unwrap_or_default() { + return None; + } + + let availability = Availability::parse( + entity + .get_platform_availability() + .expect("class availability"), + ); + // println!("Availability: {:?}", entity.get_platform_availability()); + let mut superclass = None; + let mut generics = Vec::new(); + + let (protocols, methods) = parse_objc_decl( + &entity, + Some(&mut superclass), + Some(&mut generics), + class_data, + ); + + if let Some(new_name) = + class_data.and_then(|data| data.new_superclass_name.as_ref()) + { + superclass = Some(Some(new_name.clone())); + } + + let superclass = superclass.expect("no superclass found"); + + Some(Self::ClassDecl { + name, + availability, + superclass, + generics, + protocols, + methods, + }) + } + EntityKind::ObjCCategoryDecl => { + let name = entity.get_name(); + let availability = Availability::parse( + entity + .get_platform_availability() + .expect("category availability"), + ); + + let mut class_name = None; + entity.visit_children(|entity, _parent| { + if entity.get_kind() == EntityKind::ObjCClassRef { + if class_name.is_some() { + panic!("could not find unique category class") + } + class_name = Some(entity.get_name().expect("class name")); + EntityVisitResult::Break + } else { + EntityVisitResult::Continue + } + }); + let class_name = class_name.expect("could not find category class"); + let class_data = config.class_data.get(&class_name); + + if class_data.map(|data| data.skipped).unwrap_or_default() { + return None; + } + + let mut generics = Vec::new(); + + let (protocols, methods) = + parse_objc_decl(&entity, None, Some(&mut generics), class_data); + + Some(Self::CategoryDecl { + class_name, + availability, + name, + generics, + protocols, + methods, + }) + } + EntityKind::ObjCProtocolDecl => { + let name = entity.get_name().expect("protocol name"); + let protocol_data = config.protocol_data.get(&name); + + if protocol_data.map(|data| data.skipped).unwrap_or_default() { + return None; + } + + let availability = Availability::parse( + entity + .get_platform_availability() + .expect("protocol availability"), + ); + + let (protocols, methods) = parse_objc_decl(&entity, None, None, protocol_data); + + Some(Self::ProtocolDecl { + name, + availability, + protocols, + methods, + }) + } + EntityKind::TypedefDecl => { + let name = entity.get_name().expect("typedef name"); + let mut struct_ = None; + let mut skip_struct = false; + + entity.visit_children(|entity, _parent| { + match entity.get_kind() { + // TODO: Parse NS_TYPED_EXTENSIBLE_ENUM vs. NS_TYPED_ENUM + EntityKind::UnexposedAttr => { + if let Some(macro_) = UnexposedMacro::parse(&entity) { + panic!("unexpected attribute: {macro_:?}"); + } + } + EntityKind::StructDecl => { + if config + .struct_data + .get(&name) + .map(|data| data.skipped) + .unwrap_or_default() + { + skip_struct = true; + return EntityVisitResult::Continue; + } + + let struct_name = entity.get_name(); + if struct_name + .map(|name| name.starts_with('_')) + .unwrap_or(true) + { + // If this struct doesn't have a name, or the + // name is private, let's parse it with the + // typedef name. + struct_ = Some(parse_struct(&entity, name.clone())) + } else { + skip_struct = true; + } + } + EntityKind::ObjCClassRef + | EntityKind::ObjCProtocolRef + | EntityKind::TypeRef + | EntityKind::ParmDecl => {} + _ => panic!("unknown typedef child in {name}: {entity:?}"), + }; + EntityVisitResult::Continue + }); + + if let Some(struct_) = struct_ { + return Some(struct_); + } + + if skip_struct { + return None; + } + + if config + .typedef_data + .get(&name) + .map(|data| data.skipped) + .unwrap_or_default() + { + return None; + } + + let ty = entity + .get_typedef_underlying_type() + .expect("typedef underlying type"); + Ty::parse_typedef(ty).map(|ty| Self::AliasDecl { name, ty }) + } + EntityKind::StructDecl => { + if let Some(name) = entity.get_name() { + if config + .struct_data + .get(&name) + .map(|data| data.skipped) + .unwrap_or_default() + { + return None; + } + if !name.starts_with('_') { + return Some(parse_struct(entity, name)); + } + } + None + } + EntityKind::EnumDecl => { + // Enum declarations show up twice for some reason, but + // luckily this flag is set on the least descriptive entity. + if !entity.is_definition() { + return None; + } + + let name = entity.get_name(); + + let data = config + .enum_data + .get(name.as_deref().unwrap_or("anonymous")) + .cloned() + .unwrap_or_default(); + if data.skipped { + return None; + } + + let ty = entity.get_enum_underlying_type().expect("enum type"); + let is_signed = ty.is_signed_integer(); + let ty = Ty::parse_enum(ty); + let mut kind = None; + let mut variants = Vec::new(); + + entity.visit_children(|entity, _parent| { + match entity.get_kind() { + EntityKind::EnumConstantDecl => { + let name = entity.get_name().expect("enum constant name"); + + if data + .constants + .get(&name) + .map(|data| data.skipped) + .unwrap_or_default() + { + return EntityVisitResult::Continue; + } + + let val = Expr::from_val( + entity + .get_enum_constant_value() + .expect("enum constant value"), + is_signed, + ); + let expr = if data.use_value { + val + } else { + Expr::parse_enum_constant(&entity).unwrap_or(val) + }; + variants.push((name, expr)); + } + EntityKind::UnexposedAttr => { + if let Some(macro_) = UnexposedMacro::parse(&entity) { + if let Some(kind) = &kind { + assert_eq!( + kind, ¯o_, + "got differing enum kinds in {name:?}" + ); + } else { + kind = Some(macro_); + } + } + } + EntityKind::FlagEnum => { + let macro_ = UnexposedMacro::Options; + if let Some(kind) = &kind { + assert_eq!(kind, ¯o_, "got differing enum kinds in {name:?}"); + } else { + kind = Some(macro_); + } + } + _ => { + panic!("unknown enum child {entity:?} in {name:?}"); + } + } + EntityVisitResult::Continue + }); + + Some(Self::EnumDecl { + name, + ty, + kind, + variants, + }) + } + EntityKind::VarDecl => { + let name = entity.get_name().expect("var decl name"); + + if config + .statics + .get(&name) + .map(|data| data.skipped) + .unwrap_or_default() + { + return None; + } + + let ty = entity.get_type().expect("var type"); + let ty = Ty::parse_static(ty); + let mut value = None; + + entity.visit_children(|entity, _parent| { + match entity.get_kind() { + EntityKind::UnexposedAttr => { + if let Some(macro_) = UnexposedMacro::parse(&entity) { + panic!("unexpected attribute: {macro_:?}"); + } + } + EntityKind::VisibilityAttr => {} + EntityKind::ObjCClassRef => {} + EntityKind::TypeRef => {} + _ if entity.is_expression() => { + if value.is_none() { + value = Some(Expr::parse_var(&entity)); + } else { + panic!("got variable value twice") + } + } + _ => panic!("unknown vardecl child in {name}: {entity:?}"), + }; + EntityVisitResult::Continue + }); + + let value = match value { + Some(Some(expr)) => Some(expr), + Some(None) => { + println!("skipped static {name}"); + return None; + } + None => None, + }; + + Some(Self::VarDecl { name, ty, value }) + } + EntityKind::FunctionDecl => { + let name = entity.get_name().expect("function name"); + + if config + .fns + .get(&name) + .map(|data| data.skipped) + .unwrap_or_default() + { + return None; + } + + if entity.is_variadic() { + println!("can't handle variadic function {name}"); + return None; + } + + let result_type = entity.get_result_type().expect("function result type"); + let result_type = Ty::parse_function_return(result_type); + let mut arguments = Vec::new(); + + assert!( + !entity.is_static_method(), + "unexpected static method {name}" + ); + + entity.visit_children(|entity, _parent| { + match entity.get_kind() { + EntityKind::UnexposedAttr => { + if let Some(macro_) = UnexposedMacro::parse(&entity) { + panic!("unexpected function attribute: {macro_:?}"); + } + } + EntityKind::ObjCClassRef | EntityKind::TypeRef => {} + EntityKind::ParmDecl => { + // Could also be retrieved via. `get_arguments` + let name = entity.get_name().unwrap_or_else(|| "_".into()); + let ty = entity.get_type().expect("function argument type"); + let ty = Ty::parse_function_argument(ty); + arguments.push((name, ty)) + } + _ => panic!("unknown function child in {name}: {entity:?}"), + }; + EntityVisitResult::Continue + }); + + let body = if entity.is_inline_function() { + Some(()) + } else { + None + }; + + Some(Self::FnDecl { + name, + arguments, + result_type, + body, + }) + } + EntityKind::UnionDecl => { + // println!( + // "union: {:?}, {:?}, {:#?}, {:#?}", + // entity.get_display_name(), + // entity.get_name(), + // entity.has_attributes(), + // entity.get_children(), + // ); + None + } + _ => { + panic!("Unknown: {:?}", entity) + } + } + } +} + +impl fmt::Display for Stmt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ClassDecl { + name, + availability: _, + superclass, + generics, + protocols: _, + methods, + } => { + let struct_generic = if generics.is_empty() { + name.clone() + } else { + format!( + "{name}<{}: Message = Object>", + generics.join(": Message = Object,") + ) + }; + + let generic_params = if generics.is_empty() { + String::new() + } else { + format!("<{}: Message>", generics.join(": Message,")) + }; + + let ty = if generics.is_empty() { + name.clone() + } else { + format!("{name}<{}>", generics.join(",")) + }; + + let superclass_name = superclass.as_deref().unwrap_or("Object"); + + // TODO: Use ty.get_objc_protocol_declarations() + + let macro_name = if generics.is_empty() { + "extern_class" + } else { + "__inner_extern_class" + }; + + writeln!(f, "{macro_name}!(")?; + writeln!(f, " #[derive(Debug)]")?; + write!(f, " pub struct {struct_generic}")?; + if generics.is_empty() { + writeln!(f, ";")?; + } else { + writeln!(f, " {{")?; + for (i, generic) in generics.iter().enumerate() { + // Invariant over the generic (for now) + writeln!(f, "_inner{i}: PhantomData<*mut {generic}>,")?; + } + writeln!(f, "}}")?; + } + writeln!(f, "")?; + writeln!(f, " unsafe impl{generic_params} ClassType for {ty} {{")?; + writeln!(f, " type Super = {superclass_name};")?; + writeln!(f, " }}")?; + writeln!(f, ");")?; + writeln!(f, "")?; + writeln!(f, "extern_methods!(")?; + writeln!(f, " unsafe impl{generic_params} {ty} {{")?; + for method in methods { + writeln!(f, "{method}")?; + } + writeln!(f, " }}")?; + writeln!(f, ");")?; + } + Self::CategoryDecl { + class_name, + availability: _, + name, + generics, + protocols: _, + methods, + } => { + let generic_params = if generics.is_empty() { + String::new() + } else { + format!("<{}: Message>", generics.join(": Message,")) + }; + + let ty = if generics.is_empty() { + class_name.clone() + } else { + format!("{class_name}<{}>", generics.join(",")) + }; + + writeln!(f, "extern_methods!(")?; + if let Some(name) = name { + writeln!(f, " /// {name}")?; + } + writeln!(f, " unsafe impl{generic_params} {ty} {{")?; + for method in methods { + writeln!(f, "{method}")?; + } + writeln!(f, " }}")?; + writeln!(f, ");")?; + } + Self::ProtocolDecl { + name, + availability: _, + protocols: _, + methods, + } => { + writeln!(f, "extern_protocol!(")?; + writeln!(f, " pub struct {name};")?; + writeln!(f, "")?; + writeln!(f, " unsafe impl {name} {{")?; + for method in methods { + writeln!(f, "{method}")?; + } + writeln!(f, " }}")?; + writeln!(f, ");")?; + } + Self::StructDecl { + name, + boxable: _, + fields, + } => { + writeln!(f, "extern_struct!(")?; + writeln!(f, " pub struct {name} {{")?; + for (name, ty) in fields { + write!(f, " ")?; + if !name.starts_with('_') { + write!(f, "pub ")?; + } + writeln!(f, "{name}: {ty},")?; + } + writeln!(f, " }}")?; + writeln!(f, ");")?; + } + Self::EnumDecl { + name, + ty, + kind, + variants, + } => { + let macro_name = match kind { + None => "extern_enum", + Some(UnexposedMacro::Enum) => "ns_enum", + Some(UnexposedMacro::Options) => "ns_options", + Some(UnexposedMacro::ClosedEnum) => "ns_closed_enum", + Some(UnexposedMacro::ErrorEnum) => "ns_error_enum", + }; + writeln!(f, "{}!(", macro_name)?; + writeln!(f, " #[underlying({ty})]")?; + write!(f, " pub enum ",)?; + if let Some(name) = name { + write!(f, "{name} ")?; + } + writeln!(f, "{{")?; + for (name, expr) in variants { + writeln!(f, " {name} = {expr},")?; + } + writeln!(f, " }}")?; + writeln!(f, ");")?; + } + Self::VarDecl { + name, + ty, + value: None, + } => { + writeln!(f, "extern_static!({name}: {ty});")?; + } + Self::VarDecl { + name, + ty, + value: Some(expr), + } => { + writeln!(f, "extern_static!({name}: {ty} = {expr});")?; + } + Self::FnDecl { + name, + arguments, + result_type, + body: None, + } => { + writeln!(f, "extern_fn!(")?; + write!(f, " pub unsafe fn {name}(")?; + for (param, arg_ty) in arguments { + write!(f, "{}: {arg_ty},", handle_reserved(¶m))?; + } + writeln!(f, "){result_type};")?; + writeln!(f, ");")?; + } + Self::FnDecl { + name, + arguments, + result_type, + body: Some(_body), + } => { + writeln!(f, "inline_fn!(")?; + write!(f, " pub unsafe fn {name}(")?; + for (param, arg_ty) in arguments { + write!(f, "{}: {arg_ty},", handle_reserved(¶m))?; + } + writeln!(f, "){result_type} {{")?; + writeln!(f, " todo!()")?; + writeln!(f, " }}")?; + writeln!(f, ");")?; + } + Self::AliasDecl { name, ty } => { + writeln!(f, "pub type {name} = {ty};")?; + } + }; + Ok(()) + } +} diff --git a/crates/header-translator/src/unexposed_macro.rs b/crates/header-translator/src/unexposed_macro.rs new file mode 100644 index 000000000..54aabfbee --- /dev/null +++ b/crates/header-translator/src/unexposed_macro.rs @@ -0,0 +1,56 @@ +use clang::{Entity, EntityKind}; + +/// Parts of `EntityKind::UnexposedAttr` that we can easily parse. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum UnexposedMacro { + Enum, + Options, + ClosedEnum, + ErrorEnum, +} + +impl UnexposedMacro { + fn from_name(s: &str) -> Option { + match s { + "NS_ENUM" => Some(Self::Enum), + "NS_OPTIONS" => Some(Self::Options), + "NS_CLOSED_ENUM" => Some(Self::ClosedEnum), + "NS_ERROR_ENUM" => Some(Self::ErrorEnum), + // TODO + "NS_FORMAT_FUNCTION" => None, + "NS_FORMAT_ARGUMENT" => None, + // Uninteresting, their data is already exposed elsewhere + "API_AVAILABLE" + | "API_UNAVAILABLE" + | "API_DEPRECATED" + | "API_DEPRECATED_WITH_REPLACEMENT" + | "NS_SWIFT_UNAVAILABLE" + | "NS_SWIFT_NAME" + | "NS_CLASS_AVAILABLE_MAC" + | "NS_AVAILABLE" + | "NS_OPENGL_DEPRECATED" + | "NS_OPENGL_CLASS_DEPRECATED" + | "NS_OPENGL_ENUM_DEPRECATED" => None, + name => panic!("unknown unexposed macro name {name}"), + } + } + + pub fn parse(entity: &Entity<'_>) -> Option { + let location = entity.get_location().expect("unexposed attr location"); + if let Some(parsed) = location.get_entity() { + match parsed.get_kind() { + EntityKind::MacroExpansion => { + let macro_name = parsed.get_name().expect("macro name"); + Self::from_name(¯o_name) + } + // Some macros can't be found using this method, + // for example NS_NOESCAPE. + _ => None, + } + } else { + // File-global macros like APPKIT_API_UNAVAILABLE_BEGIN_MACCATALYST + // are not findable with this approach + None + } + } +} diff --git a/crates/icrate/src/AppKit/translation-config.toml b/crates/icrate/src/AppKit/translation-config.toml new file mode 100644 index 000000000..d457a9139 --- /dev/null +++ b/crates/icrate/src/AppKit/translation-config.toml @@ -0,0 +1,177 @@ +imports = ["AppKit", "CoreData", "Foundation"] + +# These return `oneway void`, which is a bit tricky to handle. +[class.NSPasteboard.methods.releaseGlobally] +skipped = true +[class.NSView.methods.releaseGState] +skipped = true + +# Works weirdly since it's defined both as a property, and as a method. +[class.NSDocument.methods.setDisplayName] +skipped = true + +# Typedef that uses a generic from a class +[typedef.NSCollectionViewDiffableDataSourceItemProvider] +skipped = true +[class.NSCollectionViewDiffableDataSource.methods.initWithCollectionView_itemProvider] +skipped = true + +# Both protocols and classes +[protocol.NSTextAttachmentCell] +skipped = true +[protocol.NSAccessibilityElement] +skipped = true + +# Both instance and class methods +[class.NSCursor.methods.pop] +skipped = true +[class.NSDrawer.methods.open] +skipped = true +[class.NSDrawer.methods.close] +skipped = true +[class.NSEvent.properties.modifierFlags] +skipped = true +[class.NSFormCell.methods.titleWidth] +skipped = true +[class.NSGestureRecognizer.properties.state] +skipped = true +[class.NSGraphicsContext.methods.saveGraphicsState] +skipped = true +[class.NSGraphicsContext.methods.restoreGraphicsState] +skipped = true +[class.NSSlider.properties.vertical] +skipped = true +[class.NSSliderCell.methods.drawKnob] +skipped = true +[class.NSSliderCell.properties.vertical] +skipped = true +[class.NSWorkspace.methods.noteFileSystemChanged] +skipped = true +[class.NSBundle.methods.loadNibFile_externalNameTable_withZone] +skipped = true + +# Uses stuff from different frameworks / system libraries +[class.NSAnimationContext.properties.timingFunction] +skipped = true +[class.NSBezierPath.methods.appendBezierPathWithCGGlyph_inFont] +skipped = true +[class.NSBezierPath.methods.appendBezierPathWithCGGlyphs_count_inFont] +skipped = true +[class.NSBitmapImageRep.methods.initWithCGImage] +skipped = true +[class.NSBitmapImageRep.methods.initWithCIImage] +skipped = true +[class.NSBitmapImageRep.properties.CGImage] +skipped = true +[class.NSColor.properties.CGColor] +skipped = true +[class.NSColor.methods.colorWithCGColor] +skipped = true +[class.NSColor.methods.colorWithCIColor] +skipped = true +[class.NSColorSpace.methods.initWithCGColorSpace] +skipped = true +[class.NSColorSpace.properties.CGColorSpace] +skipped = true +[class.NSCIImageRep] +skipped = true +[class.NSEvent.properties.CGEvent] +skipped = true +[class.NSEvent.methods.eventWithCGEvent] +skipped = true +[class.NSFont.methods] +boundingRectForCGGlyph = { skipped = true } +advancementForCGGlyph = { skipped = true } +getBoundingRects_forCGGlyphs_count = { skipped = true } +getAdvancements_forCGGlyphs_count = { skipped = true } +[class.NSGlyphInfo.methods.glyphInfoWithCGGlyph_forFont_baseString] +skipped = true +[class.NSGlyphInfo.properties.glyphID] +skipped = true +[class.NSGraphicsContext.methods.graphicsContextWithCGContext_flipped] +skipped = true +[class.NSGraphicsContext.properties.CGContext] +skipped = true +[class.NSGraphicsContext.properties.CIContext] +skipped = true +[class.NSImage.methods] +initWithCGImage_size = { skipped = true } +CGImageForProposedRect_context_hints = { skipped = true } +initWithIconRef = { skipped = true } +[class.NSImageRep.methods.CGImageForProposedRect_context_hints] +skipped = true +[class.NSItemProvider.methods.registerCloudKitShareWithPreparationHandler] +skipped = true +[class.NSItemProvider.methods.registerCloudKitShare_container] +skipped = true +[class.NSLayoutManager.methods] +setGlyphs_properties_characterIndexes_font_forGlyphRange = { skipped = true } +CGGlyphAtIndex_isValidIndex = { skipped = true } +CGGlyphAtIndex = { skipped = true } +getGlyphsInRange_glyphs_properties_characterIndexes_bidiLevels = { skipped = true } +glyphIndexForPoint_inTextContainer_fractionOfDistanceThroughGlyph = { skipped = true } +showCGGlyphs_positions_count_font_textMatrix_attributes_inContext = { skipped = true } +showCGGlyphs_positions_count_font_matrix_attributes_inContext = { skipped = true } +[class.NSLayoutManagerDelegate.methods.layoutManager_shouldGenerateGlyphs_properties_characterIndexes_font_forGlyphRange] +skipped = true +[class.NSMovie.methods.initWithMovie] +skipped = true +[class.NSMovie.methods.QTMovie] +skipped = true +[class.NSOpenGLContext] +skipped = true +[class.NSOpenGLLayer] +skipped = true +[class.NSOpenGLPixelFormat] +skipped = true +[class.NSOpenGLPixelBuffer] +skipped = true +[class.NSOpenGLView] +skipped = true +[fn.NSOpenGLSetOption] +skipped = true +[fn.NSOpenGLGetOption] +skipped = true +[fn.NSOpenGLGetVersion] +skipped = true +[class.NSTextLayoutFragment.methods.drawAtPoint_inContext] +skipped = true +[class.NSTextLineFragment.methods.drawAtPoint_inContext] +skipped = true +[class.NSTextView.methods.quickLookPreviewableItemsInRanges] +skipped = true +[class.NSRunningApplication.properties.processIdentifier] +skipped = true +[class.NSRunningApplication.methods.runningApplicationWithProcessIdentifier] +skipped = true +[class.NSSavePanel.properties.allowedContentTypes] +skipped = true +[class.NSView.properties] +layer = { skipped = true } +backgroundFilters = { skipped = true } +compositingFilter = { skipped = true } +contentFilters = { skipped = true } +[class.NSView.methods] +makeBackingLayer = { skipped = true } +[class.NSObject.methods] +layer_shouldInheritContentsScale_fromWindow = { skipped = true } +[class.NSWorkspace.methods] +iconForContentType = { skipped = true } +URLForApplicationToOpenContentType = { skipped = true } +URLsForApplicationsToOpenContentType = { skipped = true } +setDefaultApplicationAtURL_toOpenContentType_completionHandler = { skipped = true } +[class.NSWorkspaceOpenConfiguration.properties.architecture] +skipped = true + +# Wrong type for enum +[enum.anonymous.constants] +NSOKButton = { skipped = true } +NSCancelButton = { skipped = true } +NSFileHandlingPanelCancelButton = { skipped = true } +NSFileHandlingPanelOKButton = { skipped = true } + +# Categories for classes defined in other frameworks +[class.CIImage] +skipped = true +[class.CIColor] +skipped = true diff --git a/crates/icrate/src/AuthenticationServices/fixes/mod.rs b/crates/icrate/src/AuthenticationServices/fixes/mod.rs index 8b1378917..a7a41b048 100644 --- a/crates/icrate/src/AuthenticationServices/fixes/mod.rs +++ b/crates/icrate/src/AuthenticationServices/fixes/mod.rs @@ -1 +1,11 @@ +use crate::Foundation::NSObject; +// TODO: UIViewController on iOS, NSViewController on macOS +pub type ASViewController = NSObject; +// TODO: UIWindow on iOS, NSWindow on macOS +pub type ASPresentationAnchor = NSObject; +// TODO: UIImage on iOS, NSImage on macOS +pub type ASImage = NSObject; + +// TODO: UIControl on iOS, NSControl on macOS +pub(crate) type ASControl = NSObject; diff --git a/crates/icrate/src/AuthenticationServices/translation-config.toml b/crates/icrate/src/AuthenticationServices/translation-config.toml new file mode 100644 index 000000000..0aa61f27f --- /dev/null +++ b/crates/icrate/src/AuthenticationServices/translation-config.toml @@ -0,0 +1,20 @@ +imports = ["AuthenticationServices", "Foundation"] + +# Uses a bit of complex feature testing setup, see ASFoundation.h +[typedef.ASPresentationAnchor] +skipped = true +[typedef.ASViewController] +skipped = true +[typedef.ASImage] +skipped = true + +# It is a bit difficult to extract the original typedef name from the +# superclass name, so let's just overwrite it here. +[class.ASCredentialProviderViewController] +superclass-name = "ASViewController" +[class.ASAccountAuthenticationModificationViewController] +superclass-name = "ASViewController" + +# Specifies UIControl or NSControl conditionally +[class.ASAuthorizationAppleIDButton] +superclass-name = "ASControl" diff --git a/crates/icrate/src/CoreData/translation-config.toml b/crates/icrate/src/CoreData/translation-config.toml new file mode 100644 index 000000000..72e235c2d --- /dev/null +++ b/crates/icrate/src/CoreData/translation-config.toml @@ -0,0 +1,34 @@ +imports = ["CoreData", "Foundation"] + +# Has `error:` parameter, but returns NSInteger (where 0 means error) +[class.NSManagedObjectContext.methods] +countForFetchRequest_error = { skipped = true } + +# Defined in multiple files +[static.NSErrorMergePolicy] +skipped = true +[static.NSMergeByPropertyObjectTrumpMergePolicy] +skipped = true +[static.NSMergeByPropertyStoreTrumpMergePolicy] +skipped = true +[static.NSOverwriteMergePolicy] +skipped = true +[static.NSRollbackMergePolicy] +skipped = true + +# Both instance and class methods +[class.NSManagedObject.methods.entity] +skipped = true + +# References classes from other frameworks +[class.NSCoreDataCoreSpotlightDelegate.methods] +attributeSetForObject = { skipped = true } +searchableIndex_reindexAllSearchableItemsWithAcknowledgementHandler = { skipped = true } +searchableIndex_reindexSearchableItemsWithIdentifiers_acknowledgementHandler = { skipped = true } +[class.NSPersistentCloudKitContainer.methods] +recordForManagedObjectID = { skipped = true } +recordsForManagedObjectIDs = { skipped = true } +recordIDForManagedObjectID = { skipped = true } +recordIDsForManagedObjectIDs = { skipped = true } +[class.NSPersistentCloudKitContainerOptions.properties.databaseScope] +skipped = true diff --git a/crates/icrate/src/Foundation/fixes/NSDecimal.rs b/crates/icrate/src/Foundation/fixes/NSDecimal.rs new file mode 100644 index 000000000..17b9b3f73 --- /dev/null +++ b/crates/icrate/src/Foundation/fixes/NSDecimal.rs @@ -0,0 +1,13 @@ +use std::ffi::c_ushort; + +extern_struct!( + pub struct NSDecimal { + // signed int _exponent:8; + // unsigned int _length:4; + // unsigned int _isNegative:1; + // unsigned int _isCompact:1; + // unsigned int _reserved:18; + _inner: i32, + _mantissa: [c_ushort; 8], + } +); diff --git a/crates/icrate/src/Foundation/fixes/NSObject.rs b/crates/icrate/src/Foundation/fixes/NSObject.rs new file mode 100644 index 000000000..b94792f3b --- /dev/null +++ b/crates/icrate/src/Foundation/fixes/NSObject.rs @@ -0,0 +1,47 @@ +objc2::__inner_extern_class! { + @__inner + pub struct (NSObject) {} + + unsafe impl () for NSObject { + INHERITS = [objc2::runtime::Object]; + } +} + +unsafe impl objc2::ClassType for NSObject { + type Super = objc2::runtime::Object; + const NAME: &'static str = "NSObject"; + + #[inline] + fn class() -> &'static objc2::runtime::Class { + objc2::class!(NSObject) + } + + fn as_super(&self) -> &Self::Super { + &self.__inner + } + + fn as_super_mut(&mut self) -> &mut Self::Super { + &mut self.__inner + } +} + +impl PartialEq for NSObject { + fn eq(&self, _other: &Self) -> bool { + todo!() + } +} + +impl Eq for NSObject {} + +impl std::hash::Hash for NSObject { + #[inline] + fn hash(&self, _state: &mut H) { + todo!() + } +} + +impl std::fmt::Debug for NSObject { + fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + todo!() + } +} diff --git a/crates/icrate/src/Foundation/fixes/NSProxy.rs b/crates/icrate/src/Foundation/fixes/NSProxy.rs new file mode 100644 index 000000000..7d3fd6803 --- /dev/null +++ b/crates/icrate/src/Foundation/fixes/NSProxy.rs @@ -0,0 +1,53 @@ +use core::fmt; +use core::hash; + +use objc2::runtime::{Class, Object}; +use objc2::{ClassType, __inner_extern_class}; + +__inner_extern_class! { + @__inner + pub struct (NSProxy) {} + + unsafe impl () for NSProxy { + INHERITS = [Object]; + } +} + +unsafe impl ClassType for NSProxy { + type Super = Object; + const NAME: &'static str = "NSProxy"; + + #[inline] + fn class() -> &'static Class { + objc2::class!(NSProxy) + } + + fn as_super(&self) -> &Self::Super { + &self.__inner + } + + fn as_super_mut(&mut self) -> &mut Self::Super { + &mut self.__inner + } +} + +impl PartialEq for NSProxy { + fn eq(&self, _other: &Self) -> bool { + todo!() + } +} + +impl Eq for NSProxy {} + +impl hash::Hash for NSProxy { + #[inline] + fn hash(&self, _state: &mut H) { + todo!() + } +} + +impl fmt::Debug for NSProxy { + fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result { + todo!() + } +} diff --git a/crates/icrate/src/Foundation/fixes/mod.rs b/crates/icrate/src/Foundation/fixes/mod.rs index 8b1378917..6b58210b0 100644 --- a/crates/icrate/src/Foundation/fixes/mod.rs +++ b/crates/icrate/src/Foundation/fixes/mod.rs @@ -1 +1,7 @@ +mod NSDecimal; +mod NSObject; +mod NSProxy; +pub use self::NSDecimal::*; +pub use self::NSObject::*; +pub use self::NSProxy::*; diff --git a/crates/icrate/src/Foundation/mod.rs b/crates/icrate/src/Foundation/mod.rs index 50273cdab..013af5add 100644 --- a/crates/icrate/src/Foundation/mod.rs +++ b/crates/icrate/src/Foundation/mod.rs @@ -4,7 +4,5 @@ mod fixes; mod generated; pub use self::additions::*; -#[allow(unreachable_pub)] pub use self::fixes::*; -#[allow(unreachable_pub)] pub use self::generated::*; diff --git a/crates/icrate/src/Foundation/translation-config.toml b/crates/icrate/src/Foundation/translation-config.toml new file mode 100644 index 000000000..2fad301d7 --- /dev/null +++ b/crates/icrate/src/Foundation/translation-config.toml @@ -0,0 +1,213 @@ +imports = ["Foundation"] + +[class.NSObject.methods] +# Uses NS_REPLACES_RECEIVER +awakeAfterUsingCoder = { skipped = true } + +[protocol.NSKeyedUnarchiverDelegate.methods] +# Uses NS_RELEASES_ARGUMENT and NS_RETURNS_RETAINED +unarchiver_didDecodeObject = { skipped = true } + +[class.NSString.methods] +new = { unsafe = false } +# Assuming that (non-)nullability is properly set +stringByAppendingString = { unsafe = false } +stringByAppendingPathComponent = { unsafe = false } +# Assuming `NSStringEncoding` can be made safe +lengthOfBytesUsingEncoding = { unsafe = false } + +[class.NSString.properties] +length = { unsafe = false } +# Safe to call, but the returned pointer may not be safe to use +UTF8String = { unsafe = false } + +[class.NSMutableString.methods] +new = { unsafe = false } +initWithString = { unsafe = false } +# "appendString:" = { unsafe = false, mutable = true } +# "setString:" = { unsafe = false, mutable = true } + +[class.NSBlockOperation.properties] +# Uses `NSArray`, which is difficult to handle +executionBlocks = { skipped = true } + +# These use `Class`, which is unsupported +[class.NSItemProvider.methods] +registerObjectOfClass_visibility_loadHandler = { skipped = true } +canLoadObjectOfClass = { skipped = true } +loadObjectOfClass_completionHandler = { skipped = true } + +# These use `SomeObject * __strong *`, which is unsupported +[class.NSNetService.methods] +getInputStream_outputStream = { skipped = true } +[class.NSPropertyListSerialization.methods] +dataFromPropertyList_format_errorDescription = { skipped = true } +propertyListFromData_mutabilityOption_format_errorDescription = { skipped = true } + +# Has `error:` parameter, but returns NSInteger (where 0 means error) +[class.NSJSONSerialization.methods.writeJSONObject_toStream_options_error] +skipped = true +[class.NSPropertyListSerialization.methods.writePropertyList_toStream_format_options_error] +skipped = true + +# Not supported on clang 11.0.0 +[class.NSBundle.methods.localizedAttributedStringForKey_value_table] +skipped = true + +# Both instance and class methods +[class.NSUnarchiver.methods.decodeClassName_asClassName] +skipped = true +[class.NSUnarchiver.methods.classNameDecodedForArchiveClassName] +skipped = true +[class.NSAutoreleasePool.methods.addObject] +skipped = true +[class.NSBundle.methods.pathForResource_ofType_inDirectory] +skipped = true +[class.NSBundle.methods.pathsForResourcesOfType_inDirectory] +skipped = true +[class.NSKeyedArchiver.methods.setClassName_forClass] +skipped = true +[class.NSKeyedArchiver.methods.classNameForClass] +skipped = true +[class.NSKeyedUnarchiver.methods.setClass_forClassName] +skipped = true +[class.NSKeyedUnarchiver.methods.classForClassName] +skipped = true +[class.NSThread.properties.threadPriority] +skipped = true +[class.NSThread.properties.isMainThread] +skipped = true +[class.NSDate.properties.timeIntervalSinceReferenceDate] +skipped = true + +# Root class +[class.NSProxy] +skipped = true + +# Contains bitfields +[struct.NSDecimal] +skipped = true + +# Uses stuff from core Darwin libraries which we have not yet mapped +[class.NSAppleEventDescriptor.methods] +descriptorWithDescriptorType_bytes_length = { skipped = true } +descriptorWithDescriptorType_data = { skipped = true } +appleEventWithEventClass_eventID_targetDescriptor_returnID_transactionID = { skipped = true } +descriptorWithProcessIdentifier = { skipped = true } +initWithAEDescNoCopy = { skipped = true } +initWithDescriptorType_bytes_length = { skipped = true } +initWithDescriptorType_data = { skipped = true } +initWithEventClass_eventID_targetDescriptor_returnID_transactionID = { skipped = true } +setParamDescriptor_forKeyword = { skipped = true } +paramDescriptorForKeyword = { skipped = true } +removeParamDescriptorWithKeyword = { skipped = true } +setAttributeDescriptor_forKeyword = { skipped = true } +attributeDescriptorForKeyword = { skipped = true } +sendEventWithOptions_timeout_error = { skipped = true } +setDescriptor_forKeyword = { skipped = true } +descriptorForKeyword = { skipped = true } +removeDescriptorWithKeyword = { skipped = true } +keywordForDescriptorAtIndex = { skipped = true } +coerceToDescriptorType = { skipped = true } +[class.NSAppleEventDescriptor.properties] +aeDesc = { skipped = true } +descriptorType = { skipped = true } +eventClass = { skipped = true } +eventID = { skipped = true } +returnID = { skipped = true } +transactionID = { skipped = true } +[class.NSAppleEventManager.methods] +setEventHandler_andSelector_forEventClass_andEventID = { skipped = true } +removeEventHandlerForEventClass_andEventID = { skipped = true } +dispatchRawAppleEvent_withRawReply_handlerRefCon = { skipped = true } +[class.NSOperationQueue.properties.underlyingQueue] +skipped = true +[class.NSRunLoop.methods.getCFRunLoop] +skipped = true +[class.NSURLCredential.methods] +initWithIdentity_certificates_persistence = { skipped = true } +credentialWithIdentity_certificates_persistence = { skipped = true } +initWithTrust = { skipped = true } +credentialForTrust = { skipped = true } +[class.NSURLCredential.properties.identity] +skipped = true +[class.NSURLProtectionSpace.properties.serverTrust] +skipped = true +[class.NSURLSessionConfiguration.properties] +TLSMinimumSupportedProtocol = { skipped = true } +TLSMaximumSupportedProtocol = { skipped = true } +TLSMinimumSupportedProtocolVersion = { skipped = true } +TLSMaximumSupportedProtocolVersion = { skipped = true } +[class.NSUUID.methods] +initWithUUIDBytes = { skipped = true } +getUUIDBytes = { skipped = true } +[class.NSXPCConnection.properties] +auditSessionIdentifier = { skipped = true } +processIdentifier = { skipped = true } +effectiveUserIdentifier = { skipped = true } +effectiveGroupIdentifier = { skipped = true } +[class.NSXPCInterface.methods] +setXPCType_forSelector_argumentIndex_ofReply = { skipped = true } +XPCTypeForSelector_argumentIndex_ofReply = { skipped = true } +[class.NSXPCCoder.methods] +encodeXPCObject_forKey = { skipped = true } +decodeXPCObjectOfType_forKey = { skipped = true } + +# Uses constants from CoreFoundation or similar frameworks +[enum.NSAppleEventSendOptions] +use-value = true +[enum.NSCalendarUnit] +use-value = true +[enum.NSDateFormatterStyle] +use-value = true +[enum.NSRectEdge] +use-value = true +[enum.NSISO8601DateFormatOptions] +use-value = true +[enum.NSLocaleLanguageDirection] +use-value = true +[enum.NSNumberFormatterStyle] +use-value = true +[enum.NSNumberFormatterPadPosition] +use-value = true +[enum.NSNumberFormatterRoundingMode] +use-value = true +[enum.NSPropertyListMutabilityOptions] +use-value = true +[enum.NSPropertyListFormat] +use-value = true +[enum.anonymous.constants.NS_UnknownByteOrder] +skipped = true +[enum.anonymous.constants.NS_LittleEndian] +skipped = true +[enum.anonymous.constants.NS_BigEndian] +skipped = true + +# Uses va_list +[class.NSAttributedString.methods.initWithFormat_options_locale_arguments] +skipped = true +[class.NSException.methods.raise_format_arguments] +skipped = true +[class.NSExpression.methods.expressionWithFormat_arguments] +skipped = true +[class.NSPredicate.methods.predicateWithFormat_arguments] +skipped = true +[class.NSString.methods.initWithFormat_arguments] +skipped = true +[class.NSString.methods.initWithFormat_locale_arguments] +skipped = true +[fn.NSLogv] +skipped = true + +# Wrong type compared to value +[enum.anonymous.constants.NSWrapCalendarComponents] +skipped = true + +# Uses NSImage, which is only available in AppKit +[class.NSUserNotification.properties.contentImage] +skipped = true + +# Has the wrong generic parameter +[class.NSDictionary.methods] +initWithContentsOfURL_error = { skipped = true } +dictionaryWithContentsOfURL_error = { skipped = true } diff --git a/crates/icrate/src/generated b/crates/icrate/src/generated index ef093626b..700fed0fa 160000 --- a/crates/icrate/src/generated +++ b/crates/icrate/src/generated @@ -1 +1 @@ -Subproject commit ef093626b60fc41b2953349e7c4f47237b1935c0 +Subproject commit 700fed0faa6d566add1a77f702eba6bf734935de diff --git a/crates/objc2/src/macros.rs b/crates/objc2/src/macros.rs index 8a3c4ca2f..423fbd6ce 100644 --- a/crates/objc2/src/macros.rs +++ b/crates/objc2/src/macros.rs @@ -1085,6 +1085,20 @@ macro_rules! msg_send_id { result = <$crate::__macro_helpers::Init as $crate::__macro_helpers::MsgSendId<_, _>>::send_message_id($obj, sel, ()); result }); + [$obj:expr, @__retain_semantics $retain_semantics:ident $($selector_and_arguments:tt)+] => { + $crate::__msg_send_parse! { + ($crate::__msg_send_id_helper) + @(send_message_id_error) + @() + @() + @($($selector_and_arguments)+) + @(send_message_id) + + @($obj) + @($retain_semantics) + } + // compile_error!(stringify!($($selector_and_arguments)*)) + }; [$obj:expr, $($selector_and_arguments:tt)+] => { $crate::__msg_send_parse! { ($crate::__msg_send_id_helper) @@ -1095,6 +1109,7 @@ macro_rules! msg_send_id { @(send_message_id) @($obj) + @() } }; } @@ -1106,6 +1121,7 @@ macro_rules! __msg_send_id_helper { { @($fn:ident) @($obj:expr) + @($($retain_semantics:ident)?) @(retain) @() } => {{ @@ -1116,6 +1132,7 @@ macro_rules! __msg_send_id_helper { { @($fn:ident) @($obj:expr) + @($($retain_semantics:ident)?) @(release) @() } => {{ @@ -1126,6 +1143,7 @@ macro_rules! __msg_send_id_helper { { @($fn:ident) @($obj:expr) + @($($retain_semantics:ident)?) @(autorelease) @() } => {{ @@ -1136,6 +1154,7 @@ macro_rules! __msg_send_id_helper { { @($fn:ident) @($obj:expr) + @($($retain_semantics:ident)?) @(dealloc) @() } => {{ @@ -1146,6 +1165,20 @@ macro_rules! __msg_send_id_helper { { @($fn:ident) @($obj:expr) + @($retain_semantics:ident) + @($sel_first:ident $(: $($sel_rest:ident :)*)?) + @($($argument:expr,)*) + } => ({ + <$crate::__macro_helpers::$retain_semantics as $crate::__macro_helpers::MsgSendId<_, _>>::$fn::<_, _>( + $obj, + $crate::sel!($sel_first $(: $($sel_rest :)*)?), + ($($argument,)*), + ) + }); + { + @($fn:ident) + @($obj:expr) + @() @($sel_first:ident $(: $($sel_rest:ident :)*)?) @($($argument:expr,)*) } => ({ diff --git a/crates/objc2/src/macros/extern_class.rs b/crates/objc2/src/macros/extern_class.rs index c28b027bf..3cbafc1c4 100644 --- a/crates/objc2/src/macros/extern_class.rs +++ b/crates/objc2/src/macros/extern_class.rs @@ -242,7 +242,7 @@ macro_rules! __inner_extern_class { // TODO: Expose this variant of the macro. ( $(#[$m:meta])* - $v:vis struct $name:ident<$($t_struct:ident $(: $b_struct:ident $(= $default:ty)?)?),*> { + $v:vis struct $name:ident<$($t_struct:ident $(: $b_struct:ident $(= $default:ty)?)?),* $(,)?> { $($field_vis:vis $field:ident: $field_ty:ty,)* } diff --git a/crates/objc2/src/macros/extern_methods.rs b/crates/objc2/src/macros/extern_methods.rs index 9c68bc5f9..90a94a395 100644 --- a/crates/objc2/src/macros/extern_methods.rs +++ b/crates/objc2/src/macros/extern_methods.rs @@ -375,10 +375,10 @@ macro_rules! __collect_msg_send { ( $macro:path; $obj:expr; - ($sel:ident); + ($(@__retain_semantics $retain_semantics:ident )? $sel:ident); (); ) => {{ - $macro![$obj, $sel] + $macro![$obj, $(@__retain_semantics $retain_semantics )? $sel] }}; // Base case @@ -396,7 +396,7 @@ macro_rules! __collect_msg_send { ( $macro:path; $obj:expr; - ($sel:ident:); + ($(@__retain_semantics $retain_semantics:ident )? $sel:ident:); ($(,)?); $($output:tt)* ) => { @@ -405,7 +405,7 @@ macro_rules! __collect_msg_send { $obj; (); (); - $($output)* $sel: _, + $($output)* $(@__retain_semantics $retain_semantics )? $sel: _, } }; @@ -413,7 +413,7 @@ macro_rules! __collect_msg_send { ( $macro:path; $obj:expr; - ($sel:ident : $($sel_rest:tt)*); + ($(@__retain_semantics $retain_semantics:ident )? $sel:ident : $($sel_rest:tt)*); ($arg:ident: $arg_ty:ty $(, $($args_rest:tt)*)?); $($output:tt)* ) => { @@ -422,7 +422,7 @@ macro_rules! __collect_msg_send { $obj; ($($sel_rest)*); ($($($args_rest)*)?); - $($output)* $sel: $arg, + $($output)* $(@__retain_semantics $retain_semantics )? $sel: $arg, } };