diff options
Diffstat (limited to 'widget/cocoa/docs')
-rw-r--r-- | widget/cocoa/docs/index.md | 11 | ||||
-rw-r--r-- | widget/cocoa/docs/macos-apis.md | 187 | ||||
-rw-r--r-- | widget/cocoa/docs/sdks.md | 240 |
3 files changed, 438 insertions, 0 deletions
diff --git a/widget/cocoa/docs/index.md b/widget/cocoa/docs/index.md new file mode 100644 index 0000000000..46c3da066d --- /dev/null +++ b/widget/cocoa/docs/index.md @@ -0,0 +1,11 @@ +# Firefox on macOS
+
+```eval_rst
+.. toctree::
+ :titlesonly:
+ :maxdepth: 1
+ :glob:
+
+ *
+
+```
diff --git a/widget/cocoa/docs/macos-apis.md b/widget/cocoa/docs/macos-apis.md new file mode 100644 index 0000000000..f1f530aeea --- /dev/null +++ b/widget/cocoa/docs/macos-apis.md @@ -0,0 +1,187 @@ +# Using macOS APIs + +With each new macOS release, new APIs are added. Due to the wide range of platforms that Firefox runs on, +and due to the [wide range of SDKs that we support building with](./sdks.html#supported-sdks), +using macOS APIs in Firefox requires some extra care. + +## Availability of APIs, and runtime checks + +First of all, if you use an API that is supported by all versions of macOS that Firefox runs on, +i.e. 10.9 and above, then you don't need to worry about anything: +The API declaration will be present in any of the supported SDKs, and you don't need any runtime checks. + +If you want to use a macOS API that was added after 10.9, then you have to have a runtime check. +This requirement is completely independent of what SDK is being used for building. + +The runtime check [should have the following form](https://developer.apple.com/documentation/macos_release_notes/macos_mojave_10_14_release_notes/appkit_release_notes_for_macos_10_14?language=objc#3014609) +(replace `10.14` with the appropriate version): + +```objc++ +if (@available(macOS 10.14, *)) { + // Code for macOS 10.14 or later +} else { + // Code for versions earlier than 10.14. +} +``` + +`@available` guards can be used in Objective-C(++) code. +(In C++ code, you can use [these `nsCocoaFeatures` methods](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/widget/cocoa/nsCocoaFeatures.h#21-27) instead.) + +For each API, the API declarations in the SDK headers are annotated with `API_AVAILABLE` macros. +For example, the definition of the `NSVisualEffectMaterial` enum looks like this: + +```objc++ +typedef NS_ENUM(NSInteger, NSVisualEffectMaterial) { + NSVisualEffectMaterialTitlebar = 3, + NSVisualEffectMaterialSelection = 4, + NSVisualEffectMaterialMenu API_AVAILABLE(macos(10.11)) = 5, + // [...] + NSVisualEffectMaterialSheet API_AVAILABLE(macos(10.14)) = 11, + // [...] +} API_AVAILABLE(macos(10.10)); +``` + +The compiler understands these annotations and makes sure that you wrap all uses of the annotated APIs +in appropriate `@available` runtime checks. + +### Frameworks + +In some rare cases, you need functionality from frameworks that are not available on all supported macOS versions. +Examples of this are `Metal.framework` (added in 10.11) and `MediaPlayer.framework` (added in 10.12.2). + +In that case, you can either `dlopen` your framework at runtime ([like we do for MediaPlayer](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/widget/cocoa/MediaPlayerWrapper.mm#21-27)), +or you can [use `-weak_framework`](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/WeakLinking.html#//apple_ref/doc/uid/20002378-107026) +[like we do for Metal](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/toolkit/library/moz.build#301-304): + +```python +if CONFIG['OS_ARCH'] == 'Darwin': + OS_LIBS += [ + # Link to Metal as required by the Metal gfx-hal backend + '-weak_framework Metal', + ] +``` + +## Using new APIs with old SDKs + +If you want to use an API that was introduced after 10.12, you now have one extra thing to worry about. +In addition to the runtime check [described in the previous section](#using-macos-apis), you also +have to jump through extra hoops in order to allow the build to succeed with older SDKs, because +[we need to support building Firefox with SDK versions all the way down to the 10.12 SDK](./sdks.html#supported-sdks). + +In order to make the compiler accept your code, you will need to copy some amount of the API declaration +into your own code. Copy it from the newest recent SDK you can get your hands on. +The exact procedure varies based on the type of API (enum, objc class, method, etc.), +but the general approach looks like this: + +```objc++ +#if !defined(MAC_OS_X_VERSION_10_12) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12 +@interface NSWindow (AutomaticWindowTabbing) +@property (class) BOOL allowsAutomaticWindowTabbing API_AVAILABLE(macos(10.12)); +@end +#endif +``` + +See the [Supporting Multiple SDKs](./sdks.html#supporting-multiple-sdks) docs for more information on the `MAC_OS_X_VERSION_MAX_ALLOWED` macro. + +Keep these three things in mind: + + - Copy only what you need. + - Wrap your declaration in `MAC_OS_X_VERSION_MAX_ALLOWED` checks so that, if an SDK is used that + already contains these declarations, your declaration does not conflict with the declaration in the SDK. + - Include the `API_AVAILABLE` annotations so that the compiler can protect you from accidentally + calling the API on unsupported macOS versions. + +Our current code does not always follow the `API_AVAILABLE` advice, but it should. + +### Enum types and C structs + +If you need a new enum type or C struct, copy the entire type declaration and wrap it in the appropriate ifdefs. Example: + +```objc++ +#if !defined(MAC_OS_X_VERSION_10_12_2) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12_2 +typedef NS_ENUM(NSUInteger, MPNowPlayingPlaybackState) { + MPNowPlayingPlaybackStateUnknown = 0, + MPNowPlayingPlaybackStatePlaying, + MPNowPlayingPlaybackStatePaused, + MPNowPlayingPlaybackStateStopped, + MPNowPlayingPlaybackStateInterrupted +} MP_API(ios(11.0), tvos(11.0), macos(10.12.2), watchos(5.0)); +#endif +``` +### New enum values for existing enum type + +If the enum type itself already exists, but gained a new value, define the value in an unnamed enum: + +```objc++ +#if !defined(MAC_OS_X_VERSION_10_12) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12 +enum { NSVisualEffectMaterialSelection = 4 }; +#endif +``` + +(This is an example of an interesting case: `NSVisualEffectMaterialSelection` is available starting with +macOS 10.10, but it's only defined in SDKs starting with the 10.12 SDK.) + +### Objective-C classes + +For a new Objective-C class, copy the entire `@interface` declaration and wrap it in the appropriate ifdefs. + +I haven't personally tested this. If this does not compile (or maybe link?), you can use the following workaround: + + - Define your methods and properties as a category on `NSObject`. + - Look up the class at runtime using `NSClassFromString()`. + - If you need to create a subclass, do it at runtime using `objc_allocateClassPair` and `class_addMethod`. + [Here's an example of that.](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/widget/cocoa/VibrancyManager.mm#44-60) + +### Objective-C properties and methods on an existing class + +If an Objective-C class that already exists gains a new method or property, you can "add" it to the +existing class declaration with the help of a [category](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html): + +```objc++ +@interface ExistingClass (YourMadeUpCategoryName) +// methods and properties here +@end +``` + +### Functions + +With free-standing functions I'm not entirely sure what to do. +In theory, copying the declarations from the new SDK headers should work. Example: + +```objc++ +extern "C" { + __attribute__((warn_unused_result)) bool +SecTrustEvaluateWithError(SecTrustRef trust, CFErrorRef _Nullable * _Nullable CF_RETURNS_RETAINED error) + API_AVAILABLE(macos(10.14), ios(12.0), tvos(12.0), watchos(5.0)); + + __nullable +CFDataRef SecCertificateCopyNormalizedSubjectSequence(SecCertificateRef certificate) + __OSX_AVAILABLE_STARTING(__MAC_10_12_4, __IPHONE_10_3); +} +``` + +I'm not sure what the linker or the dynamic linker do when the symbol is not available. +Does this require [`__attribute__((weak_import))` annotations](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Concepts/WeakLinking.html#//apple_ref/doc/uid/20002378-107262-CJBJAEID)? + +And maybe this is where .tbd files in the SDK come in? So that the linker knows which symbols to allow? +So then that part cannot be worked around by copying code from headers. + +Anyway, what always works is the pure runtime approach: + + 1. Define types for the functions you need, but not the functions themselves. + 2. At runtime, look up the functions using `dlsym`. + +## Notes on Rust + +If you call macOS APIs from Rust code, you're kind of on your own. Apple does not provide any Rust +"headers", so there isn't really an SDK to speak of. So you have to supply your own API declarations +anyway, regardless of what SDK is being used for building. + +In a way, you're side-stepping some of the build time trouble. You don't need to worry about any +`#ifdefs` because there are no system headers you could conflict with. + +On the other hand, you still need to worry about API availability at runtime. +And in Rust, there are no [availability attributes](https://clang.llvm.org/docs/AttributeReference.html#availability) +on your API declarations, and there are no +[`@available` runtime check helpers](https://clang.llvm.org/docs/LanguageExtensions.html#objective-c-available), +and the compiler cannot warn you if you call APIs outside of availability checks. diff --git a/widget/cocoa/docs/sdks.md b/widget/cocoa/docs/sdks.md new file mode 100644 index 0000000000..d705b4fb52 --- /dev/null +++ b/widget/cocoa/docs/sdks.md @@ -0,0 +1,240 @@ +# A primer on macOS SDKs
+
+## Overview
+
+A macOS SDK is an on-disk directory that contains header files and meta information for macOS APIs.
+Apple distributes SDKs as part of the Xcode app bundle. Each Xcode version comes with one macOS SDK,
+the SDK for the most recent released version of macOS at the time of the Xcode release.
+The SDK is located at `/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk`.
+
+Compiling Firefox for macOS requires a macOS SDK. The build system uses the SDK from Xcode.app by
+default, and you can select a different SDK using the `mozconfig` option `--with-macos-sdk`:
+
+```text
+ac_add_options --with-macos-sdk=/Users/username/SDKs/MacOSX10.12.sdk
+```
+
+## Supported SDKs
+
+First off, Firefox runs on 10.9 and above. This is called the "minimum deployment target" and is
+independent of the SDK version.
+
+Our official Firefox builds compiled in CI (continuous integration) currently use the 10.12 SDK.
+[Bug 1475652](https://bugzilla.mozilla.org/show_bug.cgi?id=1475652) tracks updating this SDK.
+
+For local builds, all SDKs from 10.12 to 10.15 are supported. Firefox should compile successfully
+with all of those SDKs, but minor differences in runtime behavior can occur.
+
+However, since only the 10.12 SDK is used in CI, compiling with different SDKs breaks from time to time.
+Such breakages should be [reported in Bugzilla](https://bugzilla.mozilla.org/enter_bug.cgi?blocked=mach-busted&bug_type=defect&cc=:spohl,:mstange&component=General&form_name=enter_bug&keywords=regression&op_sys=macOS&product=Firefox%20Build%20System&rep_platform=All) and fixed quickly.
+
+Aside: Firefox seems to be a bit of a special snowflake with its ability to build with an arbitrary SDK.
+For example, at the time of this writing (June 2020),
+[building Chrome requires the 10.15 SDK](https://chromium.googlesource.com/chromium/src/+/master/docs/mac_build_instructions.md#system-requirements).
+Some apps even require a certain version of Xcode and only support building with the SDK of that Xcode version.
+
+Why are we using such an old SDK in CI, you ask? It basically comes down to the fact that macOS
+hardware is expensive, and the fact that the compilers and linkers supplied by Xcode don't run on Linux.
+
+## Obtaining SDKs
+
+Sometimes you need an SDK that's different from the one in your Xcode.app, for example
+to check whether your code change breaks building with other SDKs, or to verify the
+runtime behavior with the SDK used for CI builds.
+
+The easy but slightly questionable way to obtain an SDK is to download it from a public github repo.
+
+Here's another option:
+
+ 1. Have your Apple ID login details ready, and bring enough time and patience for a 5GB download.
+ 2. Check [these tables in the Xcode wikipedia article](https://en.wikipedia.org/wiki/Xcode#Xcode_7.0_-_10.x_(since_Free_On-Device_Development))
+ and find an Xcode version that contains the SDK you need.
+ 3. Look up the Xcode version number on [xcodereleases.com](https://xcodereleases.com/) and click the Download link for it.
+ 4. Log in with your Apple ID. Then the download should start.
+ 5. Wait for the 5GB Xcode_*.xip download to finish.
+ 6. Open the downloaded xip file. This will extract the Xcode.app bundle.
+ 7. Inside the app bundle, the SDK is at `Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk`.
+
+## Effects of the SDK version
+
+An SDK only contains declarations of APIs. It does not contain the implementations for these APIs.
+
+The implementation of an API is provided by the OS that the app runs on. It is supplied at runtime,
+when your app starts up, by the dynamic linker. For example, the AppKit implementation comes
+from `/System/Library/Frameworks/AppKit.framework` from the OS that the app is run on, regardless
+of what SDK was used when compiling the app.
+
+In other words, building with a macOS SDK of a higher version doesn't magically make new APIs available
+when running on older versions of macOS. And, conversely, building with a lower macOS SDK doesn't limit
+which APIs you can use if your app is run on a newer version of macOS, assuming you manage to convince the
+compiler to accept your code.
+
+The SDK used for building an app determines three things:
+
+ 1. Whether your code compiles at all,
+ 2. which range of macOS versions your app can run on (available deployment targets), and
+ 3. certain aspects of runtime behavior.
+
+The first is straightforward: An SDK contains header files. If you call an API that's not declared
+anywhere - neither in a header file nor in your own code - then your compiler will emit an error.
+(Special case: Calling an unknown Objective-C method usually only emits a warning, not an error.)
+
+The second aspect, available deployment targets, is usually not worth worrying about:
+SDKs have large ranges of supported macOS deployment targets.
+For example, the 10.15 SDK supports running your app on macOS versions all the way back to 10.6.
+This information is written down in the SDK's `SDKSettings.plist`.
+
+The third aspect, varying runtime behavior, is perhaps the most insidious and surprising aspect, and is described
+in the next section.
+
+## Runtime differences based on macOS SDK version
+
+When a new version of macOS is released, existing APIs can change their behavior.
+These changes are usually described in the AppKit release notes:
+
+ - [macOS 10.15 release notes](https://developer.apple.com/documentation/macos_release_notes/macos_catalina_10_15_release_notes?language=objc)
+ - [macOS 10.14 AppKit release notes](https://developer.apple.com/documentation/macos_release_notes/macos_mojave_10_14_release_notes/appkit_release_notes_for_macos_10_14?language=objc)
+ - [macOS 10.13 AppKit release notes](https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKit/)
+ - [macOS 10.12 and older AppKit release notes](https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKitOlderNotes/)
+
+Sometimes, these differences in behavior have the potential to break existing apps. In those instances,
+Apple often provides the old (compatible) behavior until the app is re-built with the new SDK, expecting
+developers to update their apps so that they work with the new behavior, at the same time as
+they update to the new SDK.
+
+Here's an [example from the 10.13 release notes](https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKit/#10_13NSCollectionView%20Responsive%20Scrolling):
+
+> Responsive Scrolling in NSCollectionViews is enabled only for apps linked on or after macOS 10.13.
+
+Here, "linked on or after macOS 10.13" means "linked against the macOS 10.13 SDK or newer".
+
+Apple's expectation is that you upgrade to the new macOS version when it is released, download a new
+Xcode version when it is released, synchronize these updates across the machines of all developers
+that work on your app, use the SDK in the newest Xcode to compile your app, and make changes to your
+app to be compatible with any behavior changes whenever you update Xcode.
+This expectation does not always match reality. It definitely doesn't match what we're doing with Firefox.
+
+For Firefox, SDK-dependent compatibility behaviors mean that developers who build Firefox locally
+can see different runtime behavior than the users of our CI builds, if they use a different SDK than
+the SDK used in CI.
+That is, unless we change the Firefox code so that it has the same behavior regardless of SDK version.
+Often this can be achieved by using APIs in a way that's more in line with the API's recommended use.
+
+For example, we've had cases of
+[broken placeholder text in search fields](https://bugzilla.mozilla.org/show_bug.cgi?id=1273106),
+[missing](https://bugzilla.mozilla.org/show_bug.cgi?id=941325) or [double-drawn focus rings](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/widget/cocoa/nsNativeThemeCocoa.mm#149-169),
+[a startup crash](https://bugzilla.mozilla.org/show_bug.cgi?id=1516437),
+[fully black windows](https://bugzilla.mozilla.org/show_bug.cgi?id=1494022),
+[fully gray windows](https://bugzilla.mozilla.org/show_bug.cgi?id=1576113#c4),
+[broken vibrancy](https://bugzilla.mozilla.org/show_bug.cgi?id=1475694), and
+[broken colors in dark mode](https://bugzilla.mozilla.org/show_bug.cgi?id=1578917).
+
+In most of these cases, the breakage was either very minor, or it was caused by Firefox doing things
+that were explicitly discouraged, like creating unexpected NSView hierarchies, or relying on unspecified
+implementation details. (With one exception: In 10.14, HiDPI-aware `NSOpenGLContext` rendering in
+layer-backed windows simply broke.)
+
+And in all of these cases, it was the SDK-dependent compatibility behavior that protected our users from being
+exposed to the breakage. Our CI builds continued to work because they were built with an older SDK.
+
+We have addressed all known cases of breakage when building Firefox with newer SDKs.
+I am not aware of any current instances of this problem as of this writing (June 2020).
+
+For more information about how these compatibility tricks work,
+read the [Overriding SDK-dependent runtime behavior](#overriding-sdk-dependent-runtime-behavior) section.
+
+## Supporting multiple SDKs
+
+As described under [Supported SDKs](#supported-sdks), Firefox can be built with a wide variety of SDK versions.
+
+This ability comes at the cost of some manual labor; it requires some well-placed `#ifdefs` and
+copying of header definitions.
+
+Every SDK defines the macro `MAC_OS_X_VERSION_MAX_ALLOWED` with a value that matches the SDK version,
+in the SDK's `AvailabilityMacros.h` header. This header also defines version constants like `MAC_OS_X_VERSION_10_12`.
+For example, I have a version of the 10.12 SDK which contains the line
+
+```cpp
+#define MAC_OS_X_VERSION_MAX_ALLOWED MAC_OS_X_VERSION_10_12_4
+```
+
+The name `MAC_OS_X_VERSION_MAX_ALLOWED` is rather misleading; a better name would be
+`MAC_OS_X_VERSION_MAX_KNOWN_BY_SDK`. Compiling with an old SDK *does not* prevent apps from running
+on newer versions of macOS.
+
+With the help of the `MAC_OS_X_VERSION_MAX_ALLOWED` macro, we can make our code adapt to the SDK that's
+being used. Here's [an example](https://searchfox.org/mozilla-central/rev/9ad88f80aeedcd3cd7d7f63be07f577861727054/toolkit/xre/MacApplicationDelegate.mm#345-351) where the 10.14 SDK changed the signature of
+[an `NSApplicationDelegate` method](https://developer.apple.com/documentation/appkit/nsapplicationdelegate/1428471-application?language=objc):
+
+```objc++
+- (BOOL)application:(NSApplication*)application
+ continueUserActivity:(NSUserActivity*)userActivity
+#if defined(MAC_OS_X_VERSION_10_14) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_14
+ restorationHandler:(void (^)(NSArray<id<NSUserActivityRestoring>>*))restorationHandler {
+#else
+ restorationHandler:(void (^)(NSArray*))restorationHandler {
+#endif
+ ...
+}
+```
+
+We can also use this macro to supply missing API definitions in such a way that
+they don't conflict with the definitions from the SDK.
+This is described in the "Using macOS APIs" document, under [Using new APIs with old SDKs](./macos-apis.html#using-new-apis-with-old-sdks).
+
+## Overriding SDK-dependent runtime behavior
+
+This section contains some more details on the compatibility tricks that cause different runtime
+behavior dependent on the SDK, as described in
+[Runtime differences based on macOS SDK version](#runtime-differences-based-on-macos-sdk-version).
+
+### How it works
+
+AppKit is the one system framework I know of that employs these tricks. Let's explore how AppKit makes this work,
+by going back to the [NSCollectionView example](https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKit/#10_13NSCollectionView%20Responsive%20Scrolling) from above:
+
+> Responsive Scrolling in NSCollectionViews is enabled only for apps linked on or after macOS 10.13.
+
+For each of these SDK-dependent behavior differences, both the old and the new behavior are implemented
+in the version of AppKit that ships with the new macOS version.
+At runtime, AppKit selects one of the behaviors based on the SDK version, with a call to
+`_CFExecutableLinkedOnOrAfter()`. This call checks the SDK version of the main executable of the
+process that's running AppKit code; in our case that's the `firefox` or `plugin-container` executable.
+The SDK version is stored in the mach-o headers of the executable by the linker.
+
+One interesting design aspect of AppKit's compatibility tricks is the fact that most of these behavior differences
+can be toggled with a "user default" preference.
+For example, the "responsive scrolling in NSCollectionViews" behavior change can be controlled with
+a user default with the name "NSCollectionViewPrefetchingEnabled".
+The SDK check only happens if "NSCollectionViewPrefetchingEnabled" is not set to either YES or NO.
+
+More precisely, this example works as follows:
+
+ - `-[NSCollectionView prepareContentInRect:]` is the function that supports both the old and the new behavior.
+ - It calls `_NSGetBoolAppConfig` for the value "NSCollectionViewPrefetchingEnabled", and also supplies a "default
+ value function".
+ - If the user default is not set, the default value function is called. This function has the name
+ `NSCollectionViewPrefetchingEnabledDefaultValueFunction`.
+ - `NSCollectionViewPrefetchingEnabledDefaultValueFunction` calls `_CFExecutableLinkedOnOrAfter(13)`.
+
+You can find many similar toggles if you list the AppKit symbols that end in `DefaultValueFunction`,
+for example by executing `nm /System/Library/Frameworks/AppKit.framework/AppKit | grep DefaultValueFunction`.
+
+### Overriding SDK-dependent runtime behavior
+
+You can set these preferences programmatically, in a way that `_NSGetBoolAppConfig()` can pick them up,
+for example with [`registerDefaults`](https://developer.apple.com/documentation/foundation/nsuserdefaults/1417065-registerdefaults?language=objc)
+or like this:
+
+```objc++
+[[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"NSViewAllowsRootLayerBacking"];
+```
+
+The AppKit release notes mention this ability but ask for it to only be used for debugging purposes:
+
+> In some cases, we provide defaults (preferences) settings which can be used to get the old or new behavior,
+> independent of what system an application was built against. Often these preferences are provided for
+> debugging purposes only; in some cases the preferences can be used to globally modify the behavior
+> of an application by registering the values (do it somewhere very early, with `-[NSUserDefaults registerDefaults:]`).
+
+It's interesting that they mention this at all because, as far as I can tell, none of these values are documented.
|