objc2 icon indicating copy to clipboard operation
objc2 copied to clipboard

Inner value of `CWPHYMode` appears to change between integer and bitflags index between macOS versions

Open keabarnes opened this issue 1 year ago • 2 comments

In the code below, the inner value of CWPHYMode which is returned by interface.activePHYMode() differs based on the macOS version.

unsafe {
  let client = CWWiFiClient::sharedWiFiClient();

  let interface = client.interface().unwrap();

  let phy_mode = interface.activePHYMode();
}

From observation I've noticed that

  • on macOS 14.5 the inner value is represented by an integer that maps well to the const ... Self(n) constants on the enum
  • on macOS 15.0 it seems like the value represents the naive conversion from a bitflag to an integer
    • 802.11n is returned as CWPHYMode(16) but should be CWPHYMode(4)
    • 802.11ax is returned as CWPHYMode(64) but should be CWPHYMode(6)

Cargo.toml for context

objc2 = { version = "0.5", features = ["apple"] }
objc2-foundation = { version = "0.2", features = ["apple"] }
objc2-core-wlan = { version = "0.2", features = [
  "CWInterface",
  "CWNetwork",
  "CWWiFiClient",
  "CWChannel",
  "CoreWLANTypes",
] }

keabarnes avatar Oct 14 '24 18:10 keabarnes

Huh. This is interesting. I ran the example code. I'm on macOS 15 with an 802.11ac network. I get a CWPHYMode(32) when I presume it should be CWPHYMode(5). I get the same incorrect result mentioned above with an 802.11n network.

The mapping looks like:

  • CWPHYMode(4) is returned as CWPHYMode(16)
  • CWPHYMode(5) is returned as CWPHYMode(32)
  • CWPHYMode(6) is returned as CWPHYMode(64).

Looks like the pattern is a bitmask rather than an integer?

simlay avatar Oct 16 '24 04:10 simlay

