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, } };