使用 macOS API

随着每个新 macOS 版本的发布,都会添加新的 API。由于 Firefox 运行在各种平台上,并且由于我们支持使用各种 SDK 进行构建,因此在 Firefox 中使用 macOS API 需要格外小心。

API 的可用性和运行时检查

首先,如果您使用的是所有 Firefox 运行的 macOS 版本(即 10.15 及更高版本)都支持的 API,那么您无需担心任何事情:API 声明将存在于所有支持的 SDK 中,并且您无需进行任何运行时检查。

如果您想使用在 10.15 之后添加的 macOS API,则必须进行运行时检查。此要求与用于构建的 SDK 完全无关。

运行时检查应具有以下形式(将11.0替换为适当的版本)

if (@available(macOS 11.0, *)) {
    // Code for macOS 11.0 or later
} else {
    // Code for versions earlier than 11.0.
}

@available 保护符可用于 Objective-C(++) 代码。(在 C++ 代码中,您可以使用这些nsCocoaFeatures 方法。)

对于每个 API,SDK 头文件中的 API 声明都用API_AVAILABLE 宏进行注释。例如,NSVisualEffectMaterial 枚举的定义如下所示

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));

编译器理解这些注释,并确保您将所有注释 API 的用法包装在适当的@available 运行时检查中。

框架

在某些极少数情况下,您需要来自并非所有受支持的 macOS 版本都可用的框架的功能。例如,Metal.framework(在 10.11 中添加)和MediaPlayer.framework(在 10.12.2 中添加)。

在这种情况下,您可以选择在运行时dlopen 您的框架(就像我们对 MediaPlayer 所做的那样),或者您可以使用-weak_framework就像我们对 Metal 所做的那样

if CONFIG['OS_ARCH'] == 'Darwin':
    OS_LIBS += [
        # Link to Metal as required by the Metal gfx-hal backend
        '-weak_framework Metal',
    ]

使用新 API 与旧 SDK

如果您想使用在 10.15 之后引入的 API,您现在需要多注意一件事。除了上一节中描述的运行时检查之外,您还必须跳过额外的障碍才能使构建成功,因为为了使 Firefox 能够在 macOS 10.15 及更高版本上运行,Firefox 的构建目标必须保持在 10.15

为了使编译器接受您的代码,您需要将一些 API 声明复制到您自己的代码中。从您能找到的最新 SDK 中复制它。确切的步骤因 API 类型(枚举、objc 类、方法等)而异,但总体方法如下所示

#if !defined(MAC_OS_VERSION_12_0) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_VERSION_12_0
@interface NSScreen (NSScreen12_0)
// https://developer.apple.com/documentation/appkit/nsscreen/3882821-safeareainsets?language=objc&changes=latest_major
@property(readonly) NSEdgeInsets safeAreaInsets;
@end
#endif

有关MAC_OS_X_VERSION_MAX_ALLOWED 宏的更多信息,请参阅支持多个 SDK 文档。

请牢记以下三点

  • 仅复制您需要的内容。

  • 将您的声明包装在MAC_OS_X_VERSION_MAX_ALLOWED 检查中,以便如果使用已经包含这些声明的 SDK,您的声明不会与 SDK 中的声明冲突。

  • 包含API_AVAILABLE 注释,以便编译器可以防止您意外地在不受支持的 macOS 版本上调用 API。

我们当前的代码并不总是遵循API_AVAILABLE 建议,但应该这样做。

枚举类型和 C 结构体

如果您需要新的枚举类型或 C 结构体,请复制整个类型声明并将其包装在适当的 ifdefs 中。示例

#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

现有枚举类型的新的枚举值

如果枚举类型本身已存在,但获得了新的值,请在未命名的枚举中定义该值

#if !defined(MAC_OS_X_VERSION_10_12) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_X_VERSION_10_12
enum { NSVisualEffectMaterialSelection = 4 };
#endif

(这是一个有趣的案例示例:NSVisualEffectMaterialSelection 从 macOS 10.10 开始可用,但仅在从 10.12 SDK 开始的 SDK 中定义。)

Objective-C 类

对于新的 Objective-C 类,请复制整个@interface 声明并将其包装在适当的 ifdefs 中。

我个人没有测试过这一点。如果这无法编译(或者可能链接?),您可以使用以下解决方法

  • 将您的方法和属性定义为NSObject 上的类别。

  • 使用NSClassFromString() 在运行时查找类。

  • 如果您需要创建子类,请使用objc_allocateClassPairclass_addMethod 在运行时进行。 这是一个示例。

现有类上的 Objective-C 属性和方法

如果已存在的 Objective-C 类获得了新的方法或属性,您可以借助类别将其“添加到”现有类声明中

@interface ExistingClass (YourMadeUpCategoryName)
// methods and properties here
@end

函数

对于独立函数,我不完全确定该怎么做。理论上,从新的 SDK 头文件中复制声明应该可以工作。示例

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);
}

我不确定链接器或动态链接器在符号不可用时会做什么。这是否需要__attribute__((weak_import)) 注释

也许 SDK 中的 .tbd 文件的作用就在于此?以便链接器知道允许哪些符号?因此,复制头文件中的代码无法解决该部分问题。

无论如何,始终有效的方法是纯粹的运行时方法

  1. 为所需的函数定义类型,但不要定义函数本身。

  2. 在运行时,使用dlsym 查找函数。

关于 Rust 的说明

如果您从 Rust 代码调用 macOS API,那么您基本上需要自行解决。Apple 没有提供任何 Rust “头文件”,因此实际上没有可供参考的 SDK。因此,无论用于构建的 SDK 是什么,您都必须自己提供 API 声明。

从某种程度上说,您可以避开一些构建时的问题。您无需担心任何#ifdefs,因为没有可能与之冲突的系统头文件。

另一方面,您仍然需要担心运行时 API 的可用性。在 Rust 中,API 声明上没有可用性属性,也没有@available 运行时检查帮助程序,并且如果在可用性检查之外调用 API,编译器不会发出警告。