I'm still on macOS 14.7, and can't test this in a virtual machine (since the virtual machines don't have WiFi), so could I get one of you to test the following:

  1. RUSTFLAGS="-Clinker-flavor=ld" cargo run (in case this is dependent on the minos/sdk version in the binary)
  2. The equivalent Swift code:
    import CoreWLAN
    
    let client = CWWiFiClient.shared()
    let interface = client.interface()!
    let phy_mode = interface.activePHYMode()
    print(phy_mode.rawValue)
    

madsmtm avatar Oct 21 '24 15:10 madsmtm

Sorry for the delay in my response, after running all the variants you asked for above, I only realized that the output had been fixed. I'm not sure if it was in this repo or the underlying framework, but it seems right now right?

I'd appreciate understanding if it was the underlying macOS version so I know to expect incorrect values from a specific version of the OS or of this crate, if you know? I'm now on macOS 15.1.1.

fn main() {
    use objc2_core_wlan::CWWiFiClient;

    unsafe {
        let client = CWWiFiClient::sharedWiFiClient();

        let interface = client.interface().unwrap();

        let phy_mode = interface.activePHYMode();

        println!("{:?}", phy_mode);
    }
}

Without flag:

❯ cargo run --example madsmtm_objc2_issues_662

Compiling ...
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1m 56s
Running `target/debug/examples/madsmtm_objc2_issues_662`

CWPHYMode(6)

With flag:

❯ RUSTFLAGS="-Clinker-flavor=ld" cargo run --example madsmtm_objc2_issues_662

Compiling ...    
Finished `dev` profile [unoptimized + debuginfo] target(s) in 3m 27s
Running `target/debug/examples/madsmtm_objc2_issues_662`

CWPHYMode(6)

Swift:

import CoreWLAN

let client = CWWiFiClient.shared()
let interface = client.interface()!
let phy_mode = interface.activePHYMode()
print(phy_mode.rawValue)
6
Program ended with exit code: 0

keabarnes avatar Jan 07 '25 13:01 keabarnes

Running above Rust code snippet again on macOS 15.0.1 with the same version of objc2-* results in the original CWPHYMode(64) issue, regardless of wether the example was run with the RUSTFLAGS set or not. It seems like this is the an issue with the underlying OS framework, do you have precedent in the codebase to handle things in a specific way given a specific version of the OS?

keabarnes avatar Jan 07 '25 14:01 keabarnes

Running above Rust code snippet again on macOS 15.0.1 with the same version of objc2-* results in the original CWPHYMode(64) issue, regardless of wether the example was run with the RUSTFLAGS set or not.

Damn! Could you check macOS 15.0 and macOS 15.1 too, just to be sure of the full range of (non-beta) versions that this happens on?

It seems like this is the an issue with the underlying OS framework, do you have precedent in the codebase to handle things in a specific way given a specific version of the OS?

Hmm, not really, especially not something like this. But I think you'd be able to handle it with something like:

  1. Modify objc2-core-wlan/translation-config.toml to contain class.CWInterface.methods.activePHYMode.skipped = true, and re-run the header translator (if it's difficult for you to set up, then I can do that part myself).
  2. Add a file that contains something like:
    impl CWInterface {
        #[cfg(feature = "CoreWLANTypes")]
        fn activePHYMode(&self) -> CWPHYMode {
            let inner: CWPHYMode = unsafe { msg_send![self, activePHYMode] };
            // These versions had a bug [explanation]
            if available!(macos = 15.0) && !available!(macos = 15.1) {
                CWPHYMode(inner.0 >> 2)
            } else {
                inner
            }
        }
    }
    

If it affects CWNetwork::supportsPHYMode too, then something similar should be done there as well.

madsmtm avatar Jan 07 '25 17:01 madsmtm

Are you still interested in this @keabarnes?

Personally, I'm leaning towards "do nothing" here, since this was fairly clearly a bug in Apple's code which has since been fixed. I'd rather recommend that if you do want to support those buggy versions of macOS, that you do the conversion in your own code. Again, it can be done with just the available! macro:

use objc2_core_wlan::{CWPHYMode, CWInterface};
use objc2::available;

fn fixed_activePHYMode(interface: &CWInterface) -> CWPHYMode {
    let inner = unsafe { interface.activePHYMode() };
    // These versions had a bug [explanation]
    if available!(macos = 15.0) && !available!(macos = 15.1) {
        CWPHYMode(inner.0 >> 2)
    } else {
        inner
    }
}

madsmtm avatar Apr 19 '25 19:04 madsmtm

@madsmtm sorry I dropped off here.

I'm happy to proceed either way, at the time I implemented something similar to what you've suggested, but without using the available! macro. I'll go ahead and change to that now as it's definitely cleaner.

My personal opinion is that this codebase is the logical owner of that kind of code, the objc2 crate should know about these nuances with the bindings to the underlying versions of macOS and handle it under the hood so the exposed types can be trusted as correct. Each person using CWPHYMode (and future instances of this issue) won't know that it has this issue, will need to figure it out and then implement the fix (likely only after finding this issue or the example).

keabarnes avatar Nov 05 '25 20:11 keabarnes

sorry I dropped off here.

No problem!

Each person using CWPHYMode (and future instances of this issue) won't know that it has this issue, will need to figure it out and then implement the fix (likely only after finding this issue or the example).

For this specific issue, for me to change this in objc2-core-wlan, I'd need someone to test this on macOS 15.0 and macOS 15.1 as well, to be sure that this fix doesn't introduce a new bug (e.g. if the bug was not fixed in macOS 15.1 but only in macOS 15.1.1, the above code is wrong). Even worse, because this specific issue can't be tested in a VM, if I were to investigate it I'd have to reinstall macOS. That'd be a huge time sink for me.

My personal opinion is that this codebase is the logical owner of that kind of code, the objc2 crate should know about these nuances with the bindings to the underlying versions of macOS and handle it under the hood so the exposed types can be trusted as correct.

More generally on the "policy" question here:

I agree that it would be nice if we could magically fix bugs like these in Apple's APIs. However, this repo has literally tens of thousands of APIs, and the only way that is in any way maintainable is with a very high degree of auto-generation. Every manually written API carries a risk, both in correctness (as discussed above), and when evolving the codebase (e.g. if I fixed #309, I'd have to manually go and update the fixed method's API docs).

A nuance here is that it's often very easy for users to upgrade to the next minor/patch release of macOS, and often harder to upgrade the "major" version (macOS 10.12, 10.15, 12, 26, etc.). So if I were to write a policy on this, maybe it could be that the project only officially supports the latest version in each major line of OS versions? I'd be much more amenable to patching this if the bug existed in e.g. macOS 13 and 14, but was fixed in macOS 15.

Another (smaller) problem is that it's hard to define where the boundary lies. At the extreme end of the spectrum, if there's a bug in AppKit's auto layout algorithm, do we bundle our own layout algorithm, and re-implement a lot of NSView's logic? In that sense, it's much easier for me to say "this project has the same amount of bugs as calling Apple's APIs from Objective-C would have" (which, we're not even there yet, if we were, the issue list would be empty ;) ).

All of this is in the end influenced by an estimated seriousness of the bug, vs. the risk it introduces, and the time/resources available to me: I suspect that the amount of people that will be hit by this specific issue is quite small, especially at this point, and that amount grows smaller over time as people upgrade their OS. But if a more widely used API like say -[NSArray new] had a serious bug, I'd be more inclined to carry a fix for it.

Does my reasoning here make sense? If not, I'd love to clarify.

madsmtm avatar Nov 06 '25 19:11 madsmtm

@madsmtm your reasoning makes total sense. My comment was in the ideal world, and even now, I'm not convinced of my argument given what you've said. I agree in reality this won't be an issue often enough, and that's good enough to ignore the "consumer won't reasonably know about this" feeling.

keabarnes avatar Nov 17 '25 07:11 keabarnes