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

Umbrella framework handling #640

Open
mikolysz opened this issue Jul 15, 2024 · 6 comments
Open

Umbrella framework handling #640

mikolysz opened this issue Jul 15, 2024 · 6 comments
Labels
A-framework Affects the framework crates and the translator for them

Comments

@mikolysz
Copy link

Apple has the concept of "umbrella frameworks", frameworks that include other "sub-frameworks". objc2 doesn't currently handle these, no crates are provided for any umbrella framework or any sub-framework.

I'm trying to add a crate for AVFoundation, which is such an umbrella framework, so I've done some digging into how those work, here are my observations:

  1. Some Umbrella frameworks contain their own definitions and headers (example: AVFoundation) in addition to having sub-frameworks, others (example: CoreServices) only have a dummy header that includes all their sub-frameworks, providing no definitions of their own.
  2. Most sub-frameworks (example: MPSCore in MetalPerformanceShaders) are "interesting" to external developers and have their own headers, others (example: MPSBenchmarkLoop in MetalPerformanceShaders) have no headers at all and seem to be Apple-internal.
  3. Some sub-frameworks (example: HIServices in ApplicationServices) are actual folders that contain some data, others (example AVFAudio in AVFoundation) are actually top-level frameworks, in such cases, the sub-framework folder is just an alias / symlink pointing to the top-level framework.
  4. Apple's documentation is completely inconsistent as to where the documentation of sub-frameworks and sub-framework members is placed. For example, everything in Quartz is under /Quartz, with QuartzFilters being under /Quartz/QuartzFilter, NOT /Quartz/QuartzFilters. This is despite some Quartz sub-frameworks (like PDFKit) being aliases to top-level frameworks. Curiously enough, the PDFKit documentation can also be directly accessed under /pdfkit, and it seems to be the exact same documentation (this is not a redirect, the navigator changes to show the entirety of quartz when you're browsing under /quartz/pdfkit, but the content seems to be the same). Meanwhile, most AVFAudio topics can be browsed through both AVFoundation and AVFAudio, with the "speech synthesis" topic not being available in AVFAudio despite its sub-pages being under /avfaudio. To make things more confusing, when viewing any page related to speech synthesis, the navigator shown is for AVFoundation and not AVFAudio, despite the page being under /avfaudio. This doesn't seem to happen for any other audio-related pages, even if we navigate to them through the AVFoundation docs and not through AVFAudio, the navigator shown is for AVFAudio.
    Apple seems to be entirely confused here about what actually constitutes a sub-framework and what is a top-level framework in its own right, and so we can no longer rely on deriving the docs URL from the framework's name.

Here are the problems we'd need to handle to get these into objc2:

  1. Where should sub-frameworks be placed? The alternatives I see are separate crates for sub-frameworks, modules inside a crate, a flat hierarchy, or some combination of the two, like separate crates for "aliased" top-level frameworks and using the same crate for the frameworks that don't exist anywhere else. The last option seems the most sensible, though probably requiring the most work (I'm not familiar with the header translator's code so not sure about this).
  2. Should parent frameworks re-export all declarations from their children? This is what is done in Objective C, and I suggest to follow suit here.
  3. When generating the documentation of a framework crate, what URL should we provide? How can we make sure that the documentation for the speech stuff is discoverable? If we provide /avfaudio, that documentation isn't there, it's under /avfoundation. Then there's the problem of QuartzFilters actually being /quartz/quartzfilter. I think we need an optional URL field in translation-config.toml.
  4. How are sub-frameworks discovered, are they found and translated automatically or do they have to be manually specified? If the former, what should we do about completely headerless frameworks?
@madsmtm madsmtm added the A-framework Affects the framework crates and the translator for them label Sep 9, 2024
@madsmtm
Copy link
Owner

madsmtm commented Sep 9, 2024

I didn't get around to this issue before now, but thank you so much for the detailed investigation!

For completeness, I'll list all the umbrella frameworks in the macOS 14.5 SDK:

AVFoundation.framework/Frameworks:
AVFAudio.framework

Accelerate.framework/Frameworks:
vImage.framework        vecLib.framework

ApplicationServices.framework/Frameworks:
ATS.framework                   ColorSyncLegacy.framework       HIServices.framework            QD.framework
ATSUI.framework                 CoreGraphics.framework          ImageIO.framework               SpeechSynthesis.framework
ColorSync.framework             CoreText.framework              PrintCore.framework

Carbon.framework/Frameworks:
CommonPanels.framework          Help.framework                  Ink.framework                   SecurityHI.framework
HIToolbox.framework             ImageCapture.framework          OpenScripting.framework         SpeechRecognition.framework

CoreServices.framework/Frameworks:
AE.framework                    FSEvents.framework              OSServices.framework
CarbonCore.framework            LaunchServices.framework        SearchKit.framework
DictionaryServices.framework    Metadata.framework              SharedFileList.framework

MetalPerformanceShaders.framework/Frameworks:
MPSBenchmarkLoop.framework      MPSFunctions.framework          MPSMatrix.framework             MPSNeuralNetwork.framework
MPSCore.framework               MPSImage.framework              MPSNDArray.framework            MPSRayIntersector.framework

OpenDirectory.framework/Frameworks:
CFOpenDirectory.framework

Quartz.framework/Frameworks:
ImageKit.framework              QuartzComposer.framework        QuickLookUI.framework
PDFKit.framework                QuartzFilters.framework

WebKit.framework/Frameworks:
WebCore.framework       WebKitLegacy.framework  libWebKitSwift.tbd

And the ones in the iPhone 17.5 SDK:

AVFoundation.framework/Frameworks:
AVFAudio.framework

Accelerate.framework/Frameworks:
vImage.framework        vecLib.framework

