| // Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:ffi'; | |
| import 'dart:isolate'; | |
| import 'dart:math'; | |
| import 'package:ffi/ffi.dart'; | |
| import 'c_bindings_generated.dart' as c; | |
| import 'internal.dart' | |
| show FailedToLoadProtocolMethodException, GetProtocolName, ObjCBlockBase; | |
| import 'objective_c_bindings_generated.dart' as objc; | |
| import 'runtime_bindings_generated.dart' as r; | |
| import 'selector.dart'; | |
| /// Helper class for building Objective-C objects that implement protocols. | |
| /// | |
| /// Use the `$Builder` class generated by `ffigen` for the protocol to | |
| /// mplement protocol methods. For example: | |
| /// | |
| /// <!-- file://./../../tool/snippets/protocol_builder_snippet.dart#protocol_builder --> | |
| /// ```dart | |
| /// // Build an object that implements an Objective-C protocol. | |
| /// final builder = ObjCProtocolBuilder(); | |
| /// NSStreamDelegate$Builder.addToBuilder( | |
| /// builder, | |
| /// stream_handleEvent_: (stream, event) { | |
| /// // Handle stream event. | |
| /// }, | |
| /// ); | |
| /// final delegate = NSStreamDelegate.as(builder.build()); | |
| /// ``` | |
| class ObjCProtocolBuilder { | |
| final objc.DartProtocolBuilder _builder; | |
| var _built = false; | |
| objc.DartProtocolBuilder get builder => _builder; | |
| ObjCProtocolBuilder({String debugName = 'DOBJCDartProtocol'}) | |
| : _builder = _createBuilder(debugName); | |
| /// Add a method implementation to the protocol. | |
| /// | |
| /// It is not recommended to call this method directly. Instead, use the | |
| /// implement methods on [ObjCProtocolMethod] and its subclasses. | |
| /// | |
| /// Note: You cannot call this method after you have called [build]. | |
| void implementMethod( | |
| Pointer<r.ObjCSelector> sel, | |
| Pointer<Char> signature, | |
| Pointer<Void> trampoline, | |
| ObjCBlockBase block, | |
| ) { | |
| if (_built) { | |
| throw StateError('Protocol is already built'); | |
| } | |
| final blockRef = block.ref; | |
| _builder.implementMethod( | |
| sel, | |
| withBlock: blockRef.pointer.cast(), | |
| withTrampoline: trampoline, | |
| withSignature: signature, | |
| ); | |
| } | |
| /// Builds the object. | |
| /// | |
| /// This can be called multiple times to construct multiple object instances | |
| /// that all implement the same protocol methods using the same functions. | |
| objc.NSObject build({bool keepIsolateAlive = true}) { | |
| if (!_built) { | |
| _builder.registerClass(); | |
| _built = true; | |
| } | |
| var disposePort = c.ILLEGAL_PORT; | |
| if (keepIsolateAlive) { | |
| late final RawReceivePort keepAlivePort; | |
| keepAlivePort = RawReceivePort((_) => keepAlivePort.close()); | |
| disposePort = keepAlivePort.sendPort.nativePort; | |
| } | |
| return _builder.buildInstance(disposePort); | |
| } | |
| /// Add the [protocol] to this implementation. | |
| /// | |
| /// This essentially declares that the implementation implements the protocol. | |
| /// There is no automatic check that ensures that the implementation actually | |
| /// implements all the methods of the protocol. | |
| void addProtocol(objc.Protocol protocol) => _builder.addProtocol(protocol); | |
| static final _rand = Random(); | |
| static objc.DartProtocolBuilder _createBuilder(String debugName) { | |
| final name = '${debugName}_${_rand.nextInt(1 << 32)}'.toNativeUtf8(); | |
| final builder = objc.DartProtocolBuilder.alloc().initWithClassName( | |
| name.cast(), | |
| ); | |
| calloc.free(name); | |
| return builder; | |
| } | |
| } | |
| /// A method in an ObjC protocol. | |
| /// | |
| /// Do not try to construct this class directly. The recommended way of getting | |
| /// a method object is to use FFIgen to generate bindings for the protocol you | |
| /// want to implement. The generated bindings will include a | |
| /// [ObjCProtocolMethod] for each method of the protocol. | |
| class ObjCProtocolMethod<T extends Function> { | |
| final Pointer<r.ObjCProtocolImpl> _proto; | |
| final Pointer<r.ObjCSelector> _sel; | |
| final Pointer<Void> _trampoline; | |
| final Pointer<Char>? _signature; | |
| final ObjCBlockBase Function(T) _createBlock; | |
| /// Only for use by FFIgen bindings. | |
| ObjCProtocolMethod( | |
| this._proto, | |
| this._sel, | |
| this._trampoline, | |
| this._signature, | |
| this._createBlock, | |
| ); | |
| /// Implement this method on the protocol [builder] using a Dart [function]. | |
| /// | |
| /// The implemented method must be invoked by ObjC code running on the same | |
| /// thread as the isolate that called implementMethod. Invoking the method on | |
| /// the wrong thread will result in a crash. | |
| /// | |
| /// Note: You cannot call this method after you have called `builder.build`. | |
| void implement(ObjCProtocolBuilder builder, T? function) { | |
| if (function != null) { | |
| implementWithBlock(builder, _createBlock(function)); | |
| } | |
| } | |
| /// Implement this method on the protocol [builder] using an ObjC [block]. | |
| /// | |
| /// **IMPORTANT**: The [block] must have the same signature as the method, | |
| /// but with an extra `void*` argument as the first parameter, before all the | |
| /// method parameters. Most users should use one of the other `implement` | |
| /// methods, which handles this signature change automatically. | |
| /// | |
| /// Note: You cannot call this method after you have called `builder.build`. | |
| void implementWithBlock(ObjCProtocolBuilder builder, ObjCBlockBase block) => | |
| builder.implementMethod(_sel, _sig, _trampoline, block); | |
| bool get isAvailable => _signature != null; | |
| Pointer<Char> get _sig { | |
| final sig = _signature; | |
| if (sig != null) return sig; | |
| throw FailedToLoadProtocolMethodException(_proto.name, _sel.toDartString()); | |
| } | |
| } | |
| /// A method in an ObjC protocol that can be implemented as a listener. | |
| /// | |
| /// Do not try to construct this class directly. The recommended way of getting | |
| /// a method object is to use FFIgen to generate bindings for the protocol you | |
| /// want to implement. The generated bindings will include a | |
| /// [ObjCProtocolMethod] for each method of the protocol. | |
| class ObjCProtocolListenableMethod<T extends Function> | |
| extends ObjCProtocolMethod<T> { | |
| final ObjCBlockBase Function(T) _createListenerBlock; | |
| final ObjCBlockBase Function(T) _createBlockingBlock; | |
| /// Only for use by FFIgen bindings. | |
| ObjCProtocolListenableMethod( | |
| super._proto, | |
| super._sel, | |
| super._trampoline, | |
| super._signature, | |
| super._createBlock, | |
| this._createListenerBlock, | |
| this._createBlockingBlock, | |
| ); | |
| /// Implement this method on the protocol [builder] as a listener using a Dart | |
| /// [function]. | |
| /// | |
| /// This is based on FFI's NativeCallable.listener, and has the same | |
| /// capabilities and limitations. This method can be invoked by ObjC from any | |
| /// thread, but only supports void functions, and is not run synchronously. | |
| /// See NativeCallable.listener for more details. | |
| /// | |
| /// Note: You cannot call this method after you have called `builder.build`. | |
| void implementAsListener(ObjCProtocolBuilder builder, T? function) { | |
| if (function != null) { | |
| implementWithBlock(builder, _createListenerBlock(function)); | |
| } | |
| } | |
| /// Implement this method on the protocol [builder] as a blocking listener | |
| /// using a Dart [function]. | |
| /// | |
| /// This callback can be invoked from any native thread, and will block the | |
| /// caller until the callback is handled by the Dart isolate that implemented | |
| /// the method. Async functions are not supported. | |
| /// | |
| /// Note: You cannot call this method after you have called `builder.build`. | |
| void implementAsBlocking(ObjCProtocolBuilder builder, T? function) { | |
| if (function != null) { | |
| implementWithBlock(builder, _createBlockingBlock(function)); | |
| } | |
| } | |
| } |