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 · 17 comments · May be fixed by #212
Open

How do we handle availability? #266

madsmtm opened this issue Sep 8, 2022 · 17 comments · May be fixed by #212
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). Done in Implement runtime availability checking #661.
    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 the original 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.

@madsmtm
Copy link
Owner Author

madsmtm commented Sep 9, 2024

This also affects the "unstable-static-class" feature, we'd have to make sure to weakly link the class if the deployment target is not high enough for it to always be available.

@madsmtm
Copy link
Owner Author

madsmtm commented Oct 3, 2024

Have been working on this recently, I think there is value in exposing just the runtime lookup functionality of this (i.e. an available! macro), and deferring the compile-time checks to later.

@madsmtm
Copy link
Owner Author

madsmtm commented Oct 3, 2024

Another idea would be to have a flag ASSUME_LOW_VERSION or something that the user can set, and which makes all availability checks return the deployment target + makes all APIs that require newer versions panic instead. That way, the user could still test their application for invalid assumptions about availability, without having the hardware to test it on.

@madsmtm
Copy link
Owner Author

madsmtm commented Oct 3, 2024

Yet another idea would be to develop this in rustc itself.

@BlackHoleFox
Copy link

Yet another idea would be to develop this in rustc itself.

I suggest that whenever the topic of OS version checking or availability come up in github.com/rust/, like before in rust-lang/rfcs#3379 (comment) (you also commented there). Maybe we should fork/revive that RFC in a smaller form? Would love to see std have an available! macro if nothing else that does the mix of static deployment target and runtime checking.

Had some experiments earlier in the year too looking at how possible it would be to duplicate the codegen LLVM injects into builds to handle dynamic checking at runtime via #available and nothing there looked unstable enough to be impossible...

@madsmtm
Copy link
Owner Author

madsmtm commented Oct 3, 2024

Yeah, I'm actually in the process of writing the available! macro right now that does static deployment target + runtime checking, see #661 for my current progress. And while doing this, it has indeed become apparent that such a macro really belongs in std.

Alongside that, I'm considering implementing a #[cfg(rustc_os_version(macos = 10.14.7, ios = ...))] attribute or similar to do the cfg-based checking of the deployment target. That way we can experiment with it in std first, before really figuring out the RFC that's needed to get it stable. I'll probably need to file an MCP for that first.

@BlackHoleFox
Copy link

Alongside that, I'm considering implementing a #[cfg(rustc_os_version(macos = 10.14.7, ios = ...))] attribute or similar

I'd try and keep rustc or cargo out of it tbh. You can set an env variable for rustc to read today but some of the proposals want to make this cargo configurable, so keeping the attribute agnostic might help it age better.

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

Successfully merging a pull request may close this issue.

2 participants