diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4c8c4abc..b0faf2786 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,18 +43,22 @@ jobs: - name: Test macOS 11 os: macos-11 target: x86_64-apple-darwin + frameworks: macos-11 - name: Test macOS 12 os: macos-12 target: x86_64-apple-darwin + frameworks: macos-12 - name: Build macOS AArch64 os: macos-latest target: aarch64-apple-darwin test-args: --no-run + frameworks: macos-11 - name: Test macOS old SDK os: macos-latest target: x86_64-apple-darwin # Oldest macOS version we support sdk: "10.7" + frameworks: macos-10-7 - name: Test macOS nightly (w. ui tests) os: macos-latest target: x86_64-apple-darwin @@ -62,6 +66,7 @@ jobs: toolchain: nightly # Run UI tests on nightly to help find regressions ui: true + frameworks: macos-11 - name: Build macOS 32bit os: macos-latest target: i686-apple-darwin @@ -75,17 +80,20 @@ jobs: test-args: --no-run # Newest SDK that supports 32-bit sdk: "10.13" + frameworks: macos-10-13 - name: Test GNUStep with libobjc2 v1.9 os: ubuntu-latest target: x86_64-unknown-linux-gnu runtime: gnustep-1-9 libobjc2: "1.9" + frameworks: gnustep - name: Test GNUStep with libobjc2 v2.0 # Fails for some reason on Ubuntu 22 os: ubuntu-20.04 target: x86_64-unknown-linux-gnu runtime: gnustep-2-0 libobjc2: "2.0" + frameworks: gnustep - name: Test GNUStep with libobjc2 v2.1 on nightly os: ubuntu-latest target: x86_64-unknown-linux-gnu @@ -94,6 +102,7 @@ jobs: runtime: gnustep-2-1 libobjc2: "2.1" fuzz: true + frameworks: gnustep - name: Test GNUStep 32bit # Ubuntu 22 image doesn't have the required C++ libraries # installed for 32-bit @@ -103,18 +112,22 @@ jobs: configureflags: --target=x86-pc-linux-gnu runtime: gnustep-1-9 libobjc2: "1.9" + frameworks: gnustep-32bit - name: Test iOS simulator x86 64bit os: macos-11 target: x86_64-apple-ios dinghy: true + frameworks: ios - name: Build iOS simulator ARM64 os: macos-latest target: aarch64-apple-ios-sim test-args: --no-run + frameworks: ios - name: Build iOS ARM64 os: macos-latest target: aarch64-apple-ios test-args: --no-run + frameworks: ios - name: Build iOS ARMv7 os: macos-latest target: armv7-apple-ios @@ -124,6 +137,7 @@ jobs: components: clippy, rust-src args: -Zbuild-std -Zdoctest-xcompile test-args: --no-run + frameworks: ios - name: Build iOS ARMv7s os: macos-latest target: armv7s-apple-ios @@ -133,6 +147,7 @@ jobs: components: clippy, rust-src args: -Zbuild-std -Zdoctest-xcompile test-args: --no-run + frameworks: ios - name: Build iOS 32bit x86 os: macos-latest target: i386-apple-ios @@ -142,6 +157,7 @@ jobs: components: clippy, rust-src args: -Zbuild-std -Zdoctest-xcompile test-args: --no-run + frameworks: ios - name: Test Compiler-RT os: ubuntu-latest target: x86_64-unknown-linux-gnu @@ -149,6 +165,7 @@ jobs: args: -p block-sys -p block2 features: ' ' unstable-features: ' ' + frameworks: none env: CARGO_BUILD_TARGET: ${{ matrix.target }} @@ -160,7 +177,13 @@ jobs: # Use --no-fail-fast, except with dinghy TESTARGS: ${{ matrix.dinghy && ' ' || '--no-fail-fast' }} ${{ matrix.test-args }} SOME_FEATURES: ${{ matrix.features || 'malloc,block,exception,Foundation' }} - FEATURES: ${{ matrix.features || 'malloc,block,exception,unstable-all-frameworks,catch-all,verify,uuid' }} + FEATURES: >- + ${{ + matrix.features || format( + 'malloc,block,exception,catch-all,verify,uuid,unstable-frameworks-{0}', + matrix.frameworks + ) + }} UNSTABLE_FEATURES: ${{ matrix.unstable-features || 'unstable-autoreleasesafe,unstable-c-unwind' }} CMD: cargo @@ -199,7 +222,7 @@ jobs: ~/extern/include ~/extern/sdk # Change this key if we start caching more things - key: ${{ matrix.name }}-extern-v4 + key: ${{ matrix.name }}-extern-v5 - name: Setup environment # These add to PATH-like variables, so they can always be set @@ -293,7 +316,7 @@ jobs: ../configure --prefix=$HOME/extern --with-library-combo=ng-gnu-gnu make install - - name: Install GNUStep-Base + - name: Install GNUStep base library if: contains(matrix.runtime, 'gnustep') && steps.extern-cache.outputs.cache-hit != 'true' run: | wget https://github.com/gnustep/libs-base/archive/refs/tags/base-1_28_0.tar.gz @@ -303,6 +326,16 @@ jobs: make install ls -al $HOME/extern/* + - name: Install GNUStep GUI + if: matrix.target == 'x86_64-unknown-linux-gnu' && steps.extern-cache.outputs.cache-hit != 'true' + run: | + wget https://github.com/gnustep/libs-gui/archive/refs/tags/gui-0_29_0.tar.gz + tar -xzf gui-0_29_0.tar.gz + cd libs-gui-gui-0_29_0 + ./configure --prefix=$HOME/extern --disable-jpeg ${{ matrix.platform.configureflags }} + make install + ls -al $HOME/extern/* + - name: Cache Cargo uses: actions/cache@v3 with: @@ -426,9 +459,11 @@ jobs: # Not using --all-features because that would enable e.g. gnustep run: cargo test ${{ env.ARGS }} ${{ env.TESTARGS }} --features=${{ env.FEATURES }},${{ env.UNSTABLE_FEATURES }} + # TODO: Re-enable this on Foundation once we do some form of + # availability checking. - name: Test static class and selectors if: ${{ !matrix.dinghy && (matrix.runtime || 'apple') == 'apple' }} - run: cargo test ${{ env.ARGS }} ${{ env.TESTARGS }} --features=Foundation,unstable-static-sel,unstable-static-class + run: cargo test ${{ env.ARGS }} ${{ env.TESTARGS }} --features=unstable-static-sel,unstable-static-class - name: Run assembly tests if: ${{ !contains(matrix.runtime, 'compiler-rt') }} diff --git a/crates/header-translator/Cargo.toml b/crates/header-translator/Cargo.toml new file mode 100644 index 000000000..c13ebb449 --- /dev/null +++ b/crates/header-translator/Cargo.toml @@ -0,0 +1,32 @@ +[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" + +[features] +run = [ + "clang", + "toml", + "serde", + "apple-sdk", + "tracing", + "tracing-subscriber", + "tracing-tree", +] + +[dependencies] +clang = { version = "2.0", features = ["runtime", "clang_10_0"], optional = true } +toml = { version = "0.5.9", optional = true } +serde = { version = "1.0.144", features = ["derive"], optional = true } +apple-sdk = { version = "0.2.0", default-features = false, optional = true } +tracing = { version = "0.1.37", default-features = false, features = ["std"], optional = true } +tracing-subscriber = { version = "0.3.16", features = ["fmt"], optional = true } +tracing-tree = { git = "https://github.com/madsmtm/tracing-tree.git", optional = true } + +[[bin]] +name = "header-translator" +required-features = ["run"] diff --git a/crates/header-translator/README.md b/crates/header-translator/README.md new file mode 100644 index 000000000..7fe3a927e --- /dev/null +++ b/crates/header-translator/README.md @@ -0,0 +1,11 @@ +# Objective-C header translator + +For use in making `icrate`. + +```console +cargo run --features=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..30e0b8ef0 --- /dev/null +++ b/crates/header-translator/src/availability.rs @@ -0,0 +1,13 @@ +use clang::PlatformAvailability; + +#[derive(Debug, Clone, PartialEq)] +pub struct Availability { + #[allow(dead_code)] + inner: Vec<()>, +} + +impl Availability { + pub fn parse(_availability: Vec) -> Self { + Self { inner: Vec::new() } + } +} diff --git a/crates/header-translator/src/cache.rs b/crates/header-translator/src/cache.rs new file mode 100644 index 000000000..414aa9c99 --- /dev/null +++ b/crates/header-translator/src/cache.rs @@ -0,0 +1,226 @@ +use std::collections::BTreeMap; +use std::mem; + +use tracing::{debug_span, warn}; + +use crate::availability::Availability; +use crate::config::{ClassData, Config}; +use crate::file::File; +use crate::method::Method; +use crate::output::Output; +use crate::rust_type::{GenericType, Ownership}; +use crate::stmt::Stmt; + +#[derive(Debug, PartialEq, Clone)] +struct MethodCache { + availability: Availability, + methods: Vec, + category_name: Option, +} + +#[derive(Debug, PartialEq, Clone, Default)] +struct ClassCache { + /// Methods that should be duplicated onto any subclass. + to_emit: Vec, + // We don't need availability here, since a superclass' availability + // should always be greater than the subclass'. +} + +impl ClassCache { + fn all_methods_data(&self) -> impl Iterator { + self.to_emit + .iter() + .flat_map(|cache| cache.methods.iter().map(|m| (m.is_class, &*m.selector))) + } +} + +/// A helper struct for doing global analysis on the output. +#[derive(Debug, PartialEq, Clone)] +pub struct Cache { + classes: BTreeMap, + ownership_map: BTreeMap, +} + +impl Cache { + pub fn new(output: &Output) -> Self { + let mut classes: BTreeMap<_, ClassCache> = BTreeMap::new(); + let mut ownership_map: BTreeMap<_, Ownership> = BTreeMap::new(); + + for (name, library) in &output.libraries { + let _span = debug_span!("library", name).entered(); + for (name, file) in &library.files { + let _span = debug_span!("file", name).entered(); + for stmt in &file.stmts { + if let Some((ty, method_cache)) = Self::cache_stmt(stmt) { + let cache = classes.entry(ty.clone()).or_default(); + cache.to_emit.push(method_cache); + } + if let Stmt::ClassDecl { ty, ownership, .. } = stmt { + if *ownership != Ownership::default() { + ownership_map.insert(ty.name.clone(), ownership.clone()); + } + } + } + } + } + + Self { + classes, + ownership_map, + } + } + + fn cache_stmt(stmt: &Stmt) -> Option<(&GenericType, MethodCache)> { + if let Stmt::Methods { + ty, + availability, + methods, + category_name, + description, + } = stmt + { + let _span = debug_span!("Stmt::Methods", %ty).entered(); + let methods: Vec = methods + .iter() + .filter(|method| method.emit_on_subclasses()) + .cloned() + .collect(); + if methods.is_empty() { + return None; + } + if description.is_some() { + warn!(description, "description was set"); + } + Some(( + ty, + MethodCache { + availability: availability.clone(), + methods, + category_name: category_name.clone(), + }, + )) + } else { + None + } + } + + pub fn update(&self, output: &mut Output, configs: &BTreeMap) { + for (name, library) in &mut output.libraries { + let _span = debug_span!("library", name).entered(); + let config = configs.get(name).expect("configs get library"); + for (name, file) in &mut library.files { + let _span = debug_span!("file", name).entered(); + self.update_file(file, config); + } + } + } + + fn update_file(&self, file: &mut File, config: &Config) { + let mut new_stmts = Vec::new(); + for stmt in &mut file.stmts { + match stmt { + Stmt::ClassDecl { + ty, superclasses, .. + } => { + let _span = debug_span!("Stmt::ClassDecl", %ty).entered(); + let data = config.class_data.get(&ty.name); + + // Used for duplicate checking (sometimes the subclass + // defines the same method that the superclass did). + let mut seen_methods: Vec<_> = self + .classes + .get(ty) + .map(|cache| cache.all_methods_data()) + .into_iter() + .flatten() + .collect(); + + for superclass in superclasses { + if let Some(cache) = self.classes.get(superclass) { + new_stmts.extend(cache.to_emit.iter().filter_map(|cache| { + let mut methods: Vec<_> = cache + .methods + .iter() + .filter(|method| { + !seen_methods.contains(&(method.is_class, &method.selector)) + }) + .filter_map(|method| { + method.clone().update(ClassData::get_method_data( + data, + &method.fn_name, + )) + }) + .collect(); + if methods.is_empty() { + return None; + } + + self.update_methods(&mut methods, &ty.name); + + Some(Stmt::Methods { + ty: ty.clone(), + availability: cache.availability.clone(), + methods, + category_name: cache.category_name.clone(), + description: Some(format!( + "Methods declared on superclass `{}`", + superclass.name + )), + }) + })); + + seen_methods.extend(cache.all_methods_data()); + } + } + } + Stmt::Methods { ty, methods, .. } => { + self.update_methods(methods, &ty.name); + } + Stmt::ProtocolDecl { name, methods, .. } => { + self.update_methods(methods, name); + } + _ => {} + } + } + file.stmts.extend(new_stmts); + + // Fix up a few typedef + enum declarations + let mut iter = mem::take(&mut file.stmts).into_iter().peekable(); + while let Some(stmt) = iter.next() { + if let Stmt::AliasDecl { + name, + ty, + kind: None, + } = &stmt + { + if let Some(Stmt::EnumDecl { + name: enum_name, + ty: enum_ty, + .. + }) = iter.peek_mut() + { + if enum_ty.is_typedef_to(&name) { + *enum_name = Some(name.clone()); + *enum_ty = ty.clone(); + // Skip adding the now-redundant alias to the list of statements + continue; + } + } + } + file.stmts.push(stmt); + } + } + + fn update_methods(&self, methods: &mut [Method], self_means: &str) { + for method in methods { + // Beware! We make instance methods return `Owned` as well, though + // those are basically never safe (since they'd refer to mutable + // data without a lifetime tied to the primary owner). + method.result_type.set_ownership(|name| { + let name = if name == "Self" { self_means } else { name }; + + self.ownership_map.get(name).cloned().unwrap_or_default() + }); + } + } +} diff --git a/crates/header-translator/src/config.rs b/crates/header-translator/src/config.rs new file mode 100644 index 000000000..549ca50ee --- /dev/null +++ b/crates/header-translator/src/config.rs @@ -0,0 +1,135 @@ +use std::collections::HashMap; +use std::fs; +use std::io::Result; +use std::path::Path; + +use serde::Deserialize; + +use crate::rust_type::Ownership; +use crate::stmt::Derives; + +#[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 = "definition-skipped")] + #[serde(default)] + pub definition_skipped: bool, + #[serde(default)] + pub methods: HashMap, + #[serde(default)] + pub derives: Derives, + #[serde(rename = "owned")] + #[serde(default)] + pub ownership: Ownership, +} + +impl ClassData { + pub fn get_method_data(this: Option<&Self>, name: &str) -> MethodData { + this.map(|data| data.methods.get(name).copied().unwrap_or_default()) + .unwrap_or_default() + } +} + +#[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, + #[serde(default = "mutating_default")] + pub mutating: bool, +} + +// TODO +pub type FnData = StructData; +pub type StaticData = StructData; +pub type TypedefData = StructData; + +fn unsafe_default() -> bool { + true +} + +fn skipped_default() -> bool { + false +} + +fn mutating_default() -> bool { + false +} + +impl Default for MethodData { + fn default() -> Self { + Self { + unsafe_: unsafe_default(), + skipped: skipped_default(), + mutating: mutating_default(), + } + } +} + +impl Config { + pub fn from_file(file: &Path) -> Result { + let s = fs::read_to_string(file)?; + + let mut res: Self = toml::from_str(&s)?; + + // Ignore categories on NSObject for now + res.class_data + .entry("NSObject".to_string()) + .or_default() + .skipped = true; + + Ok(res) + } +} diff --git a/crates/header-translator/src/expr.rs b/crates/header-translator/src/expr.rs new file mode 100644 index 000000000..922b07450 --- /dev/null +++ b/crates/header-translator/src/expr.rs @@ -0,0 +1,139 @@ +use std::fmt; +use std::fmt::Write; + +use clang::token::TokenKind; +use clang::{Entity, EntityKind, EntityVisitResult}; + +use crate::immediate_children; +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, pointer_width: usize) -> Self { + let (signed_max, unsigned_max) = match pointer_width { + 64 => (i64::MAX, u64::MAX), + 32 => (i32::MAX as i64, u32::MAX as u64), + 16 => (i16::MAX as i64, u16::MAX as u64), + pw => panic!("unhandled pointer width {pw}"), + }; + + let s = if unsigned == unsigned_max { + "NSUIntegerMax as _".to_string() + } else if signed == signed_max { + "NSIntegerMax as _".to_string() + } else 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; + + immediate_children(entity, |entity, _span| 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"); + } + } + }); + + 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/file.rs b/crates/header-translator/src/file.rs new file mode 100644 index 000000000..d1430a9f0 --- /dev/null +++ b/crates/header-translator/src/file.rs @@ -0,0 +1,79 @@ +use std::fmt; + +use tracing::debug_span; + +use crate::config::Config; +use crate::stmt::Stmt; + +pub(crate) const FILE_PRELUDE: &str = r#"//! This file has been automatically generated by `objc2`'s `header-translator`. +//! DO NOT EDIT"#; + +#[derive(Debug, PartialEq)] +pub struct File { + imports: Vec, + pub(crate) stmts: Vec, +} + +impl File { + pub fn new(config: &Config) -> Self { + Self { + imports: config.imports.clone(), + stmts: Vec::new(), + } + } + + pub(crate) fn declared_types(&self) -> impl Iterator { + self.stmts + .iter() + .filter_map(|stmt| match stmt { + Stmt::ClassDecl { ty, .. } => Some(&*ty.name), + Stmt::Methods { .. } => None, + Stmt::ProtocolDecl { name, .. } => Some(name), + Stmt::ProtocolImpl { .. } => None, + Stmt::StructDecl { name, .. } => Some(name), + Stmt::EnumDecl { name, .. } => name.as_deref(), + Stmt::VarDecl { name, .. } => Some(name), + Stmt::FnDecl { name, body, .. } if body.is_none() => Some(name), + // TODO + Stmt::FnDecl { .. } => None, + Stmt::AliasDecl { name, .. } => Some(name), + }) + .chain(self.stmts.iter().flat_map(|stmt| { + if let Stmt::EnumDecl { variants, .. } = stmt { + variants.iter().map(|(name, _)| &**name).collect() + } else { + vec![] + } + })) + } + + pub fn add_stmt(&mut self, stmt: Stmt) { + self.stmts.push(stmt); + } + + pub fn compare(&self, other: &Self) { + super::compare_slice(&self.stmts, &other.stmts, |i, self_stmt, other_stmt| { + let _span = debug_span!("stmt", i).entered(); + self_stmt.compare(other_stmt); + }); + } +} + +impl fmt::Display for File { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "{FILE_PRELUDE}")?; + + writeln!(f, "use crate::common::*;")?; + for import in &self.imports { + writeln!(f, "use crate::{import}::*;")?; + } + + writeln!(f)?; + + for stmt in &self.stmts { + writeln!(f, "{stmt}")?; + } + + Ok(()) + } +} diff --git a/crates/header-translator/src/lib.rs b/crates/header-translator/src/lib.rs new file mode 100644 index 000000000..b50e8c2a9 --- /dev/null +++ b/crates/header-translator/src/lib.rs @@ -0,0 +1,112 @@ +#![cfg(feature = "run")] +use std::collections::BTreeMap; +use std::fmt; +use std::path::Path; +use std::process::{Command, Stdio}; + +use clang::{Entity, EntityVisitResult}; +use tracing::debug_span; +use tracing::span::EnteredSpan; + +mod availability; +mod cache; +mod config; +mod expr; +mod file; +mod library; +mod method; +mod objc2_utils; +mod output; +mod property; +mod rust_type; +mod stmt; +mod unexposed_macro; + +pub use self::cache::Cache; +pub use self::config::Config; +pub use self::file::File; +pub use self::library::Library; +pub use self::output::Output; +pub use self::stmt::Stmt; + +pub fn compare_btree( + data1: &BTreeMap, + data2: &BTreeMap, + f: impl Fn(&str, &T, &T), +) { + for (key1, item1) in data1 { + let item2 = data2 + .get(key1) + .unwrap_or_else(|| panic!("did not find key {key1} in data2")); + f(key1, item1, item2); + } + assert_eq!(data1.len(), data2.len(), "too few items in first map"); +} + +pub fn compare_slice(data1: &[T], data2: &[T], f: impl Fn(usize, &T, &T)) { + let mut iter2 = data1.iter(); + for (i, item1) in data2.iter().enumerate() { + if let Some(item2) = iter2.next() { + f(i, item1, item2); + } else { + panic!("no more statements in second vec at index {i}"); + } + } + let remaining: Vec<_> = iter2.collect(); + if !remaining.is_empty() { + panic!("remaining statements in second vec: {remaining:#?}"); + } +} + +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 fmt::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 +} + +fn immediate_children<'tu>( + entity: &Entity<'tu>, + mut closure: impl FnMut(Entity<'tu>, EnteredSpan), +) { + entity.visit_children(|entity, _parent| { + let span = debug_span!( + "child", + kind = ?entity.get_kind(), + dbg = entity.get_name(), + ) + .entered(); + + closure(entity, span); + + EntityVisitResult::Continue + }); +} diff --git a/crates/header-translator/src/library.rs b/crates/header-translator/src/library.rs new file mode 100644 index 000000000..eec68f416 --- /dev/null +++ b/crates/header-translator/src/library.rs @@ -0,0 +1,69 @@ +use std::collections::BTreeMap; +use std::fmt; +use std::fs; +use std::io; +use std::path::Path; + +use tracing::debug_span; + +use crate::file::{File, FILE_PRELUDE}; + +#[derive(Debug, PartialEq, Default)] +pub struct Library { + pub files: BTreeMap, +} + +impl Library { + pub fn new() -> Self { + Self { + files: BTreeMap::new(), + } + } + + pub fn output(&self, path: &Path) -> io::Result<()> { + for (name, file) in &self.files { + let mut path = path.join(name); + path.set_extension("rs"); + fs::write(&path, file.to_string())?; + } + + // truncate if the file exists + fs::write(path.join("mod.rs"), self.to_string())?; + + Ok(()) + } + + pub fn compare(&self, other: &Self) { + super::compare_btree(&self.files, &other.files, |name, self_file, other_file| { + let _span = debug_span!("file", name).entered(); + self_file.compare(other_file); + }); + } +} + +impl fmt::Display for Library { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "{FILE_PRELUDE}")?; + writeln!(f, "#![allow(unused_imports)]")?; + + for name in self.files.keys() { + writeln!(f, "#[path = \"{name}.rs\"]")?; + writeln!(f, "mod __{name};")?; + } + + writeln!(f)?; + + for (name, file) in &self.files { + let mut iter = file.declared_types(); + if let Some(item) = iter.next() { + writeln!(f, "pub use self::__{name}::{{{item}")?; + for item in iter { + writeln!(f, ", {item}")?; + } + writeln!(f, "}};")?; + } + } + + Ok(()) + } +} diff --git a/crates/header-translator/src/main.rs b/crates/header-translator/src/main.rs new file mode 100644 index 000000000..5bba3c43f --- /dev/null +++ b/crates/header-translator/src/main.rs @@ -0,0 +1,363 @@ +use std::collections::{BTreeMap, HashMap}; +use std::io; +use std::path::{Path, PathBuf}; + +use apple_sdk::{AppleSdk, DeveloperDirectory, Platform, SdkPath, SimpleSdk}; +use clang::{Clang, Entity, EntityKind, EntityVisitResult, Index, TranslationUnit}; +use tracing::{debug_span, info, info_span, trace, trace_span}; +use tracing_subscriber::filter::LevelFilter; +use tracing_subscriber::layer::{Layer, SubscriberExt}; +use tracing_subscriber::registry::Registry; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_tree::HierarchicalLayer; + +use header_translator::{run_cargo_fmt, Cache, Config, File, Output, Stmt}; + +fn main() { + // use tracing_subscriber::fmt; + Registry::default() + // .with( + // fmt::Layer::default() + // .compact() + // .without_time() + // .with_target(false) + // .with_span_events(fmt::format::FmtSpan::ACTIVE) + // .with_filter(LevelFilter::INFO) + // .with_filter(tracing_subscriber::filter::filter_fn(|metadata| { + // metadata.is_span() && metadata.level() == &tracing::Level::INFO + // })), + // ) + .with( + HierarchicalLayer::new(2) + .with_targets(false) + .with_indent_lines(true) + // Note: Change this to DEBUG if you want to see more info + .with_filter(LevelFilter::INFO), + ) + .init(); + + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let workspace_dir = manifest_dir.parent().unwrap(); + let crate_src = workspace_dir.join("icrate/src"); + + let configs = load_configs(&crate_src); + + let clang = Clang::new().unwrap(); + let index = Index::new(&clang, true, true); + + let developer_dir = DeveloperDirectory::from(PathBuf::from( + std::env::args_os() + .nth(1) + .expect("must specify developer directory as first argument"), + )); + + let sdks: Vec<_> = developer_dir + .platforms() + .expect("developer dir platforms") + .into_iter() + .map(|platform| { + 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() + }) + .collect(); + + assert_eq!(sdks.len(), 8, "should have one of each platform: {sdks:?}"); + + let mut final_result = None; + + // TODO: Compare between SDKs + for sdk in sdks { + // These are found using the `get_llvm_targets.fish` helper script + let llvm_targets: &[_] = match &sdk.platform { + Platform::MacOsX => &[ + "x86_64-apple-macosx10.7.0", + // "arm64-apple-macosx11.0.0", + // "i686-apple-macosx10.7.0", + ], + Platform::IPhoneOs => &[ + // "arm64-apple-ios7.0.0", + // "armv7-apple-ios7.0.0", + // "armv7s-apple-ios", + // "arm64-apple-ios14.0-macabi", + // "x86_64-apple-ios13.0-macabi", + ], + // Platform::IPhoneSimulator => &[ + // "arm64-apple-ios7.0.0-simulator", + // "x86_64-apple-ios7.0.0-simulator", + // "i386-apple-ios7.0.0-simulator", + // ], + // Platform::AppleTvOs => &["arm64-apple-tvos", "x86_64-apple-tvos"], + // Platform::WatchOs => &["arm64_32-apple-watchos", "armv7k-apple-watchos"], + // Platform::WatchSimulator => &[ + // "arm64-apple-watchos5.0.0-simulator", + // "x86_64-apple-watchos5.0.0-simulator", + // ], + _ => continue, + }; + + let mut result: Option = None; + + for llvm_target in llvm_targets { + let _span = info_span!("parsing", platform = ?sdk.platform, llvm_target).entered(); + let curr_result = parse_sdk(&index, &sdk, llvm_target, &configs); + + if let Some(prev_result) = &result { + let _span = info_span!("comparing results").entered(); + prev_result.compare(&curr_result); + + // Extra check in case our comparison above was not exaustive + assert_eq!(*prev_result, curr_result); + } else { + result = Some(curr_result); + } + } + + if sdk.platform == Platform::MacOsX { + final_result = result; + } + } + + let mut final_result = final_result.expect("got a result"); + let span = info_span!("analyzing").entered(); + let cache = Cache::new(&final_result); + cache.update(&mut final_result, &configs); + drop(span); + + for (library_name, files) in final_result.libraries { + let _span = info_span!("writing", library_name).entered(); + let output_path = crate_src.join("generated").join(&library_name); + files.output(&output_path).unwrap(); + } + + let _span = info_span!("formatting").entered(); + run_cargo_fmt("icrate"); +} + +fn load_configs(crate_src: &Path) -> BTreeMap { + let _span = info_span!("loading configs").entered(); + + 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_sdk( + index: &Index<'_>, + sdk: &SdkPath, + llvm_target: &str, + configs: &BTreeMap, +) -> Output { + let tu = get_translation_unit(index, sdk, llvm_target); + + let framework_dir = sdk.path.join("System/Library/Frameworks"); + + let mut preprocessing = true; + let mut result = Output::from_configs(configs.keys()); + + let mut library_span = None; + let mut library_span_name = String::new(); + let mut file_span = None; + let mut file_span_name = String::new(); + + let mut macro_invocations = HashMap::new(); + + tu.get_entity().visit_children(|entity, _parent| { + let _span = trace_span!("entity", ?entity).entered(); + if let Some((library_name, file_name)) = extract_framework_name(&entity, &framework_dir) { + if library_span_name != library_name { + library_span.take(); + file_span.take(); + file_span_name = String::new(); + + library_span_name = library_name.clone(); + library_span = Some(debug_span!("library", name = library_name).entered()); + } + if file_span_name != file_name { + file_span.take(); + + file_span_name = file_name.clone(); + file_span = Some(debug_span!("file", name = file_name).entered()); + } + + if let Some(config) = configs.get(&library_name) { + let library = result.libraries.get_mut(&library_name).expect("library"); + match entity.get_kind() { + EntityKind::InclusionDirective if preprocessing => { + 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_name { + 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_name { + // The file is often included twice, even + // within the same file, so insertion can fail + library + .files + .entry(included) + .or_insert_with(|| File::new(config)); + } + } + } + EntityKind::MacroExpansion if preprocessing => { + let name = entity.get_name().expect("macro name"); + let location = entity.get_location().expect("macro location"); + macro_invocations.insert(location.get_spelling_location(), name); + } + EntityKind::MacroDefinition if preprocessing => { + // let name = entity.get_name().expect("macro def name"); + // entity.is_function_like_macro(); + // trace!("macrodef", name); + } + _ => { + if preprocessing { + info!("done preprocessing"); + } + preprocessing = false; + // No more includes / macro expansions after this line + let file = library.files.get_mut(&file_name).expect("file"); + for stmt in Stmt::parse(&entity, config, ¯o_invocations) { + file.add_stmt(stmt); + } + } + } + } else { + trace!("library not found"); + } + } + EntityVisitResult::Continue + }); + + result +} + +fn get_translation_unit<'i: 'tu, 'tu>( + index: &'i Index<'tu>, + sdk: &SdkPath, + llvm_target: &str, +) -> TranslationUnit<'tu> { + let _span = info_span!("initializing translation unit").entered(); + + let target = format!("--target={llvm_target}"); + + 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", + "-isysroot", + sdk.path.to_str().unwrap(), + ]) + .parse() + .unwrap(); + + // 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); + + tu +} + +pub fn extract_framework_name( + entity: &Entity<'_>, + framework_dir: &Path, +) -> Option<(String, String)> { + 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_name = components + .next() + .expect("components next") + .as_os_str() + .to_str() + .expect("component to_str") + .strip_suffix(".framework") + .expect("framework fileending") + .to_string(); + + let path = components.as_path(); + let file_name = path + .file_stem() + .expect("path file stem") + .to_string_lossy() + .to_string(); + + return Some((library_name, file_name)); + } + } + } + None +} diff --git a/crates/header-translator/src/method.rs b/crates/header-translator/src/method.rs new file mode 100644 index 000000000..069a1aaed --- /dev/null +++ b/crates/header-translator/src/method.rs @@ -0,0 +1,479 @@ +use std::fmt; + +use clang::{Entity, EntityKind, ObjCQualifiers}; +use tracing::span::EnteredSpan; +use tracing::{debug_span, error, warn}; + +use crate::availability::Availability; +use crate::config::MethodData; +use crate::immediate_children; +use crate::objc2_utils::in_selector_family; +use crate::property::PartialProperty; +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, "{self:?} did not match {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, + "{self:?} did not match {sel}" + ); + } else { + if self == Self::ReturnsInnerPointer { + return; + } + assert!(self == Self::Normal, "{self:?} did not match {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, PartialEq)] +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 arguments: Vec<(String, Option, Ty)>, + pub result_type: Ty, + pub safe: bool, + pub mutating: 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 _span = debug_span!("method", fn_name).entered(); + + let is_class = match entity.get_kind() { + EntityKind::ObjCInstanceMethodDecl => false, + EntityKind::ObjCClassMethodDecl => true, + _ => unreachable!("unknown method kind"), + }; + + PartialMethod { + entity, + selector, + is_class, + fn_name, + _span, + } + } + + /// Takes `EntityKind::ObjCPropertyDecl`. + pub fn partial_property(entity: Entity<'_>) -> PartialProperty<'_> { + let attributes = entity.get_objc_attributes(); + let has_setter = attributes.map(|a| !a.readonly).unwrap_or(true); + + let name = entity.get_display_name().expect("property name"); + let _span = debug_span!("property", name).entered(); + + PartialProperty { + entity, + 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, + _span, + } + } + + pub fn update(mut self, data: MethodData) -> Option { + if data.skipped { + return None; + } + + self.mutating = data.mutating; + self.safe = !data.unsafe_; + + Some(self) + } +} + +#[derive(Debug)] +pub struct PartialMethod<'tu> { + entity: Entity<'tu>, + selector: String, + pub is_class: bool, + pub fn_name: String, + _span: EnteredSpan, +} + +impl<'tu> PartialMethod<'tu> { + pub fn parse(self, data: MethodData) -> Option<(bool, Method)> { + let Self { + entity, + selector, + is_class, + fn_name, + _span, + } = self; + + if data.skipped { + return None; + } + + if entity.is_variadic() { + warn!("Can't handle variadic method"); + 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 _span = debug_span!("method argument", name).entered(); + let qualifier = entity.get_objc_qualifiers().map(Qualifier::parse); + let mut is_consumed = false; + + immediate_children(&entity, |entity, _span| match entity.get_kind() { + EntityKind::ObjCClassRef + | EntityKind::ObjCProtocolRef + | EntityKind::TypeRef + | EntityKind::ParmDecl => { + // Ignore + } + EntityKind::NSConsumed => { + if is_consumed { + error!("got NSConsumed twice"); + } + is_consumed = true; + } + EntityKind::UnexposedAttr => { + if let Some(macro_) = UnexposedMacro::parse(&entity) { + warn!(?macro_, "unknown macro"); + } + } + // For some reason we recurse into array types + EntityKind::IntegerLiteral => {} + _ => warn!("unknown"), + }); + + 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); + error!(?qualifier, "unexpected qualifier on return type"); + } + + 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; + + immediate_children(&entity, |entity, _span| match entity.get_kind() { + EntityKind::ObjCClassRef + | EntityKind::ObjCProtocolRef + | EntityKind::TypeRef + | EntityKind::ParmDecl => { + // Ignore + } + EntityKind::ObjCDesignatedInitializer => { + if designated_initializer { + error!("encountered ObjCDesignatedInitializer twice"); + } + designated_initializer = true; + } + EntityKind::NSConsumesSelf => { + consumes_self = true; + } + EntityKind::NSReturnsRetained => { + if memory_management != MemoryManagement::Normal { + error!("got unexpected NSReturnsRetained") + } + memory_management = MemoryManagement::ReturnsRetained; + } + EntityKind::ObjCReturnsInnerPointer => { + if memory_management != MemoryManagement::Normal { + error!("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) { + warn!(?macro_, "unknown macro"); + } + } + _ => warn!("unknown"), + }); + + if consumes_self { + if memory_management != MemoryManagement::ReturnsRetained { + error!("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); + } + + if data.mutating && (is_class || MemoryManagement::is_init(&selector)) { + error!("invalid mutating method"); + } + + Some(( + designated_initializer, + Method { + selector, + fn_name, + availability, + is_class, + is_optional_protocol: entity.is_objc_optional(), + memory_management, + arguments, + result_type, + safe: !data.unsafe_, + mutating: data.mutating, + }, + )) + } +} + +impl Method { + pub(crate) fn emit_on_subclasses(&self) -> bool { + if !self.result_type.is_instancetype() { + return false; + } + if self.is_class { + !matches!(&*self.selector, "new" | "supportsSecureCoding") + } else { + self.memory_management == MemoryManagement::Init + && !matches!(&*self.selector, "init" | "initWithCoder:") + } + } +} + +impl fmt::Display for Method { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let _span = debug_span!("method", self.fn_name).entered(); + + 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 if self.mutating { + write!(f, "&mut self, ")?; + } 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/output.rs b/crates/header-translator/src/output.rs new file mode 100644 index 000000000..6207ccef6 --- /dev/null +++ b/crates/header-translator/src/output.rs @@ -0,0 +1,31 @@ +use std::collections::BTreeMap; + +use tracing::debug_span; + +use crate::library::Library; + +#[derive(Debug, PartialEq)] +pub struct Output { + pub libraries: BTreeMap, +} + +impl Output { + pub fn from_configs(libraries: impl IntoIterator>) -> Self { + let libraries = libraries + .into_iter() + .map(|name| (name.into(), Library::new())) + .collect(); + Self { libraries } + } + + pub fn compare(&self, other: &Self) { + super::compare_btree( + &self.libraries, + &other.libraries, + |libary_name, self_library, other_library| { + let _span = debug_span!("library", libary_name).entered(); + self_library.compare(other_library); + }, + ); + } +} diff --git a/crates/header-translator/src/property.rs b/crates/header-translator/src/property.rs new file mode 100644 index 000000000..1c978185f --- /dev/null +++ b/crates/header-translator/src/property.rs @@ -0,0 +1,141 @@ +use clang::{Entity, EntityKind, Nullability, ObjCAttributes}; +use tracing::span::EnteredSpan; +use tracing::{error, warn}; + +use crate::availability::Availability; +use crate::config::MethodData; +use crate::immediate_children; +use crate::method::{MemoryManagement, Method, Qualifier}; +use crate::rust_type::Ty; +use crate::unexposed_macro::UnexposedMacro; + +#[derive(Debug)] +pub struct PartialProperty<'tu> { + pub entity: Entity<'tu>, + pub name: String, + pub getter_name: String, + pub setter_name: Option, + pub is_class: bool, + pub attributes: Option, + pub _span: EnteredSpan, +} + +impl PartialProperty<'_> { + pub fn parse( + self, + getter_data: MethodData, + setter_data: Option, + ) -> (Option, Option) { + let Self { + entity, + name, + getter_name, + setter_name, + is_class, + attributes, + _span, + } = self; + + 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 mut memory_management = MemoryManagement::Normal; + + immediate_children(&entity, |entity, _span| match entity.get_kind() { + EntityKind::ObjCClassRef + | EntityKind::ObjCProtocolRef + | EntityKind::TypeRef + | EntityKind::ParmDecl => { + // Ignore + } + EntityKind::ObjCReturnsInnerPointer => { + if memory_management != MemoryManagement::Normal { + error!(?memory_management, "unexpected ObjCReturnsInnerPointer") + } + memory_management = MemoryManagement::ReturnsInnerPointer; + } + EntityKind::ObjCInstanceMethodDecl => { + warn!("method in property"); + } + EntityKind::IbOutletAttr => { + // TODO: What is this? + } + EntityKind::UnexposedAttr => { + if let Some(macro_) = UnexposedMacro::parse(&entity) { + warn!(?macro_, "unknown macro"); + } + } + _ => warn!("unknown"), + }); + + let qualifier = entity.get_objc_qualifiers().map(Qualifier::parse); + if qualifier.is_some() { + error!("properties do not support qualifiers"); + } + + let getter = if !getter_data.skipped { + let ty = Ty::parse_property_return( + entity.get_type().expect("property type"), + default_nullability, + ); + + Some(Method { + selector: getter_name.clone(), + fn_name: getter_name, + availability: availability.clone(), + is_class, + is_optional_protocol: entity.is_objc_optional(), + memory_management, + arguments: Vec::new(), + result_type: ty, + safe: !getter_data.unsafe_, + mutating: getter_data.mutating, + }) + } else { + None + }; + + let setter = if let Some(setter_name) = setter_name { + let setter_data = setter_data.expect("setter_data must be present if setter_name was"); + if !setter_data.skipped { + let ty = Ty::parse_property( + entity.get_type().expect("property type"), + Nullability::Unspecified, + ); + + Some(Method { + selector: setter_name.clone() + ":", + fn_name: setter_name, + availability, + is_class, + is_optional_protocol: entity.is_objc_optional(), + memory_management, + arguments: vec![(name, None, ty)], + result_type: Ty::VOID_RESULT, + safe: !setter_data.unsafe_, + mutating: setter_data.mutating, + }) + } else { + None + } + } else { + None + }; + + (getter, setter) + } +} diff --git a/crates/header-translator/src/rust_type.rs b/crates/header-translator/src/rust_type.rs new file mode 100644 index 000000000..3159678d0 --- /dev/null +++ b/crates/header-translator/src/rust_type.rs @@ -0,0 +1,1320 @@ +use std::fmt; + +use clang::{CallingConvention, Nullability, Type, TypeKind}; +use serde::Deserialize; +use tracing::{debug_span, error, warn}; + +use crate::method::MemoryManagement; + +#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[serde(from = "bool")] +pub enum Ownership { + Owned, + Shared, +} + +impl From for Ownership { + fn from(b: bool) -> Self { + if b { + Self::Owned + } else { + Self::Shared + } + } +} + +impl Default for Ownership { + fn default() -> Self { + Ownership::Shared + } +} + +impl fmt::Display for Ownership { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Owned => write!(f, "Owned"), + Self::Shared => write!(f, "Shared"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +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: _, + ownership: _, + } => type_, + // TODO: Handle this better + RustType::Class { nullability: _ } => Self { + name: "TodoClass".to_string(), + generics: Vec::new(), + }, + param => { + panic!("invalid generic parameter {param:?} in {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 { + // TODO: Handle this better! + match &*self.name { + "NSCopying" => write!(f, "Object")?, + "NSMutableCopying" => write!(f, "Object")?, + name => write!(f, "{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; + while modified_ty.get_kind() == TypeKind::Attributed { + // debug!("{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 + warn!("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(); + error!("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, + ownership: Ownership, + }, + 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 { + let _span = debug_span!("ty", ?ty, is_consumed); + + // debug!("{:?}, {:?}", 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, + ); + + // debug!("{:?}: {:?}", ty.get_kind(), ty.get_display_name()); + + use TypeKind::*; + 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, + ownership: Ownership::Shared, + }, + 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, + ownership: Ownership::Shared, + } + } + 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, + ownership: Ownership::Shared, + }, + _ => { + 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, + ownership: Ownership::Shared, + } + } + _ => 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, + ownership: _, + } => { + 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, "Option") + } + } + 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, "ArrayUnknownABI<[{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 { + MethodReturn, + FnDeclReturn, + MethodReturnWithError, + Static, + Typedef, + MethodArgument, + FnDeclArgument, + Struct, + Enum, + FnArgument, + FnReturn, + BlockArgument, + BlockReturn, +} + +#[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::MethodReturn, + }; + + 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::MethodArgument, + } + } + + 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::MethodReturn, + } + } + + pub fn parse_function_argument(ty: Type<'_>) -> Self { + let mut this = Self::parse_method_argument(ty, false); + this.kind = TyKind::FnDeclArgument; + this + } + + pub fn parse_function_return(ty: Type<'_>) -> Self { + let mut this = Self::parse_method_return(ty); + this.kind = TyKind::FnDeclReturn; + 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::Typedef, + }) + } + RustType::IncompleteArray { .. } => { + unimplemented!("incomplete array in struct") + } + _ => Some(Self { + ty, + kind: TyKind::Typedef, + }), + } + } + + 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::MethodArgument, + } + } + + 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::MethodReturn, + } + } + + 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::Struct, + } + } + + 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::Enum, + } + } + + 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::Static, + } + } + + 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::FnArgument, + } + } + + 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::FnReturn, + } + } + + fn set_block(&mut self) { + self.kind = match self.kind { + TyKind::FnArgument => TyKind::BlockArgument, + TyKind::FnReturn => TyKind::BlockReturn, + _ => unreachable!("set block kind"), + } + } + + pub(crate) fn set_ownership(&mut self, mut get_ownership: impl FnMut(&str) -> Ownership) { + assert!(matches!( + self.kind, + TyKind::MethodReturn | TyKind::MethodReturnWithError + )); + if let RustType::Id { + type_: ty, + ownership, + .. + } = &mut self.ty + { + *ownership = get_ownership(&ty.name); + } + } +} + +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, + ownership, + } = &**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:?}" + ); + assert_eq!(*ownership, Ownership::Shared, "errors cannot be owned"); + 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: _, + ownership: Ownership::Shared, + } if ty.name == "Self" && ty.generics.is_empty() => { + ty.name = "Allocated".into(); + ty.generics = vec![GenericType { + name: "Self".into(), + generics: vec![], + }]; + } + _ => error!(?self, "invalid alloc return type"), + } + } + + pub fn set_is_error(&mut self) { + assert_eq!(self.kind, TyKind::MethodReturn); + self.kind = TyKind::MethodReturnWithError; + } + + pub fn is_instancetype(&self) -> bool { + matches!(&self.ty, RustType::Id { + type_: ty, + .. + } if ty.name == "Self" && ty.generics.is_empty()) + } + + pub fn is_typedef_to(&self, s: &str) -> bool { + matches!(&self.ty, RustType::TypeDef { name } if name == s) + } + + /// 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"); + let is_related = if is_class { + MemoryManagement::is_new(selector) || MemoryManagement::is_alloc(selector) + } else { + MemoryManagement::is_init(selector) || selector == "self" + }; + + if is_related { + type_.name = "Self".into(); + } + } + } + } + + pub fn is_nsstring(&self) -> bool { + if let RustType::Id { type_: ty, .. } = &self.ty { + ty.name == "NSString" + } else { + false + } + } +} + +impl fmt::Display for Ty { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.kind { + TyKind::MethodReturn => { + 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, + ownership, + } => { + if *nullability == Nullability::NonNull { + write!(f, "Id<{ty}, {ownership}>") + } 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::MethodReturnWithError => match &self.ty { + RustType::Id { + type_: ty, + lifetime: Lifetime::Unspecified, + is_const: false, + nullability: Nullability::Nullable, + ownership, + } => { + // NULL -> error + write!(f, " -> Result, Id>") + } + RustType::ObjcBool => { + // NO -> error + write!(f, " -> Result<(), Id>") + } + _ => panic!("unknown error result type {self:?}"), + }, + TyKind::Static => match &self.ty { + RustType::Id { + type_: ty, + is_const: false, + lifetime: Lifetime::Strong | Lifetime::Unspecified, + nullability, + ownership: Ownership::Shared, + } => { + 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::Typedef => 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, + ownership: Ownership::Shared, + } => { + 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::MethodArgument | TyKind::FnDeclArgument => match &self.ty { + RustType::Id { + type_: ty, + is_const: false, + lifetime: Lifetime::Unspecified | Lifetime::Strong, + nullability, + ownership: Ownership::Shared, + } => { + 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::MethodArgument => 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, + // ownership: Ownership::Shared, + // } if self.kind == TyKind::MethodArgument => { + // 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::Struct => match &self.ty { + RustType::Array { + element_type, + num_elements, + } => write!(f, "[{element_type}; {num_elements}]"), + ty => write!(f, "{ty}"), + }, + TyKind::Enum => write!(f, "{}", self.ty), + TyKind::FnArgument | TyKind::BlockArgument => write!(f, "{}", self.ty), + TyKind::FnDeclReturn | TyKind::FnReturn => { + if let RustType::Void = &self.ty { + // Don't output anything + return Ok(()); + } + + write!(f, " -> {}", self.ty) + } + TyKind::BlockReturn => 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..cee3855d4 --- /dev/null +++ b/crates/header-translator/src/stmt.rs @@ -0,0 +1,1096 @@ +use std::borrow::Cow; +use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::iter; +use std::mem; + +use clang::source::Location; +use clang::{Entity, EntityKind, EntityVisitResult}; +use tracing::{debug, debug_span, error, warn}; + +use crate::availability::Availability; +use crate::config::{ClassData, Config}; +use crate::expr::Expr; +use crate::immediate_children; +use crate::method::{handle_reserved, Method}; +use crate::rust_type::{GenericType, Ownership, Ty}; +use crate::unexposed_macro::UnexposedMacro; + +#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct Derives(Cow<'static, str>); + +impl Default for Derives { + fn default() -> Self { + Derives("Debug, PartialEq, Eq, Hash".into()) + } +} + +impl fmt::Display for Derives { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.0.is_empty() { + write!(f, "#[derive({})]", self.0)?; + } + Ok(()) + } +} + +fn parse_superclass<'ty>(entity: &Entity<'ty>) -> Option<(Entity<'ty>, GenericType)> { + let mut superclass = None; + let mut generics = Vec::new(); + + immediate_children(entity, |entity, _span| match entity.get_kind() { + EntityKind::ObjCSuperClassRef => { + superclass = Some(entity); + } + EntityKind::TypeRef => { + let name = entity.get_name().expect("typeref name"); + generics.push(GenericType { + name, + generics: Vec::new(), + }); + } + _ => {} + }); + + superclass.map(|entity| { + ( + entity + .get_reference() + .expect("ObjCSuperClassRef to reference entity"), + GenericType { + name: entity.get_name().expect("superclass name"), + generics, + }, + ) + }) +} + +/// Takes one of: +/// - `EntityKind::ObjCInterfaceDecl` +/// - `EntityKind::ObjCProtocolDecl` +/// - `EntityKind::ObjCCategoryDecl` +fn parse_objc_decl( + entity: &Entity<'_>, + superclass: bool, + mut generics: Option<&mut Vec>, + data: Option<&ClassData>, +) -> (Vec, Vec, Vec) { + let mut protocols = Vec::new(); + let mut methods = Vec::new(); + let mut designated_initializers = 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(); + + immediate_children(entity, |entity, span| match entity.get_kind() { + EntityKind::ObjCExplicitProtocolImpl if generics.is_none() && !superclass => { + // TODO NS_PROTOCOL_REQUIRES_EXPLICIT_IMPLEMENTATION + } + EntityKind::ObjCIvarDecl if superclass => { + // Explicitly ignored + } + EntityKind::ObjCSuperClassRef | EntityKind::TypeRef if superclass => { + // Parsed in parse_superclass + } + EntityKind::ObjCRootClass => { + debug!("parsing root class"); + } + EntityKind::ObjCClassRef if generics.is_some() => { + // debug!("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(GenericType { + name, + generics: Vec::new(), + }); + } else { + error!("unsupported generics"); + } + } + EntityKind::ObjCProtocolRef => { + protocols.push(entity.get_name().expect("protocolref to have name")); + } + EntityKind::ObjCInstanceMethodDecl | EntityKind::ObjCClassMethodDecl => { + drop(span); + let partial = Method::partial(entity); + + if !properties.remove(&(partial.is_class, partial.fn_name.clone())) { + let data = ClassData::get_method_data(data, &partial.fn_name); + if let Some((designated_initializer, method)) = partial.parse(data) { + if designated_initializer { + designated_initializers.push(method.fn_name.clone()); + } + methods.push(method); + } + } + } + EntityKind::ObjCPropertyDecl => { + drop(span); + let partial = Method::partial_property(entity); + + 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" + ); + } + + let getter_data = ClassData::get_method_data(data, &partial.getter_name); + let setter_data = partial + .setter_name + .as_ref() + .map(|setter_name| ClassData::get_method_data(data, setter_name)); + + let (getter, setter) = partial.parse(getter_data, setter_data); + if let Some(getter) = getter { + methods.push(getter); + } + if let Some(setter) = setter { + methods.push(setter); + } + } + EntityKind::VisibilityAttr => { + // Already exposed as entity.get_visibility() + } + EntityKind::ObjCException if superclass => { + // Maybe useful for knowing when to implement `Error` for the type + } + EntityKind::UnexposedAttr => { + if let Some(macro_) = UnexposedMacro::parse(&entity) { + warn!(?macro_, "unknown macro"); + } + } + _ => warn!("unknown"), + }); + + if !properties.is_empty() { + if properties == HashSet::from([(false, "setDisplayName".to_owned())]) { + // TODO + } else { + error!( + ?methods, + ?properties, + "did not properly add methods to properties" + ); + } + } + + (protocols, methods, designated_initializers) +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Stmt { + /// @interface name: superclass + /// -> + /// extern_class! + ClassDecl { + ty: GenericType, + availability: Availability, + superclasses: Vec, + designated_initializers: Vec, + derives: Derives, + ownership: Ownership, + }, + /// @interface class_name (name) + /// -> + /// extern_methods! + Methods { + ty: GenericType, + availability: Availability, + methods: Vec, + /// For the categories that have a name (though some don't, see NSClipView) + category_name: Option, + description: Option, + }, + /// @protocol name + /// -> + /// extern_protocol! + ProtocolDecl { + name: String, + availability: Availability, + protocols: Vec, + methods: Vec, + }, + /// @interface ty: _ + /// @interface ty (_) + ProtocolImpl { + ty: GenericType, + availability: Availability, + protocol: String, + }, + /// 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, + kind: Option, + }, +} + +fn parse_struct(entity: &Entity<'_>, name: String) -> Stmt { + let mut boxable = false; + let mut fields = Vec::new(); + + immediate_children(entity, |entity, span| match entity.get_kind() { + EntityKind::UnexposedAttr => { + if let Some(macro_) = UnexposedMacro::parse(&entity) { + warn!(?macro_, "unknown macro"); + } + } + EntityKind::FieldDecl => { + drop(span); + let name = entity.get_name().expect("struct field name"); + let _span = debug_span!("field", name).entered(); + + let ty = entity.get_type().expect("struct field type"); + let ty = Ty::parse_struct_field(ty); + + if entity.is_bit_field() { + error!("unsound struct bitfield"); + } + + fields.push((name, ty)) + } + EntityKind::ObjCBoxable => { + boxable = true; + } + _ => warn!("unknown"), + }); + + Stmt::StructDecl { + name, + boxable, + fields, + } +} + +impl Stmt { + pub fn parse( + entity: &Entity<'_>, + config: &Config, + macro_invocations: &HashMap, String>, + ) -> Vec { + let _span = debug_span!( + "stmt", + kind = ?entity.get_kind(), + dbg = entity.get_name(), + ) + .entered(); + + match entity.get_kind() { + // These are inconsequential for us, since we resolve imports differently + EntityKind::ObjCClassRef | EntityKind::ObjCProtocolRef => vec![], + 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 vec![]; + } + + let availability = Availability::parse( + entity + .get_platform_availability() + .expect("class availability"), + ); + let mut generics = Vec::new(); + + let (protocols, methods, designated_initializers) = + parse_objc_decl(entity, true, Some(&mut generics), class_data); + + let ty = GenericType { name, generics }; + + let mut superclass_entity = *entity; + let mut superclasses = vec![]; + + while let Some((next_entity, superclass)) = parse_superclass(&superclass_entity) { + superclass_entity = next_entity; + superclasses.push(superclass); + } + + let methods = Self::Methods { + ty: ty.clone(), + availability: availability.clone(), + methods, + category_name: None, + description: None, + }; + + if !class_data + .map(|data| data.definition_skipped) + .unwrap_or_default() + { + iter::once(Self::ClassDecl { + ty: ty.clone(), + availability: availability.clone(), + superclasses, + designated_initializers, + derives: class_data + .map(|data| data.derives.clone()) + .unwrap_or_default(), + ownership: class_data + .map(|data| data.ownership.clone()) + .unwrap_or_default(), + }) + .chain(protocols.into_iter().map(|protocol| Self::ProtocolImpl { + ty: ty.clone(), + availability: availability.clone(), + protocol, + })) + .chain(iter::once(methods)) + .collect() + } else { + vec![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 vec![]; + } + + let mut class_generics = Vec::new(); + + let (protocols, methods, designated_initializers) = + parse_objc_decl(entity, false, Some(&mut class_generics), class_data); + + if !designated_initializers.is_empty() { + warn!( + ?designated_initializers, + "designated initializer in category" + ) + } + + let ty = GenericType { + name: class_name, + generics: class_generics, + }; + + iter::once(Self::Methods { + ty: ty.clone(), + availability: availability.clone(), + methods, + category_name: name, + description: None, + }) + .into_iter() + .chain(protocols.into_iter().map(|protocol| Self::ProtocolImpl { + ty: ty.clone(), + availability: availability.clone(), + protocol, + })) + .collect() + } + 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 vec![]; + } + + let availability = Availability::parse( + entity + .get_platform_availability() + .expect("protocol availability"), + ); + + let (protocols, methods, designated_initializers) = + parse_objc_decl(entity, false, None, protocol_data); + + if !designated_initializers.is_empty() { + warn!( + ?designated_initializers, + "designated initializer in protocol" + ) + } + + vec![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; + let mut kind = None; + + immediate_children(entity, |entity, _span| match entity.get_kind() { + EntityKind::UnexposedAttr => { + if let Some(macro_) = + UnexposedMacro::parse_plus_macros(&entity, macro_invocations) + { + if kind.is_some() { + panic!("got multiple unexposed macros {kind:?}, {macro_:?}"); + } + kind = Some(macro_); + } + } + EntityKind::StructDecl => { + if config + .struct_data + .get(&name) + .map(|data| data.skipped) + .unwrap_or_default() + { + skip_struct = true; + return; + } + + 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 => {} + _ => warn!("unknown"), + }); + + if let Some(struct_) = struct_ { + assert_eq!(kind, None, "should not have parsed anything"); + return vec![struct_]; + } + + if skip_struct { + return vec![]; + } + + if config + .typedef_data + .get(&name) + .map(|data| data.skipped) + .unwrap_or_default() + { + return vec![]; + } + + let ty = entity + .get_typedef_underlying_type() + .expect("typedef underlying type"); + if let Some(ty) = Ty::parse_typedef(ty) { + vec![Self::AliasDecl { name, ty, kind }] + } else { + vec![] + } + } + EntityKind::StructDecl => { + if let Some(name) = entity.get_name() { + if config + .struct_data + .get(&name) + .map(|data| data.skipped) + .unwrap_or_default() + { + return vec![]; + } + if !name.starts_with('_') { + return vec![parse_struct(entity, name)]; + } + } + vec![] + } + 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 vec![]; + } + + 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 vec![]; + } + + 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(); + + immediate_children(entity, |entity, _span| 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; + } + + let pointer_width = + entity.get_translation_unit().get_target().pointer_width; + + let val = Expr::from_val( + entity + .get_enum_constant_value() + .expect("enum constant value"), + is_signed, + pointer_width, + ); + 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:?}"); + } + }); + + if name.is_none() && variants.is_empty() { + return vec![]; + } + + vec![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 vec![]; + } + + let ty = entity.get_type().expect("var type"); + let ty = Ty::parse_static(ty); + let mut value = None; + + immediate_children(entity, |entity, _span| 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:?}"), + }); + + let value = match value { + Some(Some(expr)) => Some(expr), + Some(None) => { + warn!("skipped static"); + return vec![]; + } + None => None, + }; + + vec![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 vec![]; + } + + if entity.is_variadic() { + warn!("can't handle variadic function"); + return vec![]; + } + + 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(); + + if entity.is_static_method() { + warn!("unexpected static method"); + } + + immediate_children(entity, |entity, _span| match entity.get_kind() { + EntityKind::UnexposedAttr => { + if let Some(macro_) = UnexposedMacro::parse(&entity) { + warn!(?macro_, "unknown 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)) + } + _ => warn!("unknown"), + }); + + let body = if entity.is_inline_function() { + Some(()) + } else { + None + }; + + vec![Self::FnDecl { + name, + arguments, + result_type, + body, + }] + } + EntityKind::UnionDecl => { + // debug!( + // "union: {:?}, {:?}, {:#?}, {:#?}", + // entity.get_display_name(), + // entity.get_name(), + // entity.has_attributes(), + // entity.get_children(), + // ); + vec![] + } + _ => { + panic!("Unknown: {entity:?}") + } + } + } + + pub fn compare(&self, other: &Self) { + if self != other { + if let ( + Self::Methods { + methods: self_methods, + .. + }, + Self::Methods { + methods: other_methods, + .. + }, + ) = (&self, &other) + { + super::compare_slice( + self_methods, + other_methods, + |i, self_method, other_method| { + let _span = debug_span!("method", i).entered(); + assert_eq!(self_method, other_method, "methods were not equal"); + }, + ); + } + + panic!("statements were not equal:\n{self:#?}\n{other:#?}"); + } + } +} + +impl fmt::Display for Stmt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let _span = debug_span!("stmt", discriminant = ?mem::discriminant(self)).entered(); + + struct GenericTyHelper<'a>(&'a GenericType); + + impl fmt::Display for GenericTyHelper<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0.name)?; + if !self.0.generics.is_empty() { + write!(f, "<")?; + for generic in &self.0.generics { + write!(f, "{generic}, ")?; + } + for generic in &self.0.generics { + write!(f, "{generic}Ownership, ")?; + } + write!(f, ">")?; + } + Ok(()) + } + } + + struct GenericParamsHelper<'a>(&'a [GenericType]); + + impl fmt::Display for GenericParamsHelper<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.0.is_empty() { + write!(f, "<")?; + for generic in self.0 { + write!(f, "{generic}: Message, ")?; + } + for generic in self.0 { + write!(f, "{generic}Ownership: Ownership, ")?; + } + write!(f, ">")?; + } + Ok(()) + } + } + + match self { + Self::ClassDecl { + ty, + availability: _, + superclasses, + designated_initializers: _, + derives, + ownership: _, + } => { + // TODO: Use ty.get_objc_protocol_declarations() + + let macro_name = if ty.generics.is_empty() { + "extern_class" + } else { + "__inner_extern_class" + }; + + writeln!(f, "{macro_name}!(")?; + writeln!(f, " {derives}")?; + write!(f, " pub struct ")?; + if ty.generics.is_empty() { + write!(f, "{}", ty.name)?; + } else { + write!(f, "{}<", ty.name)?; + for generic in &ty.generics { + write!(f, "{generic}: Message = Object, ")?; + } + for generic in &ty.generics { + write!(f, "{generic}Ownership: Ownership = Shared, ")?; + } + write!(f, ">")?; + }; + if ty.generics.is_empty() { + writeln!(f, ";")?; + } else { + writeln!(f, " {{")?; + for (i, generic) in ty.generics.iter().enumerate() { + // Invariant over the generic (for now) + writeln!( + f, + "_inner{i}: PhantomData<*mut ({generic}, {generic}Ownership)>," + )?; + } + writeln!(f, "notunwindsafe: PhantomData<&'static mut ()>,")?; + writeln!(f, "}}")?; + } + writeln!(f)?; + writeln!( + f, + " unsafe impl{} ClassType for {} {{", + GenericParamsHelper(&ty.generics), + GenericTyHelper(ty) + )?; + let (superclass, rest) = superclasses.split_at(1); + let superclass = superclass.get(0).expect("must have a least one superclass"); + if !rest.is_empty() { + write!(f, " #[inherits(")?; + let mut iter = rest.iter(); + // Using generics in here is not technically correct, but + // should work for our use-cases. + if let Some(superclass) = iter.next() { + write!(f, "{}", GenericTyHelper(superclass))?; + } + for superclass in iter { + write!(f, ", {}", GenericTyHelper(superclass))?; + } + writeln!(f, ")]")?; + } + writeln!(f, " type Super = {};", GenericTyHelper(superclass))?; + writeln!(f, " }}")?; + writeln!(f, ");")?; + } + Self::Methods { + ty, + availability: _, + methods, + category_name, + description, + } => { + writeln!(f, "extern_methods!(")?; + if let Some(description) = description { + writeln!(f, " /// {description}")?; + if category_name.is_some() { + writeln!(f, " ///")?; + } + } + if let Some(category_name) = category_name { + writeln!(f, " /// {category_name}")?; + } + writeln!( + f, + " unsafe impl{} {} {{", + GenericParamsHelper(&ty.generics), + GenericTyHelper(ty) + )?; + for method in methods { + writeln!(f, "{method}")?; + } + writeln!(f, " }}")?; + writeln!(f, ");")?; + } + Self::ProtocolImpl { + ty: _, + availability: _, + protocol: _, + } => { + // TODO + } + Self::ProtocolDecl { + name, + availability: _, + protocols: _, + methods, + } => { + writeln!(f, "extern_protocol!(")?; + writeln!(f, " pub struct {name};")?; + writeln!(f)?; + writeln!(f, " unsafe impl ProtocolType for {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", + _ => panic!("invalid enum kind"), + }; + 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(param))?; + } + 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(param))?; + } + writeln!(f, "){result_type} {{")?; + writeln!(f, " todo!()")?; + writeln!(f, " }}")?; + writeln!(f, ");")?; + } + Self::AliasDecl { name, ty, kind } => { + match kind { + Some(UnexposedMacro::TypedEnum) => { + writeln!(f, "typed_enum!(pub type {name} = {ty};);")?; + } + Some(UnexposedMacro::TypedExtensibleEnum) => { + writeln!(f, "typed_extensible_enum!(pub type {name} = {ty};);")?; + } + None | Some(UnexposedMacro::BridgedTypedef) => { + // "bridged" typedefs should just use a normal type + // alias. + writeln!(f, "pub type {name} = {ty};")?; + } + kind => panic!("invalid alias kind {kind:?} for {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..c3b3d823f --- /dev/null +++ b/crates/header-translator/src/unexposed_macro.rs @@ -0,0 +1,83 @@ +use std::collections::HashMap; + +use clang::source::Location; +use clang::{Entity, EntityKind}; +use tracing::warn; + +/// Parts of `EntityKind::UnexposedAttr` that we can easily parse. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum UnexposedMacro { + Enum, + Options, + ClosedEnum, + ErrorEnum, + TypedEnum, + TypedExtensibleEnum, + BridgedTypedef, +} + +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), + "NS_TYPED_ENUM" => Some(Self::TypedEnum), + "NS_TYPED_EXTENSIBLE_ENUM" => Some(Self::TypedExtensibleEnum), + "NS_SWIFT_BRIDGED_TYPEDEF" => Some(Self::BridgedTypedef), + // 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" + | "OBJC_AVAILABLE" + | "OBJC_SWIFT_UNAVAILABLE" + | "OBJC_DEPRECATED" + | "APPKIT_API_UNAVAILABLE_BEGIN_MACCATALYST" => None, + name => { + warn!(name, "unknown unexposed macro"); + None + } + } + } + + pub fn parse(entity: &Entity<'_>) -> Option { + Self::parse_plus_macros(entity, &HashMap::new()) + } + + pub fn parse_plus_macros( + entity: &Entity<'_>, + macro_invocations: &HashMap, String>, + ) -> Option { + let location = entity.get_location().expect("unexposed attr location"); + + if let Some(macro_name) = macro_invocations.get(&location.get_spelling_location()) { + return Self::from_name(¯o_name); + } + + 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 { + None + } + } +} diff --git a/crates/icrate/CHANGELOG.md b/crates/icrate/CHANGELOG.md index bed2b15dc..c374441cb 100644 --- a/crates/icrate/CHANGELOG.md +++ b/crates/icrate/CHANGELOG.md @@ -16,6 +16,60 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed * **BREAKING**: Moved from `objc2::foundation` into `icrate::Foundation`. +* **BREAKING**: Changed the following methods: + - `NSString` + - `concat` -> `stringByAppendingString` + - `join_path` -> `stringByAppendingPathComponent` + - `has_prefix` -> `hasPrefix` + - `has_suffix` -> `hasSuffix` + - `NSMutableString` + - `from_nsstring` -> `stringWithString` + - `with_capacity` -> `stringWithCapacity` + - `push_nsstring` -> `appendString` + - `replace` -> `setString` + - `NSAttributedString` + - `init_with_attributes` -> `unsafe initWithString_attributes` + - `init_with_string` -> `initWithString` + - `new_with_attributes` -> `unsafe new_with_attributes` + - `len_utf16` -> `length` + - `NSMutableAttributedString` + - `replace` -> `setAttributedString` + - `NSBundle` + - `main` -> `mainBundle` + - `info` -> `infoDictionary` + - `NSDictionary` + - `keys_array` -> `allKeys` + - `into_values_array` -> `allValues` + - `NSMutableDictionary` + - `clear` -> `removeAllObjects` + - `NSMutableArray` + - `clear` -> `removeAllObjects` + - `NSMutableSet` + - `clear` -> `removeAllObjects` + - `NSError` + - `user_info` -> `userInfo` + - `localized_description` -> `localizedDescription` + - `NSException` + - `user_info` -> `userInfo` + - `NSMutableData` + - `from_data` -> `dataWithData` + - `with_capacity` -> `dataWithCapacity` + - `set_len` -> `setLength` + - `NSUUID` + - `new_v4` -> `UUID` + - `string` -> `UUIDString` + - `NSThread` + - `current` -> `currentThread` + - `main` -> `mainThread` + - `is_main` -> `isMainThread` + - `NSProcessInfo` + - `process_info` -> `processInfo` +* **BREAKING**: Make `NSComparisonResult` work like all other enums. +* **BREAKING**: Changed `NSDictionary` to be `Shared` by default. +* **BREAKING** (TEMPORARY): Renamed `NSEnumerator`, `NSFastEnumeration` and + `NSFastEnumerator` until the story around them are properly figured out. +* **BREAKING**: Make `NSArray::objects_in_range` return an `Option` (it was + unsound before). ### Fixed * Fixed `NSZone` not being `#[repr(C)]`. diff --git a/crates/icrate/Cargo.toml b/crates/icrate/Cargo.toml index 9536b3961..ebe1b0214 100644 --- a/crates/icrate/Cargo.toml +++ b/crates/icrate/Cargo.toml @@ -27,7 +27,7 @@ uuid = { version = "1.1.2", optional = true, default-features = false } [package.metadata.docs.rs] default-target = "x86_64-apple-darwin" -features = ["block", "objective-c", "uuid", "unstable-all-frameworks", "unstable-docsrs"] +features = ["block", "objective-c", "uuid", "unstable-frameworks-all", "unstable-docsrs"] targets = [ # MacOS @@ -92,4 +92,13 @@ AppKit = ["Foundation", "CoreData"] AuthenticationServices = ["Foundation"] CoreData = ["Foundation"] Foundation = ["objective-c", "block"] -unstable-all-frameworks = ["AppKit", "AuthenticationServices", "CoreData", "Foundation"] + +# Helps with CI +unstable-frameworks-all = ["AppKit", "AuthenticationServices", "CoreData", "Foundation"] +unstable-frameworks-gnustep = ["AppKit", "Foundation"] +unstable-frameworks-gnustep-32bit = ["Foundation"] +unstable-frameworks-ios = ["AuthenticationServices", "CoreData", "Foundation"] +unstable-frameworks-macos-10-7 = ["AppKit", "CoreData", "Foundation"] +unstable-frameworks-macos-10-13 = ["unstable-frameworks-macos-10-7"] +unstable-frameworks-macos-11 = ["unstable-frameworks-macos-10-13", "AuthenticationServices"] +unstable-frameworks-macos-12 = ["unstable-frameworks-macos-11"] diff --git a/crates/icrate/README.md b/crates/icrate/README.md index 13471be5b..eaed779b5 100644 --- a/crates/icrate/README.md +++ b/crates/icrate/README.md @@ -10,7 +10,7 @@ Rust bindings to Apple's frameworks. These are automatically generated from the SDKs in Xcode 14.0.1 (will be periodically updated). Currently supports: -- macOS: `10.7-12.3` (WIP) +- macOS: `10.7-12.3` - iOS/iPadOS: `7.0-16.0` (WIP) - tvOS: `9.0-16.0` (WIP) - watchOS: `1.0-9.0` (WIP) diff --git a/crates/icrate/src/AppKit/fixes/mod.rs b/crates/icrate/src/AppKit/fixes/mod.rs index 8b1378917..694351336 100644 --- a/crates/icrate/src/AppKit/fixes/mod.rs +++ b/crates/icrate/src/AppKit/fixes/mod.rs @@ -1 +1,54 @@ +#![allow(clippy::bool_to_int_with_if)] +use crate::common::*; +use crate::AppKit::NSResponder; +use crate::Foundation::NSObject; +/// (!TARGET_CPU_X86_64 || (TARGET_OS_IPHONE && !TARGET_OS_MACCATALYST)) +/// +/// https://github.com/xamarin/xamarin-macios/issues/12111 +// TODO: Make this work with mac catalyst +const TARGET_ABI_USES_IOS_VALUES: bool = + !cfg!(any(target_arch = "x86", target_arch = "x86_64")) || cfg!(not(target_os = "macos")); + +ns_enum!( + #[underlying(NSInteger)] + pub enum NSImageResizingMode { + NSImageResizingModeStretch = if TARGET_ABI_USES_IOS_VALUES { 0 } else { 1 }, + NSImageResizingModeTile = if TARGET_ABI_USES_IOS_VALUES { 1 } else { 0 }, + } +); + +ns_enum!( + #[underlying(NSInteger)] + pub enum NSTextAlignment { + NSTextAlignmentLeft = 0, + NSTextAlignmentRight = if TARGET_ABI_USES_IOS_VALUES { 2 } else { 1 }, + NSTextAlignmentCenter = if TARGET_ABI_USES_IOS_VALUES { 1 } else { 2 }, + NSTextAlignmentJustified = 3, + NSTextAlignmentNatural = 4, + } +); + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub struct NSPopover; + + unsafe impl ClassType for NSPopover { + #[inherits(NSObject)] + type Super = NSResponder; + } +); + +__inner_extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub struct NSLayoutAnchor { + _inner0: PhantomData<*mut (AnchorType, AnchorTypeOwnership)>, + notunwindsafe: PhantomData<&'static mut ()>, + } + + unsafe impl ClassType + for NSLayoutAnchor + { + type Super = NSObject; + } +); diff --git a/crates/icrate/src/AppKit/mod.rs b/crates/icrate/src/AppKit/mod.rs index e8d57a1ac..c88f4468c 100644 --- a/crates/icrate/src/AppKit/mod.rs +++ b/crates/icrate/src/AppKit/mod.rs @@ -4,3 +4,7 @@ mod generated; pub use self::fixes::*; pub use self::generated::*; + +#[cfg_attr(feature = "apple", link(name = "AppKit", kind = "framework"))] +#[cfg_attr(feature = "gnustep-1-7", link(name = "gnustep-gui", kind = "dylib"))] +extern "C" {} diff --git a/crates/icrate/src/AppKit/translation-config.toml b/crates/icrate/src/AppKit/translation-config.toml new file mode 100644 index 000000000..e20d3ee0b --- /dev/null +++ b/crates/icrate/src/AppKit/translation-config.toml @@ -0,0 +1,247 @@ +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 property and method +[class.NSDrawer.methods.open] +skipped = true +[class.NSDrawer.methods.close] +skipped = true +[class.NSFormCell.methods.titleWidth] +skipped = true +[class.NSSliderCell.methods.drawKnob] +skipped = true +[class.NSWorkspace.methods.noteFileSystemChanged] +skipped = true + +# Duplicated method +[class.NSSlider.methods.isVertical] +skipped = true +[class.NSSliderCell.methods.isVertical] +skipped = true +[class.NSGestureRecognizer.methods.state] +skipped = true + +# Both instance and class methods +[class.NSCursor.methods.pop] +skipped = true +[class.NSEvent.methods.modifierFlags] +skipped = true +[class.NSGraphicsContext.methods.saveGraphicsState] +skipped = true +[class.NSGraphicsContext.methods.restoreGraphicsState] +skipped = true +[class.NSBundle.methods.loadNibFile_externalNameTable_withZone] +skipped = true + +# Uses stuff from different frameworks / system libraries +[class.NSAnimationContext.methods.timingFunction] +skipped = true +[class.NSAnimationContext.methods.setTimingFunction] +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.methods.CGImage] +skipped = true +[class.NSColor.methods.CGColor] +skipped = true +[class.NSColor.methods.colorWithCGColor] +skipped = true +[class.NSColor.methods.colorWithCIColor] +skipped = true +[class.NSColorSpace.methods.initWithCGColorSpace] +skipped = true +[class.NSColorSpace.methods.CGColorSpace] +skipped = true +[class.NSCIImageRep] +skipped = true +[class.NSEvent.methods.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.methods.glyphID] +skipped = true +[class.NSGraphicsContext.methods.graphicsContextWithCGContext_flipped] +skipped = true +[class.NSGraphicsContext.methods.CGContext] +skipped = true +[class.NSGraphicsContext.methods.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.methods.processIdentifier] +skipped = true +[class.NSRunningApplication.methods.runningApplicationWithProcessIdentifier] +skipped = true +[class.NSSavePanel.methods.allowedContentTypes] +skipped = true +[class.NSSavePanel.methods.setAllowedContentTypes] +skipped = true +[class.NSView.methods] +layer = { skipped = true } +setLayer = { skipped = true } +backgroundFilters = { skipped = true } +setBackgroundFilters = { skipped = true } +compositingFilter = { skipped = true } +setCompositingFilter = { skipped = true } +contentFilters = { skipped = true } +setContentFilters = { skipped = true } +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.methods.architecture] +skipped = true +[class.NSWorkspaceOpenConfiguration.methods.setArchitecture] +skipped = true +[protocol.NSApplicationDelegate.methods] +application_handlerForIntent = { skipped = true } +application_userDidAcceptCloudKitShareWithMetadata = { skipped = true } +[protocol.NSLayoutManagerDelegate.methods] +layoutManager_shouldGenerateGlyphs_properties_characterIndexes_font_forGlyphRange = { skipped = true } +[protocol.NSCloudSharingServiceDelegate.methods] +sharingService_didSaveShare = { skipped = true } +sharingService_didStopSharing = { skipped = true } +[protocol.NSCloudSharingValidation] +skipped = true +[protocol.NSViewLayerContentScaleDelegate] +skipped = true + +# Uses a pointer to SEL, which doesn't implement Encode yet +[protocol.NSMenuDelegate.methods] +menuHasKeyEquivalent_forEvent_target_action = { skipped = true } + +# These subclass a generic struct, and hence the type parameter defaults to +# `Object`, which is not PartialEq, Eq nor Hash. +[class.NSLayoutXAxisAnchor] +derives = "Debug" +[class.NSLayoutYAxisAnchor] +derives = "Debug" +[class.NSLayoutDimension] +derives = "Debug" + +# 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 + +# Different definitions depending on target +[enum.NSImageResizingMode] +skipped = true +[enum.NSTextAlignment] +skipped = true + +# Different definitions depending on deployment target +[class.NSLayoutAnchor] +definition-skipped = true +[class.NSPopover] +definition-skipped = true +[class.NSPopover.methods] +appearance = { skipped = true } +setAppearance = { skipped = true } +effectiveAppearance = { skipped = true } + +# Protocol class methods (currently unsupported) +[protocol.NSAnimatablePropertyContainer.methods.defaultAnimationForKey] +skipped = true +[protocol.NSPasteboardReading.methods.readableTypesForPasteboard] +skipped = true +[protocol.NSPasteboardReading.methods.readingOptionsForType_pasteboard] +skipped = true +[protocol.NSWindowRestoration.methods.restoreWindowWithIdentifier_state_completionHandler] +skipped = true + +# I'm unsure of the ABI of the array this takes +[fn.NSDrawBitmap] +skipped = true +[class.NSView.methods.getRectsExposedDuringLiveResize_count] +skipped = true diff --git a/crates/icrate/src/AuthenticationServices/fixes/mod.rs b/crates/icrate/src/AuthenticationServices/fixes/mod.rs index 8b1378917..2f372dcc8 100644 --- a/crates/icrate/src/AuthenticationServices/fixes/mod.rs +++ b/crates/icrate/src/AuthenticationServices/fixes/mod.rs @@ -1 +1,40 @@ +use objc2::{extern_class, ClassType}; +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; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub struct ASCredentialProviderViewController; + + unsafe impl ClassType for ASCredentialProviderViewController { + type Super = ASViewController; + } +); + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub struct ASAccountAuthenticationModificationViewController; + + unsafe impl ClassType for ASAccountAuthenticationModificationViewController { + type Super = ASViewController; + } +); + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub struct ASAuthorizationAppleIDButton; + + unsafe impl ClassType for ASAuthorizationAppleIDButton { + type Super = ASControl; + } +); diff --git a/crates/icrate/src/AuthenticationServices/mod.rs b/crates/icrate/src/AuthenticationServices/mod.rs index 851f9e90c..76dd901f9 100644 --- a/crates/icrate/src/AuthenticationServices/mod.rs +++ b/crates/icrate/src/AuthenticationServices/mod.rs @@ -4,3 +4,6 @@ mod generated; pub use self::fixes::*; pub use self::generated::*; + +#[link(name = "AuthenticationServices", kind = "framework")] +extern "C" {} diff --git a/crates/icrate/src/AuthenticationServices/translation-config.toml b/crates/icrate/src/AuthenticationServices/translation-config.toml new file mode 100644 index 000000000..c99b9b9a8 --- /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 + +# The original superclass typedef is a bit difficult to extract from the +# superclass name, so let's just overwrite it. +[class.ASCredentialProviderViewController] +definition-skipped = true +[class.ASAccountAuthenticationModificationViewController] +definition-skipped = true + +# Specifies superclass as UIControl or NSControl conditionally +[class.ASAuthorizationAppleIDButton] +definition-skipped = true diff --git a/crates/icrate/src/CoreData/mod.rs b/crates/icrate/src/CoreData/mod.rs index dff7c9c5b..6a45f4bfa 100644 --- a/crates/icrate/src/CoreData/mod.rs +++ b/crates/icrate/src/CoreData/mod.rs @@ -2,5 +2,9 @@ mod fixes; #[path = "../generated/CoreData/mod.rs"] mod generated; +#[allow(unreachable_pub)] pub use self::fixes::*; pub use self::generated::*; + +#[cfg_attr(feature = "apple", link(name = "CoreData", kind = "framework"))] +extern "C" {} diff --git a/crates/icrate/src/CoreData/translation-config.toml b/crates/icrate/src/CoreData/translation-config.toml new file mode 100644 index 000000000..4d10287be --- /dev/null +++ b/crates/icrate/src/CoreData/translation-config.toml @@ -0,0 +1,37 @@ +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.methods] +databaseScope = { skipped = true } +setDatabaseScope = { skipped = true } +[protocol.NSFetchedResultsControllerDelegate.methods] +controller_didChangeContentWithSnapshot = { skipped = true } diff --git a/crates/icrate/src/Foundation/additions/__ns_string.rs b/crates/icrate/src/Foundation/__ns_string.rs similarity index 100% rename from crates/icrate/src/Foundation/additions/__ns_string.rs rename to crates/icrate/src/Foundation/__ns_string.rs diff --git a/crates/icrate/src/Foundation/additions/array.rs b/crates/icrate/src/Foundation/additions/array.rs index eed927f11..685fafe96 100644 --- a/crates/icrate/src/Foundation/additions/array.rs +++ b/crates/icrate/src/Foundation/additions/array.rs @@ -1,78 +1,39 @@ use alloc::vec::Vec; use core::fmt; -use core::marker::PhantomData; +use core::mem; use core::ops::{Index, IndexMut, Range}; use core::panic::{RefUnwindSafe, UnwindSafe}; +use core::ptr::NonNull; -use super::{ - NSCopying, NSEnumerator, NSFastEnumeration, NSFastEnumerator, NSMutableArray, NSMutableCopying, - NSObject, NSRange, -}; use objc2::rc::{DefaultId, Id, Owned, Ownership, Shared, SliceId}; use objc2::runtime::Object; -use objc2::{ClassType, Message, __inner_extern_class, extern_methods, msg_send, msg_send_id}; - -__inner_extern_class!( - /// An immutable ordered collection of objects. - /// - /// This is the Objective-C equivalent of a "boxed slice" (`Box<[T]>`), - /// so effectively a `Vec` where you can't change the number of - /// elements. - /// - /// The type of the contained objects is described by the generic - /// parameter `T`, and the ownership of the objects is described with the - /// generic parameter `O`. - /// - /// - /// # Ownership - /// - /// While `NSArray` _itself_ is immutable, i.e. the number of objects it - /// contains can't change, it is still possible to modify the contained - /// objects themselves, if you know you're the sole owner of them - - /// quite similar to how you can modify elements in `Box<[T]>`. - /// - /// To mutate the contained objects the ownership must be `O = Owned`. A - /// summary of what the different "types" of arrays allow you to do can be - /// found below. `Array` refers to either `NSArray` or `NSMutableArray`. - /// - `Id, Owned>`: Allows you to mutate the - /// objects, and the array itself. - /// - `Id, Owned>`: Allows you to mutate the - /// array itself, but not it's contents. - /// - `Id, Owned>`: Allows you to mutate the objects, - /// but not the array itself. - /// - `Id, Owned>`: Effectively the same as the below. - /// - `Id, Shared>`: Allows you to copy the array, but - /// does not allow you to modify it in any way. - /// - `Id, Shared>`: Pretty useless compared to the - /// others, avoid this. - /// - /// See [Apple's documentation][apple-doc]. - /// - /// [apple-doc]: https://developer.apple.com/documentation/foundation/nsarray?language=objc - // `T: PartialEq` bound correct because `NSArray` does deep (instead of - // shallow) equality comparisons. - #[derive(PartialEq, Eq, Hash)] - pub struct NSArray { - item: PhantomData>, - notunwindsafe: PhantomData<&'static mut ()>, - } +use objc2::{extern_methods, msg_send, msg_send_id, ClassType, Message}; - unsafe impl ClassType for NSArray { - type Super = NSObject; - } -); +use crate::Foundation::{ + NSArray, NSCopying, NSEnumerator2, NSFastEnumeration2, NSFastEnumerator2, NSMutableArray, + NSMutableCopying, NSRange, +}; -// SAFETY: Same as Id (which is what NSArray effectively stores). +// SAFETY: Same as Id (what NSArray and NSMutableArray effectively store). unsafe impl Sync for NSArray {} unsafe impl Send for NSArray {} unsafe impl Sync for NSArray {} unsafe impl Send for NSArray {} +unsafe impl Sync for NSMutableArray {} +unsafe impl Send for NSMutableArray {} +unsafe impl Sync for NSMutableArray {} +unsafe impl Send for NSMutableArray {} + // Also same as Id impl RefUnwindSafe for NSArray {} impl UnwindSafe for NSArray {} impl UnwindSafe for NSArray {} +impl RefUnwindSafe for NSMutableArray {} +impl UnwindSafe for NSMutableArray {} +impl UnwindSafe for NSMutableArray {} + #[track_caller] pub(crate) unsafe fn with_objects( objects: &[&T], @@ -133,8 +94,9 @@ extern_methods!( /// Generic accessor methods. unsafe impl NSArray { #[doc(alias = "count")] - #[method(count)] - pub fn len(&self) -> usize; + pub fn len(&self) -> usize { + self.count() + } pub fn is_empty(&self) -> bool { self.len() == 0 @@ -163,28 +125,34 @@ extern_methods!( pub fn last(&self) -> Option<&T>; #[doc(alias = "objectEnumerator")] - pub fn iter(&self) -> NSEnumerator<'_, T> { + pub fn iter(&self) -> NSEnumerator2<'_, T> { unsafe { let result: *mut Object = msg_send![self, objectEnumerator]; - NSEnumerator::from_ptr(result) + NSEnumerator2::from_ptr(result) } } - #[method(getObjects:range:)] - unsafe fn get_objects(&self, ptr: *mut &T, range: NSRange); - - pub fn objects_in_range(&self, range: Range) -> Vec<&T> { + unsafe fn objects_in_range_unchecked(&self, range: Range) -> Vec<&T> { let range = NSRange::from(range); - let mut vec = Vec::with_capacity(range.length); + let mut vec: Vec> = Vec::with_capacity(range.length); unsafe { - self.get_objects(vec.as_mut_ptr(), range); + self.getObjects_range(NonNull::new(vec.as_mut_ptr()).unwrap(), range); vec.set_len(range.length); + mem::transmute(vec) + } + } + + pub fn objects_in_range(&self, range: Range) -> Option> { + if range.end > self.len() { + return None; } - vec + // SAFETY: Just checked that the range is in bounds + Some(unsafe { self.objects_in_range_unchecked(range) }) } pub fn to_vec(&self) -> Vec<&T> { - self.objects_in_range(0..self.len()) + // SAFETY: The range is know to be in bounds + unsafe { self.objects_in_range_unchecked(0..self.len()) } } // TODO: Take Id ? @@ -257,13 +225,13 @@ impl alloc::borrow::ToOwned for NSArray { } } -unsafe impl NSFastEnumeration for NSArray { +unsafe impl NSFastEnumeration2 for NSArray { type Item = T; } impl<'a, T: Message, O: Ownership> IntoIterator for &'a NSArray { type Item = &'a T; - type IntoIter = NSFastEnumerator<'a, NSArray>; + type IntoIter = NSFastEnumerator2<'a, NSArray>; fn into_iter(self) -> Self::IntoIter { self.iter_fast() @@ -306,7 +274,7 @@ mod tests { use alloc::vec::Vec; use super::*; - use crate::Foundation::{NSNumber, NSString}; + use crate::Foundation::{NSNumber, NSObject, NSString}; use objc2::rc::{__RcTestObject, __ThreadTestData}; fn sample_array(len: usize) -> Id, Owned> { @@ -467,15 +435,15 @@ mod tests { fn test_objects_in_range() { let array = sample_array(4); - let middle_objs = array.objects_in_range(1..3); + let middle_objs = array.objects_in_range(1..3).unwrap(); assert_eq!(middle_objs.len(), 2); assert_eq!(middle_objs[0], array.get(1).unwrap()); assert_eq!(middle_objs[1], array.get(2).unwrap()); - let empty_objs = array.objects_in_range(1..1); + let empty_objs = array.objects_in_range(1..1).unwrap(); assert!(empty_objs.is_empty()); - let all_objs = array.objects_in_range(0..4); + let all_objs = array.objects_in_range(0..4).unwrap(); assert_eq!(all_objs.len(), 4); } diff --git a/crates/icrate/src/Foundation/additions/attributed_string.rs b/crates/icrate/src/Foundation/additions/attributed_string.rs index f7bcebb01..85048ae6d 100644 --- a/crates/icrate/src/Foundation/additions/attributed_string.rs +++ b/crates/icrate/src/Foundation/additions/attributed_string.rs @@ -1,33 +1,13 @@ -use core::fmt; use core::panic::{RefUnwindSafe, UnwindSafe}; -use super::{ - NSCopying, NSDictionary, NSMutableAttributedString, NSMutableCopying, NSObject, NSString, -}; -use objc2::rc::{Allocated, DefaultId, Id, Shared}; +use objc2::rc::{DefaultId, Id, Shared}; use objc2::runtime::Object; -use objc2::{extern_class, extern_methods, ClassType}; - -extern_class!( - /// A string that has associated attributes for portions of its text. - /// - /// Examples of attributes could be: Visual style, hyperlinks, or - /// accessibility data. - /// - /// Conceptually, each UTF-16 code unit in an attributed string has its - /// own collection of attributes - most often though - /// - /// Only the most basic functionality is defined here, the `AppKit` - /// framework contains most of the extension methods. - /// - /// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nsattributedstring?language=objc). - #[derive(PartialEq, Eq, Hash)] - pub struct NSAttributedString; - - unsafe impl ClassType for NSAttributedString { - type Super = NSObject; - } -); +use objc2::{extern_methods, ClassType}; + +use crate::Foundation::{ + NSAttributedString, NSAttributedStringKey, NSCopying, NSDictionary, NSMutableAttributedString, + NSMutableCopying, NSString, +}; // SAFETY: `NSAttributedString` is immutable and `NSMutableAttributedString` // can only be mutated from `&mut` methods. @@ -38,9 +18,6 @@ unsafe impl Send for NSAttributedString {} impl UnwindSafe for NSAttributedString {} impl RefUnwindSafe for NSAttributedString {} -/// Attributes that you can apply to text in an attributed string. -pub type NSAttributedStringKey = NSString; - extern_methods!( /// Creating attributed strings. unsafe impl NSAttributedString { @@ -48,67 +25,28 @@ extern_methods!( #[method_id(new)] pub fn new() -> Id; - #[method_id(initWithString:attributes:)] - fn init_with_attributes( - this: Option>, - string: &NSString, - attributes: &NSDictionary, - ) -> Id; - - #[method_id(initWithString:)] - fn init_with_string(this: Option>, string: &NSString) -> Id; - /// Creates a new attributed string from the given string and attributes. /// /// The attributes are associated with every UTF-16 code unit in the /// string. + /// + /// # Safety + /// + /// The attributes must be valid. #[doc(alias = "initWithString:")] - pub fn new_with_attributes( + pub unsafe fn new_with_attributes( string: &NSString, - // TODO: Mutability of the dictionary should be (Shared, Shared) attributes: &NSDictionary, ) -> Id { - Self::init_with_attributes(Self::alloc(), string, attributes) + unsafe { Self::initWithString_attributes(Self::alloc(), string, Some(attributes)) } } /// Creates a new attributed string without any attributes. #[doc(alias = "initWithString:")] pub fn from_nsstring(string: &NSString) -> Id { - Self::init_with_string(Self::alloc(), string) + Self::initWithString(Self::alloc(), string) } } - - /// Querying. - unsafe impl NSAttributedString { - // TODO: Lifetimes? - #[method_id(string)] - pub fn string(&self) -> Id; - - /// Alias for `self.string().len_utf16()`. - #[doc(alias = "length")] - #[method(length)] - pub fn len_utf16(&self) -> usize; - - // /// TODO - // /// - // /// See [Apple's documentation on Effective and Maximal Ranges][doc]. - // /// - // /// [doc]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/AttributedStrings/Tasks/AccessingAttrs.html#//apple_ref/doc/uid/20000161-SW2 - // #[doc(alias = "attributesAtIndex:effectiveRange:")] - // pub fn attributes_in_effective_range( - // &self, - // index: usize, - // range: Range, - // ) -> Id { - // let range = NSRange::from(range); - // todo!() - // } - // - // attributesAtIndex:longestEffectiveRange:inRange: - - // TODO: attributedSubstringFromRange: - // TODO: enumerateAttributesInRange:options:usingBlock: - } ); impl DefaultId for NSAttributedString { @@ -136,22 +74,16 @@ impl alloc::borrow::ToOwned for NSAttributedString { } } -impl fmt::Debug for NSAttributedString { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Use -[NSAttributedString description] since it is pretty good - let obj: &NSObject = self; - fmt::Debug::fmt(obj, f) - } -} - #[cfg(test)] mod tests { use alloc::string::ToString; use alloc::{format, vec}; - use super::*; use objc2::rc::{autoreleasepool, Owned}; + use super::*; + use crate::Foundation::NSObject; + #[test] fn test_new() { let s = NSAttributedString::new(); @@ -200,10 +132,12 @@ mod tests { let obj: Id = unsafe { Id::cast(NSObject::new()) }; let ptr: *const Object = &*obj; - let s = NSAttributedString::new_with_attributes( - &NSString::from_str("abc"), - &NSDictionary::from_keys_and_objects(&[&*NSString::from_str("test")], vec![obj]), - ); + let s = unsafe { + NSAttributedString::new_with_attributes( + &NSString::from_str("abc"), + &NSDictionary::from_keys_and_objects(&[&*NSString::from_str("test")], vec![obj]), + ) + }; let expected = if cfg!(feature = "gnustep-1-7") { format!("abc{{test = \"\"; }}") } else { diff --git a/crates/icrate/src/Foundation/additions/bundle.rs b/crates/icrate/src/Foundation/additions/bundle.rs index 6ced266b7..ccc748b55 100644 --- a/crates/icrate/src/Foundation/additions/bundle.rs +++ b/crates/icrate/src/Foundation/additions/bundle.rs @@ -1,22 +1,9 @@ -use core::fmt; use core::panic::{RefUnwindSafe, UnwindSafe}; -use super::{NSCopying, NSDictionary, NSObject, NSString}; use objc2::rc::{Id, Shared}; -use objc2::{extern_class, extern_methods, ClassType}; +use objc2::runtime::Object; -extern_class!( - /// A representation of the code and resources stored in a bundle - /// directory on disk. - /// - /// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nsbundle?language=objc). - #[derive(PartialEq, Eq, Hash)] - pub struct NSBundle; - - unsafe impl ClassType for NSBundle { - type Super = NSObject; - } -); +use crate::Foundation::{NSBundle, NSCopying, NSString}; // SAFETY: Bundles are documented as thread-safe. unsafe impl Sync for NSBundle {} @@ -25,33 +12,16 @@ unsafe impl Send for NSBundle {} impl UnwindSafe for NSBundle {} impl RefUnwindSafe for NSBundle {} -extern_methods!( - unsafe impl NSBundle { - #[method_id(mainBundle)] - pub fn main() -> Id; - - #[method_id(infoDictionary)] - pub fn info(&self) -> Id, Shared>; - - pub fn name(&self) -> Option> { - // TODO: Use ns_string! - self.info() - .get(&NSString::from_str("CFBundleName")) - .map(|name| { - let ptr: *const NSObject = name; - let ptr: *const NSString = ptr.cast(); - // SAFETY: TODO - let name = unsafe { ptr.as_ref().unwrap_unchecked() }; - name.copy() - }) - } - } -); - -impl fmt::Debug for NSBundle { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Delegate to NSObject - (**self).fmt(f) +impl NSBundle { + pub fn name(&self) -> Option> { + let info = self.infoDictionary()?; + // TODO: Use ns_string! + let name = info.get(&NSString::from_str("CFBundleName"))?; + let ptr: *const Object = name; + let ptr: *const NSString = ptr.cast(); + // SAFETY: TODO + let name = unsafe { ptr.as_ref().unwrap_unchecked() }; + Some(name.copy()) } } @@ -66,9 +36,9 @@ mod tests { fn try_running_functions() { // This is mostly empty since cargo doesn't bundle the application // before executing. - let bundle = NSBundle::main(); + let bundle = NSBundle::mainBundle(); println!("{bundle:?}"); - assert_eq!(format!("{:?}", bundle.info()), "{}"); + assert_eq!(format!("{:?}", bundle.infoDictionary().unwrap()), "{}"); assert_eq!(bundle.name(), None); } } diff --git a/crates/icrate/src/Foundation/additions/data.rs b/crates/icrate/src/Foundation/additions/data.rs index ec8ebcf5a..626cb85a7 100644 --- a/crates/icrate/src/Foundation/additions/data.rs +++ b/crates/icrate/src/Foundation/additions/data.rs @@ -6,24 +6,11 @@ use core::ops::Index; use core::panic::{RefUnwindSafe, UnwindSafe}; use core::slice::{self, SliceIndex}; -use super::{NSCopying, NSMutableCopying, NSMutableData, NSObject}; use objc2::rc::{DefaultId, Id, Shared}; use objc2::runtime::{Class, Object}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; - -extern_class!( - /// A static byte buffer in memory. - /// - /// This is similar to a [`slice`][`prim@slice`] of [`u8`]. - /// - /// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nsdata?language=objc). - #[derive(PartialEq, Eq, Hash)] - pub struct NSData; - - unsafe impl ClassType for NSData { - type Super = NSObject; - } -); +use objc2::{extern_methods, msg_send_id, ClassType}; + +use crate::Foundation::{NSCopying, NSData, NSMutableCopying, NSMutableData}; // SAFETY: `NSData` is immutable and `NSMutableData` can only be mutated from // `&mut` methods. @@ -63,9 +50,9 @@ extern_methods!( /// Accessor methods. unsafe impl NSData { - #[method(length)] - #[doc(alias = "length")] - pub fn len(&self) -> usize; + pub fn len(&self) -> usize { + self.length() + } pub fn is_empty(&self) -> bool { self.len() == 0 diff --git a/crates/icrate/src/Foundation/additions/dictionary.rs b/crates/icrate/src/Foundation/additions/dictionary.rs index 61675981d..966ee8924 100644 --- a/crates/icrate/src/Foundation/additions/dictionary.rs +++ b/crates/icrate/src/Foundation/additions/dictionary.rs @@ -1,43 +1,43 @@ use alloc::vec::Vec; use core::cmp::min; use core::fmt; -use core::marker::PhantomData; +use core::mem; use core::ops::Index; use core::panic::{RefUnwindSafe, UnwindSafe}; -use core::ptr; +use core::ptr::{self, NonNull}; -use super::{NSArray, NSCopying, NSEnumerator, NSFastEnumeration, NSObject}; +use crate::Foundation::{ + NSCopying, NSDictionary, NSEnumerator2, NSFastEnumeration2, NSMutableDictionary, +}; use objc2::rc::{DefaultId, Id, Owned, Shared, SliceId}; -use objc2::{ClassType, __inner_extern_class, extern_methods, msg_send, msg_send_id, Message}; - -__inner_extern_class!( - #[derive(PartialEq, Eq, Hash)] - pub struct NSDictionary { - key: PhantomData>, - obj: PhantomData>, - } - - unsafe impl ClassType for NSDictionary { - type Super = NSObject; - } -); +use objc2::{extern_methods, msg_send, msg_send_id, ClassType, Message}; // TODO: SAFETY // Approximately same as `NSArray` -unsafe impl Sync for NSDictionary {} -unsafe impl Send for NSDictionary {} +unsafe impl Sync for NSDictionary {} +unsafe impl Send for NSDictionary {} + +unsafe impl Sync for NSMutableDictionary {} +unsafe impl Send for NSMutableDictionary {} // Approximately same as `NSArray` -impl UnwindSafe for NSDictionary {} +impl UnwindSafe for NSDictionary {} impl RefUnwindSafe for NSDictionary {} + +impl UnwindSafe + for NSMutableDictionary +{ +} +// impl RefUnwindSafe for NSMutableDictionary {} + extern_methods!( unsafe impl NSDictionary { #[method_id(new)] pub fn new() -> Id; - #[doc(alias = "count")] - #[method(count)] - pub fn len(&self) -> usize; + pub fn len(&self) -> usize { + self.count() + } pub fn is_empty(&self) -> bool { self.len() == 0 @@ -47,63 +47,57 @@ extern_methods!( #[method(objectForKey:)] pub fn get(&self, key: &K) -> Option<&V>; - #[method(getObjects:andKeys:)] - unsafe fn get_objects_and_keys(&self, objects: *mut &V, keys: *mut &K); - #[doc(alias = "getObjects:andKeys:")] pub fn keys(&self) -> Vec<&K> { let len = self.len(); - let mut keys = Vec::with_capacity(len); + let mut keys: Vec> = Vec::with_capacity(len); unsafe { - self.get_objects_and_keys(ptr::null_mut(), keys.as_mut_ptr()); + self.getObjects_andKeys(ptr::null_mut(), keys.as_mut_ptr()); keys.set_len(len); + mem::transmute(keys) } - keys } #[doc(alias = "getObjects:andKeys:")] pub fn values(&self) -> Vec<&V> { let len = self.len(); - let mut vals = Vec::with_capacity(len); + let mut vals: Vec> = Vec::with_capacity(len); unsafe { - self.get_objects_and_keys(vals.as_mut_ptr(), ptr::null_mut()); + self.getObjects_andKeys(vals.as_mut_ptr(), ptr::null_mut()); vals.set_len(len); + mem::transmute(vals) } - vals } #[doc(alias = "getObjects:andKeys:")] pub fn keys_and_objects(&self) -> (Vec<&K>, Vec<&V>) { let len = self.len(); - let mut keys = Vec::with_capacity(len); - let mut objs = Vec::with_capacity(len); + let mut keys: Vec> = Vec::with_capacity(len); + let mut objs: Vec> = Vec::with_capacity(len); unsafe { - self.get_objects_and_keys(objs.as_mut_ptr(), keys.as_mut_ptr()); + self.getObjects_andKeys(objs.as_mut_ptr(), keys.as_mut_ptr()); keys.set_len(len); objs.set_len(len); + (mem::transmute(keys), mem::transmute(objs)) } - (keys, objs) } #[doc(alias = "keyEnumerator")] - pub fn iter_keys(&self) -> NSEnumerator<'_, K> { + pub fn iter_keys(&self) -> NSEnumerator2<'_, K> { unsafe { let result = msg_send![self, keyEnumerator]; - NSEnumerator::from_ptr(result) + NSEnumerator2::from_ptr(result) } } #[doc(alias = "objectEnumerator")] - pub fn iter_values(&self) -> NSEnumerator<'_, V> { + pub fn iter_values(&self) -> NSEnumerator2<'_, V> { unsafe { let result = msg_send![self, objectEnumerator]; - NSEnumerator::from_ptr(result) + NSEnumerator2::from_ptr(result) } } - #[method_id(allKeys)] - pub fn keys_array(&self) -> Id, Shared>; - pub fn from_keys_and_objects(keys: &[&T], vals: Vec>) -> Id where T: NSCopying, @@ -120,10 +114,6 @@ extern_methods!( ] } } - - pub fn into_values_array(dict: Id) -> Id, Shared> { - unsafe { msg_send_id![&dict, allValues] } - } } ); @@ -136,7 +126,7 @@ impl DefaultId for NSDictionary { } } -unsafe impl NSFastEnumeration for NSDictionary { +unsafe impl NSFastEnumeration2 for NSDictionary { type Item = K; } @@ -161,10 +151,11 @@ mod tests { use alloc::format; use alloc::vec; - use super::*; - use crate::Foundation::NSString; use objc2::rc::autoreleasepool; + use super::*; + use crate::Foundation::{NSObject, NSString}; + fn sample_dict(key: &str) -> Id, Shared> { let string = NSString::from_str(key); let obj = NSObject::new(); @@ -239,7 +230,7 @@ mod tests { fn test_arrays() { let dict = sample_dict("abcd"); - let keys = dict.keys_array(); + let keys = dict.allKeys(); assert_eq!(keys.len(), 1); autoreleasepool(|pool| { assert_eq!(keys[0].as_str(pool), "abcd"); diff --git a/crates/icrate/src/Foundation/additions/enumerator.rs b/crates/icrate/src/Foundation/additions/enumerator.rs index 998e6a1ae..f5cd31c04 100644 --- a/crates/icrate/src/Foundation/additions/enumerator.rs +++ b/crates/icrate/src/Foundation/additions/enumerator.rs @@ -9,17 +9,17 @@ use objc2::runtime::Object; use objc2::{msg_send, Encode, Encoding, Message, RefEncode}; // TODO: https://doc.rust-lang.org/stable/reference/trait-bounds.html#lifetime-bounds -pub struct NSEnumerator<'a, T: Message> { +pub struct NSEnumerator2<'a, T: Message> { id: Id, item: PhantomData<&'a T>, } -impl<'a, T: Message> NSEnumerator<'a, T> { +impl<'a, T: Message> NSEnumerator2<'a, T> { /// TODO /// /// # Safety /// - /// The object pointer must be a valid `NSEnumerator` with `Owned` + /// The object pointer must be a valid `NSEnumerator2` with `Owned` /// ownership. pub unsafe fn from_ptr(ptr: *mut Object) -> Self { Self { @@ -29,7 +29,7 @@ impl<'a, T: Message> NSEnumerator<'a, T> { } } -impl<'a, T: Message> Iterator for NSEnumerator<'a, T> { +impl<'a, T: Message> Iterator for NSEnumerator2<'a, T> { type Item = &'a T; fn next(&mut self) -> Option<&'a T> { @@ -37,11 +37,11 @@ impl<'a, T: Message> Iterator for NSEnumerator<'a, T> { } } -pub unsafe trait NSFastEnumeration: Message { +pub unsafe trait NSFastEnumeration2: Message { type Item: Message; - fn iter_fast(&self) -> NSFastEnumerator<'_, Self> { - NSFastEnumerator::new(self) + fn iter_fast(&self) -> NSFastEnumerator2<'_, Self> { + NSFastEnumerator2::new(self) } } @@ -69,7 +69,7 @@ unsafe impl RefEncode for NSFastEnumerationState { const ENCODING_REF: Encoding = Encoding::Pointer(&Self::ENCODING); } -fn enumerate<'a, 'b: 'a, C: NSFastEnumeration + ?Sized>( +fn enumerate<'a, 'b: 'a, C: NSFastEnumeration2 + ?Sized>( object: &'b C, state: &mut NSFastEnumerationState, buf: &'a mut [*const C::Item], @@ -94,7 +94,7 @@ fn enumerate<'a, 'b: 'a, C: NSFastEnumeration + ?Sized>( const FAST_ENUM_BUF_SIZE: usize = 16; -pub struct NSFastEnumerator<'a, C: 'a + NSFastEnumeration + ?Sized> { +pub struct NSFastEnumerator2<'a, C: 'a + NSFastEnumeration2 + ?Sized> { object: &'a C, ptr: *const *const C::Item, @@ -104,7 +104,7 @@ pub struct NSFastEnumerator<'a, C: 'a + NSFastEnumeration + ?Sized> { buf: [*const C::Item; FAST_ENUM_BUF_SIZE], } -impl<'a, C: NSFastEnumeration + ?Sized> NSFastEnumerator<'a, C> { +impl<'a, C: NSFastEnumeration2 + ?Sized> NSFastEnumerator2<'a, C> { fn new(object: &'a C) -> Self { Self { object, @@ -149,7 +149,7 @@ impl<'a, C: NSFastEnumeration + ?Sized> NSFastEnumerator<'a, C> { } } -impl<'a, C: NSFastEnumeration + ?Sized> Iterator for NSFastEnumerator<'a, C> { +impl<'a, C: NSFastEnumeration2 + ?Sized> Iterator for NSFastEnumerator2<'a, C> { type Item = &'a C::Item; fn next(&mut self) -> Option<&'a C::Item> { @@ -167,7 +167,8 @@ impl<'a, C: NSFastEnumeration + ?Sized> Iterator for NSFastEnumerator<'a, C> { #[cfg(test)] mod tests { - use super::NSFastEnumeration; + use super::*; + use crate::Foundation::{NSArray, NSNumber}; #[test] diff --git a/crates/icrate/src/Foundation/additions/error.rs b/crates/icrate/src/Foundation/additions/error.rs index d9c5c9159..1358fc3c1 100644 --- a/crates/icrate/src/Foundation/additions/error.rs +++ b/crates/icrate/src/Foundation/additions/error.rs @@ -1,27 +1,12 @@ use core::fmt; use core::panic::{RefUnwindSafe, UnwindSafe}; -use super::{NSCopying, NSDictionary, NSObject, NSString}; -use objc2::ffi::NSInteger; use objc2::rc::{Id, Shared}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use objc2::ClassType; -extern_class!( - /// Information about an error condition including a domain, a - /// domain-specific error code, and application-specific information. - /// - /// See also Apple's [documentation on error handling][err], and their - /// NSError [API reference][api]. - /// - /// [err]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorHandling/ErrorHandling.html#//apple_ref/doc/uid/TP40001806-CH201-SW1 - /// [api]: https://developer.apple.com/documentation/foundation/nserror?language=objc - #[derive(PartialEq, Eq, Hash)] - pub struct NSError; - - unsafe impl ClassType for NSError { - type Super = NSObject; - } -); +use crate::Foundation::{ + NSCopying, NSError, NSErrorDomain, NSErrorUserInfoKey, NSInteger, NSLocalizedDescriptionKey, +}; // SAFETY: Error objects are immutable data containers. unsafe impl Sync for NSError {} @@ -30,96 +15,38 @@ unsafe impl Send for NSError {} impl UnwindSafe for NSError {} impl RefUnwindSafe for NSError {} -pub type NSErrorUserInfoKey = NSString; -pub type NSErrorDomain = NSString; - -extern_methods!( - /// Creation methods. - unsafe impl NSError { - /// Construct a new [`NSError`] with the given code in the given domain. - pub fn new(code: NSInteger, domain: &NSString) -> Id { - unsafe { Self::with_user_info(code, domain, None) } - } - - // TODO: Figure out safety of `user_info` dict! - unsafe fn with_user_info( - code: NSInteger, - domain: &NSString, - user_info: Option<&NSDictionary>, - ) -> Id { - // SAFETY: `domain` and `user_info` are copied to the error object, so - // even if the `&NSString` came from a `&mut NSMutableString`, we're - // still good! - unsafe { - msg_send_id![ - Self::alloc(), - initWithDomain: domain, - code: code, - userInfo: user_info, - ] - } - } +/// Creation methods. +impl NSError { + /// Construct a new [`NSError`] with the given code in the given domain. + pub fn new(code: NSInteger, domain: &NSErrorDomain) -> Id { + // SAFETY: `domain` and `user_info` are copied to the error object, so + // even if the `&NSString` came from a `&mut NSMutableString`, we're + // still good! + unsafe { Self::initWithDomain_code_userInfo(Self::alloc(), domain, code, None) } } +} - /// Accessor methods. - unsafe impl NSError { - #[method_id(domain)] - pub fn domain(&self) -> Id; - - #[method(code)] - pub fn code(&self) -> NSInteger; - - #[method_id(userInfo)] - pub fn user_info(&self) -> Option, Shared>>; - - pub fn localized_description(&self) -> Id { - // TODO: For some reason this leaks a lot? - let obj: Option<_> = unsafe { msg_send_id![self, localizedDescription] }; - obj.expect( - "unexpected NULL localized description; a default should have been generated!", - ) - } - - // TODO: localizedRecoveryOptions - // TODO: localizedRecoverySuggestion - // TODO: localizedFailureReason - // TODO: helpAnchor - // TODO: +setUserInfoValueProviderForDomain:provider: - // TODO: +userInfoValueProviderForDomain: - - // TODO: recoveryAttempter - // TODO: attemptRecoveryFromError:... - - // TODO: Figure out if this is a good design, or if we should do something - // differently (like a Rusty name for the function, or putting a bunch of - // statics in a module instead)? - #[allow(non_snake_case)] - pub fn NSLocalizedDescriptionKey() -> &'static NSErrorUserInfoKey { - extern "C" { - #[link_name = "NSLocalizedDescriptionKey"] - static VALUE: &'static NSErrorUserInfoKey; - } - unsafe { VALUE } - } - - // TODO: Other NSErrorUserInfoKey values - // TODO: NSErrorDomain values +/// Accessor methods. +impl NSError { + #[allow(non_snake_case)] + pub fn NSLocalizedDescriptionKey() -> &'static NSErrorUserInfoKey { + unsafe { NSLocalizedDescriptionKey } } -); +} impl fmt::Debug for NSError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("NSError") .field("domain", &self.domain()) .field("code", &self.code()) - .field("user_info", &self.user_info()) + .field("user_info", &self.userInfo()) .finish() } } impl fmt::Display for NSError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.localized_description()) + write!(f, "{}", self.localizedDescription()) } } diff --git a/crates/icrate/src/Foundation/additions/exception.rs b/crates/icrate/src/Foundation/additions/exception.rs index 06349c0f4..26e2b0f40 100644 --- a/crates/icrate/src/Foundation/additions/exception.rs +++ b/crates/icrate/src/Foundation/additions/exception.rs @@ -2,29 +2,14 @@ use core::fmt; use core::hint::unreachable_unchecked; use core::panic::{RefUnwindSafe, UnwindSafe}; -use super::{NSCopying, NSDictionary, NSObject, NSString}; use objc2::exception::Exception; use objc2::rc::{Id, Shared}; use objc2::runtime::Object; -use objc2::{extern_class, extern_methods, msg_send_id, sel, ClassType}; - -extern_class!( - /// A special condition that interrupts the normal flow of program - /// execution. - /// - /// Exceptions can be thrown and caught using the `objc2::exception` - /// module. - /// - /// See also [Apple's documentation][doc]. - /// - /// [doc]: https://developer.apple.com/documentation/foundation/nsexception?language=objc - #[derive(PartialEq, Eq, Hash)] - pub struct NSException; - - unsafe impl ClassType for NSException { - type Super = NSObject; - } -); +use objc2::{extern_methods, msg_send_id, sel, ClassType}; + +use crate::Foundation::{ + NSCopying, NSDictionary, NSException, NSExceptionName, NSObject, NSString, +}; // SAFETY: Exception objects are immutable data containers, and documented as // thread safe. @@ -34,7 +19,6 @@ unsafe impl Send for NSException {} impl UnwindSafe for NSException {} impl RefUnwindSafe for NSException {} -type NSExceptionName = NSString; extern_methods!( unsafe impl NSException { /// Create a new [`NSException`] object. @@ -77,24 +61,6 @@ extern_methods!( unsafe { unreachable_unchecked() } } - /// A that uniquely identifies the type of exception. - /// - /// See [Apple's documentation][doc] for some of the different values this - /// can take. - /// - /// [doc]: https://developer.apple.com/documentation/foundation/nsexceptionname?language=objc - #[method_id(name)] - // Nullability not documented, but a name is expected in most places. - pub fn name(&self) -> Id; - - /// A human-readable message summarizing the reason for the exception. - #[method_id(reason)] - pub fn reason(&self) -> Option>; - - /// Application-specific data pertaining to the exception. - #[method_id(userInfo)] - pub fn user_info(&self) -> Option, Shared>>; - /// Convert this into an [`Exception`] object. pub fn into_exception(this: Id) -> Id { // SAFETY: Downcasting to "subclass" @@ -171,7 +137,7 @@ mod tests { assert_eq!(exc.name(), NSString::from_str("abc")); assert_eq!(exc.reason().unwrap(), NSString::from_str("def")); - assert!(exc.user_info().is_none()); + assert!(exc.userInfo().is_none()); let debug = format!(" 'abc' reason:def"); assert_eq!(format!("{exc:?}"), debug); diff --git a/crates/icrate/src/Foundation/additions/geometry.rs b/crates/icrate/src/Foundation/additions/geometry.rs index fb43d062c..63cdc3f8c 100644 --- a/crates/icrate/src/Foundation/additions/geometry.rs +++ b/crates/icrate/src/Foundation/additions/geometry.rs @@ -1,4 +1,5 @@ use objc2::encode::{Encode, Encoding, RefEncode}; +use objc2::ffi::NSUInteger; #[cfg(target_pointer_width = "64")] type InnerFloat = f64; @@ -367,8 +368,19 @@ pub type NSSize = CGSize; /// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nsrect?language=objc). pub type NSRect = CGRect; -// TODO: struct NSEdgeInsets -// TODO: enum NSRectEdge +ns_enum!( + #[underlying(NSUInteger)] + pub enum NSRectEdge { + NSRectEdgeMinX = 0, + NSRectEdgeMinY = 1, + NSRectEdgeMaxX = 2, + NSRectEdgeMaxY = 3, + NSMinXEdge = NSRectEdgeMinX, + NSMinYEdge = NSRectEdgeMinY, + NSMaxXEdge = NSRectEdgeMaxX, + NSMaxYEdge = NSRectEdgeMaxY, + } +); #[cfg(test)] mod tests { @@ -391,14 +403,7 @@ mod tests { #[test] #[cfg(any(all(feature = "apple", target_os = "macos"), feature = "gnustep-1-7"))] // or macabi fn test_partial_eq() { - use objc2::runtime::Bool; - - // Note: No need to use "C-unwind" - extern "C" { - fn NSEqualPoints(a: NSPoint, b: NSPoint) -> Bool; - fn NSEqualSizes(a: NSSize, b: NSSize) -> Bool; - fn NSEqualRects(a: NSRect, b: NSRect) -> Bool; - } + use crate::Foundation::{NSEqualPoints, NSEqualRects, NSEqualSizes}; // We assume that comparisons handle e.g. `x` and `y` in the same way, // therefore we just set the coordinates / dimensions to the same. diff --git a/crates/icrate/src/Foundation/additions/lock.rs b/crates/icrate/src/Foundation/additions/lock.rs index f49a6a86c..af997e3ea 100644 --- a/crates/icrate/src/Foundation/additions/lock.rs +++ b/crates/icrate/src/Foundation/additions/lock.rs @@ -1,29 +1,10 @@ -use super::{NSObject, NSString}; -use objc2::rc::{Id, Owned, Shared}; -use objc2::{extern_class, extern_methods, extern_protocol, ClassType, ConformsTo, ProtocolType}; +use objc2::rc::{Id, Owned}; +use objc2::ClassType; +use objc2::{extern_methods, ConformsTo}; -// TODO: Proper Send/Sync impls here - -extern_protocol!( - pub struct NSLocking; - - unsafe impl ProtocolType for NSLocking { - #[method(lock)] - pub unsafe fn lock(&self); - - #[method(unlock)] - pub unsafe fn unlock(&self); - } -); - -extern_class!( - #[derive(Debug)] - pub struct NSLock; +use crate::Foundation::{NSLock, NSLocking}; - unsafe impl ClassType for NSLock { - type Super = NSObject; - } -); +// TODO: Proper Send/Sync impls here unsafe impl ConformsTo for NSLock {} @@ -31,15 +12,6 @@ extern_methods!( unsafe impl NSLock { #[method_id(new)] pub fn new() -> Id; - - #[method(tryLock)] - pub unsafe fn try_lock(&self) -> bool; - - #[method_id(name)] - pub fn name(&self) -> Option>; - - #[method(setName:)] - pub fn set_name(&mut self, name: Option<&NSString>); } ); @@ -53,9 +25,9 @@ mod tests { let locking: &NSLocking = lock.as_protocol(); unsafe { locking.lock(); - assert!(!lock.try_lock()); + assert!(!lock.tryLock()); locking.unlock(); - assert!(lock.try_lock()); + assert!(lock.tryLock()); locking.unlock(); } } diff --git a/crates/icrate/src/Foundation/additions/mod.rs b/crates/icrate/src/Foundation/additions/mod.rs index 1b75960e3..1d9e7a508 100644 --- a/crates/icrate/src/Foundation/additions/mod.rs +++ b/crates/icrate/src/Foundation/additions/mod.rs @@ -1,105 +1,15 @@ -//! Bindings to the `Foundation` framework. -//! -//! This is the [`std`] equivalent for Objective-C, containing essential data -//! types, collections, and operating-system services. -//! -//! See [Apple's documentation](https://developer.apple.com/documentation/foundation?language=objc). -//! -//! -//! ## Philosophy -//! -//! The `Foundation` framework is _huge_! If we aspired to map every API it -//! exposes (a lot of it is just helper methods to make Objective-C more -//! ergonomic), this library would never be finished. Instead, our focus lies -//! on conversion methods, to allow easily using them from Rust. -//! -//! If you find some API that an object doesn't expose (but should), we gladly -//! accept [pull requests]. If it is something that is out of scope, these -//! objects implement the [`Message`] trait, so you can always just manually -//! call a method on them using the [`msg_send!`] family of macros. -//! -//! [pull requests]: https://github.com/madsmtm/objc2/pulls -//! [`Message`]: crate::objc2::Message -//! [`msg_send!`]: crate::objc2::msg_send -//! -//! -//! # Use of `Deref` -//! -//! `icrate` uses the [`Deref`] trait in a bit special way: All objects deref -//! to their superclasses. For example, `NSMutableArray` derefs to `NSArray`, -//! which in turn derefs to `NSObject`. -//! -//! Note that this is explicitly recommended against in [the -//! documentation][`Deref`] and [the Rust Design patterns -//! book][anti-pattern-deref] (see those links for details). -//! -//! Due to Objective-C objects only ever being accessible behind pointers in -//! the first place, the problems stated there are less severe, and having the -//! implementation just means that everything is much nicer when you actually -//! want to use the objects! -//! -//! All objects also implement [`AsRef`] and [`AsMut`] to their superclass, -//! and can be used in [`Id::into_super`], so if you favour explicit -//! conversion, that is a possibility too. -//! -//! [`Deref`]: std::ops::Deref -//! [`ClassType`]: crate::objc2::ClassType -//! [anti-pattern-deref]: https://rust-unofficial.github.io/patterns/anti_patterns/deref.html -//! [`Id::into_super`]: objc2::rc::Id::into_super - -// TODO: Remove these -#![allow(missing_docs)] #![allow(clippy::missing_safety_doc)] -use std::os::raw::c_double; - -pub use self::array::NSArray; -pub use self::attributed_string::{NSAttributedString, NSAttributedStringKey}; -pub use self::bundle::NSBundle; pub use self::comparison_result::NSComparisonResult; pub use self::copying::{NSCopying, NSMutableCopying}; -pub use self::data::NSData; -pub use self::dictionary::NSDictionary; -pub use self::enumerator::{NSEnumerator, NSFastEnumeration, NSFastEnumerator}; -pub use self::error::{NSError, NSErrorDomain, NSErrorUserInfoKey}; -pub use self::exception::NSException; -pub use self::geometry::{CGFloat, CGPoint, CGRect, CGSize, NSPoint, NSRect, NSSize}; -pub use self::lock::{NSLock, NSLocking}; -pub use self::mutable_array::NSMutableArray; -pub use self::mutable_attributed_string::NSMutableAttributedString; -pub use self::mutable_data::NSMutableData; -pub use self::mutable_dictionary::NSMutableDictionary; -pub use self::mutable_set::NSMutableSet; -pub use self::mutable_string::NSMutableString; -pub use self::number::NSNumber; -pub use self::process_info::NSProcessInfo; +pub use self::enumerator::{NSEnumerator2, NSFastEnumeration2, NSFastEnumerator2}; +pub use self::geometry::{ + CGFloat, CGPoint, CGRect, CGSize, NSMaxXEdge, NSMaxYEdge, NSMinXEdge, NSMinYEdge, NSPoint, + NSRect, NSRectEdge, NSRectEdgeMaxX, NSRectEdgeMaxY, NSRectEdgeMinX, NSRectEdgeMinY, NSSize, +}; pub use self::range::NSRange; -pub use self::set::NSSet; -pub use self::string::NSString; -pub use self::thread::{is_main_thread, is_multi_threaded, MainThreadMarker, NSThread}; -#[cfg(not(macos_10_7))] // Temporary -pub use self::uuid::NSUUID; -pub use self::value::NSValue; -pub use objc2::runtime::{NSObject, NSZone}; - -// Available under Foundation, so makes sense here as well: -// https://developer.apple.com/documentation/foundation/numbers_data_and_basic_values?language=objc -#[doc(no_inline)] -pub use objc2::ffi::{NSInteger, NSUInteger}; - -/// A value indicating that a requested item couldn’t be found or doesn’t exist. -/// -/// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nsnotfound?language=objc). -#[allow(non_upper_case_globals)] -pub const NSNotFound: NSInteger = objc2::ffi::NSIntegerMax; - -/// A number of seconds. -/// -/// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nstimeinterval?language=objc). -pub type NSTimeInterval = c_double; +pub use self::thread::{is_main_thread, is_multi_threaded, MainThreadMarker}; -#[doc(hidden)] -pub mod __ns_string; mod array; mod attributed_string; mod bundle; @@ -124,8 +34,6 @@ mod range; mod set; mod string; mod thread; -// Temporarily disable testing UUID on macOS 10.7 until -#[cfg(not(macos_10_7))] mod uuid; mod value; @@ -133,7 +41,7 @@ mod value; mod tests { use core::panic::{RefUnwindSafe, UnwindSafe}; - use super::*; + use crate::Foundation::*; use objc2::rc::{Id, Owned, Shared}; // We expect most Foundation types to be UnwindSafe and RefUnwindSafe, @@ -177,8 +85,8 @@ mod tests { assert_auto_traits::, Owned>>(); assert_auto_traits::, Owned>>(); // TODO: Figure out if Send + Sync is safe? - // assert_auto_traits::>(); - // assert_auto_traits::>>(); + // assert_auto_traits::>(); + // assert_auto_traits::>>(); assert_auto_traits::(); assert_auto_traits::(); assert_auto_traits::(); diff --git a/crates/icrate/src/Foundation/additions/mutable_array.rs b/crates/icrate/src/Foundation/additions/mutable_array.rs index cc3912881..78648d38f 100644 --- a/crates/icrate/src/Foundation/additions/mutable_array.rs +++ b/crates/icrate/src/Foundation/additions/mutable_array.rs @@ -1,43 +1,17 @@ use alloc::vec::Vec; use core::cmp::Ordering; use core::ffi::c_void; -use core::fmt; -use core::marker::PhantomData; use core::ops::{Index, IndexMut}; +use core::ptr::NonNull; -use super::array::with_objects; -use super::{ - NSArray, NSComparisonResult, NSCopying, NSFastEnumeration, NSFastEnumerator, NSMutableCopying, - NSObject, -}; use objc2::rc::{DefaultId, Id, Owned, Ownership, Shared, SliceId}; -use objc2::{ClassType, Message, __inner_extern_class, extern_methods, msg_send}; - -__inner_extern_class!( - /// A growable ordered collection of objects. - /// - /// See the documentation for [`NSArray`] and/or [Apple's - /// documentation][apple-doc] for more information. - /// - /// [apple-doc]: https://developer.apple.com/documentation/foundation/nsmutablearray?language=objc - #[derive(PartialEq, Eq, Hash)] - pub struct NSMutableArray { - p: PhantomData<*mut ()>, - } +use objc2::{extern_methods, ClassType, Message}; - unsafe impl ClassType for NSMutableArray { - #[inherits(NSObject)] - type Super = NSArray; - } -); - -// SAFETY: Same as NSArray -// -// Put here because rustdoc doesn't show these otherwise -unsafe impl Sync for NSMutableArray {} -unsafe impl Send for NSMutableArray {} -unsafe impl Sync for NSMutableArray {} -unsafe impl Send for NSMutableArray {} +use super::array::with_objects; +use crate::Foundation::{ + NSArray, NSComparisonResult, NSCopying, NSFastEnumeration2, NSFastEnumerator2, NSInteger, + NSMutableArray, NSMutableCopying, +}; extern_methods!( /// Generic creation methods. @@ -67,8 +41,8 @@ extern_methods!( unsafe impl NSMutableArray { #[doc(alias = "addObject:")] pub fn push(&mut self, obj: Id) { - // SAFETY: The object is not nil - unsafe { msg_send![self, addObject: &*obj] } + // SAFETY: The object has correct ownership. + unsafe { self.addObject(&obj) } } #[doc(alias = "insertObject:atIndex:")] @@ -76,9 +50,9 @@ extern_methods!( // TODO: Replace this check with catching the thrown NSRangeException let len = self.len(); if index < len { - // SAFETY: The object is not nil and the index is checked to be in - // bounds. - unsafe { msg_send![self, insertObject: &*obj, atIndex: index] } + // SAFETY: The object has correct ownership, and the index is + // checked to be in bounds. + unsafe { self.insertObject_atIndex(&obj, index) } } else { panic!( "insertion index (is {}) should be <= len (is {})", @@ -93,19 +67,11 @@ extern_methods!( let obj = self.get(index).unwrap(); Id::retain_autoreleased(obj as *const T as *mut T).unwrap_unchecked() }; - unsafe { - let _: () = msg_send![ - self, - replaceObjectAtIndex: index, - withObject: &*obj, - ]; - } + // SAFETY: The object has correct ownership. + unsafe { self.replaceObjectAtIndex_withObject(index, &obj) }; old_obj } - #[method(removeObjectAtIndex:)] - unsafe fn remove_at(&mut self, index: usize); - #[doc(alias = "removeObjectAtIndex:")] pub fn remove(&mut self, index: usize) -> Id { let obj = if let Some(obj) = self.get(index) { @@ -113,57 +79,50 @@ extern_methods!( } else { panic!("removal index should be < len"); }; - unsafe { self.remove_at(index) }; + // SAFETY: The index is checked to be in bounds. + unsafe { self.removeObjectAtIndex(index) }; obj } - #[method(removeLastObject)] - unsafe fn remove_last(&mut self); - #[doc(alias = "removeLastObject")] pub fn pop(&mut self) -> Option> { self.last() .map(|obj| unsafe { Id::retain(obj as *const T as *mut T).unwrap_unchecked() }) .map(|obj| { // SAFETY: `Self::last` just checked that there is an object - unsafe { self.remove_last() }; + unsafe { self.removeLastObject() }; obj }) } - #[doc(alias = "removeAllObjects")] - #[method(removeAllObjects)] - pub fn clear(&mut self); - #[doc(alias = "sortUsingFunction:context:")] pub fn sort_by Ordering>(&mut self, compare: F) { // TODO: "C-unwind" - extern "C" fn compare_with_closure Ordering>( - obj1: &U, - obj2: &U, + unsafe extern "C" fn compare_with_closure Ordering>( + obj1: NonNull, + obj2: NonNull, context: *mut c_void, - ) -> NSComparisonResult { + ) -> NSInteger { + let context: *mut F = context.cast(); // Bring back a reference to the closure. // Guaranteed to be unique, we gave `sortUsingFunction` unique is // ownership, and that method only runs one function at a time. - let closure: &mut F = unsafe { context.cast::().as_mut().unwrap_unchecked() }; + let closure: &mut F = unsafe { context.as_mut().unwrap_unchecked() }; - NSComparisonResult::from((*closure)(obj1, obj2)) + // SAFETY: The objects are guaranteed to be valid + let (obj1, obj2) = unsafe { (obj1.as_ref(), obj2.as_ref()) }; + + NSComparisonResult::from((*closure)(obj1, obj2)) as _ } - // We can't name the actual lifetimes in use here, so use `_`. - // See also https://github.com/rust-lang/rust/issues/56105 - let f: extern "C" fn(_, _, *mut c_void) -> NSComparisonResult = - compare_with_closure::; + // Create function pointer + let f: unsafe extern "C" fn(_, _, _) -> _ = compare_with_closure::; // Grab a type-erased pointer to the closure (a pointer to stack). let mut closure = compare; let context: *mut F = &mut closure; - let context: *mut c_void = context.cast(); - unsafe { - let _: () = msg_send![self, sortUsingFunction: f, context: context]; - } + unsafe { self.sortUsingFunction_context(f, context.cast()) }; // Keep the closure alive until the function has run. drop(closure); } @@ -190,13 +149,13 @@ impl alloc::borrow::ToOwned for NSMutableArray { } } -unsafe impl NSFastEnumeration for NSMutableArray { +unsafe impl NSFastEnumeration2 for NSMutableArray { type Item = T; } impl<'a, T: Message, O: Ownership> IntoIterator for &'a NSMutableArray { type Item = &'a T; - type IntoIter = NSFastEnumerator<'a, NSMutableArray>; + type IntoIter = NSFastEnumerator2<'a, NSMutableArray>; fn into_iter(self) -> Self::IntoIter { self.iter_fast() @@ -233,13 +192,6 @@ impl DefaultId for NSMutableArray { } } -impl fmt::Debug for NSMutableArray { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&**self, f) - } -} - #[cfg(test)] mod tests { use alloc::vec; @@ -304,7 +256,7 @@ mod tests { expected.assert_current(); assert_eq!(array.len(), 2); - array.clear(); + array.removeAllObjects(); expected.release += 2; expected.dealloc += 2; expected.assert_current(); diff --git a/crates/icrate/src/Foundation/additions/mutable_attributed_string.rs b/crates/icrate/src/Foundation/additions/mutable_attributed_string.rs index 945476a88..37ff1a5f0 100644 --- a/crates/icrate/src/Foundation/additions/mutable_attributed_string.rs +++ b/crates/icrate/src/Foundation/additions/mutable_attributed_string.rs @@ -1,21 +1,9 @@ -use core::fmt; - -use super::{NSAttributedString, NSCopying, NSMutableCopying, NSObject, NSString}; use objc2::rc::{DefaultId, Id, Owned, Shared}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; - -extern_class!( - /// A mutable string that has associated attributes. - /// - /// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nsmutableattributedstring?language=objc). - #[derive(PartialEq, Eq, Hash)] - pub struct NSMutableAttributedString; +use objc2::{extern_methods, msg_send_id, ClassType}; - unsafe impl ClassType for NSMutableAttributedString { - #[inherits(NSObject)] - type Super = NSAttributedString; - } -); +use crate::Foundation::{ + NSAttributedString, NSCopying, NSMutableAttributedString, NSMutableCopying, NSString, +}; extern_methods!( /// Creating mutable attributed strings. @@ -36,19 +24,6 @@ extern_methods!( unsafe { msg_send_id![Self::alloc(), initWithAttributedString: attributed_string] } } } - - /// Modifying the attributed string. - unsafe impl NSMutableAttributedString { - // TODO - // - mutableString - // - replaceCharactersInRange:withString: - // - setAttributes:range: - - /// Replaces the entire attributed string. - #[doc(alias = "setAttributedString:")] - #[method(setAttributedString:)] - pub fn replace(&mut self, attributed_string: &NSAttributedString); - } ); impl DefaultId for NSMutableAttributedString { @@ -76,12 +51,6 @@ impl alloc::borrow::ToOwned for NSMutableAttributedString { } } -impl fmt::Debug for NSMutableAttributedString { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&**self, f) - } -} - #[cfg(test)] mod tests { use alloc::string::ToString; diff --git a/crates/icrate/src/Foundation/additions/mutable_data.rs b/crates/icrate/src/Foundation/additions/mutable_data.rs index 5aed4a636..076574427 100644 --- a/crates/icrate/src/Foundation/additions/mutable_data.rs +++ b/crates/icrate/src/Foundation/additions/mutable_data.rs @@ -1,32 +1,16 @@ #[cfg(feature = "block")] use alloc::vec::Vec; use core::ffi::c_void; -use core::fmt; use core::ops::{Index, IndexMut, Range}; +use core::ptr::NonNull; use core::slice::{self, SliceIndex}; use std::io; -use super::data::with_slice; -use super::{NSCopying, NSData, NSMutableCopying, NSObject, NSRange}; use objc2::rc::{DefaultId, Id, Owned, Shared}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use objc2::{extern_methods, ClassType}; -extern_class!( - /// A dynamic byte buffer in memory. - /// - /// This is the Objective-C equivalent of a [`Vec`] containing [`u8`]. - /// - /// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nsmutabledata?language=objc). - /// - /// [`Vec`]: std::vec::Vec - #[derive(PartialEq, Eq, Hash)] - pub struct NSMutableData; - - unsafe impl ClassType for NSMutableData { - #[inherits(NSObject)] - type Super = NSData; - } -); +use super::data::with_slice; +use crate::Foundation::{NSCopying, NSData, NSMutableCopying, NSMutableData, NSRange}; extern_methods!( /// Creation methods @@ -42,66 +26,39 @@ extern_methods!( pub fn from_vec(bytes: Vec) -> Id { unsafe { Id::from_shared(Id::cast(super::data::with_vec(Self::class(), bytes))) } } - - // TODO: Use malloc_buf/mbox and `initWithBytesNoCopy:...`? - - #[doc(alias = "initWithData:")] - pub fn from_data(data: &NSData) -> Id { - // Not provided on NSData, one should just use NSData::copy or similar - unsafe { msg_send_id![Self::alloc(), initWithData: data] } - } - - #[doc(alias = "initWithCapacity:")] - pub fn with_capacity(capacity: usize) -> Id { - unsafe { msg_send_id![Self::alloc(), initWithCapacity: capacity] } - } } /// Mutation methods unsafe impl NSMutableData { - /// Expands with zeroes, or truncates the buffer. - #[doc(alias = "setLength:")] - #[method(setLength:)] - pub fn set_len(&mut self, len: usize); - - #[method(mutableBytes)] - fn bytes_mut_raw(&mut self) -> *mut c_void; - #[doc(alias = "mutableBytes")] pub fn bytes_mut(&mut self) -> &mut [u8] { - let ptr = self.bytes_mut_raw(); - let ptr: *mut u8 = ptr.cast(); - // The bytes pointer may be null for length zero - if ptr.is_null() { - &mut [] - } else { + if let Some(ptr) = self.mutableBytes() { + let ptr: *mut u8 = ptr.as_ptr().cast(); unsafe { slice::from_raw_parts_mut(ptr, self.len()) } + } else { + // The bytes pointer may be null for length zero on GNUStep + &mut [] } } - #[method(appendBytes:length:)] - unsafe fn append_raw(&mut self, ptr: *const c_void, len: usize); - #[doc(alias = "appendBytes:length:")] pub fn extend_from_slice(&mut self, bytes: &[u8]) { - let bytes_ptr: *const c_void = bytes.as_ptr().cast(); - unsafe { self.append_raw(bytes_ptr, bytes.len()) } + let bytes_ptr: NonNull = + NonNull::new(bytes.as_ptr() as *mut u8).unwrap().cast(); + unsafe { self.appendBytes_length(bytes_ptr, bytes.len()) } } pub fn push(&mut self, byte: u8) { self.extend_from_slice(&[byte]) } - #[method(replaceBytesInRange:withBytes:length:)] - unsafe fn replace_raw(&mut self, range: NSRange, ptr: *const c_void, len: usize); - #[doc(alias = "replaceBytesInRange:withBytes:length:")] pub fn replace_range(&mut self, range: Range, bytes: &[u8]) { let range = NSRange::from(range); // No need to verify the length of the range here, // `replaceBytesInRange:` just zero-fills if out of bounds. - let ptr: *const c_void = bytes.as_ptr().cast(); - unsafe { self.replace_raw(range, ptr, bytes.len()) } + let ptr = bytes.as_ptr() as *mut c_void; + unsafe { self.replaceBytesInRange_withBytes_length(range, ptr, bytes.len()) } } pub fn set_bytes(&mut self, bytes: &[u8]) { @@ -211,13 +168,6 @@ impl DefaultId for NSMutableData { } } -impl fmt::Debug for NSMutableData { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&**self, f) - } -} - impl<'a> IntoIterator for &'a NSMutableData { type Item = &'a u8; type IntoIter = core::slice::Iter<'a, u8>; @@ -239,8 +189,11 @@ impl<'a> IntoIterator for &'a mut NSMutableData { #[cfg(test)] mod tests { use super::*; + use objc2::runtime::Object; + use crate::Foundation::NSObject; + #[test] fn test_bytes_mut() { let mut data = NSMutableData::with_bytes(&[7, 16]); @@ -251,11 +204,11 @@ mod tests { #[test] fn test_set_len() { let mut data = NSMutableData::with_bytes(&[7, 16]); - data.set_len(4); + data.setLength(4); assert_eq!(data.len(), 4); assert_eq!(data.bytes(), [7, 16, 0, 0]); - data.set_len(1); + data.setLength(1); assert_eq!(data.len(), 1); assert_eq!(data.bytes(), [7]); } @@ -287,13 +240,13 @@ mod tests { #[test] fn test_from_data() { let data = NSData::with_bytes(&[1, 2]); - let mut_data = NSMutableData::from_data(&data); + let mut_data = NSMutableData::dataWithData(&data); assert_eq!(&*data, &**mut_data); } #[test] fn test_with_capacity() { - let mut data = NSMutableData::with_capacity(5); + let mut data = NSMutableData::dataWithCapacity(5).unwrap(); assert_eq!(data.bytes(), &[]); data.extend_from_slice(&[1, 2, 3, 4, 5]); assert_eq!(data.bytes(), &[1, 2, 3, 4, 5]); diff --git a/crates/icrate/src/Foundation/additions/mutable_dictionary.rs b/crates/icrate/src/Foundation/additions/mutable_dictionary.rs index dff65e2be..8d0804321 100644 --- a/crates/icrate/src/Foundation/additions/mutable_dictionary.rs +++ b/crates/icrate/src/Foundation/additions/mutable_dictionary.rs @@ -1,43 +1,13 @@ use alloc::vec::Vec; -use core::fmt; -use core::marker::PhantomData; use core::ops::{Index, IndexMut}; -use core::panic::{RefUnwindSafe, UnwindSafe}; use core::ptr; -use super::{NSArray, NSCopying, NSDictionary, NSFastEnumeration, NSObject}; use objc2::rc::{DefaultId, Id, Owned, Shared}; -use objc2::{ClassType, __inner_extern_class, extern_methods, msg_send_id, Message}; - -__inner_extern_class!( - /// A mutable collection of objects associated with unique keys. - /// - /// See the documentation for [`NSDictionary`] and/or [Apple's - /// documentation][apple-doc] for more information. - /// - /// [apple-doc]: https://developer.apple.com/documentation/foundation/nsmutabledictionary?language=objc - #[derive(PartialEq, Eq, Hash)] - pub struct NSMutableDictionary { - key: PhantomData>, - obj: PhantomData>, - } - - unsafe impl ClassType for NSMutableDictionary { - #[inherits(NSObject)] - type Super = NSDictionary; - } -); - -// Same as `NSDictionary` -unsafe impl Sync for NSMutableDictionary {} -unsafe impl Send for NSMutableDictionary {} +use objc2::{extern_methods, msg_send_id, ClassType, Message}; -// Same as `NSDictionary` -impl UnwindSafe for NSMutableDictionary {} -impl RefUnwindSafe - for NSMutableDictionary -{ -} +use crate::Foundation::{ + NSArray, NSCopying, NSDictionary, NSFastEnumeration2, NSMutableDictionary, +}; extern_methods!( unsafe impl NSMutableDictionary { @@ -56,9 +26,6 @@ extern_methods!( #[method_id(new)] pub fn new() -> Id; - #[method(setDictionary:)] - fn set_dictionary(&mut self, dict: &NSDictionary); - /// Creates an [`NSMutableDictionary`] from a slice of keys and a /// vector of values. /// @@ -81,7 +48,7 @@ extern_methods!( T: NSCopying, { let mut dict = NSMutableDictionary::new(); - dict.set_dictionary(&*NSDictionary::from_keys_and_objects(keys, vals)); + dict.setDictionary(&*NSDictionary::from_keys_and_objects(keys, vals)); dict } @@ -158,9 +125,6 @@ extern_methods!( obj } - #[method(removeObjectForKey:)] - fn remove_object_for_key(&mut self, key: &K); - /// Removes a key from the dictionary, returning the value at the key /// if the key was previously in the dictionary. /// @@ -183,26 +147,10 @@ extern_methods!( let obj = self.get(key).map(|obj| unsafe { Id::retain_autoreleased(obj as *const V as *mut V).unwrap_unchecked() }); - self.remove_object_for_key(key); + self.removeObjectForKey(key); obj } - /// Clears the dictionary, removing all key-value pairs. - /// - /// # Examples - /// - /// ``` - /// use icrate::Foundation::{NSMutableDictionary, NSObject, NSString}; - /// - /// let mut dict = NSMutableDictionary::new(); - /// dict.insert(NSString::from_str("one"), NSObject::new()); - /// dict.clear(); - /// assert!(dict.is_empty()); - /// ``` - #[doc(alias = "removeAllObjects")] - #[method(removeAllObjects)] - pub fn clear(&mut self); - /// Returns an [`NSArray`] containing the dictionary's values, /// consuming the dictionary. /// @@ -222,7 +170,7 @@ extern_methods!( } ); -unsafe impl NSFastEnumeration for NSMutableDictionary { +unsafe impl NSFastEnumeration2 for NSMutableDictionary { type Item = K; } @@ -249,19 +197,12 @@ impl DefaultId for NSMutableDictionary { } } -impl fmt::Debug for NSMutableDictionary { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&**self, f) - } -} - #[cfg(test)] mod tests { use super::*; use alloc::vec; - use crate::Foundation::{NSNumber, NSString}; + use crate::Foundation::{NSNumber, NSObject, NSString}; use objc2::rc::{__RcTestObject, __ThreadTestData}; fn sample_dict() -> Id, Owned> { @@ -341,7 +282,7 @@ mod tests { let mut dict = sample_dict(); assert_eq!(dict.len(), 3); - dict.clear(); + dict.removeAllObjects(); assert!(dict.is_empty()); } @@ -365,7 +306,7 @@ mod tests { expected.assert_current(); assert_eq!(dict.len(), 2); - dict.clear(); + dict.removeAllObjects(); expected.release += 2; expected.dealloc += 2; expected.assert_current(); diff --git a/crates/icrate/src/Foundation/additions/mutable_set.rs b/crates/icrate/src/Foundation/additions/mutable_set.rs index fbcd72b71..0939e4c73 100644 --- a/crates/icrate/src/Foundation/additions/mutable_set.rs +++ b/crates/icrate/src/Foundation/additions/mutable_set.rs @@ -1,35 +1,12 @@ use alloc::vec::Vec; -use core::fmt; -use core::marker::PhantomData; -use super::set::with_objects; -use super::{NSCopying, NSFastEnumeration, NSFastEnumerator, NSMutableCopying, NSObject, NSSet}; use objc2::rc::{DefaultId, Id, Owned, Ownership, Shared, SliceId}; -use objc2::{ClassType, Message, __inner_extern_class, extern_methods}; - -__inner_extern_class!( - /// A growable unordered collection of unique objects. - /// - /// See the documentation for [`NSSet`] and/or [Apple's - /// documentation][apple-doc] for more information. - /// - /// [apple-doc]: https://developer.apple.com/documentation/foundation/nsmutableset?language=objc - #[derive(PartialEq, Eq, Hash)] - pub struct NSMutableSet { - p: PhantomData<*mut ()>, - } - - unsafe impl ClassType for NSMutableSet { - #[inherits(NSObject)] - type Super = NSSet; - } -); +use objc2::{extern_methods, ClassType, Message}; -// SAFETY: Same as NSSet -unsafe impl Sync for NSMutableSet {} -unsafe impl Send for NSMutableSet {} -unsafe impl Sync for NSMutableSet {} -unsafe impl Send for NSMutableSet {} +use super::set::with_objects; +use crate::Foundation::{ + NSCopying, NSFastEnumeration2, NSFastEnumerator2, NSMutableCopying, NSMutableSet, NSSet, +}; extern_methods!( unsafe impl NSMutableSet { @@ -64,22 +41,6 @@ extern_methods!( unsafe { with_objects(vec.as_slice_ref()) } } - /// Clears the set, removing all values. - /// - /// # Examples - /// - /// ``` - /// use icrate::Foundation::{NSMutableSet, NSString}; - /// - /// let mut set = NSMutableSet::new(); - /// set.insert(NSString::from_str("one")); - /// set.clear(); - /// assert!(set.is_empty()); - /// ``` - #[doc(alias = "removeAllObjects")] - #[method(removeAllObjects)] - pub fn clear(&mut self); - /// Returns a [`Vec`] containing the set's elements, consuming the set. /// /// # Examples @@ -202,13 +163,13 @@ impl alloc::borrow::ToOwned for NSMutableSet { } } -unsafe impl NSFastEnumeration for NSMutableSet { +unsafe impl NSFastEnumeration2 for NSMutableSet { type Item = T; } impl<'a, T: Message, O: Ownership> IntoIterator for &'a NSMutableSet { type Item = &'a T; - type IntoIter = NSFastEnumerator<'a, NSMutableSet>; + type IntoIter = NSFastEnumerator2<'a, NSMutableSet>; fn into_iter(self) -> Self::IntoIter { self.iter_fast() @@ -232,13 +193,6 @@ impl DefaultId for NSMutableSet { } } -impl fmt::Debug for NSMutableSet { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&**self, f) - } -} - #[cfg(test)] mod tests { use alloc::vec; @@ -273,7 +227,7 @@ mod tests { let mut set = NSMutableSet::from_slice(&strs); assert_eq!(set.len(), 3); - set.clear(); + set.removeAllObjects(); assert!(set.is_empty()); } @@ -288,12 +242,12 @@ mod tests { let mut vec = NSMutableSet::into_vec(set); for str in vec.iter_mut() { - str.push_nsstring(ns_string!(" times zero is zero")); + str.appendString(ns_string!(" times zero is zero")); } assert_eq!(vec.len(), 3); let suffix = ns_string!("zero"); - assert!(vec.iter().all(|str| str.has_suffix(suffix))); + assert!(vec.iter().all(|str| str.hasSuffix(suffix))); } #[test] @@ -344,7 +298,7 @@ mod tests { } let mut expected = __ThreadTestData::current(); - set.clear(); + set.removeAllObjects(); expected.release += 4; expected.dealloc += 4; expected.assert_current(); diff --git a/crates/icrate/src/Foundation/additions/mutable_string.rs b/crates/icrate/src/Foundation/additions/mutable_string.rs index 45359b1db..5e20bbaa0 100644 --- a/crates/icrate/src/Foundation/additions/mutable_string.rs +++ b/crates/icrate/src/Foundation/additions/mutable_string.rs @@ -3,22 +3,10 @@ use core::fmt; use core::ops::AddAssign; use core::str; -use super::{NSCopying, NSMutableCopying, NSObject, NSString}; use objc2::rc::{DefaultId, Id, Owned, Shared}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use objc2::{extern_methods, ClassType}; -extern_class!( - /// A dynamic plain-text Unicode string object. - /// - /// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nsmutablestring?language=objc). - #[derive(PartialEq, Eq, Hash)] - pub struct NSMutableString; - - unsafe impl ClassType for NSMutableString { - #[inherits(NSObject)] - type Super = NSString; - } -); +use crate::Foundation::{NSCopying, NSMutableCopying, NSMutableString, NSString}; extern_methods!( /// Creating mutable strings. @@ -36,38 +24,6 @@ extern_methods!( Id::new(obj.cast()).unwrap() } } - - /// Creates a new [`NSMutableString`] from the given [`NSString`]. - #[doc(alias = "initWithString:")] - pub fn from_nsstring(string: &NSString) -> Id { - unsafe { msg_send_id![Self::alloc(), initWithString: string] } - } - - #[doc(alias = "initWithCapacity:")] - pub fn with_capacity(capacity: usize) -> Id { - unsafe { msg_send_id![Self::alloc(), initWithCapacity: capacity] } - } - } - - /// Mutating strings. - unsafe impl NSMutableString { - /// Appends the given [`NSString`] onto the end of this. - #[doc(alias = "appendString:")] - // SAFETY: The string is not nil - #[method(appendString:)] - pub fn push_nsstring(&mut self, nsstring: &NSString); - - /// Replaces the entire string. - #[doc(alias = "setString:")] - // SAFETY: The string is not nil - #[method(setString:)] - pub fn replace(&mut self, nsstring: &NSString); - - // TODO: - // - deleteCharactersInRange: - // - replaceCharactersInRange:withString: - // - insertString:atIndex: - // Figure out how these work on character boundaries } ); @@ -99,7 +55,7 @@ impl alloc::borrow::ToOwned for NSMutableString { impl AddAssign<&NSString> for NSMutableString { #[inline] fn add_assign(&mut self, other: &NSString) { - self.push_nsstring(other) + self.appendString(other) } } @@ -148,7 +104,7 @@ impl Ord for NSMutableString { impl fmt::Write for NSMutableString { fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> { let nsstring = NSString::from_str(s); - self.push_nsstring(&nsstring); + self.appendString(&nsstring); Ok(()) } } @@ -160,13 +116,6 @@ impl fmt::Display for NSMutableString { } } -impl fmt::Debug for NSMutableString { - #[inline] - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&**self, f) - } -} - #[cfg(test)] mod tests { use alloc::format; @@ -184,28 +133,28 @@ mod tests { #[test] fn test_from_nsstring() { let s = NSString::from_str("abc"); - let s = NSMutableString::from_nsstring(&s); + let s = NSMutableString::stringWithString(&s); assert_eq!(&s.to_string(), "abc"); } #[test] - fn test_push_nsstring() { + fn test_append() { let mut s = NSMutableString::from_str("abc"); - s.push_nsstring(&NSString::from_str("def")); + s.appendString(&NSString::from_str("def")); *s += &NSString::from_str("ghi"); assert_eq!(&s.to_string(), "abcdefghi"); } #[test] - fn test_replace() { + fn test_set() { let mut s = NSMutableString::from_str("abc"); - s.replace(&NSString::from_str("def")); + s.setString(&NSString::from_str("def")); assert_eq!(&s.to_string(), "def"); } #[test] fn test_with_capacity() { - let mut s = NSMutableString::with_capacity(3); + let mut s = NSMutableString::stringWithCapacity(3); *s += &NSString::from_str("abc"); *s += &NSString::from_str("def"); assert_eq!(&s.to_string(), "abcdef"); diff --git a/crates/icrate/src/Foundation/additions/number.rs b/crates/icrate/src/Foundation/additions/number.rs index caaa3ee81..283066261 100644 --- a/crates/icrate/src/Foundation/additions/number.rs +++ b/crates/icrate/src/Foundation/additions/number.rs @@ -1,50 +1,23 @@ +//! Note that due to limitations in Objective-C type encodings, it is not +//! possible to distinguish between an `NSNumber` created from [`bool`], +//! and one created from an [`i8`]/[`u8`]. You should use the getter +//! methods that fit your use-case instead! +//! +//! This does not implement [`Eq`] nor [`Ord`], since it may contain a +//! floating point value. Beware that the implementation of [`PartialEq`] +//! and [`PartialOrd`] does not properly handle NaNs either. Compare +//! [`NSNumber::encoding`] with [`Encoding::Float`] or +//! [`Encoding::Double`], and use [`NSNumber::as_f32`] or +//! [`NSNumber::as_f64`] to get the desired floating point value directly. use core::cmp::Ordering; use core::fmt; use core::hash; use core::panic::{RefUnwindSafe, UnwindSafe}; -use std::os::raw::{ - c_char, c_double, c_float, c_int, c_longlong, c_short, c_uchar, c_uint, c_ulonglong, c_ushort, -}; -use super::{ - CGFloat, NSComparisonResult, NSCopying, NSInteger, NSObject, NSString, NSUInteger, NSValue, -}; +use objc2::encode::Encoding; use objc2::rc::{Id, Shared}; -use objc2::{extern_class, extern_methods, msg_send, msg_send_id, ClassType, Encoding}; -extern_class!( - /// An object wrapper for primitive scalars. - /// - /// This is the Objective-C equivalant of a Rust enum containing the - /// common scalar types `i8`, `u8`, `i16`, `u16`, `i32`, `u32`, `i64`, - /// `u64`, `f32`, `f64` and the two C types `c_long` and `c_ulong`. - /// - /// All accessor methods are safe, though they may return unexpected - /// results if the number was not created from said type. Consult [Apple's - /// documentation][apple-doc] for details. - /// - /// Note that due to limitations in Objective-C type encodings, it is not - /// possible to distinguish between an `NSNumber` created from [`bool`], - /// and one created from an [`i8`]/[`u8`]. You should use the getter - /// methods that fit your use-case instead! - /// - /// This does not implement [`Eq`] nor [`Ord`], since it may contain a - /// floating point value. Beware that the implementation of [`PartialEq`] - /// and [`PartialOrd`] does not properly handle NaNs either. Compare - /// [`NSNumber::encoding`] with [`Encoding::Float`] or - /// [`Encoding::Double`], and use [`NSNumber::as_f32`] or - /// [`NSNumber::as_f64`] to get the desired floating point value directly. - /// - /// See [Apple's documentation][apple-doc] for more information. - /// - /// [apple-doc]: https://developer.apple.com/documentation/foundation/nsnumber?language=objc - pub struct NSNumber; - - unsafe impl ClassType for NSNumber { - #[inherits(NSObject)] - type Super = NSValue; - } -); +use crate::Foundation::{CGFloat, NSCopying, NSNumber}; // SAFETY: `NSNumber` is just a wrapper around an integer/float/bool, and it // is immutable. @@ -57,14 +30,11 @@ impl RefUnwindSafe for NSNumber {} macro_rules! def_new_fn { {$( $(#[$($m:meta)*])* - ($fn_name:ident($fn_inp:ty); $method_name:ident: $method_inp:ty), + ($fn_name:ident($fn_inp:ty); $method_name:ident), )*} => {$( $(#[$($m)*])* pub fn $fn_name(val: $fn_inp) -> Id { - let val = val as $method_inp; - unsafe { - msg_send_id![Self::class(), $method_name: val] - } + Self::$method_name(val as _) } )*} } @@ -72,19 +42,19 @@ macro_rules! def_new_fn { /// Creation methods. impl NSNumber { def_new_fn! { - (new_bool(bool); numberWithBool: bool), - (new_i8(i8); numberWithChar: c_char), - (new_u8(u8); numberWithUnsignedChar: c_uchar), - (new_i16(i16); numberWithShort: c_short), - (new_u16(u16); numberWithUnsignedShort: c_ushort), - (new_i32(i32); numberWithInt: c_int), - (new_u32(u32); numberWithUnsignedInt: c_uint), - (new_i64(i64); numberWithLongLong: c_longlong), - (new_u64(u64); numberWithUnsignedLongLong: c_ulonglong), - (new_isize(isize); numberWithInteger: NSInteger), - (new_usize(usize); numberWithUnsignedInteger: NSUInteger), - (new_f32(f32); numberWithFloat: c_float), - (new_f64(f64); numberWithDouble: c_double), + (new_bool(bool); numberWithBool), + (new_i8(i8); numberWithChar), + (new_u8(u8); numberWithUnsignedChar), + (new_i16(i16); numberWithShort), + (new_u16(u16); numberWithUnsignedShort), + (new_i32(i32); numberWithInt), + (new_u32(u32); numberWithUnsignedInt), + (new_i64(i64); numberWithLongLong), + (new_u64(u64); numberWithUnsignedLongLong), + (new_isize(isize); numberWithInteger), + (new_usize(usize); numberWithUnsignedInteger), + (new_f32(f32); numberWithFloat), + (new_f64(f64); numberWithDouble), } #[inline] @@ -103,12 +73,11 @@ impl NSNumber { macro_rules! def_get_fn { {$( $(#[$($m:meta)*])* - ($fn_name:ident -> $fn_ret:ty; $method_name:ident -> $method_ret:ty), + ($fn_name:ident -> $fn_ret:ty; $method_name:ident), )*} => {$( $(#[$($m)*])* pub fn $fn_name(&self) -> $fn_ret { - let ret: $method_ret = unsafe { msg_send![self, $method_name] }; - ret as $fn_ret + self.$method_name() as _ } )*} } @@ -116,20 +85,19 @@ macro_rules! def_get_fn { /// Getter methods. impl NSNumber { def_get_fn! { - (as_bool -> bool; boolValue -> bool), - (as_i8 -> i8; charValue -> c_char), - (as_u8 -> u8; unsignedCharValue -> c_uchar), - (as_i16 -> i16; shortValue -> c_short), - (as_u16 -> u16; unsignedShortValue -> c_ushort), - (as_i32 -> i32; intValue -> c_int), - (as_u32 -> u32; unsignedIntValue -> c_uint), - // TODO: Getter methods for `long` and `unsigned long` - (as_i64 -> i64; longLongValue -> c_longlong), - (as_u64 -> u64; unsignedLongLongValue -> c_ulonglong), - (as_isize -> isize; integerValue -> NSInteger), - (as_usize -> usize; unsignedIntegerValue -> NSUInteger), - (as_f32 -> f32; floatValue -> c_float), - (as_f64 -> f64; doubleValue -> c_double), + (as_bool -> bool; boolValue), + (as_i8 -> i8; charValue), + (as_u8 -> u8; unsignedCharValue), + (as_i16 -> i16; shortValue), + (as_u16 -> u16; unsignedShortValue), + (as_i32 -> i32; intValue), + (as_u32 -> u32; unsignedIntValue), + (as_i64 -> i64; longLongValue), + (as_u64 -> u64; unsignedLongLongValue), + (as_isize -> isize; integerValue), + (as_usize -> usize; unsignedIntegerValue), + (as_f32 -> f32; floatValue), + (as_f64 -> f64; doubleValue), } #[inline] @@ -236,19 +204,6 @@ impl NSNumber { } } -extern_methods!( - unsafe impl NSNumber { - #[method(compare:)] - fn compare(&self, other: &Self) -> NSComparisonResult; - - #[method(isEqualToNumber:)] - fn is_equal_to_number(&self, other: &Self) -> bool; - - #[method_id(stringValue)] - fn string(&self) -> Id; - } -); - unsafe impl NSCopying for NSNumber { type Ownership = Shared; type Output = NSNumber; @@ -261,20 +216,20 @@ impl alloc::borrow::ToOwned for NSNumber { } } +impl hash::Hash for NSNumber { + #[inline] + fn hash(&self, state: &mut H) { + (**self).hash(state) + } +} + /// Beware: This uses the Objective-C method "isEqualToNumber:", which has /// different floating point NaN semantics than Rust! impl PartialEq for NSNumber { #[doc(alias = "isEqualToNumber:")] fn eq(&self, other: &Self) -> bool { // Use isEqualToNumber: instaed of isEqual: since it is faster - self.is_equal_to_number(other) - } -} - -impl hash::Hash for NSNumber { - fn hash(&self, state: &mut H) { - // Delegate to NSObject - (***self).hash(state) + self.isEqualToNumber(other) } } @@ -290,15 +245,7 @@ impl PartialOrd for NSNumber { impl fmt::Display for NSNumber { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&self.string(), f) - } -} - -impl fmt::Debug for NSNumber { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Delegate to -[NSObject description] - // (happens to return the same as -[NSNumber stringValue]) - fmt::Debug::fmt(&***self, f) + fmt::Display::fmt(&self.stringValue(), f) } } diff --git a/crates/icrate/src/Foundation/additions/process_info.rs b/crates/icrate/src/Foundation/additions/process_info.rs index 362932722..8b51eef3c 100644 --- a/crates/icrate/src/Foundation/additions/process_info.rs +++ b/crates/icrate/src/Foundation/additions/process_info.rs @@ -1,21 +1,7 @@ use core::fmt; use core::panic::{RefUnwindSafe, UnwindSafe}; -use super::{NSObject, NSString}; -use objc2::rc::{Id, Shared}; -use objc2::{extern_class, extern_methods, ClassType}; - -extern_class!( - /// A collection of information about the current process. - /// - /// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nsprocessinfo?language=objc). - #[derive(PartialEq, Eq, Hash)] - pub struct NSProcessInfo; - - unsafe impl ClassType for NSProcessInfo { - type Super = NSObject; - } -); +use crate::Foundation::NSProcessInfo; // SAFETY: The documentation explicitly states: // > NSProcessInfo is thread-safe in macOS 10.7 and later. @@ -25,22 +11,10 @@ unsafe impl Sync for NSProcessInfo {} impl UnwindSafe for NSProcessInfo {} impl RefUnwindSafe for NSProcessInfo {} -extern_methods!( - unsafe impl NSProcessInfo { - #[method_id(processInfo)] - pub fn process_info() -> Id; - - #[method_id(processName)] - pub fn process_name(&self) -> Id; - - // TODO: This contains a lot more important functionality! - } -); - impl fmt::Debug for NSProcessInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("NSProcessInfo") - .field("process_name", &self.process_name()) + .field("processName", &self.processName()) .finish_non_exhaustive() } } @@ -53,10 +27,10 @@ mod tests { #[test] fn test_debug() { - let info = NSProcessInfo::process_info(); + let info = NSProcessInfo::processInfo(); let expected = format!( - "NSProcessInfo {{ process_name: {:?}, .. }}", - info.process_name() + "NSProcessInfo {{ processName: {:?}, .. }}", + info.processName() ); assert_eq!(format!("{info:?}"), expected); } diff --git a/crates/icrate/src/Foundation/additions/range.rs b/crates/icrate/src/Foundation/additions/range.rs index 95f601a53..23413f3fb 100644 --- a/crates/icrate/src/Foundation/additions/range.rs +++ b/crates/icrate/src/Foundation/additions/range.rs @@ -1,8 +1,9 @@ use core::ops::Range; -use super::NSUInteger; use objc2::encode::{Encode, Encoding, RefEncode}; +use crate::Foundation::NSUInteger; + /// TODO. /// /// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nsrange?language=objc). diff --git a/crates/icrate/src/Foundation/additions/set.rs b/crates/icrate/src/Foundation/additions/set.rs index b749be52c..69c55e68f 100644 --- a/crates/icrate/src/Foundation/additions/set.rs +++ b/crates/icrate/src/Foundation/additions/set.rs @@ -1,31 +1,14 @@ use alloc::vec::Vec; use core::fmt; -use core::marker::PhantomData; use core::panic::{RefUnwindSafe, UnwindSafe}; -use super::{ - NSArray, NSCopying, NSEnumerator, NSFastEnumeration, NSFastEnumerator, NSMutableCopying, - NSMutableSet, NSObject, -}; use objc2::rc::{DefaultId, Id, Owned, Ownership, Shared, SliceId}; -use objc2::{ClassType, Message, __inner_extern_class, extern_methods, msg_send, msg_send_id}; - -__inner_extern_class!( - /// An immutable unordered collection of unique objects. - /// - /// See [Apple's documentation][apple-doc]. - /// - /// [apple-doc]: https://developer.apple.com/documentation/foundation/nsset?language=objc - #[derive(PartialEq, Eq, Hash)] - pub struct NSSet { - item: PhantomData>, - notunwindsafe: PhantomData<&'static mut ()>, - } +use objc2::{extern_methods, msg_send, msg_send_id, ClassType, Message}; - unsafe impl ClassType for NSSet { - type Super = NSObject; - } -); +use crate::Foundation::{ + NSArray, NSCopying, NSEnumerator2, NSFastEnumeration2, NSFastEnumerator2, NSMutableCopying, + NSMutableSet, NSSet, +}; // SAFETY: Same as NSArray unsafe impl Sync for NSSet {} @@ -33,11 +16,20 @@ unsafe impl Send for NSSet {} unsafe impl Sync for NSSet {} unsafe impl Send for NSSet {} +unsafe impl Sync for NSMutableSet {} +unsafe impl Send for NSMutableSet {} +unsafe impl Sync for NSMutableSet {} +unsafe impl Send for NSMutableSet {} + // SAFETY: Same as NSArray impl RefUnwindSafe for NSSet {} impl UnwindSafe for NSSet {} impl UnwindSafe for NSSet {} +impl RefUnwindSafe for NSMutableSet {} +impl UnwindSafe for NSMutableSet {} +impl UnwindSafe for NSMutableSet {} + #[track_caller] pub(crate) unsafe fn with_objects( objects: &[&T], @@ -102,8 +94,9 @@ extern_methods!( /// assert_eq!(set.len(), 3); /// ``` #[doc(alias = "count")] - #[method(count)] - pub fn len(&self) -> usize; + pub fn len(&self) -> usize { + self.count() + } /// Returns `true` if the set contains no elements. /// @@ -150,10 +143,10 @@ extern_methods!( /// } /// ``` #[doc(alias = "objectEnumerator")] - pub fn iter(&self) -> NSEnumerator<'_, T> { + pub fn iter(&self) -> NSEnumerator2<'_, T> { unsafe { let result = msg_send![self, objectEnumerator]; - NSEnumerator::from_ptr(result) + NSEnumerator2::from_ptr(result) } } @@ -215,12 +208,11 @@ extern_methods!( /// assert_eq!(set.to_array().len(), 3); /// assert!(set.to_array().iter().all(|i| nums.contains(&i.as_i32()))); /// ``` - // SAFETY: - // We only define this method for sets with shared elements - // because we can't return copies of owned elements. - #[method_id(allObjects)] #[doc(alias = "allObjects")] - pub fn to_array(&self) -> Id, Shared>; + pub fn to_array(&self) -> Id, Shared> { + // SAFETY: The set's elements are shared + unsafe { self.allObjects() } + } } // We're explicit about `T` being `PartialEq` for these methods because the @@ -240,8 +232,9 @@ extern_methods!( /// assert!(set.contains(ns_string!("one"))); /// ``` #[doc(alias = "containsObject:")] - #[method(containsObject:)] - pub fn contains(&self, value: &T) -> bool; + pub fn contains(&self, value: &T) -> bool { + unsafe { self.containsObject(value) } + } /// Returns a reference to the value in the set, if any, that is equal /// to the given value. @@ -337,13 +330,13 @@ impl alloc::borrow::ToOwned for NSSet { } } -unsafe impl NSFastEnumeration for NSSet { +unsafe impl NSFastEnumeration2 for NSSet { type Item = T; } impl<'a, T: Message, O: Ownership> IntoIterator for &'a NSSet { type Item = &'a T; - type IntoIter = NSFastEnumerator<'a, NSSet>; + type IntoIter = NSFastEnumerator2<'a, NSSet>; fn into_iter(self) -> Self::IntoIter { self.iter_fast() @@ -373,7 +366,7 @@ mod tests { use super::*; use crate::ns_string; - use crate::Foundation::{NSMutableString, NSNumber, NSString}; + use crate::Foundation::{NSMutableString, NSNumber, NSObject, NSString}; use objc2::rc::{__RcTestObject, __ThreadTestData}; #[test] @@ -540,12 +533,12 @@ mod tests { let mut vec = NSSet::into_vec(set); for str in vec.iter_mut() { - str.push_nsstring(ns_string!(" times zero is zero")); + str.appendString(ns_string!(" times zero is zero")); } assert_eq!(vec.len(), 3); let suffix = ns_string!("zero"); - assert!(vec.iter().all(|str| str.has_suffix(suffix))); + assert!(vec.iter().all(|str| str.hasSuffix(suffix))); } #[test] diff --git a/crates/icrate/src/Foundation/additions/string.rs b/crates/icrate/src/Foundation/additions/string.rs index 8e7d757a1..9d80f40ca 100644 --- a/crates/icrate/src/Foundation/additions/string.rs +++ b/crates/icrate/src/Foundation/additions/string.rs @@ -12,31 +12,12 @@ use core::str; #[cfg(feature = "apple")] use std::os::raw::c_char; -use super::{NSComparisonResult, NSCopying, NSError, NSMutableCopying, NSMutableString, NSObject}; use objc2::rc::{autoreleasepool, AutoreleasePool, DefaultId, Id, Shared}; use objc2::runtime::__nsstring::{nsstring_len, nsstring_to_str, UTF8_ENCODING}; use objc2::runtime::{Class, Object}; -use objc2::{extern_class, extern_methods, msg_send, ClassType}; +use objc2::{msg_send, ClassType}; -extern_class!( - /// An immutable, plain-text Unicode string object. - /// - /// Can be created statically using the [`ns_string!`] macro. - /// - /// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nsstring?language=objc). - /// - /// [`ns_string!`]: crate::ns_string - #[derive(PartialEq, Eq, Hash)] - pub struct NSString; - // TODO: Use isEqualToString: for comparison (instead of just isEqual:) - // The former is more performant - - // TODO: Check if performance of NSSelectorFromString is worthwhile - - unsafe impl ClassType for NSString { - type Super = NSObject; - } -); +use crate::Foundation::{NSCopying, NSMutableCopying, NSMutableString, NSString}; // SAFETY: `NSString` is immutable and `NSMutableString` can only be mutated // from `&mut` methods. @@ -48,200 +29,114 @@ unsafe impl Send for NSString {} impl UnwindSafe for NSString {} impl RefUnwindSafe for NSString {} -extern_methods!( - unsafe impl NSString { - /// Construct an empty NSString. - #[method_id(new)] - pub fn new() -> Id; - - /// Create a new string by appending the given string to self. - /// - /// - /// # Example - /// - /// ``` - /// use icrate::ns_string; - /// let error_tag = ns_string!("Error: "); - /// let error_string = ns_string!("premature end of file."); - /// let error_message = error_tag.concat(error_string); - /// assert_eq!(&*error_message, ns_string!("Error: premature end of file.")); - /// ``` - // SAFETY: The other string is non-null, and won't be retained by the - // function. - #[method_id(stringByAppendingString:)] - #[doc(alias = "stringByAppendingString")] - #[doc(alias = "stringByAppendingString:")] - pub fn concat(&self, other: &Self) -> Id; - - /// Create a new string by appending the given string, separated by - /// a path separator. - /// - /// This is similar to [`Path::join`][std::path::Path::join]. - /// - /// Note that this method only works with file paths (not, for - /// example, string representations of URLs). - /// - /// - /// # Examples - /// - /// ``` - /// use icrate::ns_string; - /// - /// let extension = ns_string!("scratch.tiff"); - /// assert_eq!(&*ns_string!("/tmp").join_path(extension), ns_string!("/tmp/scratch.tiff")); - /// assert_eq!(&*ns_string!("/tmp/").join_path(extension), ns_string!("/tmp/scratch.tiff")); - /// assert_eq!(&*ns_string!("/").join_path(extension), ns_string!("/scratch.tiff")); - /// assert_eq!(&*ns_string!("").join_path(extension), ns_string!("scratch.tiff")); - /// ``` - // SAFETY: Same as `Self::concat`. - #[method_id(stringByAppendingPathComponent:)] - #[doc(alias = "stringByAppendingPathComponent")] - #[doc(alias = "stringByAppendingPathComponent:")] - pub fn join_path(&self, other: &Self) -> Id; - - /// The number of UTF-8 code units in `self`. - #[doc(alias = "lengthOfBytesUsingEncoding")] - #[doc(alias = "lengthOfBytesUsingEncoding:")] - pub fn len(&self) -> usize { - // SAFETY: This is an instance of `NSString` - unsafe { nsstring_len(self) } - } +impl NSString { + /// Construct an empty NSString. + pub fn new() -> Id { + Self::init(Self::alloc()) + } - /// The number of UTF-16 code units in the string. - /// - /// See also [`NSString::len`]. - #[doc(alias = "length")] - #[method(length)] - pub fn len_utf16(&self) -> usize; - - pub fn is_empty(&self) -> bool { - // TODO: lengthOfBytesUsingEncoding: might sometimes return 0 for - // other reasons, so this is not really correct! - self.len() == 0 - } + /// The number of UTF-8 code units in `self`. + #[doc(alias = "lengthOfBytesUsingEncoding")] + #[doc(alias = "lengthOfBytesUsingEncoding:")] + pub fn len(&self) -> usize { + // SAFETY: This is an instance of `NSString` + unsafe { nsstring_len(self) } + } - /// Get the [`str`](`prim@str`) representation of this string if it can be - /// done efficiently. - /// - /// Returns [`None`] if the internal storage does not allow this to be - /// done efficiently. Use [`NSString::as_str`] or `NSString::to_string` - /// if performance is not an issue. - #[doc(alias = "CFStringGetCStringPtr")] - #[allow(unused)] - #[cfg(feature = "apple")] - // TODO: Finish this - fn as_str_wip(&self) -> Option<&str> { - type CFStringEncoding = u32; - #[allow(non_upper_case_globals)] - // https://developer.apple.com/documentation/corefoundation/cfstringbuiltinencodings/kcfstringencodingutf8?language=objc - const kCFStringEncodingUTF8: CFStringEncoding = 0x08000100; - extern "C" { - // https://developer.apple.com/documentation/corefoundation/1542133-cfstringgetcstringptr?language=objc - fn CFStringGetCStringPtr(s: &NSString, encoding: CFStringEncoding) - -> *const c_char; - } - let bytes = unsafe { CFStringGetCStringPtr(self, kCFStringEncodingUTF8) }; - NonNull::new(bytes as *mut u8).map(|bytes| { - let len = self.len(); - let bytes: &[u8] = unsafe { slice::from_raw_parts(bytes.as_ptr(), len) }; - str::from_utf8(bytes).unwrap() - }) - } + /// The number of UTF-16 code units in the string. + /// + /// See also [`NSString::len`]. + #[doc(alias = "length")] + pub fn len_utf16(&self) -> usize { + self.length() + } - /// Get an [UTF-16] string slice if it can be done efficiently. - /// - /// Returns [`None`] if the internal storage of `self` does not allow this - /// to be returned efficiently. - /// - /// See [`as_str`](Self::as_str) for the UTF-8 equivalent. - /// - /// [UTF-16]: https://en.wikipedia.org/wiki/UTF-16 - #[allow(unused)] - #[cfg(feature = "apple")] - // TODO: Finish this - fn as_utf16(&self) -> Option<&[u16]> { - extern "C" { - // https://developer.apple.com/documentation/corefoundation/1542939-cfstringgetcharactersptr?language=objc - fn CFStringGetCharactersPtr(s: &NSString) -> *const u16; - } - let ptr = unsafe { CFStringGetCharactersPtr(self) }; - NonNull::new(ptr as *mut u16) - .map(|ptr| unsafe { slice::from_raw_parts(ptr.as_ptr(), self.len_utf16()) }) - } + pub fn is_empty(&self) -> bool { + // TODO: lengthOfBytesUsingEncoding: might sometimes return 0 for + // other reasons, so this is not really correct! + self.len() == 0 + } - /// Get the [`str`](`prim@str`) representation of this. - /// - /// TODO: Further explain this. - #[doc(alias = "UTF8String")] - pub fn as_str<'r, 's: 'r, 'p: 'r>(&'s self, pool: &'p AutoreleasePool) -> &'r str { - // SAFETY: This is an instance of `NSString` - unsafe { nsstring_to_str(self, pool) } + /// Get the [`str`](`prim@str`) representation of this string if it can be + /// done efficiently. + /// + /// Returns [`None`] if the internal storage does not allow this to be + /// done efficiently. Use [`NSString::as_str`] or `NSString::to_string` + /// if performance is not an issue. + #[doc(alias = "CFStringGetCStringPtr")] + #[allow(unused)] + #[cfg(feature = "apple")] + // TODO: Finish this + fn as_str_wip(&self) -> Option<&str> { + type CFStringEncoding = u32; + #[allow(non_upper_case_globals)] + // https://developer.apple.com/documentation/corefoundation/cfstringbuiltinencodings/kcfstringencodingutf8?language=objc + const kCFStringEncodingUTF8: CFStringEncoding = 0x08000100; + extern "C" { + // https://developer.apple.com/documentation/corefoundation/1542133-cfstringgetcstringptr?language=objc + fn CFStringGetCStringPtr(s: &NSString, encoding: CFStringEncoding) -> *const c_char; } + let bytes = unsafe { CFStringGetCStringPtr(self, kCFStringEncodingUTF8) }; + NonNull::new(bytes as *mut u8).map(|bytes| { + let len = self.len(); + let bytes: &[u8] = unsafe { slice::from_raw_parts(bytes.as_ptr(), len) }; + str::from_utf8(bytes).unwrap() + }) + } - // TODO: Allow usecases where the NUL byte from `UTF8String` is kept? - - /// Creates an immutable `NSString` by copying the given string slice. - /// - /// Prefer using the [`ns_string!`] macro when possible. - /// - /// [`ns_string!`]: crate::ns_string - #[doc(alias = "initWithBytes")] - #[doc(alias = "initWithBytes:length:encoding:")] - #[allow(clippy::should_implement_trait)] // Not really sure of a better name - pub fn from_str(string: &str) -> Id { - unsafe { - let obj = from_str(Self::class(), string); - Id::new(obj.cast()).unwrap() - } + /// Get an [UTF-16] string slice if it can be done efficiently. + /// + /// Returns [`None`] if the internal storage of `self` does not allow this + /// to be returned efficiently. + /// + /// See [`as_str`](Self::as_str) for the UTF-8 equivalent. + /// + /// [UTF-16]: https://en.wikipedia.org/wiki/UTF-16 + #[allow(unused)] + #[cfg(feature = "apple")] + // TODO: Finish this + fn as_utf16(&self) -> Option<&[u16]> { + extern "C" { + // https://developer.apple.com/documentation/corefoundation/1542939-cfstringgetcharactersptr?language=objc + fn CFStringGetCharactersPtr(s: &NSString) -> *const u16; } + let ptr = unsafe { CFStringGetCharactersPtr(self) }; + NonNull::new(ptr as *mut u16) + .map(|ptr| unsafe { slice::from_raw_parts(ptr.as_ptr(), self.len_utf16()) }) + } - // TODO: initWithBytesNoCopy:, maybe add lifetime parameter to NSString? - // See https://github.com/nvzqz/fruity/blob/320efcf715c2c5fbd2f3084f671f2be2e03a6f2b/src/foundation/ns_string/mod.rs#L350-L381 - // Might be quite difficult, as Objective-C code might assume the NSString - // is always alive? - // See https://github.com/drewcrawford/foundationr/blob/b27683417a35510e8e5d78a821f081905b803de6/src/nsstring.rs - - /// Whether the given string matches the beginning characters of this - /// string. - /// - /// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nsstring/1410309-hasprefix?language=objc). - #[doc(alias = "hasPrefix")] - #[doc(alias = "hasPrefix:")] - #[method(hasPrefix:)] - pub fn has_prefix(&self, prefix: &NSString) -> bool; - - /// Whether the given string matches the ending characters of this string. - /// - /// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nsstring/1416529-hassuffix?language=objc). - #[doc(alias = "hasSuffix")] - #[doc(alias = "hasSuffix:")] - #[method(hasSuffix:)] - pub fn has_suffix(&self, suffix: &NSString) -> bool; - - // TODO: Other comparison methods: - // - compare:options: - // - compare:options:range: - // - compare:options:range:locale: - // - localizedCompare: - // - caseInsensitiveCompare: - // - localizedCaseInsensitiveCompare: - // - localizedStandardCompare: - #[method(compare:)] - fn compare(&self, other: &Self) -> NSComparisonResult; - - // pub fn from_nsrange(range: NSRange) -> Id - // https://developer.apple.com/documentation/foundation/1415155-nsstringfromrange?language=objc - - // TODO: Safety - #[method(writeToFile:atomically:encoding:error:)] - pub unsafe fn write_to_file( - &self, - path: &NSString, - atomically: bool, - encoding: usize, - ) -> Result<(), Id>; + /// Get the [`str`](`prim@str`) representation of this. + /// + /// TODO: Further explain this. + #[doc(alias = "UTF8String")] + pub fn as_str<'r, 's: 'r, 'p: 'r>(&'s self, pool: &'p AutoreleasePool) -> &'r str { + // SAFETY: This is an instance of `NSString` + unsafe { nsstring_to_str(self, pool) } } -); + + // TODO: Allow usecases where the NUL byte from `UTF8String` is kept? + + /// Creates an immutable `NSString` by copying the given string slice. + /// + /// Prefer using the [`ns_string!`] macro when possible. + /// + /// [`ns_string!`]: crate::ns_string + #[doc(alias = "initWithBytes")] + #[doc(alias = "initWithBytes:length:encoding:")] + #[allow(clippy::should_implement_trait)] // Not really sure of a better name + pub fn from_str(string: &str) -> Id { + unsafe { + let obj = from_str(Self::class(), string); + Id::new(obj.cast()).unwrap() + } + } + + // TODO: initWithBytesNoCopy:, maybe add lifetime parameter to NSString? + // See https://github.com/nvzqz/fruity/blob/320efcf715c2c5fbd2f3084f671f2be2e03a6f2b/src/foundation/ns_string/mod.rs#L350-L381 + // Might be quite difficult, as Objective-C code might assume the NSString + // is always alive? + // See https://github.com/drewcrawford/foundationr/blob/b27683417a35510e8e5d78a821f081905b803de6/src/nsstring.rs +} pub(crate) fn from_str(cls: &Class, string: &str) -> *mut Object { let bytes: *const c_void = string.as_ptr().cast(); @@ -459,10 +354,10 @@ mod tests { let s = NSString::from_str("abcdef"); let prefix = NSString::from_str("abc"); let suffix = NSString::from_str("def"); - assert!(s.has_prefix(&prefix)); - assert!(s.has_suffix(&suffix)); - assert!(!s.has_prefix(&suffix)); - assert!(!s.has_suffix(&prefix)); + assert!(s.hasPrefix(&prefix)); + assert!(s.hasSuffix(&suffix)); + assert!(!s.hasPrefix(&suffix)); + assert!(!s.hasSuffix(&prefix)); } #[test] @@ -492,4 +387,33 @@ mod tests { assert!(s > shorter); assert!(s < longer); } + + #[test] + fn test_append() { + let error_tag = NSString::from_str("Error: "); + let error_string = NSString::from_str("premature end of file."); + let error_message = error_tag.stringByAppendingString(&error_string); + assert_eq!( + error_message, + NSString::from_str("Error: premature end of file.") + ); + + let extension = NSString::from_str("scratch.tiff"); + assert_eq!( + NSString::from_str("/tmp").stringByAppendingPathComponent(&extension), + NSString::from_str("/tmp/scratch.tiff") + ); + assert_eq!( + NSString::from_str("/tmp/").stringByAppendingPathComponent(&extension), + NSString::from_str("/tmp/scratch.tiff") + ); + assert_eq!( + NSString::from_str("/").stringByAppendingPathComponent(&extension), + NSString::from_str("/scratch.tiff") + ); + assert_eq!( + NSString::from_str("").stringByAppendingPathComponent(&extension), + NSString::from_str("scratch.tiff") + ); + } } diff --git a/crates/icrate/src/Foundation/additions/thread.rs b/crates/icrate/src/Foundation/additions/thread.rs index f9dd1e311..f546a50dd 100644 --- a/crates/icrate/src/Foundation/additions/thread.rs +++ b/crates/icrate/src/Foundation/additions/thread.rs @@ -2,21 +2,11 @@ use core::fmt; use core::marker::PhantomData; use core::panic::{RefUnwindSafe, UnwindSafe}; -use super::{NSObject, NSString}; +use objc2::extern_methods; use objc2::rc::{Id, Shared}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType}; +use objc2::ClassType; -extern_class!( - /// A thread of execution. - /// - /// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nsthread?language=objc). - #[derive(PartialEq, Eq, Hash)] - pub struct NSThread; - - unsafe impl ClassType for NSThread { - type Super = NSObject; - } -); +use crate::Foundation::NSThread; unsafe impl Send for NSThread {} unsafe impl Sync for NSThread {} @@ -26,56 +16,19 @@ impl RefUnwindSafe for NSThread {} extern_methods!( unsafe impl NSThread { - /// Returns the [`NSThread`] object representing the current thread. - #[method_id(currentThread)] - pub fn current() -> Id; - - /// Returns the [`NSThread`] object representing the main thread. - pub fn main() -> Id { - // The main thread static may not have been initialized - // This can at least fail in GNUStep! - let obj: Option<_> = unsafe { msg_send_id![Self::class(), mainThread] }; - obj.expect("Could not retrieve main thread.") - } - - /// Returns `true` if the thread is the main thread. - #[method(isMainThread)] - pub fn is_main(&self) -> bool; - - /// The name of the thread. - #[method_id(name)] - pub fn name(&self) -> Option>; - #[method_id(new)] unsafe fn new() -> Id; - - #[method(start)] - unsafe fn start(&self); - - #[method(isMainThread)] - fn is_current_main() -> bool; - - #[method(isMultiThreaded)] - fn is_global_multi() -> bool; } ); -impl fmt::Debug for NSThread { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Use -[NSThread description] since that includes the thread number - let obj: &NSObject = self; - fmt::Debug::fmt(obj, f) - } -} - /// Whether the application is multithreaded according to Cocoa. pub fn is_multi_threaded() -> bool { - NSThread::is_global_multi() + NSThread::isMultiThreaded() } /// Whether the current thread is the main thread. pub fn is_main_thread() -> bool { - NSThread::is_current_main() + NSThread::class_isMainThread() } #[allow(unused)] @@ -173,25 +126,26 @@ mod tests { ignore = "Retrieving main thread is weirdly broken, only works with --test-threads=1" )] fn test_main_thread() { - let current = NSThread::current(); - let main = NSThread::main(); + let current = NSThread::currentThread(); + let main = NSThread::mainThread(); - assert!(main.is_main()); + assert!(main.isMainThread()); if main == current { - assert!(current.is_main()); + assert!(current.isMainThread()); assert!(is_main_thread()); } else { - assert!(!current.is_main()); + assert!(!current.isMainThread()); assert!(!is_main_thread()); } } #[test] fn test_not_main_thread() { - let res = std::thread::spawn(|| (is_main_thread(), NSThread::current().is_main())) - .join() - .unwrap(); + let res = + std::thread::spawn(|| (is_main_thread(), NSThread::currentThread().isMainThread())) + .join() + .unwrap(); assert_eq!(res, (false, false)); } @@ -208,7 +162,7 @@ mod tests { ignore = "Retrieving main thread is weirdly broken, only works with --test-threads=1" )] fn test_debug() { - let thread = NSThread::main(); + let thread = NSThread::mainThread(); let actual = format!("{thread:?}"); let expected = [ diff --git a/crates/icrate/src/Foundation/additions/uuid.rs b/crates/icrate/src/Foundation/additions/uuid.rs index 9ec1eefbd..5b45cb126 100644 --- a/crates/icrate/src/Foundation/additions/uuid.rs +++ b/crates/icrate/src/Foundation/additions/uuid.rs @@ -1,40 +1,10 @@ use core::fmt; use core::panic::{RefUnwindSafe, UnwindSafe}; -use super::{NSCopying, NSObject, NSString}; use objc2::rc::{DefaultId, Id, Shared}; -use objc2::{extern_class, extern_methods, msg_send_id, ClassType, Encode, Encoding, RefEncode}; - -extern_class!( - /// A universally unique value. - /// - /// Can be used to identify types, interfaces, and other items. - /// - /// Conversion methods to/from UUIDs from the `uuid` crate can be - /// enabled with the `uuid` crate feature. - /// - /// macOS: This is only available on 10.8 and above. - /// - /// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nsuuid?language=objc). - #[derive(PartialEq, Eq, Hash)] - pub struct NSUUID; - - unsafe impl ClassType for NSUUID { - type Super = NSObject; - } -); - -/// The headers describe `initWithUUIDBytes:` and `getUUIDBytes:` as -/// taking `uuid_t`, but something fishy is going on, in reality they -/// expect a reference to these! -/// -/// Hence we create this newtype to change the encoding. -#[repr(transparent)] -struct UuidBytes([u8; 16]); - -unsafe impl RefEncode for UuidBytes { - const ENCODING_REF: Encoding = Encoding::Array(16, &u8::ENCODING); -} +use objc2::ClassType; + +use crate::Foundation::{NSCopying, NSString, UuidBytes, NSUUID}; // SAFETY: `NSUUID` is immutable. unsafe impl Sync for NSUUID {} @@ -43,49 +13,38 @@ unsafe impl Send for NSUUID {} impl UnwindSafe for NSUUID {} impl RefUnwindSafe for NSUUID {} -extern_methods!( - unsafe impl NSUUID { - #[method_id(new)] - pub fn new_v4() -> Id; - - /// The 'nil UUID'. - pub fn nil() -> Id { - Self::from_bytes([0; 16]) - } - - pub fn from_bytes(bytes: [u8; 16]) -> Id { - let bytes = UuidBytes(bytes); - unsafe { msg_send_id![Self::alloc(), initWithUUIDBytes: &bytes] } - } - - pub fn from_string(string: &NSString) -> Option> { - unsafe { msg_send_id![Self::alloc(), initWithUUIDString: string] } - } +impl NSUUID { + /// The 'nil UUID'. + pub fn nil() -> Id { + Self::from_bytes([0; 16]) + } - #[method(getUUIDBytes:)] - fn get_bytes_raw(&self, bytes: &mut UuidBytes); + pub fn from_bytes(bytes: [u8; 16]) -> Id { + let bytes = UuidBytes(bytes); + Self::initWithUUIDBytes(Self::alloc(), &bytes) + } - pub fn as_bytes(&self) -> [u8; 16] { - let mut bytes = UuidBytes([0; 16]); - self.get_bytes_raw(&mut bytes); - bytes.0 - } + pub fn from_string(string: &NSString) -> Option> { + Self::initWithUUIDString(Self::alloc(), string) + } - #[method_id(UUIDString)] - pub fn string(&self) -> Id; + pub fn as_bytes(&self) -> [u8; 16] { + let mut bytes = UuidBytes([0; 16]); + self.getUUIDBytes(&mut bytes); + bytes.0 } -); +} impl fmt::Display for NSUUID { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&self.string(), f) + fmt::Display::fmt(&self.UUIDString(), f) } } impl fmt::Debug for NSUUID { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // The `uuid` crate does `Debug` and `Display` equally, and so do we - fmt::Display::fmt(&self.string(), f) + fmt::Display::fmt(&self.UUIDString(), f) } } @@ -144,8 +103,8 @@ mod tests { #[test] fn test_new() { - let uuid1 = NSUUID::new_v4(); - let uuid2 = NSUUID::new_v4(); + let uuid1 = NSUUID::UUID(); + let uuid2 = NSUUID::UUID(); assert_ne!(uuid1, uuid2, "Statistically very unlikely"); } @@ -173,7 +132,7 @@ mod tests { #[cfg(feature = "uuid")] #[test] fn test_convert_roundtrip() { - let nsuuid1 = NSUUID::new_v4(); + let nsuuid1 = NSUUID::UUID(); let uuid = nsuuid1.as_uuid(); let nsuuid2 = NSUUID::from_uuid(uuid); assert_eq!(nsuuid1, nsuuid2); diff --git a/crates/icrate/src/Foundation/additions/value.rs b/crates/icrate/src/Foundation/additions/value.rs index 3a9f3bf85..a3987c348 100644 --- a/crates/icrate/src/Foundation/additions/value.rs +++ b/crates/icrate/src/Foundation/additions/value.rs @@ -1,193 +1,154 @@ use alloc::string::ToString; -use core::ffi::c_void; use core::fmt; use core::hash; use core::mem::MaybeUninit; use core::ptr::NonNull; use core::str; use std::ffi::{CStr, CString}; -use std::os::raw::c_char; -use super::{NSCopying, NSObject, NSPoint, NSRange, NSRect, NSSize}; +use objc2::encode::Encode; use objc2::rc::{Id, Shared}; -use objc2::{extern_class, extern_methods, msg_send, msg_send_id, ClassType, Encode}; +use objc2::ClassType; -extern_class!( - /// A container wrapping any encodable type as an Obective-C object. +use crate::Foundation::{NSCopying, NSPoint, NSRange, NSRect, NSSize, NSValue}; + +// We can't implement any auto traits for NSValue, since it can contain an +// arbitary object! + +/// Creation methods. +impl NSValue { + /// Create a new `NSValue` containing the given type. /// - /// Since Objective-C collections like [`NSArray`] can only contain - /// objects, it is common to wrap pointers or structures like [`NSRange`]. + /// Be careful when using this since you may accidentally pass a reference + /// when you wanted to pass a concrete type instead. /// - /// Note that creating `NSValue`s is not `unsafe`, but almost all usage of - /// it is, since we cannot guarantee that the type that was used to - /// construct it is the same as the expected output type. /// - /// See also the [`NSNumber`] subclass for when you want to wrap numbers. + /// # Examples /// - /// See [Apple's documentation][apple-doc] for more information. + /// Create an `NSValue` containing an [`NSPoint`]. /// - /// [`NSArray`]: super::NSArray - /// [`NSRange`]: super::NSRange - /// [`NSNumber`]: super::NSNumber - /// [apple-doc]: https://developer.apple.com/documentation/foundation/nsnumber?language=objc - pub struct NSValue; - - unsafe impl ClassType for NSValue { - type Super = NSObject; - } -); - -// We can't implement any auto traits for NSValue, since it can contain an -// arbitary object! - -extern_methods!( - /// Creation methods. - unsafe impl NSValue { - // Default / empty new is not provided because `-init` returns `nil` on - // Apple and GNUStep throws an exception on all other messages to this - // invalid instance. - - /// Create a new `NSValue` containing the given type. - /// - /// Be careful when using this since you may accidentally pass a reference - /// when you wanted to pass a concrete type instead. - /// - /// - /// # Examples - /// - /// Create an `NSValue` containing an [`NSPoint`][super::NSPoint]. - /// - /// ``` - /// use icrate::Foundation::{NSPoint, NSValue}; - /// let val = NSValue::new::(NSPoint::new(1.0, 1.0)); - /// ``` - pub fn new(value: T) -> Id { - let bytes: *const T = &value; - let bytes: *const c_void = bytes.cast(); - let encoding = CString::new(T::ENCODING.to_string()).unwrap(); - unsafe { - msg_send_id![ - Self::alloc(), - initWithBytes: bytes, - objCType: encoding.as_ptr(), - ] - } + /// ``` + /// use icrate::Foundation::{NSPoint, NSValue}; + /// + /// let val = NSValue::new::(NSPoint::new(1.0, 1.0)); + /// ``` + /// + /// [`NSPoint`]: crate::Foundation::NSPoint + pub fn new(value: T) -> Id { + let bytes: NonNull = NonNull::from(&value); + let encoding = CString::new(T::ENCODING.to_string()).unwrap(); + unsafe { + Self::initWithBytes_objCType( + Self::alloc(), + bytes.cast(), + NonNull::new(encoding.as_ptr() as *mut _).unwrap(), + ) } } +} - /// Getter methods. - unsafe impl NSValue { - /// Retrieve the data contained in the `NSValue`. - /// - /// Note that this is broken on GNUStep for some types, see - /// [gnustep/libs-base#216]. - /// - /// [gnustep/libs-base#216]: https://github.com/gnustep/libs-base/pull/216 - /// - /// - /// # Safety - /// - /// The type of `T` must be what the NSValue actually stores, and any - /// safety invariants that the value has must be upheld. - /// - /// Note that it may be, but is not always, enough to simply check whether - /// [`contains_encoding`] returns `true`. For example, `NonNull` have - /// the same encoding as `*const T`, but `NonNull` is clearly not - /// safe to return from this function even if you've checked the encoding - /// beforehand. - /// - /// [`contains_encoding`]: Self::contains_encoding - /// - /// - /// # Examples - /// - /// Store a pointer in `NSValue`, and retrieve it again afterwards. - /// - /// ``` - /// use std::ffi::c_void; - /// use std::ptr; - /// use icrate::Foundation::NSValue; - /// - /// let val = NSValue::new::<*const c_void>(ptr::null()); - /// // SAFETY: The value was just created with a pointer - /// let res = unsafe { val.get::<*const c_void>() }; - /// assert!(res.is_null()); - /// ``` - pub unsafe fn get(&self) -> T { - debug_assert!( - self.contains_encoding::(), - "wrong encoding. NSValue tried to return something with encoding {}, but the encoding of the given type was {}", - self.encoding().unwrap_or("(NULL)"), - T::ENCODING, - ); - let mut value = MaybeUninit::::uninit(); - let ptr: *mut c_void = value.as_mut_ptr().cast(); - let _: () = unsafe { msg_send![self, getValue: ptr] }; - // SAFETY: We know that `getValue:` initialized the value, and user - // ensures that it is safe to access. - unsafe { value.assume_init() } - } - - pub fn get_range(&self) -> Option { - if self.contains_encoding::() { - // SAFETY: We just checked that this contains an NSRange - Some(unsafe { msg_send![self, rangeValue] }) - } else { - None - } - } +/// Getter methods. +impl NSValue { + /// Retrieve the data contained in the `NSValue`. + /// + /// Note that this is broken on GNUStep for some types, see + /// [gnustep/libs-base#216]. + /// + /// [gnustep/libs-base#216]: https://github.com/gnustep/libs-base/pull/216 + /// + /// + /// # Safety + /// + /// The type of `T` must be what the NSValue actually stores, and any + /// safety invariants that the value has must be upheld. + /// + /// Note that it may be, but is not always, enough to simply check whether + /// [`contains_encoding`] returns `true`. For example, `NonNull` have + /// the same encoding as `*const T`, but `NonNull` is clearly not + /// safe to return from this function even if you've checked the encoding + /// beforehand. + /// + /// [`contains_encoding`]: Self::contains_encoding + /// + /// + /// # Examples + /// + /// Store a pointer in `NSValue`, and retrieve it again afterwards. + /// + /// ``` + /// use std::ffi::c_void; + /// use std::ptr; + /// use icrate::Foundation::NSValue; + /// + /// let val = NSValue::new::<*const c_void>(ptr::null()); + /// // SAFETY: The value was just created with a pointer + /// let res = unsafe { val.get::<*const c_void>() }; + /// assert!(res.is_null()); + /// ``` + pub unsafe fn get(&self) -> T { + debug_assert!( + self.contains_encoding::(), + "wrong encoding. NSValue tried to return something with encoding {}, but the encoding of the given type was {}", + self.encoding().unwrap_or("(NULL)"), + T::ENCODING, + ); + let mut value = MaybeUninit::::uninit(); + let ptr: NonNull = NonNull::new(value.as_mut_ptr()).unwrap(); + unsafe { self.getValue(ptr.cast()) }; + // SAFETY: We know that `getValue:` initialized the value, and user + // ensures that it is safe to access. + unsafe { value.assume_init() } + } - pub fn get_point(&self) -> Option { - if self.contains_encoding::() { - // SAFETY: We just checked that this contains an NSPoint - // - // Note: The documentation says that `pointValue`, `sizeValue` and - // `rectValue` is only available on macOS, but turns out that they - // are actually available everywhere! - let res = unsafe { msg_send![self, pointValue] }; - Some(res) - } else { - None - } + pub fn get_range(&self) -> Option { + if self.contains_encoding::() { + // SAFETY: We just checked that this contains an NSRange + Some(unsafe { self.rangeValue() }) + } else { + None } + } - pub fn get_size(&self) -> Option { - if self.contains_encoding::() { - // SAFETY: We just checked that this contains an NSSize - let res = unsafe { msg_send![self, sizeValue] }; - Some(res) - } else { - None - } + pub fn get_point(&self) -> Option { + if self.contains_encoding::() { + // SAFETY: We just checked that this contains an NSPoint + // + // Note: The documentation says that `pointValue`, `sizeValue` and + // `rectValue` is only available on macOS, but turns out that they + // are actually available everywhere! + Some(unsafe { self.pointValue() }) + } else { + None } + } - pub fn get_rect(&self) -> Option { - if self.contains_encoding::() { - // SAFETY: We just checked that this contains an NSRect - let res = unsafe { msg_send![self, rectValue] }; - Some(res) - } else { - None - } + pub fn get_size(&self) -> Option { + if self.contains_encoding::() { + // SAFETY: We just checked that this contains an NSSize + Some(unsafe { self.sizeValue() }) + } else { + None } + } - pub fn encoding(&self) -> Option<&str> { - let result: Option> = unsafe { msg_send![self, objCType] }; - result.map(|s| unsafe { CStr::from_ptr(s.as_ptr()) }.to_str().unwrap()) + pub fn get_rect(&self) -> Option { + if self.contains_encoding::() { + // SAFETY: We just checked that this contains an NSRect + Some(unsafe { self.rectValue() }) + } else { + None } + } - pub fn contains_encoding(&self) -> bool { - if let Some(encoding) = self.encoding() { - T::ENCODING.equivalent_to_str(encoding) - } else { - panic!("missing NSValue encoding"); - } - } + pub fn encoding(&self) -> Option<&str> { + let ptr = self.objCType().as_ptr(); + Some(unsafe { CStr::from_ptr(ptr) }.to_str().unwrap()) + } - #[method(isEqualToValue:)] - fn is_equal_to_value(&self, other: &Self) -> bool; + pub fn contains_encoding(&self) -> bool { + T::ENCODING.equivalent_to_str(self.encoding().unwrap()) } -); +} unsafe impl NSCopying for NSValue { type Ownership = Shared; @@ -201,18 +162,18 @@ impl alloc::borrow::ToOwned for NSValue { } } +impl hash::Hash for NSValue { + #[inline] + fn hash(&self, state: &mut H) { + (**self).hash(state) + } +} + impl PartialEq for NSValue { #[doc(alias = "isEqualToValue:")] fn eq(&self, other: &Self) -> bool { // Use isEqualToValue: instaed of isEqual: since it is faster - self.is_equal_to_value(other) - } -} - -impl hash::Hash for NSValue { - fn hash(&self, state: &mut H) { - // Delegate to NSObject - (**self).hash(state) + self.isEqualToValue(other) } } @@ -231,6 +192,7 @@ impl fmt::Debug for NSValue { mod tests { use alloc::format; use core::{ptr, slice}; + use std::ffi::c_char; use super::*; use objc2::rc::{__RcTestObject, __ThreadTestData}; 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/NSNotFound.rs b/crates/icrate/src/Foundation/fixes/NSNotFound.rs new file mode 100644 index 000000000..b4c42a879 --- /dev/null +++ b/crates/icrate/src/Foundation/fixes/NSNotFound.rs @@ -0,0 +1,6 @@ +use objc2::ffi::{NSInteger, NSIntegerMax}; + +/// A value indicating that a requested item couldn’t be found or doesn’t exist. +/// +/// See [Apple's documentation](https://developer.apple.com/documentation/foundation/nsnotfound?language=objc). +pub const NSNotFound: NSInteger = NSIntegerMax; 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/NSThread.rs b/crates/icrate/src/Foundation/fixes/NSThread.rs new file mode 100644 index 000000000..ccb5435a3 --- /dev/null +++ b/crates/icrate/src/Foundation/fixes/NSThread.rs @@ -0,0 +1,14 @@ +use objc2::{extern_methods, ClassType}; + +use crate::Foundation::NSThread; + +extern_methods!( + unsafe impl NSThread { + /// Returns `true` if the thread is the main thread. + #[method(isMainThread)] + pub fn isMainThread(&self) -> bool; + + #[method(isMainThread)] + pub(crate) fn class_isMainThread() -> bool; + } +); diff --git a/crates/icrate/src/Foundation/fixes/NSUUID.rs b/crates/icrate/src/Foundation/fixes/NSUUID.rs new file mode 100644 index 000000000..715335af5 --- /dev/null +++ b/crates/icrate/src/Foundation/fixes/NSUUID.rs @@ -0,0 +1,30 @@ +use objc2::encode::{Encode, Encoding, RefEncode}; +use objc2::extern_methods; +use objc2::rc::{Allocated, Id, Shared}; + +use crate::Foundation::NSUUID; + +/// The headers describe `initWithUUIDBytes:` and `getUUIDBytes:` as +/// taking `uuid_t`, but something fishy is going on, in reality they +/// expect a reference to these! +/// +/// Hence we create this newtype to change the encoding. +#[repr(transparent)] +pub(crate) struct UuidBytes(pub(crate) [u8; 16]); + +unsafe impl RefEncode for UuidBytes { + const ENCODING_REF: Encoding = Encoding::Array(16, &u8::ENCODING); +} + +extern_methods!( + unsafe impl NSUUID { + #[method_id(initWithUUIDBytes:)] + pub(crate) fn initWithUUIDBytes( + this: Option>, + bytes: &UuidBytes, + ) -> Id; + + #[method(getUUIDBytes:)] + pub(crate) fn getUUIDBytes(&self, bytes: &mut UuidBytes); + } +); diff --git a/crates/icrate/src/Foundation/fixes/debug.rs b/crates/icrate/src/Foundation/fixes/debug.rs new file mode 100644 index 000000000..5ecea9959 --- /dev/null +++ b/crates/icrate/src/Foundation/fixes/debug.rs @@ -0,0 +1,81 @@ +use core::fmt; + +use objc2::rc::Ownership; +use objc2::Message; + +use crate::Foundation::{ + NSAttributedString, NSBundle, NSMutableArray, NSMutableAttributedString, NSMutableData, + NSMutableDictionary, NSMutableSet, NSMutableString, NSNumber, NSObject, NSThread, +}; + +impl fmt::Debug for NSAttributedString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Use -[NSAttributedString description] since it is pretty good + let obj: &NSObject = self; + fmt::Debug::fmt(obj, f) + } +} + +impl fmt::Debug for NSBundle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Delegate to NSObject + (**self).fmt(f) + } +} + +impl fmt::Debug for NSMutableArray { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&**self, f) + } +} + +impl fmt::Debug for NSMutableAttributedString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&**self, f) + } +} + +impl fmt::Debug for NSMutableData { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&**self, f) + } +} + +impl fmt::Debug for NSMutableDictionary { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&**self, f) + } +} + +impl fmt::Debug for NSMutableSet { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&**self, f) + } +} + +impl fmt::Debug for NSMutableString { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(&**self, f) + } +} + +impl fmt::Debug for NSThread { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Use -[NSThread description] since that includes the thread number + let obj: &NSObject = self; + fmt::Debug::fmt(obj, f) + } +} + +impl fmt::Debug for NSNumber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Delegate to -[NSObject description] + // (happens to return the same as -[NSNumber stringValue]) + fmt::Debug::fmt(&***self, f) + } +} diff --git a/crates/icrate/src/Foundation/fixes/generic_return.rs b/crates/icrate/src/Foundation/fixes/generic_return.rs new file mode 100644 index 000000000..d2cde7a86 --- /dev/null +++ b/crates/icrate/src/Foundation/fixes/generic_return.rs @@ -0,0 +1,103 @@ +use objc2::rc::{Allocated, Id, Ownership, Shared}; +use objc2::{extern_methods, Message}; + +use crate::Foundation::{ + NSArray, NSDictionary, NSError, NSMutableArray, NSMutableDictionary, NSString, NSURL, +}; + +extern_methods!( + /// NSArrayCreation + unsafe impl + NSArray + { + #[method_id(initWithContentsOfURL:error:)] + pub unsafe fn initWithContentsOfURL_error( + this: Option>, + url: &NSURL, + ) -> Result, Id>; + } +); + +extern_methods!( + /// NSDeprecated + unsafe impl + NSArray + { + #[method_id(initWithContentsOfFile:)] + pub unsafe fn initWithContentsOfFile( + this: Option>, + path: &NSString, + ) -> Option>; + + #[method_id(initWithContentsOfURL:)] + pub unsafe fn initWithContentsOfURL( + this: Option>, + url: &NSURL, + ) -> Option>; + } +); + +extern_methods!( + /// NSMutableArrayCreation + unsafe impl + NSMutableArray + { + #[method_id(initWithContentsOfFile:)] + pub unsafe fn initWithContentsOfFile( + this: Option>, + path: &NSString, + ) -> Option>; + + #[method_id(initWithContentsOfURL:)] + pub unsafe fn initWithContentsOfURL( + this: Option>, + url: &NSURL, + ) -> Option>; + } +); + +extern_methods!( + /// NSDeprecated + unsafe impl< + KeyType: Message, + ObjectType: Message, + KeyTypeOwnership: Ownership, + ObjectTypeOwnership: Ownership, + > NSDictionary + { + #[method_id(initWithContentsOfFile:)] + pub unsafe fn initWithContentsOfFile( + this: Option>, + path: &NSString, + ) -> Option>; + + #[method_id(initWithContentsOfURL:)] + pub unsafe fn initWithContentsOfURL( + this: Option>, + url: &NSURL, + ) -> Option>; + } +); + +extern_methods!( + /// NSMutableDictionaryCreation + unsafe impl< + KeyType: Message, + ObjectType: Message, + KeyTypeOwnership: Ownership, + ObjectTypeOwnership: Ownership, + > NSMutableDictionary + { + #[method_id(initWithContentsOfFile:)] + pub unsafe fn initWithContentsOfFile( + this: Option>, + path: &NSString, + ) -> Option>; + + #[method_id(initWithContentsOfURL:)] + pub unsafe fn initWithContentsOfURL( + this: Option>, + url: &NSURL, + ) -> Option>; + } +); diff --git a/crates/icrate/src/Foundation/fixes/gnustep.rs b/crates/icrate/src/Foundation/fixes/gnustep.rs new file mode 100644 index 000000000..7e36e48ba --- /dev/null +++ b/crates/icrate/src/Foundation/fixes/gnustep.rs @@ -0,0 +1,13 @@ +use core::ffi::c_void; +use core::ptr::NonNull; + +use objc2::extern_methods; + +use crate::Foundation::NSMutableData; + +extern_methods!( + unsafe impl NSMutableData { + #[method(mutableBytes)] + pub fn mutableBytes(&mut self) -> Option>; + } +); diff --git a/crates/icrate/src/Foundation/fixes/mod.rs b/crates/icrate/src/Foundation/fixes/mod.rs index 8b1378917..89189ac4d 100644 --- a/crates/icrate/src/Foundation/fixes/mod.rs +++ b/crates/icrate/src/Foundation/fixes/mod.rs @@ -1 +1,16 @@ +mod NSThread; +mod NSUUID; +#[path = "NSDecimal.rs"] +mod __NSDecimal; +#[path = "NSNotFound.rs"] +mod __NSNotFound; +#[path = "NSProxy.rs"] +mod __NSProxy; +mod debug; +mod generic_return; +mod gnustep; +pub use self::__NSDecimal::NSDecimal; +pub use self::__NSNotFound::NSNotFound; +pub use self::__NSProxy::NSProxy; +pub(crate) use self::NSUUID::UuidBytes; diff --git a/crates/icrate/src/Foundation/mod.rs b/crates/icrate/src/Foundation/mod.rs index 50273cdab..894fb4846 100644 --- a/crates/icrate/src/Foundation/mod.rs +++ b/crates/icrate/src/Foundation/mod.rs @@ -1,10 +1,126 @@ +//! Bindings to the `Foundation` framework. +//! +//! This is the [`std`] equivalent for Objective-C, containing essential data +//! types, collections, and operating-system services. +//! +//! See [Apple's documentation](https://developer.apple.com/documentation/foundation?language=objc). +//! +//! +//! ## Philosophy +//! +//! The `Foundation` framework is _huge_! If we aspired to map every API it +//! exposes (a lot of it is just helper methods to make Objective-C more +//! ergonomic), this library would never be finished. Instead, our focus lies +//! on conversion methods, to allow easily using them from Rust. +//! +//! If you find some API that an object doesn't expose (but should), we gladly +//! accept [pull requests]. If it is something that is out of scope, these +//! objects implement the [`Message`] trait, so you can always just manually +//! call a method on them using the [`msg_send!`] family of macros. +//! +//! [pull requests]: https://github.com/madsmtm/objc2/pulls +//! [`Message`]: crate::objc2::Message +//! [`msg_send!`]: crate::objc2::msg_send +//! +//! +//! # Use of `Deref` +//! +//! `icrate` uses the [`Deref`] trait in a bit special way: All objects deref +//! to their superclasses. For example, `NSMutableArray` derefs to `NSArray`, +//! which in turn derefs to `NSObject`. +//! +//! Note that this is explicitly recommended against in [the +//! documentation][`Deref`] and [the Rust Design patterns +//! book][anti-pattern-deref] (see those links for details). +//! +//! Due to Objective-C objects only ever being accessible behind pointers in +//! the first place, the problems stated there are less severe, and having the +//! implementation just means that everything is much nicer when you actually +//! want to use the objects! +//! +//! All objects also implement [`AsRef`] and [`AsMut`] to their superclass, +//! and can be used in [`Id::into_super`], so if you favour explicit +//! conversion, that is a possibility too. +//! +//! [`Deref`]: std::ops::Deref +//! [`ClassType`]: crate::objc2::ClassType +//! [anti-pattern-deref]: https://rust-unofficial.github.io/patterns/anti_patterns/deref.html +//! [`Id::into_super`]: objc2::rc::Id::into_super +//! +//! +//! # Ownership +//! +//! TODO. +//! +//! While `NSArray` _itself_ is immutable, i.e. the number of objects it +//! contains can't change, it is still possible to modify the contained +//! objects themselves, if you know you're the sole owner of them - +//! quite similar to how you can modify elements in `Box<[T]>`. +//! +//! To mutate the contained objects the ownership must be `O = Owned`. A +//! summary of what the different "types" of arrays allow you to do can be +//! found below. `Array` refers to either `NSArray` or `NSMutableArray`. +//! - `Id, Owned>`: Allows you to mutate the +//! objects, and the array itself. +//! - `Id, Owned>`: Allows you to mutate the +//! array itself, but not it's contents. +//! - `Id, Owned>`: Allows you to mutate the objects, +//! but not the array itself. +//! - `Id, Owned>`: Effectively the same as the below. +//! - `Id, Shared>`: Allows you to copy the array, but +//! does not allow you to modify it in any way. +//! - `Id, Shared>`: Pretty useless compared to the +//! others, avoid this. +//! +//! +//! # Rust vs. Objective-C types +//! +//! | Objective-C | (approximately) equivalent Rust | +//! | --- | --- | +//! | `NSData` | `Box<[u8]>` | +//! | `NSMutableData` | `Vec` | +//! | `NSString` | `Box` | +//! | `NSMutableString` | `String` | +//! | `NSValue` | `Box` | +//! | `NSNumber` | `enum { I8(i8), U8(u8), I16(i16), U16(u16), I32(i32), U32(u32), I64(i64), U64(u64), F32(f32), F64(f64), CLong(ffi::c_long), CULong(ffi::c_ulong) }` | +//! | `NSError` | `Box` | +//! | `NSException` | `Box` | +//! | `NSRange` | `ops::Range` | +//! | `NSComparisonResult` | `cmp::Ordering` | +//! | `NSArray` | `Box<[Arc]>` | +//! | `NSArray` | `Box<[T]>` | +//! | `NSMutableArray` | `Vec>` | +//! | `NSMutableArray` | `Vec` | +//! | `NSDictionary` | `ImmutableMap, Arc>` | +//! | `NSDictionary` | `ImmutableMap` | +//! | `NSMutableDictionary` | `Map, Arc>` | +//! | `NSMutableDictionary` | `Map` | +//! | `NSEnumerator` | `impl Iterator>` | +//! | `NSEnumerator` | `impl Iterator` | +//! | `@protocol NSCopying` | `trait Clone` | +//! +//! +//! # Feature flags +//! +//! `"uuid"` -> Enables conversion methods between `NSUUID` and `uuid::Uuid`. + +#[doc(hidden)] +pub mod __ns_string; mod additions; mod fixes; #[path = "../generated/Foundation/mod.rs"] mod generated; pub use self::additions::*; -#[allow(unreachable_pub)] pub use self::fixes::*; -#[allow(unreachable_pub)] pub use self::generated::*; + +pub use objc2::runtime::{NSObject, NSZone}; +// Available under Foundation, so makes sense here as well: +// https://developer.apple.com/documentation/foundation/numbers_data_and_basic_values?language=objc +pub use objc2::ffi::{NSInteger, NSUInteger}; + +// Link to the correct framework +#[cfg_attr(feature = "apple", link(name = "Foundation", kind = "framework"))] +#[cfg_attr(feature = "gnustep-1-7", link(name = "gnustep-base", kind = "dylib"))] +extern "C" {} diff --git a/crates/icrate/src/Foundation/translation-config.toml b/crates/icrate/src/Foundation/translation-config.toml new file mode 100644 index 000000000..77f4a7ca1 --- /dev/null +++ b/crates/icrate/src/Foundation/translation-config.toml @@ -0,0 +1,603 @@ +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.NSBlockOperation.methods] +# 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.methods.threadPriority] +skipped = true +[class.NSThread.methods.setThreadPriority] +skipped = true +[class.NSThread.methods.isMainThread] +skipped = true +[class.NSDate.methods.timeIntervalSinceReferenceDate] +skipped = true + +# Root class +[class.NSProxy] +definition-skipped = true +[class.NSProxy.methods] +alloc = { skipped = true } +allocWithZone = { 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 } +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.methods.underlyingQueue] +skipped = true +[class.NSOperationQueue.methods.setUnderlyingQueue] +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.methods.identity] +skipped = true +[class.NSURLProtectionSpace.methods.serverTrust] +skipped = true +[class.NSURLSessionConfiguration.methods] +TLSMinimumSupportedProtocol = { skipped = true } +setTLSMinimumSupportedProtocol = { skipped = true } +TLSMaximumSupportedProtocol = { skipped = true } +setTLSMaximumSupportedProtocol = { skipped = true } +TLSMinimumSupportedProtocolVersion = { skipped = true } +setTLSMinimumSupportedProtocolVersion = { skipped = true } +TLSMaximumSupportedProtocolVersion = { skipped = true } +setTLSMaximumSupportedProtocolVersion = { skipped = true } +[class.NSUUID.methods] +initWithUUIDBytes = { skipped = true } +getUUIDBytes = { skipped = true } +[class.NSXPCConnection.methods] +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.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.methods.contentImage] +skipped = true +[class.NSUserNotification.methods.setContentImage] +skipped = true + +# Outlier that really should have been part of the original enum +[enum.anonymous.constants.NSProprietaryStringEncoding] +skipped = true + +# Has the wrong generic parameter +[class.NSDictionary.methods] +initWithContentsOfURL_error = { skipped = true } +dictionaryWithContentsOfURL_error = { skipped = true } + +# Protocol class methods (currently unsupported) +[protocol.NSItemProviderWriting.methods.writableTypeIdentifiersForItemProvider] +skipped = true +[protocol.NSItemProviderWriting.methods.itemProviderVisibilityForRepresentationWithTypeIdentifier] +skipped = true +[protocol.NSItemProviderReading.methods.readableTypeIdentifiersForItemProvider] +skipped = true +[protocol.NSItemProviderReading.methods.objectWithItemProviderData_typeIdentifier_error] +skipped = true +[protocol.NSSecureCoding.methods.supportsSecureCoding] +skipped = true + +# Custom implementation for now +[struct.NSRange] +skipped = true +[enum.NSComparisonResult] +skipped = true + +# Different definitions on 32-bit +[typedef.NSPoint] +skipped = true +[struct.NSPoint] +skipped = true +[typedef.NSSize] +skipped = true +[struct.NSSize] +skipped = true +[typedef.NSRect] +skipped = true +[struct.NSRect] +skipped = true +[enum.NSRectEdge] +skipped = true +[enum.anonymous.constants.NSRectEdgeMinX] +skipped = true +[enum.anonymous.constants.NSRectEdgeMinY] +skipped = true +[enum.anonymous.constants.NSRectEdgeMaxX] +skipped = true +[enum.anonymous.constants.NSRectEdgeMaxY] +skipped = true +[enum.anonymous.constants.NSMinXEdge] +skipped = true +[enum.anonymous.constants.NSMinYEdge] +skipped = true +[enum.anonymous.constants.NSMaxXEdge] +skipped = true +[enum.anonymous.constants.NSMaxYEdge] +skipped = true + +# We do a custom implementation of these +[protocol.NSCopying] +skipped = true +[protocol.NSMutableCopying] +skipped = true + +# Our implementation of superclass methods currently place them in the wrong +# module, so we do this hack for now. +[class.NSMutableAttributedString.methods] +initWithURL_options_documentAttributes_error = { skipped = true } +initWithData_options_documentAttributes_error = { skipped = true } +initWithRTF_documentAttributes = { skipped = true } +initWithRTFD_documentAttributes = { skipped = true } +initWithHTML_documentAttributes = { skipped = true } +initWithHTML_baseURL_documentAttributes = { skipped = true } +initWithDocFormat_documentAttributes = { skipped = true } +initWithHTML_options_documentAttributes = { skipped = true } +initWithRTFDFileWrapper_documentAttributes = { skipped = true } +initWithURL_documentAttributes = { skipped = true } +initWithPath_documentAttributes = { skipped = true } + +# Overridden fmt::Debug because we're missing https://github.com/madsmtm/objc2/issues/267 +# See fixes/debug.rs +[class.NSAttributedString] +derives = "PartialEq, Eq, Hash" +[class.NSBundle] +derives = "PartialEq, Eq, Hash" +[class.NSThread] +derives = "PartialEq, Eq, Hash" +[class.NSMutableData] +derives = "PartialEq, Eq, Hash" +owned = true +[class.NSMutableArray] +derives = "PartialEq, Eq, Hash" +owned = true +[class.NSMutableAttributedString] +derives = "PartialEq, Eq, Hash" +owned = true +[class.NSMutableSet] +derives = "PartialEq, Eq, Hash" +owned = true +[class.NSMutableString] +derives = "PartialEq, Eq, Hash" +owned = true +[class.NSMutableDictionary] +derives = "PartialEq, Eq, Hash" +owned = true + +# Overridden fmt::Debug because it's prettier +[class.NSArray] +derives = "PartialEq, Eq, Hash" +[class.NSData] +derives = "PartialEq, Eq, Hash" +[class.NSDictionary] +derives = "PartialEq, Eq, Hash" +[class.NSError] +derives = "PartialEq, Eq, Hash" +[class.NSException] +derives = "PartialEq, Eq, Hash" +[class.NSProcessInfo] +derives = "PartialEq, Eq, Hash" +[class.NSSet] +derives = "PartialEq, Eq, Hash" +[class.NSString] +derives = "PartialEq, Eq, Hash" +[class.NSUUID] +derives = "PartialEq, Eq, Hash" + +# Overridden because whether or not it is Eq depends on the inner value +[class.NSValue] +derives = "" +[class.NSNumber] +derives = "" +[class.NSDecimalNumber] +derives = "Debug, PartialEq, Hash" + +# Returns `nil` on Apple and GNUStep throws an exception on all other messages +# to this invalid instance. +[class.NSValue.methods.init] +skipped = true +[class.NSValue.methods.new] +skipped = true + +# Returns `NSMyType` instead of `Self` or `NSMyType` +# See fixes/generic_return.rs +[class.NSArray.methods.initWithContentsOfURL_error] +skipped = true +[class.NSArray.methods.initWithContentsOfFile] +skipped = true +[class.NSArray.methods.initWithContentsOfURL] +skipped = true +[class.NSMutableArray.methods.initWithContentsOfFile] +skipped = true +[class.NSMutableArray.methods.initWithContentsOfURL] +skipped = true +[class.NSDictionary.methods.initWithContentsOfFile] +skipped = true +[class.NSDictionary.methods.initWithContentsOfURL] +skipped = true +[class.NSMutableDictionary.methods.initWithContentsOfFile] +skipped = true +[class.NSMutableDictionary.methods.initWithContentsOfURL] +skipped = true + +# Manually found to be safe +[class.NSString.methods.init] +unsafe = false +[class.NSString.methods.compare] +unsafe = false +[class.NSString.methods.hasPrefix] +unsafe = false +[class.NSString.methods.hasSuffix] +unsafe = false +[class.NSString.methods.stringByAppendingString] +unsafe = false # The other string is non-null, and won't be retained +[class.NSString.methods.stringByAppendingPathComponent] +unsafe = false +[class.NSString.methods.lengthOfBytesUsingEncoding] +unsafe = false # Assuming `NSStringEncoding` can be made safe +[class.NSString.methods.length] +unsafe = false +[class.NSString.methods.UTF8String] +unsafe = false # Safe to call, but the returned pointer may not be safe to use +[class.NSString.methods.initWithString] +unsafe = false +[class.NSString.methods.stringWithString] +unsafe = false +[class.NSMutableString.methods.initWithCapacity] +unsafe = false +[class.NSMutableString.methods.stringWithCapacity] +unsafe = false +[class.NSMutableString.methods.initWithString] +unsafe = false +[class.NSMutableString.methods.stringWithString] +unsafe = false +[class.NSMutableString.methods.appendString] +unsafe = false +mutating = true +[class.NSMutableString.methods.setString] +unsafe = false +mutating = true +[class.NSAttributedString.methods.initWithString] +unsafe = false +[class.NSAttributedString.methods.initWithAttributedString] +unsafe = false +[class.NSMutableAttributedString.methods.initWithString] +unsafe = false +[class.NSMutableAttributedString.methods.initWithAttributedString] +unsafe = false +[class.NSAttributedString.methods.string] +unsafe = false +[class.NSAttributedString.methods.length] +unsafe = false +[class.NSBundle.methods.mainBundle] +unsafe = false +[class.NSBundle.methods.infoDictionary] +unsafe = false +[class.NSData.methods.length] +unsafe = false +[class.NSData.methods.bytes] +skipped = true +unsafe = false +[class.NSData.methods.initWithData] +unsafe = false +[class.NSData.methods.dataWithData] +unsafe = false +[class.NSDictionary.methods.count] +unsafe = false +[class.NSDictionary.methods.objectForKey] +unsafe = false +[class.NSDictionary.methods.allKeys] +unsafe = false +[class.NSDictionary.methods.allValues] +unsafe = false +[class.NSMutableDictionary.methods.setDictionary] +unsafe = false +mutating = true +[class.NSMutableDictionary.methods.removeObjectForKey] +unsafe = false +mutating = true +[class.NSMutableDictionary.methods.removeAllObjects] +unsafe = false +mutating = true +[class.NSError.methods.domain] +unsafe = false +[class.NSError.methods.code] +unsafe = false +[class.NSError.methods.userInfo] +unsafe = false +[class.NSError.methods.localizedDescription] +unsafe = false +[class.NSException.methods.raise] +skipped = true +[class.NSException.methods.name] +unsafe = false +[class.NSException.methods.reason] +unsafe = false +[class.NSException.methods.userInfo] +unsafe = false +[class.NSLock.methods.name] +unsafe = false +[class.NSLock.methods.setName] +unsafe = false +[class.NSMutableAttributedString.methods.setAttributedString] +unsafe = false +mutating = true +[class.NSMutableData.methods.mutableBytes] +unsafe = false +mutating = true +skipped = true # Wrong type on GNUStep +[class.NSMutableData.methods.length] +skipped = true +[class.NSMutableData.methods.setLength] +unsafe = false +mutating = true +[class.NSMutableData.methods.dataWithCapacity] +unsafe = false +[class.NSMutableData.methods.initWithCapacity] +unsafe = false +[class.NSMutableData.methods.dataWithData] +unsafe = false +[class.NSValue.methods.objCType] +unsafe = false +[class.NSValue.methods.isEqualToValue] +unsafe = false +[class.NSNumber.methods] +initWithChar = { unsafe = false } +initWithUnsignedChar = { unsafe = false } +initWithShort = { unsafe = false } +initWithUnsignedShort = { unsafe = false } +initWithInt = { unsafe = false } +initWithUnsignedInt = { unsafe = false } +initWithLong = { unsafe = false } +initWithUnsignedLong = { unsafe = false } +initWithLongLong = { unsafe = false } +initWithUnsignedLongLong = { unsafe = false } +initWithFloat = { unsafe = false } +initWithDouble = { unsafe = false } +initWithBool = { unsafe = false } +initWithInteger = { unsafe = false } +initWithUnsignedInteger = { unsafe = false } +numberWithChar = { unsafe = false } +numberWithUnsignedChar = { unsafe = false } +numberWithShort = { unsafe = false } +numberWithUnsignedShort = { unsafe = false } +numberWithInt = { unsafe = false } +numberWithUnsignedInt = { unsafe = false } +numberWithLong = { unsafe = false } +numberWithUnsignedLong = { unsafe = false } +numberWithLongLong = { unsafe = false } +numberWithUnsignedLongLong = { unsafe = false } +numberWithFloat = { unsafe = false } +numberWithDouble = { unsafe = false } +numberWithBool = { unsafe = false } +numberWithInteger = { unsafe = false } +numberWithUnsignedInteger = { unsafe = false } +compare = { unsafe = false } +isEqualToNumber = { unsafe = false } +charValue = { unsafe = false } +unsignedCharValue = { unsafe = false } +shortValue = { unsafe = false } +unsignedShortValue = { unsafe = false } +intValue = { unsafe = false } +unsignedIntValue = { unsafe = false } +longValue = { unsafe = false } +unsignedLongValue = { unsafe = false } +longLongValue = { unsafe = false } +unsignedLongLongValue = { unsafe = false } +floatValue = { unsafe = false } +doubleValue = { unsafe = false } +boolValue = { unsafe = false } +integerValue = { unsafe = false } +unsignedIntegerValue = { unsafe = false } +stringValue = { unsafe = false } +[class.NSUUID.methods.UUID] +unsafe = false +[class.NSUUID.methods.init] +unsafe = false +[class.NSUUID.methods.initWithUUIDString] +unsafe = false +[class.NSUUID.methods.UUIDString] +unsafe = false +[class.NSThread.methods.currentThread] +unsafe = false +[class.NSThread.methods.mainThread] +unsafe = false +# [class.NSThread.methods.isMainThread] +# unsafe = false +[class.NSThread.methods.name] +unsafe = false +[class.NSThread.methods.isMultiThreaded] +unsafe = false +[class.NSProcessInfo.methods.processInfo] +unsafe = false +[class.NSProcessInfo.methods.processName] +unsafe = false +[class.NSArray.methods.count] +unsafe = false +[class.NSMutableArray.methods.removeAllObjects] +unsafe = false +mutating = true +[class.NSMutableArray.methods.addObject] +mutating = true +[class.NSMutableArray.methods.insertObject_atIndex] +mutating = true +[class.NSMutableArray.methods.replaceObjectAtIndex_withObject] +mutating = true +[class.NSMutableArray.methods.removeObjectAtIndex] +mutating = true +[class.NSMutableArray.methods.removeLastObject] +mutating = true +[class.NSMutableArray.methods.sortUsingFunction_context] +mutating = true +[class.NSSet.methods.count] +unsafe = false +[class.NSMutableSet.methods.removeAllObjects] +unsafe = false +mutating = true +[class.NSMutableSet.methods.addObject] +mutating = true + +# Mutable classes +# +# Some of these are commented out because they're set above +# [class.NSMutableArray] +# owned = true +[class.NSMutableCharacterSet] +owned = true +# [class.NSMutableData] +# owned = true +# [class.NSMutableDictionary] +# owned = true +# [class.NSMutableSet] +# owned = true +[class.NSMutableOrderedSet] +owned = true +[class.NSMutableIndexSet] +owned = true +# [class.NSMutableString] +# owned = true +# [class.NSMutableAttributedString] +# owned = true +[class.NSMutableURLRequest] +owned = true diff --git a/crates/icrate/src/common.rs b/crates/icrate/src/common.rs index 6d9ab2690..17b07b5e7 100644 --- a/crates/icrate/src/common.rs +++ b/crates/icrate/src/common.rs @@ -11,13 +11,16 @@ pub(crate) use std::ffi::{ }; #[cfg(feature = "objective-c")] -pub(crate) use objc2::ffi::{NSInteger, NSUInteger}; +pub(crate) use objc2::ffi::{NSInteger, NSIntegerMax, NSUInteger, NSUIntegerMax}; #[cfg(feature = "objective-c")] -pub(crate) use objc2::rc::{Allocated, Id, Shared}; +pub(crate) use objc2::rc::{Allocated, Id, Owned, Ownership, Shared}; #[cfg(feature = "objective-c")] pub(crate) use objc2::runtime::{Bool, Class, Object, Sel}; #[cfg(feature = "objective-c")] -pub(crate) use objc2::{__inner_extern_class, extern_class, extern_methods, ClassType, Message}; +pub(crate) use objc2::{ + __inner_extern_class, extern_class, extern_methods, extern_protocol, ClassType, Message, + ProtocolType, +}; #[cfg(feature = "block")] pub(crate) use block2::Block; diff --git a/crates/icrate/src/generated b/crates/icrate/src/generated index ef093626b..47853891c 160000 --- a/crates/icrate/src/generated +++ b/crates/icrate/src/generated @@ -1 +1 @@ -Subproject commit ef093626b60fc41b2953349e7c4f47237b1935c0 +Subproject commit 47853891c3e24e1e387f2ab0e22b3a37e8a593f1 diff --git a/crates/icrate/src/lib.rs b/crates/icrate/src/lib.rs index 4677e3609..ddf84d8f0 100644 --- a/crates/icrate/src/lib.rs +++ b/crates/icrate/src/lib.rs @@ -2,7 +2,7 @@ #![cfg_attr(feature = "unstable-docsrs", feature(doc_auto_cfg))] #![warn(elided_lifetimes_in_paths)] #![deny(non_ascii_idents)] -// #![warn(unreachable_pub)] +#![warn(unreachable_pub)] #![deny(unsafe_op_in_unsafe_fn)] #![warn(clippy::cargo)] #![warn(clippy::ptr_as_ptr)] @@ -10,8 +10,12 @@ #![allow(non_camel_case_types)] #![allow(non_upper_case_globals)] #![allow(non_snake_case)] +#![allow(clippy::too_many_arguments)] +#![allow(clippy::type_complexity)] +#![allow(clippy::identity_op)] // Update in Cargo.toml as well. #![doc(html_root_url = "https://docs.rs/icrate/0.0.1")] +#![recursion_limit = "512"] #[cfg(feature = "alloc")] extern crate alloc; diff --git a/crates/icrate/src/macros.rs b/crates/icrate/src/macros.rs index f26d28dae..058e3dd9c 100644 --- a/crates/icrate/src/macros.rs +++ b/crates/icrate/src/macros.rs @@ -1,5 +1,20 @@ #![allow(unused_macros)] +macro_rules! impl_encode { + ($name:ident = $encoding:expr;) => { + #[cfg(feature = "objective-c")] + unsafe impl objc2::Encode for $name { + const ENCODING: objc2::Encoding = $encoding; + } + + #[cfg(feature = "objective-c")] + unsafe impl objc2::RefEncode for $name { + const ENCODING_REF: objc2::Encoding = + objc2::Encoding::Pointer(&::ENCODING); + } + }; +} + macro_rules! extern_struct { ( $v:vis struct $name:ident { @@ -12,18 +27,12 @@ macro_rules! extern_struct { $($field_v $field: $ty,)* } - #[cfg(feature = "objective-c")] - unsafe impl objc2::Encode for $name { - const ENCODING: objc2::Encoding = objc2::Encoding::Struct( + impl_encode! { + $name = objc2::Encoding::Struct( stringify!($name), &[$(<$ty as objc2::Encode>::ENCODING),*], ); } - - #[cfg(feature = "objective-c")] - unsafe impl objc2::RefEncode for $name { - const ENCODING_REF: objc2::Encoding = objc2::Encoding::Pointer(&::ENCODING); - } }; } @@ -92,13 +101,19 @@ macro_rules! ns_enum { ( #[underlying($ty:ty)] $v:vis enum $($name:ident)? { - $($field:ident = $value:expr),* $(,)? + $( + $(#[$m:meta])* + $field:ident = $value:expr + ),* $(,)? } ) => { extern_enum! { #[underlying($ty)] $v enum $($name)? { - $($field = $value),* + $( + $(#[$m])* + $field = $value + ),* } } }; @@ -172,6 +187,20 @@ macro_rules! ns_error_enum { }; } +macro_rules! typed_enum { + ($v:vis type $name:ident = $ty:ty $(;)?) => { + // TODO + pub type $name = $ty; + }; +} + +macro_rules! typed_extensible_enum { + ($v:vis type $name:ident = $ty:ty $(;)?) => { + // TODO + pub type $name = $ty; + }; +} + macro_rules! extern_static { ($name:ident: $ty:ty) => { extern "C" { 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..21f9cfc16 100644 --- a/crates/objc2/src/macros/extern_class.rs +++ b/crates/objc2/src/macros/extern_class.rs @@ -242,12 +242,12 @@ 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,)* } - unsafe impl<$($t_for:ident $(: $b_for:ident)?),*> ClassType for $for:ty { - $(#[inherits($($inheritance_rest:ty),+)])? + unsafe impl<$($t_for:ident $(: $b_for:ident)?),* $(,)?> ClassType for $for:ty { + $(#[inherits($($inheritance_rest:ty),+ $(,)?)])? type Super = $superclass:ty; $(const NAME: &'static str = $name_const:literal;)? diff --git a/crates/objc2/src/macros/extern_methods.rs b/crates/objc2/src/macros/extern_methods.rs index 9c68bc5f9..a48ad3644 100644 --- a/crates/objc2/src/macros/extern_methods.rs +++ b/crates/objc2/src/macros/extern_methods.rs @@ -162,7 +162,7 @@ macro_rules! extern_methods { ( $( $(#[$impl_m:meta])* - unsafe impl<$($t:ident $(: $b:ident $(+ $rest:ident)*)?),*> $type:ty { + unsafe impl<$($t:ident $(: $b:ident $(+ $rest:ident)*)?),* $(,)?> $type:ty { $($methods:tt)* } )+ @@ -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, } }; diff --git a/crates/test-assembly/crates/test_ns_string/expected/gnustep-x86.s b/crates/test-assembly/crates/test_ns_string/expected/gnustep-x86.s index dd0cceae1..814b44d81 100644 --- a/crates/test-assembly/crates/test_ns_string/expected/gnustep-x86.s +++ b/crates/test-assembly/crates/test_ns_string/expected/gnustep-x86.s @@ -23,7 +23,7 @@ get_ascii: lea eax, [ebx + .Lanon.[ID].0@GOTOFF] push 3 push eax - call SYM(icrate::Foundation::additions::string::NSString::from_str::GENERATED_ID, 0)@PLT + call SYM(icrate::Foundation::additions::string::::from_str::GENERATED_ID, 0)@PLT add esp, 16 mov ecx, eax xchg dword ptr [ebx + SYM(test_ns_string[CRATE_ID]::get_ascii::CACHED_NSSTRING, 0).0@GOTOFF], ecx @@ -56,7 +56,7 @@ get_utf16: lea eax, [ebx + .Lanon.[ID].1@GOTOFF] push 5 push eax - call SYM(icrate::Foundation::additions::string::NSString::from_str::GENERATED_ID, 0)@PLT + call SYM(icrate::Foundation::additions::string::::from_str::GENERATED_ID, 0)@PLT add esp, 16 mov ecx, eax xchg dword ptr [ebx + SYM(test_ns_string[CRATE_ID]::get_utf16::CACHED_NSSTRING, 0).0@GOTOFF], ecx @@ -89,7 +89,7 @@ get_with_nul: lea eax, [ebx + .Lanon.[ID].2@GOTOFF] push 6 push eax - call SYM(icrate::Foundation::additions::string::NSString::from_str::GENERATED_ID, 0)@PLT + call SYM(icrate::Foundation::additions::string::::from_str::GENERATED_ID, 0)@PLT add esp, 16 mov ecx, eax xchg dword ptr [ebx + SYM(test_ns_string[CRATE_ID]::get_with_nul::CACHED_NSSTRING, 0).0@GOTOFF], ecx diff --git a/crates/test-assembly/crates/test_ns_string/expected/gnustep-x86_64.s b/crates/test-assembly/crates/test_ns_string/expected/gnustep-x86_64.s index fdbe85019..87bc1b1c5 100644 --- a/crates/test-assembly/crates/test_ns_string/expected/gnustep-x86_64.s +++ b/crates/test-assembly/crates/test_ns_string/expected/gnustep-x86_64.s @@ -14,7 +14,7 @@ get_ascii: .LBB0_1: lea rdi, [rip + .Lanon.[ID].0] mov esi, 3 - call qword ptr [rip + SYM(icrate::Foundation::additions::string::NSString::from_str::GENERATED_ID, 0)@GOTPCREL] + call qword ptr [rip + SYM(icrate::Foundation::additions::string::::from_str::GENERATED_ID, 0)@GOTPCREL] mov rcx, rax xchg qword ptr [rip + SYM(test_ns_string[CRATE_ID]::get_ascii::CACHED_NSSTRING, 0).0], rcx pop rcx @@ -36,7 +36,7 @@ get_utf16: .LBB1_1: lea rdi, [rip + .Lanon.[ID].1] mov esi, 5 - call qword ptr [rip + SYM(icrate::Foundation::additions::string::NSString::from_str::GENERATED_ID, 0)@GOTPCREL] + call qword ptr [rip + SYM(icrate::Foundation::additions::string::::from_str::GENERATED_ID, 0)@GOTPCREL] mov rcx, rax xchg qword ptr [rip + SYM(test_ns_string[CRATE_ID]::get_utf16::CACHED_NSSTRING, 0).0], rcx pop rcx @@ -58,7 +58,7 @@ get_with_nul: .LBB2_1: lea rdi, [rip + .Lanon.[ID].2] mov esi, 6 - call qword ptr [rip + SYM(icrate::Foundation::additions::string::NSString::from_str::GENERATED_ID, 0)@GOTPCREL] + call qword ptr [rip + SYM(icrate::Foundation::additions::string::::from_str::GENERATED_ID, 0)@GOTPCREL] mov rcx, rax xchg qword ptr [rip + SYM(test_ns_string[CRATE_ID]::get_with_nul::CACHED_NSSTRING, 0).0], rcx pop rcx diff --git a/crates/test-ui/ui/msg_send_invalid_error.stderr b/crates/test-ui/ui/msg_send_invalid_error.stderr index f2d44aaca..e85c00b78 100644 --- a/crates/test-ui/ui/msg_send_invalid_error.stderr +++ b/crates/test-ui/ui/msg_send_invalid_error.stderr @@ -41,13 +41,13 @@ error[E0277]: the trait bound `i32: Message` is not satisfied | = help: the following other types implement trait `Message`: Exception - NSArray - NSAttributedString - NSBundle - NSData - NSDictionary - NSError - NSException + NSAffineTransform + NSAppleEventDescriptor + NSAppleEventManager + NSAppleScript + NSArchiver + NSArray + NSAssertionHandler and $N others note: required by a bound in `__send_message_error` --> $WORKSPACE/crates/objc2/src/message/mod.rs diff --git a/crates/test-ui/ui/nsarray_bound_not_send_sync.stderr b/crates/test-ui/ui/nsarray_bound_not_send_sync.stderr index a6f5f8fb1..d0e4ee9ab 100644 --- a/crates/test-ui/ui/nsarray_bound_not_send_sync.stderr +++ b/crates/test-ui/ui/nsarray_bound_not_send_sync.stderr @@ -7,7 +7,7 @@ error[E0277]: `UnsafeCell, PhantomPinned)>>` = help: within `objc2::runtime::Object`, the trait `Sync` is not implemented for `UnsafeCell, PhantomPinned)>>` = note: required because it appears within the type `objc_object` = note: required because it appears within the type `objc2::runtime::Object` - = note: required for `NSArray` to implement `Sync` + = note: required for `NSArray` to implement `Sync` note: required by a bound in `needs_sync` --> ui/nsarray_bound_not_send_sync.rs | @@ -26,7 +26,7 @@ error[E0277]: `*const UnsafeCell<()>` cannot be sent between threads safely = note: required because it appears within the type `UnsafeCell, PhantomPinned)>>` = note: required because it appears within the type `objc_object` = note: required because it appears within the type `objc2::runtime::Object` - = note: required for `NSArray` to implement `Sync` + = note: required for `NSArray` to implement `Sync` note: required by a bound in `needs_sync` --> ui/nsarray_bound_not_send_sync.rs | @@ -42,7 +42,7 @@ error[E0277]: `UnsafeCell, PhantomPinned)>>` = help: within `objc2::runtime::Object`, the trait `Sync` is not implemented for `UnsafeCell, PhantomPinned)>>` = note: required because it appears within the type `objc_object` = note: required because it appears within the type `objc2::runtime::Object` - = note: required for `NSArray` to implement `Send` + = note: required for `NSArray` to implement `Send` note: required by a bound in `needs_send` --> ui/nsarray_bound_not_send_sync.rs | @@ -61,7 +61,7 @@ error[E0277]: `*const UnsafeCell<()>` cannot be sent between threads safely = note: required because it appears within the type `UnsafeCell, PhantomPinned)>>` = note: required because it appears within the type `objc_object` = note: required because it appears within the type `objc2::runtime::Object` - = note: required for `NSArray` to implement `Send` + = note: required for `NSArray` to implement `Send` note: required by a bound in `needs_send` --> ui/nsarray_bound_not_send_sync.rs | diff --git a/crates/tests/Cargo.toml b/crates/tests/Cargo.toml index 3ac42be02..03bf07c95 100644 --- a/crates/tests/Cargo.toml +++ b/crates/tests/Cargo.toml @@ -12,8 +12,9 @@ build = "build.rs" [features] default = ["apple", "std"] std = ["block2/std", "objc2/std", "icrate/std"] -exception = ["objc2/exception"] +exception = ["objc2/exception", "Foundation"] catch-all = ["objc2/catch-all", "exception"] +Foundation = ["icrate/Foundation"] apple = ["block2/apple", "objc2/apple", "icrate/apple"] gnustep-1-7 = ["block2/gnustep-1-7", "objc2/gnustep-1-7", "icrate/gnustep-1-7"] @@ -29,7 +30,7 @@ block2 = { path = "../block2", default-features = false } block-sys = { path = "../block-sys", default-features = false } objc-sys = { path = "../objc-sys", default-features = false } objc2 = { path = "../objc2", default-features = false } -icrate = { path = "../icrate", default-features = false, features = ["Foundation"] } +icrate = { path = "../icrate", default-features = false } [build-dependencies] cc = "1.0" diff --git a/crates/tests/src/exception.rs b/crates/tests/src/exception.rs index 8e30f2656..270974ba5 100644 --- a/crates/tests/src/exception.rs +++ b/crates/tests/src/exception.rs @@ -48,7 +48,7 @@ fn throw_catch_raise_catch() { assert_eq!(exc.name(), name); assert_eq!(exc.reason().unwrap(), reason); - assert!(exc.user_info().is_none()); + assert!(exc.userInfo().is_none()); } #[test] @@ -143,7 +143,7 @@ fn catch_actual() { let exc = NSException::from_exception(exc).unwrap(); assert_eq!(exc.name(), NSString::from_str(name)); assert_eq!(exc.reason().unwrap(), NSString::from_str(reason)); - let user_info = exc.user_info(); + let user_info = exc.userInfo(); if cfg!(feature = "gnustep-1-7") { let user_info = user_info.unwrap(); assert_eq!(user_info.len(), 3); diff --git a/crates/tests/src/test_object.rs b/crates/tests/src/test_object.rs index db6e7ef2c..e3dbb39db 100644 --- a/crates/tests/src/test_object.rs +++ b/crates/tests/src/test_object.rs @@ -1,8 +1,11 @@ use core::mem::{size_of, ManuallyDrop}; use std::os::raw::c_int; +#[cfg(feature = "Foundation")] use icrate::Foundation::NSNumber; -use objc2::rc::{autoreleasepool, AutoreleasePool, Id, Owned, Shared}; +#[cfg(feature = "Foundation")] +use objc2::rc::Shared; +use objc2::rc::{autoreleasepool, AutoreleasePool, Id, Owned}; use objc2::runtime::{Bool, Class, NSObject, Object, Protocol}; #[cfg(feature = "malloc")] use objc2::sel; @@ -25,6 +28,7 @@ extern_protocol!( // #[method(b)] // fn b() -> c_int; + #[cfg(feature = "Foundation")] #[method_id(c)] fn c(&self) -> Id; @@ -41,6 +45,7 @@ extern_protocol!( // #[optional] // fn f() -> c_int; + #[cfg(feature = "Foundation")] #[optional] #[method_id(g)] fn g(&self) -> Id; @@ -312,10 +317,12 @@ fn test_protocol() { let proto: Id = Id::into_protocol(obj); assert_eq!(proto.a(), 1); // TODO: assert_eq!(MyTestObject::b(), 2); + #[cfg(feature = "Foundation")] assert_eq!(proto.c().as_i32(), 3); // TODO: assert_eq!(MyTestObject::d().as_i32(), 4); assert_eq!(proto.e(), 5); // TODO: assert_eq!(MyTestObject::f(), 6); + #[cfg(feature = "Foundation")] assert_eq!(proto.g().as_i32(), 7); // TODO: assert_eq!(MyTestObject::h().as_i32(), 8); diff --git a/helper-scripts/get_llvm_targets.fish b/helper-scripts/get_llvm_targets.fish new file mode 100644 index 000000000..40372554f --- /dev/null +++ b/helper-scripts/get_llvm_targets.fish @@ -0,0 +1,7 @@ +set APPLE_TARGETS x86_64-apple-darwin aarch64-apple-darwin aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios aarch64-apple-ios-macabi aarch64-apple-tvos aarch64-apple-watchos-sim arm64_32-apple-watchos armv7-apple-ios armv7k-apple-watchos armv7s-apple-ios i386-apple-ios i686-apple-darwin x86_64-apple-ios-macabi x86_64-apple-tvos x86_64-apple-watchos-sim + +for TARGET in $APPLE_TARGETS + echo "" + echo "$TARGET" + rustc +nightly --print target-spec-json --target=$TARGET | jq '.["llvm-target"]' +end