AutomaticAssessmentConfiguration.framework/Frameworks:
AACClient.framework             AACDependencies.framework

MetalPerformanceShaders.framework/Frameworks:
MPSBenchmarkLoop.framework      MPSFunctions.framework          MPSMatrix.framework             MPSNeuralNetwork.framework
MPSCore.framework               MPSImage.framework              MPSNDArray.framework            MPSRayIntersector.framewor

@madsmtm
Copy link
Owner

madsmtm commented Sep 9, 2024

I think another interesting point is that the headers in sub-frameworks are not actually importable by themselves, i.e. you cannot do #import <MPSCore/MPSCommandBuffer.h>, you have to go through #import <MetalPerformanceShaders/MetalPerformanceShaders.h>. This makes their paths effectively "private", which means that we cannot depend on them when generating Cargo features.

One more point of interest is the actual linking, i.e. when you reference a static from a sub-framework, which framework does the dynamic linker load it from?

And here again, it seems like you can only link to the umbrella framework itself, the sub-frameworks are considered "private" in this sense (they do still exist in the eyes of dyld, but they are not stable, prime example being the location of AVFAudio changing from a subframework to a top-level framework over time).

@madsmtm
Copy link
Owner

madsmtm commented Sep 9, 2024

I think the reason that some of these are extra weird is because they pre-date the public introduction of their sub-framework.

E.g. AVFoundation existed for a long time, and contained AVFAudio internally, before that was introduced as a top-level framework. So I suspect there would be value in us having both objc-av-foundation and objc2-avf-audio, in case the latter begins getting functionality that the former decides to not expose. In most cases, you would want to actually use objc2-av-foundation, because then your code would be available to more users?

@madsmtm
Copy link
Owner

madsmtm commented Sep 9, 2024

So to answer your initially posed questions:

  1. Where should sub-frameworks be placed? The alternatives I see are separate crates for sub-frameworks, modules inside a crate, a flat hierarchy, or some combination of the two, like separate crates for "aliased" top-level frameworks and using the same crate for the frameworks that don't exist anywhere else. The last option seems the most sensible, though probably requiring the most work (I'm not familiar with the header translator's code so not sure about this).

I think I'd like it to be implemented as modules inside of the crate, to make the generated code easier to read, but the modules would be private and have a pub use my_module::*; if the top-level header "exposes" the sub-framework.

  1. Should parent frameworks re-export all declarations from their children? This is what is done in Objective C, and I suggest to follow suit here.

Yeah, I think that's the only viable option.

  1. When generating the documentation of a framework crate, what URL should we provide? How can we make sure that the documentation for the speech stuff is discoverable? If we provide /avfaudio, that documentation isn't there, it's under /avfoundation. Then there's the problem of QuartzFilters actually being /quartz/quartzfilter. I think we need an optional URL field in translation-config.toml.

I think it's possible for us to link to it as-if?

So e.g. AVAudioSession, if we choose to emit that as objc2_avf_audio::AVAudioSession, we'd link to /AVFAudio/AVAudioSession, but if we had emitted it in objc2_av_foundation::AVAudioSession, we'd link to /AVFoundation/AVAudioSession (which redirects to the former).

For QuartzFilters, since it's not a public framework, we would never generate a link to it. Instead, the types under it like QuartzFilterManager would be exposed as objc2_quartz::QuartzFilterManager, and we'd link to /Quartz/QuartzFilterManager.

Maybe I'm not understanding the question right?

  1. How are sub-frameworks discovered, are they found and translated automatically or do they have to be manually specified? If the former, what should we do about completely headerless frameworks?

The "proper" way to do anything like this at all is to read the .apinotes file, the location of these sub-frameworks is described there. libclang somewhat does it for us, though we have to backtrack to figure out if something is a sub-framework. It's a bit of a mess, but doable.

@madsmtm
Copy link
Owner

madsmtm commented Sep 9, 2024

Hmm, I think I was a bit wrong regarding AVFoundation, while it may have in the past included all the header files, nowadays the sub-framework is literally a symbolic link.

In any case, the hard part is going to be de-duplication. Both AVFoundation and AVFAudio "declares" (in C terms) the AVAudioPlayer, but we should only expose it as one type in our Rust bindings. So probably objc2_avf_audio::AVAudioPlayer, which is then re-exported in objc2_av_foundation.

But this comes with another complication: when using the types from AVFoundation, you don't actually want to link to AVFAudio, since that is not available on older platforms, and your code would fail to launch there. Swift's auto linking seems to be able to figure this out (e.g. it links to AVFoundation even if you only import and use AVFAudio).

@madsmtm
Copy link
Owner

madsmtm commented Sep 9, 2024

Swift's auto linking seems to be able to figure this out (e.g. it links to AVFoundation even if you only import and use AVFAudio).

Wait, that's actually done by the linker! So we don't have to do anything here, if we just write #[link(name = "AVFAudio", kind = "framework")], the linker will figure out based on the deployment target whether it actually wants to link to AVFAudio.

That does beg the question of what would happen if some symbol was only available in AVFAudio, but not in AVFoundation? But that isn't the case right now, and the .tdb files seem to indicate that AVFoundation is indeed re-exporting everything, so we'll deal with that if it becomes relevant.

tl;dr you can use AVFAudio, ColorSync or other such sub-frameworks that were promoted to top-level frameworks without worrying about backwards compat, the linker will figure it out!

(Of course this only works if the linker is actually configured with the correct deployment target, which it hasn't historically been, see rust-lang/rust#129369).

madsmtm added a commit that referenced this issue Dec 15, 2024
The exact semantics of Cargo features in umbrella frameworks are still
undecided, see #640.

So for now I've gone with the solution that required the least
code-changes, which is that each file still gets its own feature.
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
Projects
None yet
Development

No branches or pull requests

2 participants