Skip to content

[ffigen] Runtime version checks #1995

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

Merged
merged 4 commits into from
Feb 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading