Skip to content

Commit

Permalink
[ffigen] Runtime version checks (#1995)
Browse files Browse the repository at this point in the history
  • Loading branch information
liamappelbe authored Feb 16, 2025
1 parent 138990d commit 23a387d
Show file tree
Hide file tree
Showing 23 changed files with 1,007 additions and 103 deletions.
2 changes: 2 additions & 0 deletions pkgs/ffigen/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
- Fix the handling of global arrays to remove the extra pointer reference.
- Add a `max` field to the `external-versions` config, and use it to determine
which APIs are generated.
- Add a runtime OS version check to ObjC APIs, which throws an error if the
current OS version is earlier than the version that the API was introduced.

## 16.1.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class ObjCBuiltInFunctions {
static const dartProxy = ObjCImport('DartProxy');
static const unimplementedOptionalMethodException =
ObjCImport('UnimplementedOptionalMethodException');
static const checkOsVersion = ObjCImport('checkOsVersion');

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

Expand Down
13 changes: 10 additions & 3 deletions pkgs/ffigen/lib/src/code_generator/objc_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.

import '../code_generator.dart';
import '../header_parser/sub_parsers/api_availability.dart';
import '../visitor/ast.dart';

import 'binding_string.dart';
Expand All @@ -20,7 +21,7 @@ class ObjCInterface extends BindingType with ObjCMethods {
final protocols = <ObjCProtocol>[];
final categories = <ObjCCategory>[];
final subtypes = <ObjCInterface>[];
final bool unavailable;
final ApiAvailability apiAvailability;

@override
final ObjCBuiltInFunctions builtInFunctions;
Expand All @@ -35,7 +36,7 @@ class ObjCInterface extends BindingType with ObjCMethods {
String? lookupName,
super.dartDoc,
required this.builtInFunctions,
this.unavailable = false,
required this.apiAvailability,
}) : lookupName = lookupName ?? originalName,
super(name: name ?? originalName) {
classObject = ObjCInternalGlobal('_class_$originalName',
Expand All @@ -60,6 +61,8 @@ class ObjCInterface extends BindingType with ObjCMethods {
@override
void sort() => sortMethods();

bool get unavailable => apiAvailability.availability == Availability.none;

@override
BindingString toBindingString(Writer w) {
final s = StringBuffer();
Expand All @@ -73,6 +76,10 @@ class ObjCInterface extends BindingType with ObjCMethods {
}
s.write(makeDartDoc(dartDoc));

final versionCheck = apiAvailability.runtimeCheck(
ObjCBuiltInFunctions.checkOsVersion.gen(w), originalName);
final ctorBody = versionCheck == null ? ';' : ' { $versionCheck }';

final rawObjType = PointerType(objCObjectType).getCType(w);
final wrapObjType = ObjCBuiltInFunctions.objectBase.gen(w);
final protoImpl = protocols.isEmpty
Expand All @@ -83,7 +90,7 @@ class ObjCInterface extends BindingType with ObjCMethods {
s.write('''
class $name extends ${superType?.getDartType(w) ?? wrapObjType} $protoImpl{
$name._($rawObjType pointer, {bool retain = false, bool release = false}) :
$superCtor(pointer, retain: retain, release: release);
$superCtor(pointer, retain: retain, release: release)$ctorBody
/// Constructs a [$name] that points to the same underlying object as [other].
$name.castFrom($wrapObjType other) :
Expand Down
10 changes: 10 additions & 0 deletions pkgs/ffigen/lib/src/code_generator/objc_methods.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'dart:collection';
import 'package:logging/logging.dart';

import '../code_generator.dart';
import '../header_parser/sub_parsers/api_availability.dart';
import '../visitor/ast.dart';

import 'utils.dart';
Expand Down Expand Up @@ -201,6 +202,7 @@ class ObjCMethod extends AstNode {
final bool isOptional;
ObjCMethodOwnership? ownershipAttribute;
final ObjCMethodFamily? family;
final ApiAvailability apiAvailability;
bool consumesSelfAttribute = false;
ObjCInternalGlobal selObject;
ObjCMsgSendFunc? msgSend;
Expand Down Expand Up @@ -228,6 +230,7 @@ class ObjCMethod extends AstNode {
required this.isOptional,
required this.returnType,
required this.family,
required this.apiAvailability,
List<Parameter>? params_,
}) : params = params_ ?? [],
selObject = builtInFunctions.getSelObject(originalName);
Expand Down Expand Up @@ -385,6 +388,13 @@ class ObjCMethod extends AstNode {
s.write(' {\n');

// Implementation.
final versionCheck = apiAvailability.runtimeCheck(
ObjCBuiltInFunctions.checkOsVersion.gen(w),
'${target.originalName}.$originalName');
if (versionCheck != null) {
s.write(' $versionCheck\n');
}

final sel = selObject.name;
if (isOptional) {
s.write('''
Expand Down
7 changes: 5 additions & 2 deletions pkgs/ffigen/lib/src/code_generator/objc_protocol.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.

import '../code_generator.dart';
import '../header_parser/sub_parsers/api_availability.dart';
import '../visitor/ast.dart';

import 'binding_string.dart';
Expand All @@ -15,7 +16,7 @@ class ObjCProtocol extends BindingType with ObjCMethods {
final ObjCInternalGlobal _protocolPointer;
late final ObjCInternalGlobal _conformsTo;
late final ObjCMsgSendFunc _conformsToMsgSend;
final bool unavailable;
final ApiAvailability apiAvailability;

// Filled by ListBindingsVisitation.
bool generateAsStub = false;
Expand All @@ -30,7 +31,7 @@ class ObjCProtocol extends BindingType with ObjCMethods {
String? lookupName,
super.dartDoc,
required this.builtInFunctions,
this.unavailable = false,
required this.apiAvailability,
}) : lookupName = lookupName ?? originalName,
_protocolPointer = ObjCInternalGlobal(
'_protocol_$originalName',
Expand All @@ -57,6 +58,8 @@ class ObjCProtocol extends BindingType with ObjCMethods {
@override
void sort() => sortMethods();

bool get unavailable => apiAvailability.availability == Availability.none;

@override
BindingString toBindingString(Writer w) {
final protocolBase = ObjCBuiltInFunctions.protocolBase.gen(w);
Expand Down
55 changes: 32 additions & 23 deletions pkgs/ffigen/lib/src/header_parser/sub_parsers/api_availability.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,32 +19,24 @@ enum Availability {
all,
}

typedef ApiAvailabilityReport = ({
Availability availability,
String? dartDoc,
});

ApiAvailabilityReport getApiAvailability(clang_types.CXCursor cursor) {
final api = ApiAvailability.fromCursor(cursor);
final availability = api.getAvailability(config.externalVersions);
return (
availability: availability,
dartDoc: availability == Availability.some ? api.dartDoc : null,
);
}

class ApiAvailability {
final bool alwaysDeprecated;
final bool alwaysUnavailable;
PlatformAvailability? ios;
PlatformAvailability? macos;
final PlatformAvailability? ios;
final PlatformAvailability? macos;

late final Availability availability;

ApiAvailability({
this.alwaysDeprecated = false,
this.alwaysUnavailable = false,
this.ios,
this.macos,
});
ExternalVersions? externalVersions,
}) {
availability =
_getAvailability(externalVersions ?? config.externalVersions);
}

static ApiAvailability fromCursor(clang_types.CXCursor cursor) {
final platformsLength = clang.clang_getCursorPlatformAvailability(
Expand Down Expand Up @@ -96,7 +88,7 @@ class ApiAvailability {
return api;
}

Availability getAvailability(ExternalVersions externalVersions) {
Availability _getAvailability(ExternalVersions externalVersions) {
final macosVer = _normalizeVersions(externalVersions.macos);
final iosVer = _normalizeVersions(externalVersions.ios);

Expand All @@ -109,7 +101,7 @@ class ApiAvailability {
return Availability.none;
}

Availability? availability;
Availability? availability_;
for (final (platform, version) in [(ios, iosVer), (macos, macosVer)]) {
// If the user hasn't specified any versions for this platform, defer to
// the other platforms.
Expand All @@ -119,9 +111,9 @@ class ApiAvailability {
// If the API is available on any platform, return that it's available.
final platAvailability =
platform?.getAvailability(version) ?? Availability.all;
availability = _mergeAvailability(availability, platAvailability);
availability_ = _mergeAvailability(availability_, platAvailability);
}
return availability ?? Availability.none;
return availability_ ?? Availability.none;
}

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

String get dartDoc =>
[ios, macos].nonNulls.map((platform) => platform.dartDoc).join('\n');
List<PlatformAvailability> get _platforms => [ios, macos].nonNulls.toList();

String? get dartDoc {
if (availability != Availability.some) return null;
final platforms = _platforms;
if (platforms.isEmpty) return null;
return platforms.map((platform) => platform.dartDoc).join('\n');
}

String? runtimeCheck(String checkOsVersion, String apiName) {
final platforms = _platforms;
if (platforms.isEmpty) return null;
final args = platforms.map((platform) => platform.checkArgs).join(', ');
return "$checkOsVersion('$apiName', $args);";
}

@override
String toString() => '''Availability {
Expand Down Expand Up @@ -210,6 +215,10 @@ class PlatformAvailability {
return s.toString();
}

String get checkArgs => '$name: ($unavailable, ${_toRecord(introduced)})';
String _toRecord(Version? v) =>
v == null ? 'null' : '(${v.major}, ${v.minor}, ${v.patch})';

@override
String toString() => 'introduced: $introduced, deprecated: $deprecated, '
'obsoleted: $obsoleted, unavailable: $unavailable';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ Compound? parseCompoundDeclaration(
declName = '';
}

final report = getApiAvailability(cursor);
if (report.availability == Availability.none) {
final apiAvailability = ApiAvailability.fromCursor(cursor);
if (apiAvailability.availability == Availability.none) {
_logger.info('Omitting deprecated $className $declName');
return null;
}
Expand All @@ -120,7 +120,8 @@ Compound? parseCompoundDeclaration(
type: compoundType,
name: incrementalNamer.name('Unnamed$className'),
usr: declUsr,
dartDoc: getCursorDocComment(cursor, availability: report.dartDoc),
dartDoc:
getCursorDocComment(cursor, availability: apiAvailability.dartDoc),
objCBuiltInFunctions: objCBuiltInFunctions,
nativeType: cursor.type().spelling(),
);
Expand All @@ -133,7 +134,8 @@ Compound? parseCompoundDeclaration(
usr: declUsr,
originalName: declName,
name: configDecl.rename(decl),
dartDoc: getCursorDocComment(cursor, availability: report.dartDoc),
dartDoc:
getCursorDocComment(cursor, availability: apiAvailability.dartDoc),
objCBuiltInFunctions: objCBuiltInFunctions,
nativeType: cursor.type().spelling(),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ final _logger = Logger('ffigen.header_parser.enumdecl_parser');
nativeType = signedToUnsignedNativeIntType[nativeType] ?? nativeType;
var hasNegativeEnumConstants = false;

final report = getApiAvailability(cursor);
if (report.availability == Availability.none) {
final apiAvailability = ApiAvailability.fromCursor(cursor);
if (apiAvailability.availability == Availability.none) {
_logger.info('Omitting deprecated enum $enumName');
return (null, nativeType);
}
Expand All @@ -55,7 +55,8 @@ final _logger = Logger('ffigen.header_parser.enumdecl_parser');
_logger.fine('++++ Adding Enum: ${cursor.completeStringRepr()}');
enumClass = EnumClass(
usr: enumUsr,
dartDoc: getCursorDocComment(cursor, availability: report.dartDoc),
dartDoc:
getCursorDocComment(cursor, availability: apiAvailability.dartDoc),
originalName: enumName,
name: config.enumClassDecl.rename(decl),
nativeType: nativeType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ List<Func> parseFunctionDeclaration(clang_types.CXCursor cursor) {
final funcUsr = cursor.usr();
final funcName = cursor.spelling();

final report = getApiAvailability(cursor);
if (report.availability == Availability.none) {
final apiAvailability = ApiAvailability.fromCursor(cursor);
if (apiAvailability.availability == Availability.none) {
_logger.info('Omitting deprecated function $funcName');
return funcs;
}
Expand Down Expand Up @@ -121,7 +121,7 @@ List<Func> parseFunctionDeclaration(clang_types.CXCursor cursor) {
dartDoc: getCursorDocComment(
cursor,
indent: nesting.length + commentPrefix.length,
availability: report.dartDoc,
availability: apiAvailability.dartDoc,
),
usr: funcUsr + vaFunc.postfix,
name: config.functionDecl.rename(decl) + vaFunc.postfix,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ ObjCCategory? parseObjCCategoryDeclaration(clang_types.CXCursor cursor) {
return cachedCategory;
}

final report = getApiAvailability(cursor);
if (report.availability == Availability.none) {
final apiAvailability = ApiAvailability.fromCursor(cursor);
if (apiAvailability.availability == Availability.none) {
_logger.info('Omitting deprecated category $name');
return null;
}
Expand Down Expand Up @@ -55,7 +55,7 @@ ObjCCategory? parseObjCCategoryDeclaration(clang_types.CXCursor cursor) {
name: config.objcCategories.rename(decl),
parent: parentInterface,
dartDoc: getCursorDocComment(cursor,
fallbackComment: name, availability: report.dartDoc),
fallbackComment: name, availability: apiAvailability.dartDoc),
builtInFunctions: objCBuiltInFunctions,
);

Expand Down
Loading

0 comments on commit 23a387d

Please sign in to comment.