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

How do we handle availability? #266

Open
madsmtm opened this issue Sep 8, 2022 · 10 comments
Open

How do we handle availability? #266

madsmtm opened this issue Sep 8, 2022 · 10 comments
Labels
A-framework Affects the framework crates and the translator for them enhancement New feature or request help wanted Extra attention is needed

Comments

@madsmtm
Copy link
Owner

madsmtm commented Sep 8, 2022

In Apple's Objective-C headers, most classes and methods are annotated with an availability attribute such as API_AVAILABLE(macos(10.13), ios(11.0), watchos(4.0), tvos(11.0)); this is absolutely a great idea (!!!), it very cleanly allows you to mark which APIs you may use, and which ones are not usable on your current deployment target.

We should have some way of doing the same as @available in Objective-C; however, since we are not a compiler like clang is, this is quite tricky!

A quick demonstration of what I want:

  1. Declare an API which is only available on a specific deployment target
    #[cfg_available(macOS(10.10), ...)]
    #[sel(doSomething:)]
    fn doSomething(&self, arg: i32);
  2. Prevent the user from using said API if their deployment target is not high enough:
    // MACOSX_DEPLOYMENT_TARGET=10.7
    obj.doSomething(32); // Fails (ideally at compile time, otherwise with debug assertions at runtime)
    
    // MACOSX_DEPLOYMENT_TARGET=10.10
    obj.doSomething(32); // Works
  3. Allow the user (usually libraries) to use the API if they've verified that the target version is high enough (this uses a dynamic runtime check except if the deployment target is high enough).
    if available!(macOS(10.12, ...)) { // Anything higher than what `doSomething` needs
        obj.doSomething(32); // Works no matter the deployment target
    }
  4. When declaring a class and overriding methods, if you know when the method was added (and hence, when it will be called), communicate this availability to the body of the function:
    #[cfg_available(macOS(10.10), ...)] // Same as superclass'
    #[sel(doSomething:)]
    fn doSomething(&self, arg: i32) {
        // Allowed to do things only available on macOS 10.10 and above
    }

For this, Contexts and capabilities come to mind, similar to how it would be useful for autorelease pools, but alas, we can't do that yet, so this will probably end up as a debug assertions runtime check.

See also upstream SSheldon/rust-objc#111, a WIP implementation can be found in #212.

@madsmtm madsmtm added enhancement New feature or request help wanted Extra attention is needed A-framework Affects the framework crates and the translator for them labels Sep 8, 2022
@madsmtm
Copy link
Owner Author

madsmtm commented Nov 1, 2022

There are effectively two versions that affect availability and what we should do: the deployment target and the SDK version.

To illustrate, let's assume an API fn foo() { ... } that's introduced in some version i, later deprecated in some version d, and finally removed in some version r*.

Deployment target SDK version fn declaration foo() if_available(introduced_version_or_above) { foo() }
..i ..i None Fails Fails
..i i..d fn foo() { assert_available(introduced_version); ... } Panics Success
..i d..r #[deprecated] fn foo() { assert_available(introduced_version); ... } Warning + Panics Warning
..i r.. None* Fails Fails
i..d i..d fn foo() { ... } Success Success
i..d d..r #[deprecated] fn foo() { ... } Warning Warning
i..d r.. None* Fails Fails
d..r d..r #[deprecated] fn foo() { ... } Warning Warning
d..r r.. None* Fails Fails
r.. r.. None* Fails Fails

*Note that I don't really know a case where an API has been removed, but we can handle it if need be.

@madsmtm
Copy link
Owner Author

madsmtm commented Nov 1, 2022

From the above, we can see that the logic in cfg_available is basically:
Deployment target < Introduced version -> Add assertion that the user has checked the availability before calling
Deprecation version <= SDK version -> Mark the item as deprecated
Removal version <= SDK version -> Remove the item

@madsmtm

This comment was marked as duplicate.

@madsmtm
Copy link
Owner Author

madsmtm commented Dec 24, 2022

We need to do both a static and a dynamic check - the static one is fairly straightforward, but I'm unsure of how we should do the dynamic one?

I would have thought that clang just called some library function, but it's actually implemented as a compiler builtin, which calls into CoreFoundation and reads /System/Library/CoreServices/SystemVersion.plist, see os_version_check.c.

Do we really need that as well? Or can we perhaps get by with just reading kCFCoreFoundationVersionNumber (or maybe NSFoundationVersionNumber)?

@madsmtm
Copy link
Owner Author

madsmtm commented Dec 24, 2022

Actually, turns out the @available in Objective-C is much newer than i thought - see the initial proposal here.

We could reconsider the check to be just is_available!(MyClass::doSomething), but I think the reasoning in that post (better for control-flow) apply to us as well, at least if we do end up getting something like contexts and capabilities. The optimization potential once the user switches to a higher deployment target is also nice.

@madsmtm
Copy link
Owner Author

madsmtm commented Dec 24, 2022

Swift's #available works similarly, see Availability.swift and Availability.mm.

Though they use (a weak symbol to) os_system_version_get_current_version, are we allowed to do that too? And where is that even defined?

@madsmtm
Copy link
Owner Author

madsmtm commented Feb 3, 2023

There's a new RFC that would help with OS compile-time detection: rust-lang/rfcs#3379 (though we'd still want a macro for the compile-time + runtime detection fallback)

@madsmtm
Copy link
Owner Author

madsmtm commented Nov 23, 2023

A short, very incomplete list I made a while ago on different behaviour in clang based on the deployment target (clang v13 source):

  • CGException.cpp getObjCPersonality (GNUStep >= 1.7)
  • Clang.cpp Clang::AddObjCRuntimeArgs (GNUStep >= 2.0)
  • isLegacyDispatchDefaultForArch (macOS < 10.6, GNUStep < 1.6)
  • hasNativeARC (macOS < 10.7, iOS < 5)
  • shouldUseARCFunctionsForRetainRelease (macOS < 10.10, iOS < 8)
  • shouldUseRuntimeFunctionsForAlloc (macOS < 10.10, iOS < 8)
  • shouldUseRuntimeFunctionForCombinedAllocInit (macOS >= 10.14.4, iOS >= 12.2, watchOS >= 5.2)
  • hasOptimizedSetter (macOS >= 10.8, iOS >= 6, GNUStep >= 1.7)
  • hasSubscripting (macOS < 10.11, iOS < 9)
  • hasTerminate (macOS < 10.8, iOS < 5)
  • hasARCUnsafeClaimAutoreleasedReturnValue (macOS >= 10.11, iOS >= 9, watchOS >= 2)
  • hasEmptyCollections (macOS >= 10.11, iOS >= 9, watchOS >= 2)

My conclusion was that it's not super important for the runtime i.e. objc2 to know the deployment target statically, especially not after #530.

(That said, it's of course still very important for the user to know, so we still need this feature in some shape or form).

@madsmtm
Copy link
Owner Author

madsmtm commented Dec 3, 2023

I wrote some ideas for how the availability check might work internally in this playground.

@madsmtm
Copy link
Owner Author

madsmtm commented Dec 5, 2023

We have rustc --print deployment-target for retrieving the current deployment target, and since rust-lang/cc-rs#848 the cc crate has been automatically using that for setting the deployment target for build scripts. I've opened rust-lang/cargo#13115 for making the deployment target even more easily accessible from build scripts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-framework Affects the framework crates and the translator for them enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

1 participant