Skip to content

Commit 23a387d

Browse files
authored
[ffigen] Runtime version checks (#1995)
1 parent 138990d commit 23a387d

23 files changed

+1007
-103
lines changed

pkgs/ffigen/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
- Fix the handling of global arrays to remove the extra pointer reference.
1717
- Add a `max` field to the `external-versions` config, and use it to determine
1818
which APIs are generated.
19+
- Add a runtime OS version check to ObjC APIs, which throws an error if the
20+
current OS version is earlier than the version that the API was introduced.
1921

2022
## 16.1.0
2123

pkgs/ffigen/lib/src/code_generator/objc_built_in_functions.dart

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class ObjCBuiltInFunctions {
4747
static const dartProxy = ObjCImport('DartProxy');
4848
static const unimplementedOptionalMethodException =
4949
ObjCImport('UnimplementedOptionalMethodException');
50+
static const checkOsVersion = ObjCImport('checkOsVersion');
5051

5152
// Keep in sync with pkgs/objective_c/ffigen_objc.yaml.
5253

pkgs/ffigen/lib/src/code_generator/objc_interface.dart

+10-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import '../code_generator.dart';
6+
import '../header_parser/sub_parsers/api_availability.dart';
67
import '../visitor/ast.dart';
78

89
import 'binding_string.dart';
@@ -20,7 +21,7 @@ class ObjCInterface extends BindingType with ObjCMethods {
2021
final protocols = <ObjCProtocol>[];
2122
final categories = <ObjCCategory>[];
2223
final subtypes = <ObjCInterface>[];
23-
final bool unavailable;
24+
final ApiAvailability apiAvailability;
2425

2526
@override
2627
final ObjCBuiltInFunctions builtInFunctions;
@@ -35,7 +36,7 @@ class ObjCInterface extends BindingType with ObjCMethods {
3536
String? lookupName,
3637
super.dartDoc,
3738
required this.builtInFunctions,
38-
this.unavailable = false,
39+
required this.apiAvailability,
3940
}) : lookupName = lookupName ?? originalName,
4041
super(name: name ?? originalName) {
4142
classObject = ObjCInternalGlobal('_class_$originalName',
@@ -60,6 +61,8 @@ class ObjCInterface extends BindingType with ObjCMethods {
6061
@override
6162
void sort() => sortMethods();
6263

64+
bool get unavailable => apiAvailability.availability == Availability.none;
65+
6366
@override
6467
BindingString toBindingString(Writer w) {
6568
final s = StringBuffer();
@@ -73,6 +76,10 @@ class ObjCInterface extends BindingType with ObjCMethods {
7376
}
7477
s.write(makeDartDoc(dartDoc));
7578

79+
final versionCheck = apiAvailability.runtimeCheck(
80+
ObjCBuiltInFunctions.checkOsVersion.gen(w), originalName);
81+
final ctorBody = versionCheck == null ? ';' : ' { $versionCheck }';
82+
7683
final rawObjType = PointerType(objCObjectType).getCType(w);
7784
final wrapObjType = ObjCBuiltInFunctions.objectBase.gen(w);
7885
final protoImpl = protocols.isEmpty
@@ -83,7 +90,7 @@ class ObjCInterface extends BindingType with ObjCMethods {
8390
s.write('''
8491
class $name extends ${superType?.getDartType(w) ?? wrapObjType} $protoImpl{
8592
$name._($rawObjType pointer, {bool retain = false, bool release = false}) :
86-
$superCtor(pointer, retain: retain, release: release);
93+
$superCtor(pointer, retain: retain, release: release)$ctorBody
8794
8895
/// Constructs a [$name] that points to the same underlying object as [other].
8996
$name.castFrom($wrapObjType other) :

pkgs/ffigen/lib/src/code_generator/objc_methods.dart

+10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'dart:collection';
77
import 'package:logging/logging.dart';
88

99
import '../code_generator.dart';
10+
import '../header_parser/sub_parsers/api_availability.dart';
1011
import '../visitor/ast.dart';
1112

1213
import 'utils.dart';
@@ -201,6 +202,7 @@ class ObjCMethod extends AstNode {
201202
final bool isOptional;
202203
ObjCMethodOwnership? ownershipAttribute;
203204
final ObjCMethodFamily? family;
205+
final ApiAvailability apiAvailability;
204206
bool consumesSelfAttribute = false;
205207
ObjCInternalGlobal selObject;
206208
ObjCMsgSendFunc? msgSend;
@@ -228,6 +230,7 @@ class ObjCMethod extends AstNode {
228230
required this.isOptional,
229231
required this.returnType,
230232
required this.family,
233+
required this.apiAvailability,
231234
List<Parameter>? params_,
232235
}) : params = params_ ?? [],
233236
selObject = builtInFunctions.getSelObject(originalName);
@@ -385,6 +388,13 @@ class ObjCMethod extends AstNode {
385388
s.write(' {\n');
386389

387390
// Implementation.
391+
final versionCheck = apiAvailability.runtimeCheck(
392+
ObjCBuiltInFunctions.checkOsVersion.gen(w),
393+
'${target.originalName}.$originalName');
394+
if (versionCheck != null) {
395+
s.write(' $versionCheck\n');
396+
}
397+
388398
final sel = selObject.name;
389399
if (isOptional) {
390400
s.write('''

pkgs/ffigen/lib/src/code_generator/objc_protocol.dart

+5-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// BSD-style license that can be found in the LICENSE file.
44

55
import '../code_generator.dart';
6+
import '../header_parser/sub_parsers/api_availability.dart';
67
import '../visitor/ast.dart';
78

89
import 'binding_string.dart';
@@ -15,7 +16,7 @@ class ObjCProtocol extends BindingType with ObjCMethods {
1516
final ObjCInternalGlobal _protocolPointer;
1617
late final ObjCInternalGlobal _conformsTo;
1718
late final ObjCMsgSendFunc _conformsToMsgSend;
18-
final bool unavailable;
19+
final ApiAvailability apiAvailability;
1920

2021
// Filled by ListBindingsVisitation.
2122
bool generateAsStub = false;
@@ -30,7 +31,7 @@ class ObjCProtocol extends BindingType with ObjCMethods {
3031
String? lookupName,
3132
super.dartDoc,
3233
required this.builtInFunctions,
33-
this.unavailable = false,
34+
required this.apiAvailability,
3435
}) : lookupName = lookupName ?? originalName,
3536
_protocolPointer = ObjCInternalGlobal(
3637
'_protocol_$originalName',
@@ -57,6 +58,8 @@ class ObjCProtocol extends BindingType with ObjCMethods {
5758
@override
5859
void sort() => sortMethods();
5960

61+
bool get unavailable => apiAvailability.availability == Availability.none;
62+
6063
@override
6164
BindingString toBindingString(Writer w) {
6265
final protocolBase = ObjCBuiltInFunctions.protocolBase.gen(w);

pkgs/ffigen/lib/src/header_parser/sub_parsers/api_availability.dart

+32-23
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,24 @@ enum Availability {
1919
all,
2020
}
2121

22-
typedef ApiAvailabilityReport = ({
23-
Availability availability,
24-
String? dartDoc,
25-
});
26-
27-
ApiAvailabilityReport getApiAvailability(clang_types.CXCursor cursor) {
28-
final api = ApiAvailability.fromCursor(cursor);
29-
final availability = api.getAvailability(config.externalVersions);
30-
return (
31-
availability: availability,
32-
dartDoc: availability == Availability.some ? api.dartDoc : null,
33-
);
34-
}
35-
3622
class ApiAvailability {
3723
final bool alwaysDeprecated;
3824
final bool alwaysUnavailable;
39-
PlatformAvailability? ios;
40-
PlatformAvailability? macos;
25+
final PlatformAvailability? ios;
26+
final PlatformAvailability? macos;
27+
28+
late final Availability availability;
4129

4230
ApiAvailability({
4331
this.alwaysDeprecated = false,
4432
this.alwaysUnavailable = false,
4533
this.ios,
4634
this.macos,
47-
});
35+
ExternalVersions? externalVersions,
36+
}) {
37+
availability =
38+
_getAvailability(externalVersions ?? config.externalVersions);
39+
}
4840

4941
static ApiAvailability fromCursor(clang_types.CXCursor cursor) {
5042
final platformsLength = clang.clang_getCursorPlatformAvailability(
@@ -96,7 +88,7 @@ class ApiAvailability {
9688
return api;
9789
}
9890

99-
Availability getAvailability(ExternalVersions externalVersions) {
91+
Availability _getAvailability(ExternalVersions externalVersions) {
10092
final macosVer = _normalizeVersions(externalVersions.macos);
10193
final iosVer = _normalizeVersions(externalVersions.ios);
10294

@@ -109,7 +101,7 @@ class ApiAvailability {
109101
return Availability.none;
110102
}
111103

112-
Availability? availability;
104+
Availability? availability_;
113105
for (final (platform, version) in [(ios, iosVer), (macos, macosVer)]) {
114106
// If the user hasn't specified any versions for this platform, defer to
115107
// the other platforms.
@@ -119,9 +111,9 @@ class ApiAvailability {
119111
// If the API is available on any platform, return that it's available.
120112
final platAvailability =
121113
platform?.getAvailability(version) ?? Availability.all;
122-
availability = _mergeAvailability(availability, platAvailability);
114+
availability_ = _mergeAvailability(availability_, platAvailability);
123115
}
124-
return availability ?? Availability.none;
116+
return availability_ ?? Availability.none;
125117
}
126118

127119
// If the min and max version are null, the versions object should be null.
@@ -131,8 +123,21 @@ class ApiAvailability {
131123
static Availability _mergeAvailability(Availability? x, Availability y) =>
132124
x == null ? y : (x == y ? x : Availability.some);
133125

134-
String get dartDoc =>
135-
[ios, macos].nonNulls.map((platform) => platform.dartDoc).join('\n');
126+
List<PlatformAvailability> get _platforms => [ios, macos].nonNulls.toList();
127+
128+
String? get dartDoc {
129+
if (availability != Availability.some) return null;
130+
final platforms = _platforms;
131+
if (platforms.isEmpty) return null;
132+
return platforms.map((platform) => platform.dartDoc).join('\n');
133+
}
134+
135+
String? runtimeCheck(String checkOsVersion, String apiName) {
136+
final platforms = _platforms;
137+
if (platforms.isEmpty) return null;
138+
final args = platforms.map((platform) => platform.checkArgs).join(', ');
139+
return "$checkOsVersion('$apiName', $args);";
140+
}
136141

137142
@override
138143
String toString() => '''Availability {
@@ -210,6 +215,10 @@ class PlatformAvailability {
210215
return s.toString();
211216
}
212217

218+
String get checkArgs => '$name: ($unavailable, ${_toRecord(introduced)})';
219+
String _toRecord(Version? v) =>
220+
v == null ? 'null' : '(${v.major}, ${v.minor}, ${v.patch})';
221+
213222
@override
214223
String toString() => 'introduced: $introduced, deprecated: $deprecated, '
215224
'obsoleted: $obsoleted, unavailable: $unavailable';

pkgs/ffigen/lib/src/header_parser/sub_parsers/compounddecl_parser.dart

+6-4
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ Compound? parseCompoundDeclaration(
107107
declName = '';
108108
}
109109

110-
final report = getApiAvailability(cursor);
111-
if (report.availability == Availability.none) {
110+
final apiAvailability = ApiAvailability.fromCursor(cursor);
111+
if (apiAvailability.availability == Availability.none) {
112112
_logger.info('Omitting deprecated $className $declName');
113113
return null;
114114
}
@@ -120,7 +120,8 @@ Compound? parseCompoundDeclaration(
120120
type: compoundType,
121121
name: incrementalNamer.name('Unnamed$className'),
122122
usr: declUsr,
123-
dartDoc: getCursorDocComment(cursor, availability: report.dartDoc),
123+
dartDoc:
124+
getCursorDocComment(cursor, availability: apiAvailability.dartDoc),
124125
objCBuiltInFunctions: objCBuiltInFunctions,
125126
nativeType: cursor.type().spelling(),
126127
);
@@ -133,7 +134,8 @@ Compound? parseCompoundDeclaration(
133134
usr: declUsr,
134135
originalName: declName,
135136
name: configDecl.rename(decl),
136-
dartDoc: getCursorDocComment(cursor, availability: report.dartDoc),
137+
dartDoc:
138+
getCursorDocComment(cursor, availability: apiAvailability.dartDoc),
137139
objCBuiltInFunctions: objCBuiltInFunctions,
138140
nativeType: cursor.type().spelling(),
139141
);

pkgs/ffigen/lib/src/header_parser/sub_parsers/enumdecl_parser.dart

+4-3
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ final _logger = Logger('ffigen.header_parser.enumdecl_parser');
3939
nativeType = signedToUnsignedNativeIntType[nativeType] ?? nativeType;
4040
var hasNegativeEnumConstants = false;
4141

42-
final report = getApiAvailability(cursor);
43-
if (report.availability == Availability.none) {
42+
final apiAvailability = ApiAvailability.fromCursor(cursor);
43+
if (apiAvailability.availability == Availability.none) {
4444
_logger.info('Omitting deprecated enum $enumName');
4545
return (null, nativeType);
4646
}
@@ -55,7 +55,8 @@ final _logger = Logger('ffigen.header_parser.enumdecl_parser');
5555
_logger.fine('++++ Adding Enum: ${cursor.completeStringRepr()}');
5656
enumClass = EnumClass(
5757
usr: enumUsr,
58-
dartDoc: getCursorDocComment(cursor, availability: report.dartDoc),
58+
dartDoc:
59+
getCursorDocComment(cursor, availability: apiAvailability.dartDoc),
5960
originalName: enumName,
6061
name: config.enumClassDecl.rename(decl),
6162
nativeType: nativeType,

pkgs/ffigen/lib/src/header_parser/sub_parsers/functiondecl_parser.dart

+3-3
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ List<Func> parseFunctionDeclaration(clang_types.CXCursor cursor) {
2222
final funcUsr = cursor.usr();
2323
final funcName = cursor.spelling();
2424

25-
final report = getApiAvailability(cursor);
26-
if (report.availability == Availability.none) {
25+
final apiAvailability = ApiAvailability.fromCursor(cursor);
26+
if (apiAvailability.availability == Availability.none) {
2727
_logger.info('Omitting deprecated function $funcName');
2828
return funcs;
2929
}
@@ -121,7 +121,7 @@ List<Func> parseFunctionDeclaration(clang_types.CXCursor cursor) {
121121
dartDoc: getCursorDocComment(
122122
cursor,
123123
indent: nesting.length + commentPrefix.length,
124-
availability: report.dartDoc,
124+
availability: apiAvailability.dartDoc,
125125
),
126126
usr: funcUsr + vaFunc.postfix,
127127
name: config.functionDecl.rename(decl) + vaFunc.postfix,

pkgs/ffigen/lib/src/header_parser/sub_parsers/objccategorydecl_parser.dart

+3-3
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ ObjCCategory? parseObjCCategoryDeclaration(clang_types.CXCursor cursor) {
2626
return cachedCategory;
2727
}
2828

29-
final report = getApiAvailability(cursor);
30-
if (report.availability == Availability.none) {
29+
final apiAvailability = ApiAvailability.fromCursor(cursor);
30+
if (apiAvailability.availability == Availability.none) {
3131
_logger.info('Omitting deprecated category $name');
3232
return null;
3333
}
@@ -55,7 +55,7 @@ ObjCCategory? parseObjCCategoryDeclaration(clang_types.CXCursor cursor) {
5555
name: config.objcCategories.rename(decl),
5656
parent: parentInterface,
5757
dartDoc: getCursorDocComment(cursor,
58-
fallbackComment: name, availability: report.dartDoc),
58+
fallbackComment: name, availability: apiAvailability.dartDoc),
5959
builtInFunctions: objCBuiltInFunctions,
6060
);
6161

0 commit comments

Comments
 (0)