Skip to content

Commit

Permalink
[pigeon] Swift implementation for ProxyApis (#6602)
Browse files Browse the repository at this point in the history
Swift portion of flutter/flutter#134777
  • Loading branch information
bparrishMines authored Oct 11, 2024
1 parent 869a3de commit 0da80be
Show file tree
Hide file tree
Showing 24 changed files with 13,250 additions and 73 deletions.
4 changes: 4 additions & 0 deletions packages/pigeon/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 22.5.0

* [swift] Adds implementation for `@ProxyApi`.

## 22.4.2

* Updates `README.md` to replace the deprecated `flutter pub run pigeon` command with `dart run pigeon`.
Expand Down
2 changes: 1 addition & 1 deletion packages/pigeon/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ This is what the temporary generated code that the _PigeonIsolate_ executes
looks like (see [State Diagram](#state-diagram)):

```dart
import 'path/to/supplied/pigeon/file.dart'
import 'path/to/supplied/pigeon/file.dart';
import 'dart:io';
import 'dart:isolate';
import 'package:pigeon/pigeon_lib.dart';
Expand Down
6 changes: 6 additions & 0 deletions packages/pigeon/lib/ast.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:meta/meta.dart';
import 'generator_tools.dart';
import 'kotlin_generator.dart' show KotlinProxyApiOptions;
import 'pigeon_lib.dart';
import 'swift_generator.dart' show SwiftProxyApiOptions;

typedef _ListEquals = bool Function(List<Object?>, List<Object?>);

Expand Down Expand Up @@ -142,6 +143,7 @@ class AstProxyApi extends Api {
required this.fields,
this.superClass,
this.interfaces = const <TypeDeclaration>{},
this.swiftOptions,
this.kotlinOptions,
});

Expand All @@ -157,6 +159,10 @@ class AstProxyApi extends Api {
/// Name of the classes this class considers to be implemented.
Set<TypeDeclaration> interfaces;

/// Options that control how Swift code will be generated for a specific
/// ProxyApi.
final SwiftProxyApiOptions? swiftOptions;

/// Options that control how Kotlin code will be generated for a specific
/// ProxyApi.
final KotlinProxyApiOptions? kotlinOptions;
Expand Down
2 changes: 1 addition & 1 deletion packages/pigeon/lib/generator_tools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import 'ast.dart';
/// The current version of pigeon.
///
/// This must match the version in pubspec.yaml.
const String pigeonVersion = '22.4.2';
const String pigeonVersion = '22.5.0';

/// Read all the content from [stdin] to a String.
String readStdin() {
Expand Down
2 changes: 1 addition & 1 deletion packages/pigeon/lib/pigeon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ export 'java_generator.dart' show JavaOptions;
export 'kotlin_generator.dart' show KotlinOptions, KotlinProxyApiOptions;
export 'objc_generator.dart' show ObjcOptions;
export 'pigeon_lib.dart';
export 'swift_generator.dart' show SwiftOptions;
export 'swift_generator.dart' show SwiftOptions, SwiftProxyApiOptions;
42 changes: 41 additions & 1 deletion packages/pigeon/lib/pigeon_lib.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import 'package:analyzer/error/error.dart' show AnalysisError;
import 'package:args/args.dart';
import 'package:collection/collection.dart' as collection;
import 'package:path/path.dart' as path;
import 'package:pub_semver/pub_semver.dart';

import 'ast.dart';
import 'ast_generator.dart';
Expand Down Expand Up @@ -139,7 +140,7 @@ class FlutterApi {
/// methods.
class ProxyApi {
/// Parametric constructor for [ProxyApi].
const ProxyApi({this.superClass, this.kotlinOptions});
const ProxyApi({this.superClass, this.kotlinOptions, this.swiftOptions});

/// The proxy api that is a super class to this one.
///
Expand All @@ -150,6 +151,10 @@ class ProxyApi {
/// with inherited method names.
final Type? superClass;

/// Options that control how Swift code will be generated for a specific
/// ProxyApi.
final SwiftProxyApiOptions? swiftOptions;

/// Options that control how Kotlin code will be generated for a specific
/// ProxyApi.
final KotlinProxyApiOptions? kotlinOptions;
Expand Down Expand Up @@ -1674,6 +1679,40 @@ class _RootBuilder extends dart_ast_visitor.RecursiveAstVisitor<Object?> {
}
}

SwiftProxyApiOptions? swiftOptions;
final Map<String, Object?>? swiftOptionsMap =
annotationMap['swiftOptions'] as Map<String, Object?>?;
if (swiftOptionsMap != null) {
swiftOptions = SwiftProxyApiOptions(
name: swiftOptionsMap['name'] as String?,
import: swiftOptionsMap['import'] as String?,
minIosApi: swiftOptionsMap['minIosApi'] as String?,
minMacosApi: swiftOptionsMap['minMacosApi'] as String?,
supportsIos: swiftOptionsMap['supportsIos'] as bool? ?? true,
supportsMacos: swiftOptionsMap['supportsMacos'] as bool? ?? true,
);
}

void tryParseApiRequirement(String? version) {
if (version == null) {
return;
}
try {
Version.parse(version);
} on FormatException catch (error) {
_errors.add(
Error(
message:
'Could not parse version: ${error.message}. Please use semantic versioning format: "1.2.3".',
lineNumber: _calculateLineNumber(source, node.offset),
),
);
}
}

tryParseApiRequirement(swiftOptions?.minIosApi);
tryParseApiRequirement(swiftOptions?.minMacosApi);

KotlinProxyApiOptions? kotlinOptions;
final Map<String, Object?>? kotlinOptionsMap =
annotationMap['kotlinOptions'] as Map<String, Object?>?;
Expand All @@ -1691,6 +1730,7 @@ class _RootBuilder extends dart_ast_visitor.RecursiveAstVisitor<Object?> {
fields: <ApiField>[],
superClass: superClass,
interfaces: interfaces,
swiftOptions: swiftOptions,
kotlinOptions: kotlinOptions,
documentationComments:
_documentationCommentsParser(node.documentationComment?.tokens),
Expand Down
256 changes: 256 additions & 0 deletions packages/pigeon/lib/swift/templates.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import '../generator_tools.dart';
import '../pigeon.dart';

/// Name of delegate that handles the callback when an object is deallocated
/// in an `InstanceManager`.
String instanceManagerFinalizerDelegateName(SwiftOptions options) =>
'${_instanceManagerFinalizerName(options)}Delegate';

/// The name of the registrar containing all the ProxyApi implementations.
String proxyApiRegistrarName(SwiftOptions options) =>
'${options.fileSpecificClassNameComponent ?? ''}${proxyApiClassNamePrefix}ProxyApiRegistrar';

/// The name of the `ReaderWriter` that handles ProxyApis.
String proxyApiReaderWriterName(SwiftOptions options) =>
'${options.fileSpecificClassNameComponent ?? ''}${classNamePrefix}ProxyApiCodecReaderWriter';

/// Name of the Swift `InstanceManager`.
String swiftInstanceManagerClassName(SwiftOptions options) =>
'${options.fileSpecificClassNameComponent ?? ''}${proxyApiClassNamePrefix}InstanceManager';

/// Template for delegate with callback when an object is deallocated.
String instanceManagerFinalizerDelegateTemplate(SwiftOptions options) => '''
/// Handles the callback when an object is deallocated.
protocol ${instanceManagerFinalizerDelegateName(options)}: AnyObject {
/// Invoked when the strong reference of an object is deallocated in an `InstanceManager`.
func onDeinit(identifier: Int64)
}
''';

/// Template for an object that tracks when an object is deallocated.
String instanceManagerFinalizerTemplate(SwiftOptions options) => '''
// Attaches to an object to receive a callback when the object is deallocated.
internal final class ${_instanceManagerFinalizerName(options)} {
private static let associatedObjectKey = malloc(1)!
private let identifier: Int64
// Reference to the delegate is weak because the callback should be ignored if the
// `InstanceManager` is deallocated.
private weak var delegate: ${instanceManagerFinalizerDelegateName(options)}?
private init(identifier: Int64, delegate: ${instanceManagerFinalizerDelegateName(options)}) {
self.identifier = identifier
self.delegate = delegate
}
internal static func attach(
to instance: AnyObject, identifier: Int64, delegate: ${instanceManagerFinalizerDelegateName(options)}
) {
let finalizer = ${_instanceManagerFinalizerName(options)}(identifier: identifier, delegate: delegate)
objc_setAssociatedObject(instance, associatedObjectKey, finalizer, .OBJC_ASSOCIATION_RETAIN)
}
static func detach(from instance: AnyObject) {
objc_setAssociatedObject(instance, associatedObjectKey, nil, .OBJC_ASSOCIATION_ASSIGN)
}
deinit {
delegate?.onDeinit(identifier: identifier)
}
}
''';

/// The Swift `InstanceManager`.
String instanceManagerTemplate(SwiftOptions options) {
return '''
/// Maintains instances used to communicate with the corresponding objects in Dart.
///
/// Objects stored in this container are represented by an object in Dart that is also stored in
/// an InstanceManager with the same identifier.
///
/// When an instance is added with an identifier, either can be used to retrieve the other.
///
/// Added instances are added as a weak reference and a strong reference. When the strong
/// reference is removed and the weak reference is deallocated,`${instanceManagerFinalizerDelegateName(options)}.onDeinit`
/// is called with the instance's identifier. However, if the strong reference is removed and then the identifier is
/// retrieved with the intention to pass the identifier to Dart (e.g. by calling `identifierWithStrongReference`),
/// the strong reference to the instance is re-added. The strong reference will then need to be removed manually
/// again.
///
/// Accessing and inserting to an InstanceManager is thread safe.
final class ${swiftInstanceManagerClassName(options)} {
// Identifiers are locked to a specific range to avoid collisions with objects
// created simultaneously from Dart.
// Host uses identifiers >= 2^16 and Dart is expected to use values n where,
// 0 <= n < 2^16.
private static let minHostCreatedIdentifier: Int64 = 65536
private let lockQueue = DispatchQueue(label: "${swiftInstanceManagerClassName(options)}")
private let identifiers: NSMapTable<AnyObject, NSNumber> = NSMapTable(
keyOptions: [.weakMemory, .objectPointerPersonality], valueOptions: .strongMemory)
private let weakInstances: NSMapTable<NSNumber, AnyObject> = NSMapTable(
keyOptions: .strongMemory, valueOptions: [.weakMemory, .objectPointerPersonality])
private let strongInstances: NSMapTable<NSNumber, AnyObject> = NSMapTable(
keyOptions: .strongMemory, valueOptions: [.strongMemory, .objectPointerPersonality])
private let finalizerDelegate: ${instanceManagerFinalizerDelegateName(options)}
private var nextIdentifier: Int64 = minHostCreatedIdentifier
public init(finalizerDelegate: ${instanceManagerFinalizerDelegateName(options)}) {
self.finalizerDelegate = finalizerDelegate
}
/// Adds a new instance that was instantiated from Dart.
///
/// The same instance can be added multiple times, but each identifier must be unique. This allows
/// two objects that are equivalent (e.g. conforms to `Equatable`) to both be added.
///
/// - Parameters:
/// - instance: the instance to be stored
/// - identifier: the identifier to be paired with instance. This value must be >= 0 and unique
func addDartCreatedInstance(_ instance: AnyObject, withIdentifier identifier: Int64) {
lockQueue.async {
self.addInstance(instance, withIdentifier: identifier)
}
}
/// Adds a new instance that was instantiated from the host platform.
///
/// - Parameters:
/// - instance: the instance to be stored. This must be unique to all other added instances.
/// - Returns: the unique identifier (>= 0) stored with instance
func addHostCreatedInstance(_ instance: AnyObject) -> Int64 {
assert(!containsInstance(instance), "Instance of \\(instance) has already been added.")
var identifier: Int64 = -1
lockQueue.sync {
identifier = nextIdentifier
nextIdentifier += 1
self.addInstance(instance, withIdentifier: identifier)
}
return identifier
}
/// Removes `instanceIdentifier` and its associated strongly referenced instance, if present, from the manager.
///
/// - Parameters:
/// - instanceIdentifier: the identifier paired to an instance.
/// - Returns: removed instance if the manager contains the given identifier, otherwise `nil` if
/// the manager doesn't contain the value
func removeInstance<T: AnyObject>(withIdentifier instanceIdentifier: Int64) throws -> T? {
var instance: AnyObject? = nil
lockQueue.sync {
instance = strongInstances.object(forKey: NSNumber(value: instanceIdentifier))
strongInstances.removeObject(forKey: NSNumber(value: instanceIdentifier))
}
return instance as? T
}
/// Retrieves the instance associated with identifier.
///
/// - Parameters:
/// - instanceIdentifier: the identifier associated with an instance
/// - Returns: the instance associated with `instanceIdentifier` if the manager contains the value, otherwise
/// `nil` if the manager doesn't contain the value
func instance<T: AnyObject>(forIdentifier instanceIdentifier: Int64) -> T? {
var instance: AnyObject? = nil
lockQueue.sync {
instance = weakInstances.object(forKey: NSNumber(value: instanceIdentifier))
}
return instance as? T
}
private func addInstance(_ instance: AnyObject, withIdentifier identifier: Int64) {
assert(identifier >= 0)
assert(
weakInstances.object(forKey: identifier as NSNumber) == nil,
"Identifier has already been added: \\(identifier)")
identifiers.setObject(NSNumber(value: identifier), forKey: instance)
weakInstances.setObject(instance, forKey: NSNumber(value: identifier))
strongInstances.setObject(instance, forKey: NSNumber(value: identifier))
${_instanceManagerFinalizerName(options)}.attach(to: instance, identifier: identifier, delegate: finalizerDelegate)
}
/// Retrieves the identifier paired with an instance.
///
/// If the manager contains a strong reference to `instance`, it will return the identifier
/// associated with `instance`. If the manager contains only a weak reference to `instance`, a new
/// strong reference to `instance` will be added and will need to be removed again with `removeInstance`.
///
/// If this method returns a nonnull identifier, this method also expects the Dart
/// `${swiftInstanceManagerClassName(options)}` to have, or recreate, a weak reference to the Dart instance the
/// identifier is associated with.
///
/// - Parameters:
/// - instance: an instance that may be stored in the manager
/// - Returns: the identifier associated with `instance` if the manager contains the value, otherwise
/// `nil` if the manager doesn't contain the value
func identifierWithStrongReference(forInstance instance: AnyObject) -> Int64? {
var identifier: Int64? = nil
lockQueue.sync {
if let existingIdentifier = identifiers.object(forKey: instance)?.int64Value {
strongInstances.setObject(instance, forKey: NSNumber(value: existingIdentifier))
identifier = existingIdentifier
}
}
return identifier
}
/// Whether this manager contains the given `instance`.
///
/// - Parameters:
/// - instance: the instance whose presence in this manager is to be tested
/// - Returns: whether this manager contains the given `instance`
func containsInstance(_ instance: AnyObject) -> Bool {
var containsInstance = false
lockQueue.sync {
containsInstance = identifiers.object(forKey: instance) != nil
}
return containsInstance
}
/// Removes all of the instances from this manager.
///
/// The manager will be empty after this call returns.
func removeAllObjects() throws {
lockQueue.sync {
identifiers.removeAllObjects()
weakInstances.removeAllObjects()
strongInstances.removeAllObjects()
nextIdentifier = ${swiftInstanceManagerClassName(options)}.minHostCreatedIdentifier
}
}
/// The number of instances stored as a strong reference.
///
/// For debugging and testing purposes.
internal var strongInstanceCount: Int {
var count: Int = 0
lockQueue.sync {
count = strongInstances.count
}
return count
}
/// The number of instances stored as a weak reference.
///
/// For debugging and testing purposes. NSMapTables that store keys or objects as weak
/// reference will be reclaimed non-deterministically.
internal var weakInstanceCount: Int {
var count: Int = 0
lockQueue.sync {
count = weakInstances.count
}
return count
}
}
''';
}

String _instanceManagerFinalizerName(SwiftOptions options) =>
'${options.fileSpecificClassNameComponent ?? ''}${classNamePrefix}Finalizer';
Loading

0 comments on commit 0da80be

Please sign in to comment.