Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Macro to conditionally emit code depending on deployment target #212

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions objc-sys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ unstable-docsrs = []
[build-dependencies]
cc = { version = "1", optional = true }

[dependencies]
objc2-proc-macros = { path = "../objc2-proc-macros" }

[package.metadata.docs.rs]
default-target = "x86_64-apple-darwin"
no-default-features = true
Expand Down
2 changes: 2 additions & 0 deletions objc-sys/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ fn main() {
}
}

println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.11");

let runtime = match (apple, gnustep, objfw) {
(true, false, false) => {
Apple(match &*target_os {
Expand Down
18 changes: 18 additions & 0 deletions objc-sys/src/internal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//! Defined in `objc-internal.h`
#[allow(unused)]
use crate::{objc_class, objc_object};
use objc2_proc_macros::cfg_available;

extern_c_unwind! {
#[cfg(apple)]
#[cfg_available(macOS(10.9), iOS(7.0), tvOS(9.0), watchOS(1.0))]
pub fn objc_alloc(cls: *const objc_class) -> *mut objc_object;

#[cfg(apple)]
#[cfg_available(macOS(10.14.4), iOS(12.2), tvOS(12.2), watchOS(5.2))]
pub fn objc_alloc_init(cls: *const objc_class) -> *mut objc_object;

#[cfg(apple)]
#[cfg_available(macOS(10.15), iOS(13.0), tvOS(13.0), watchOS(6.0))]
pub fn objc_opt_new(cls: *const objc_class) -> *mut objc_object;
}
2 changes: 2 additions & 0 deletions objc-sys/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ mod constants;

mod exception;
mod image_info;
mod internal;
mod message;
mod method;
mod object;
Expand All @@ -154,6 +155,7 @@ pub use class::*;
pub use constants::*;
pub use exception::*;
pub use image_info::*;
pub use internal::*;
pub use message::*;
pub use method::*;
pub use object::*;
Expand Down
248 changes: 248 additions & 0 deletions objc2-proc-macros/src/available.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
//! https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/cross_development/Introduction/Introduction.html
//! https://developer.apple.com/library/archive/technotes/tn2064/_index.html
//! See also AvailabilityMacros.h and Availability.h
//! https://clang.llvm.org/docs/LanguageExtensions.html#objective-c-available

use std::str::FromStr;

use crate::deployment_target::{self, Version};
use proc_macro::{Delimiter, TokenStream, TokenTree};

fn macos(min: Version) -> Option<&'static str> {
// Note: Implicitly assumes that `macos_aarch64` will return an equal or
// higher value than `macos`

if deployment_target::macos() >= min {
// Available on all macOS
Some(r#"target_os = "macos""#)
} else if deployment_target::macos_aarch64() >= min {
// Available on Aarch64, not available on others
Some(r#"not(all(target_os = "macos", not(target_arch = "aarch64")))"#)
} else {
// Not available on macOS
None
}
}

fn ios(min: Version) -> Option<&'static str> {
if deployment_target::ios() >= min {
Some(r#"target_os = "ios""#)
} else {
None
}
}

fn tvos(min: Version) -> Option<&'static str> {
match deployment_target::tvos() {
Some(v) if v >= min => Some(r#"target_os = "tvos""#),
// Disable everything on tvOS if deployment target is not specified
// TODO: Change this once rustc sets a default deployment target
_ => None,
}
}

fn watchos(min: Version) -> Option<&'static str> {
if deployment_target::watchos() >= min {
Some(r#"target_os = "watchos""#)
} else {
None
}
}

#[derive(Debug, Default)]
pub(crate) struct AvailableSince {
macos: Option<Version>,
ios: Option<Version>,
tvos: Option<Version>,
watchos: Option<Version>,
}

impl AvailableSince {
pub(crate) fn from_tokenstream(attr: TokenStream) -> Self {
let mut this = Self::default();
let mut iter = attr.into_iter();
loop {
let ident = match iter.next() {
Some(TokenTree::Ident(ident)) => ident,
None => return this,
_ => panic!("expected ident"),
};
let version = match iter.next() {
Some(TokenTree::Group(group)) => {
assert_eq!(
group.delimiter(),
Delimiter::Parenthesis,
"Invalid delimiters"
);
Version::from_str(&group.stream().to_string()).expect("Invalid version")
}
_ => panic!("expected version string"),
};
let os = ident.to_string();
match &*os {
"macOS" => this.macos = Some(version),
"iOS" => this.ios = Some(version),
"tvOS" => this.tvos = Some(version),
"watchOS" => this.watchos = Some(version),
_ => panic!("Unknown OS {}", os),
}
let _comma = match iter.next() {
Some(TokenTree::Punct(ident)) => ident,
None => return this,
_ => panic!("expected ident"),
};
}
}

fn into_cfg_string(&self) -> String {
let mut result = "any(".to_string();
if let Some(s) = self.macos.and_then(macos) {
result += s;
result += ", ";
}
if let Some(s) = self.ios.and_then(ios) {
result += s;
result += ", ";
}
if let Some(s) = self.tvos.and_then(tvos) {
result += s;
result += ", ";
}
if let Some(s) = self.watchos.and_then(watchos) {
result += s;
result += ", ";
}
if result == "any(" {
// If didn't change
result.push_str("__not_available_anywhere");
} else {
// Remove extra ", "
result.pop();
result.pop();
}
result += ")";
result
}

pub(crate) fn into_cfg(self) -> TokenStream {
format!("#[cfg({})]", self.into_cfg_string())
.parse()
.expect("invalid cfg string")
}

pub(crate) fn into_not_cfg(self) -> TokenStream {
format!("#[cfg(not({}))]", self.into_cfg_string())
.parse()
.expect("invalid cfg string")
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_availability_cfg_string() {
#[track_caller]
fn assert_cfg(available_since: &AvailableSince, expected: &str) {
assert_eq!(available_since.into_cfg_string(), expected);
}

let mut available_since = AvailableSince::default();
assert_cfg(&available_since, r#"any(__not_available_anywhere)"#);

available_since.macos = Some(Version::new(12, 0));
assert_cfg(&available_since, r#"any(__not_available_anywhere)"#);
available_since.macos = Some(Version::new(10, 0));
assert_cfg(&available_since, r#"any(target_os = "macos")"#);

available_since.ios = Some(Version::new(9, 0));
assert_cfg(&available_since, r#"any(target_os = "macos")"#);
available_since.ios = Some(Version::new(5, 0));
assert_cfg(
&available_since,
r#"any(target_os = "macos", target_os = "ios")"#,
);

available_since.tvos = Some(Version::new(0, 0));
assert_cfg(
&available_since,
r#"any(target_os = "macos", target_os = "ios")"#,
);

available_since.watchos = Some(Version::new(0, 0));
assert_cfg(
&available_since,
r#"any(target_os = "macos", target_os = "ios", target_os = "watchos")"#,
);
}
}

#[cfg(ideas)]
mod ideas {
//! Ideas for usage

// Procedural macros

// Mimic Objective-C:
// __API_AVAILABLE(macos(10.4), ios(8.0), watchos(2.0), tvos(10.0))
#[available_since(macos(10.9), ios(8.0))]
// #[not_available(macos(10.9), ios(8.0))]
fn my_fn() {}

// Mimic Swift's @available
#[available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)]
#[available(macOS, introduced: 10.15)]
#[available(iOS, introduced: 13)]
#[available(iOS, deprecated: 14)]
#[available(iOS, obsoleted: 15)]
#[available(iOS, unavailable)]
fn my_fn() {}

#[available::macos(10.9)]
#[available::ios(8.0)]
// #[available::macos(not(10.9))]
// #[available::ios(not(8.0))]
// #[available::not_macos(10.9)]
// #[available::not_ios(8.0)]
fn my_fn() {}

// https://crates.io/crates/assert2
#[available(macos >= 10.9, ios >= 8.0)]
fn my_fn() {}

// Helper functions / macros for doing runtime version checks (and omit
// them when the DEPLOYMENT_TARGET is high enough).

fn some_fn() {
if available_macos(10.9) && available_ios(6.0) {
} else {
}

if available!(macos(10.9), ios(6.0)) {
} else {
}

#[cfg(target_os = "macos")]
if available_macos(10.9) {
} else {
}
#[cfg(target_os = "ios")]
if available_ios(6.0) {
} else {
}

#[is_available(macos(10.9), ios(6.0))]
{}
#[not(is_available(macos(10.9), ios(6.0)))]
{}

if available(Availability {
macos: 10.9,
ios: 6.0,
..Default::default()
}) {
} else {
}
}
}
Loading