diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 153df02..faea4ee 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -156,6 +156,27 @@ jobs: run: flutter build apk working-directory: example + build_web_ubuntu: + name: Build Web on Ubuntu + if: ${{ !contains(github.event.pull_request.labels.*.name, 'ci:skip') }} + strategy: + matrix: + version: ['2.10.5', '3.0.0', '3.3.9', '3.7.3', '3.10.0'] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-java@v1 + with: + java-version: '11' + - uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ matrix.version }} + cache: true + - run: flutter pub get + - name: Run flutter build web + run: flutter build web + working-directory: example + run_flutter_unit_test: name: Run flutter unit test if: ${{ !contains(github.event.pull_request.labels.*.name, 'ci:skip') }} @@ -175,6 +196,25 @@ jobs: - run: flutter packages get - run: flutter test + run_flutter_unit_test_web: + name: Run flutter unit test web + if: ${{ !contains(github.event.pull_request.labels.*.name, 'ci:skip') }} + strategy: + matrix: + version: ['2.10.5', '3.0.0', '3.3.9', '3.7.3', '3.10.0'] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-java@v1 + with: + java-version: '11' + - uses: subosito/flutter-action@v1 + with: + flutter-version: ${{ matrix.version }} + cache: true + - run: flutter packages get + - run: flutter test -d chrome + integration_test_windows: name: windows integration test if: ${{ !contains(github.event.pull_request.labels.*.name, 'ci:skip') }} @@ -191,7 +231,7 @@ jobs: - name: windows integration test run: | flutter packages get - flutter test integration_test + flutter test integration_test -d windows working-directory: example integration_test_ios: @@ -234,7 +274,7 @@ jobs: - name: run macos integration test run: | flutter packages get - flutter test integration_test/iris_event_smoke_test.dart + flutter test integration_test/iris_event_smoke_test.dart -d macos working-directory: example integration_test_android: diff --git a/.metadata b/.metadata index 2dc72d8..af41864 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled. version: - revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + revision: 796c8ef79279f9c774545b3771238c3098dbefab channel: stable project_type: plugin @@ -13,17 +13,20 @@ project_type: plugin migration: platforms: - platform: root - create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 - base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab - platform: android - create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 - base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab - platform: ios - create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 - base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab - platform: macos - create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 - base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab + - platform: web + create_revision: 796c8ef79279f9c774545b3771238c3098dbefab + base_revision: 796c8ef79279f9c774545b3771238c3098dbefab - platform: windows create_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 base_revision: fb57da5f945d02ef4f98dfd9409a72b7cce74268 diff --git a/example/.gitignore b/example/.gitignore index 61b1e62..a956bd6 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -33,7 +33,6 @@ migrate_working_dir/ /build/ # Web related -lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols diff --git a/example/integration_test/iris_event_smoke_test.dart b/example/integration_test/iris_event_smoke_test.dart index 9a27e58..47038e0 100644 --- a/example/integration_test/iris_event_smoke_test.dart +++ b/example/integration_test/iris_event_smoke_test.dart @@ -1,23 +1,21 @@ - import 'dart:isolate'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:iris_method_channel/iris_method_channel.dart'; +import 'package:iris_method_channel/src/platform/io/iris_event_io.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('IrisEvent smoke test', - (tester) async { - await tester.pumpAndSettle(); + testWidgets('IrisEvent smoke test', (tester) async { + await tester.pumpAndSettle(); - IrisEvent irisEvent = IrisEvent(); - irisEvent.initialize(); - final testPort = ReceivePort(); - irisEvent.registerEventHandler(testPort.sendPort); - irisEvent.unregisterEventHandler(testPort.sendPort); - irisEvent.onEventPtr; - irisEvent.dispose(); - }); -} \ No newline at end of file + IrisEventIO irisEvent = IrisEventIO(); + irisEvent.initialize(); + final testPort = ReceivePort(); + irisEvent.registerEventHandler(testPort.sendPort); + irisEvent.unregisterEventHandler(testPort.sendPort); + irisEvent.onEventPtr; + irisEvent.dispose(); + }); +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 2086d42..c00de06 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,9 +1,52 @@ import 'package:flutter/material.dart'; +import 'package:iris_method_channel/iris_method_channel.dart'; void main() { runApp(const MyApp()); } +class _FakePlatformBindingsDelegateInterface + implements PlatformBindingsDelegateInterface { + @override + int callApi(IrisMethodCall methodCall, IrisApiEngineHandle apiEnginePtr, + IrisApiParamHandle param) { + return 0; + } + + @override + Future callApiAsync(IrisMethodCall methodCall, + IrisApiEngineHandle apiEnginePtr, IrisApiParamHandle param) async { + return CallApiResult(irisReturnCode: 0, data: {}); + } + + @override + CreateApiEngineResult createApiEngine(List args) { + return const CreateApiEngineResult(IrisApiEngineHandle(0)); + } + + @override + IrisEventHandlerHandle createIrisEventHandler( + IrisCEventHandlerHandle eventHandler) { + return const IrisEventHandlerHandle(0); + } + + @override + void destroyIrisEventHandler(IrisEventHandlerHandle handler) {} + + @override + void destroyNativeApiEngine(IrisApiEngineHandle apiEnginePtr) {} + + @override + void initialize() {} +} + +class _FakePlatformBindingsProvider extends PlatformBindingsProvider { + @override + PlatformBindingsDelegateInterface provideNativeBindingDelegate() { + return _FakePlatformBindingsDelegateInterface(); + } +} + class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @@ -12,7 +55,7 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - String _platformVersion = 'Unknown'; + final String _platformVersion = 'Unknown'; @override void initState() { @@ -22,16 +65,10 @@ class _MyAppState extends State { // Platform messages are asynchronous, so we initialize in an async method. Future initPlatformState() async { - String platformVersion = ''; - - // If the widget was removed from the tree while the asynchronous platform - // message was in flight, we want to discard the reply rather than calling - // setState to update our non-existent appearance. - if (!mounted) return; - - setState(() { - _platformVersion = platformVersion; - }); + IrisMethodChannel irisMethodChannel = + IrisMethodChannel(_FakePlatformBindingsProvider()); + await irisMethodChannel.initilize([]); + await irisMethodChannel.dispose(); } @override diff --git a/example/web/favicon.png b/example/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/example/web/favicon.png differ diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/example/web/icons/Icon-192.png differ diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/example/web/icons/Icon-512.png differ diff --git a/example/web/icons/Icon-maskable-192.png b/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/example/web/icons/Icon-maskable-192.png differ diff --git a/example/web/icons/Icon-maskable-512.png b/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/example/web/icons/Icon-maskable-512.png differ diff --git a/example/web/index.html b/example/web/index.html new file mode 100644 index 0000000..87859b9 --- /dev/null +++ b/example/web/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + iris_method_channel_example + + + + + + + + + + diff --git a/example/web/manifest.json b/example/web/manifest.json new file mode 100644 index 0000000..59c04a6 --- /dev/null +++ b/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "iris_method_channel_example", + "short_name": "iris_method_channel_example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "Demonstrates how to use the iris_method_channel plugin.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/lib/iris_method_channel.dart b/lib/iris_method_channel.dart index 7fbf7a0..099257c 100644 --- a/lib/iris_method_channel.dart +++ b/lib/iris_method_channel.dart @@ -1,5 +1,7 @@ -export 'src/bindings/native_iris_api_common_bindings.dart'; -export 'src/iris_event.dart'; +export 'src/iris_handles.dart'; export 'src/iris_method_channel.dart'; -export 'src/native_bindings_delegate.dart'; +export 'src/platform/iris_event_interface.dart'; +export 'src/platform/iris_method_channel_interface.dart'; +export 'src/platform/platform_bindings_delegate_interface.dart'; +export 'src/platform/utils.dart'; export 'src/scoped_objects.dart'; diff --git a/lib/iris_method_channel_bindings_io.dart b/lib/iris_method_channel_bindings_io.dart new file mode 100644 index 0000000..048607d --- /dev/null +++ b/lib/iris_method_channel_bindings_io.dart @@ -0,0 +1,3 @@ +/// Export the common bindings of iris of `dart:io` + +export 'src/platform/io/bindings/native_iris_api_common_bindings.dart'; diff --git a/lib/iris_method_channel_bindings_web.dart b/lib/iris_method_channel_bindings_web.dart new file mode 100644 index 0000000..33a3e92 --- /dev/null +++ b/lib/iris_method_channel_bindings_web.dart @@ -0,0 +1,3 @@ +/// Export the common bindings of iris of web + +export 'src/platform/web/bindings/iris_api_common_bindings_js.dart'; diff --git a/lib/iris_method_channel_web.dart b/lib/iris_method_channel_web.dart new file mode 100644 index 0000000..333dabe --- /dev/null +++ b/lib/iris_method_channel_web.dart @@ -0,0 +1,17 @@ +// In order to *not* need this ignore, consider extracting the "web" version +// of your plugin as a separate package, instead of inlining it in the same +// package as the core of your plugin. +// ignore: avoid_web_libraries_in_flutter + +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +/// A web implementation of the IrisMethodChannelPlatform of the IrisMethodChannel plugin. +class IrisMethodChannelWeb { + /// Constructs a IrisMethodChannelWeb + IrisMethodChannelWeb(); + + // ignore: public_member_api_docs + static void registerWith(Registrar registrar) { + // do nothing + } +} diff --git a/lib/src/iris_event.dart b/lib/src/iris_event.dart deleted file mode 100644 index 4db4b18..0000000 --- a/lib/src/iris_event.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'dart:ffi' as ffi; -import 'dart:io'; -import 'dart:isolate'; -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; - -import 'package:iris_method_channel/src/bindings/native_iris_event_bindings.dart'; - -const _libName = 'iris_method_channel'; - -/// Iris event handler interface -abstract class EventLoopEventHandler { - /// Callback when received events - bool handleEvent( - String eventName, String eventData, List buffers) { - return handleEventInternal(eventName, eventData, buffers); - } - - @protected - // ignore: public_member_api_docs - bool handleEventInternal( - String eventName, String eventData, List buffers); -} - -ffi.DynamicLibrary _loadLib() { - if (Platform.isWindows) { - return ffi.DynamicLibrary.open('$_libName.dll'); - } - - if (Platform.isAndroid) { - return ffi.DynamicLibrary.open('lib$_libName.so'); - } - - return ffi.DynamicLibrary.process(); -} - -/// Object to hold the iris event infos -class IrisEventMessage { - /// Construct [IrisEventMessage] - const IrisEventMessage(this.event, this.data, this.buffers); - - /// The event name - final String event; - - /// The json data - final String data; - - /// Byte buffers - final List buffers; -} - -/// Iris event handler which forward events to dart side. -/// See native implementation src/iris_event.cc -class IrisEvent { - /// Construct [IrisEvent] - IrisEvent() { - _nativeIrisEventBinding = NativeIrisEventBinding(_loadLib()); - } - - /// Parse message to [IrisEventMessage] object - // ignore: avoid_annotating_with_dynamic - static IrisEventMessage parseMessage(dynamic message) { - final dataList = List.from(message); - final String event = dataList[0]; - String data = dataList[1] as String; - if (data.isEmpty) { - data = '{}'; - } - - String res = dataList[1] as String; - if (res.isEmpty) { - res = '{}'; - } - final List buffers = dataList.length == 3 - ? List.from(dataList[2]) - : []; - - return IrisEventMessage(event, data, buffers); - } - - late final NativeIrisEventBinding _nativeIrisEventBinding; - - /// Initialize the [IrisEvent], which call `InitDartApiDL` directly - void initialize() { - _nativeIrisEventBinding.InitDartApiDL(ffi.NativeApi.initializeApiDLData); - } - - /// Register dart [SendPort] to send the message from native - void registerEventHandler(SendPort sendPort) { - _nativeIrisEventBinding.RegisterDartPort(sendPort.nativePort); - } - - /// Unregister dart [SendPort] which used to send the message from native - void unregisterEventHandler(SendPort sendPort) { - _nativeIrisEventBinding.UnregisterDartPort(sendPort.nativePort); - } - - /// Clean up native resources - void dispose() { - _nativeIrisEventBinding.Dispose(); - } - - /// Get the onEvent function pointer from C - ffi.Pointer)>> - get onEventPtr => _nativeIrisEventBinding.addresses.OnEvent; -} diff --git a/lib/src/iris_handles.dart b/lib/src/iris_handles.dart new file mode 100644 index 0000000..0f88c26 --- /dev/null +++ b/lib/src/iris_handles.dart @@ -0,0 +1,56 @@ +/// Callable class that represent the `Handle` type. You can get the actual handle +/// value as a callable function. e.g., +/// ```dart +/// final IrisHandle handle = IrisApiEngineHandle(10); +/// final value = handle(); // value = 10 +/// ``` +abstract class IrisHandle { + /// Construct the [IrisHandle] + const IrisHandle(); + + /// Callable function that allow you to get the handle value as function. + Object call(); +} + +/// [IrisHandle] implementation that hold a value of [Object] type. And return the +/// value when the callable function is called. +class _ObjectIrisHandle extends IrisHandle { + const _ObjectIrisHandle(this._h); + + final Object _h; + + @override + Object call() { + return _h; + } +} + +/// The [IrisHandle] of the iris's `IrisApiEngine` +class IrisApiEngineHandle extends _ObjectIrisHandle { + /// Construct the [IrisApiEngineHandle] + const IrisApiEngineHandle(Object h) : super(h); +} + +/// The [IrisHandle] of the iris's `ApiParam` +class IrisApiParamHandle extends _ObjectIrisHandle { + /// Construct the [IrisApiParamHandle] + const IrisApiParamHandle(Object h) : super(h); +} + +/// The [IrisHandle] of the iris's `IrisCEventHandler` +class IrisCEventHandlerHandle extends _ObjectIrisHandle { + /// Construct the [IrisCEventHandlerHandle] + const IrisCEventHandlerHandle(Object h) : super(h); +} + +/// The [IrisHandle] of the iris's `IrisEventHandler` +class IrisEventHandlerHandle extends _ObjectIrisHandle { + /// Construct the [IrisEventHandlerHandle] + const IrisEventHandlerHandle(Object h) : super(h); +} + +/// The [IrisHandle] of the `BufferParam` +class BufferParamHandle extends _ObjectIrisHandle { + /// Construct the [BufferParamHandle] + const BufferParamHandle(Object h) : super(h); +} diff --git a/lib/src/iris_method_channel.dart b/lib/src/iris_method_channel.dart index bdc66e2..c2fabd0 100644 --- a/lib/src/iris_method_channel.dart +++ b/lib/src/iris_method_channel.dart @@ -1,413 +1,34 @@ import 'dart:async'; -import 'dart:ffi' as ffi; -import 'dart:isolate'; -import 'dart:typed_data'; -import 'package:async/async.dart'; -import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart' - show SynchronousFuture, VoidCallback, debugPrint, visibleForTesting; + show VoidCallback, debugPrint, visibleForTesting; import 'package:flutter/services.dart' show MethodChannel; -import 'package:iris_method_channel/src/bindings/native_iris_api_common_bindings.dart' - as iris; -import 'package:iris_method_channel/src/iris_event.dart'; -import 'package:iris_method_channel/src/native_bindings_delegate.dart'; -import 'package:iris_method_channel/src/scoped_objects.dart'; +import 'package:iris_method_channel/iris_method_channel.dart'; +import 'package:iris_method_channel/src/platform/iris_method_channel_internal.dart'; // ignore_for_file: public_member_api_docs -class IrisMethodCall { - const IrisMethodCall(this.funcName, this.params, - {this.buffers, this.rawBufferParams}); - final String funcName; - final String params; - final List? buffers; - final List? rawBufferParams; -} - -const int kBasicResultLength = 64 * 1024; -const int kDisposedIrisMethodCallReturnCode = 1000; -const Map kDisposedIrisMethodCallData = {'result': 0}; - -class CallApiResult { - CallApiResult( - {required this.irisReturnCode, required this.data, this.rawData = ''}); - - final int irisReturnCode; - - final Map data; - - // TODO(littlegnal): Remove rawData after EP-253 landed. - final String rawData; -} - -class _EventHandlerHolderKey implements ScopedKey { - const _EventHandlerHolderKey({ - required this.registerName, - required this.unregisterName, - }); - final String registerName; - final String unregisterName; - - @override - bool operator ==(Object other) { - if (other.runtimeType != runtimeType) { - return false; - } - return other is _EventHandlerHolderKey && - other.registerName == registerName && - other.unregisterName == unregisterName; - } - - @override - int get hashCode => Object.hash(registerName, unregisterName); -} - -@visibleForTesting -class EventHandlerHolder - with ScopedDisposableObjectMixin - implements DisposableObject { - EventHandlerHolder({required this.key}); - final _EventHandlerHolderKey key; - final Set _eventHandlers = {}; - - int nativeEventHandlerIntPtr = 0; - - void addEventHandler(EventLoopEventHandler eventHandler) { - _eventHandlers.add(eventHandler); - } - - Future removeEventHandler(EventLoopEventHandler eventHandler) async { - _eventHandlers.remove(eventHandler); - } - - Set getEventHandlers() => _eventHandlers; - - @override - Future dispose() { - _eventHandlers.clear(); - return SynchronousFuture(null); - } -} - -Uint8List uint8ListFromPtr(int intPtr, int length) { - final ptr = ffi.Pointer.fromAddress(intPtr); - final memoryList = ptr.asTypedList(length); - return Uint8List.fromList(memoryList); -} - -ffi.Pointer uint8ListToPtr(Uint8List buffer) { - final ffi.Pointer bufferData = - calloc.allocate(buffer.length); - - final pointerList = bufferData.asTypedList(buffer.length); - pointerList.setAll(0, buffer); - - return bufferData.cast(); -} - -void freePointer(ffi.Pointer ptr) { - calloc.free(ptr); -} - -class _Messenger implements DisposableObject { - _Messenger(this.requestPort, this.responseQueue); - final SendPort requestPort; - final StreamQueue responseQueue; - bool _isDisposed = false; - - Future send(_Request request) async { - if (_isDisposed) { - return CallApiResult( - irisReturnCode: kDisposedIrisMethodCallReturnCode, - data: kDisposedIrisMethodCallData); - } - requestPort.send(request); - return await responseQueue.next; - } - - Future> listSend(_Request request) async { - if (_isDisposed) { - return [ - CallApiResult( - irisReturnCode: kDisposedIrisMethodCallReturnCode, - data: kDisposedIrisMethodCallData) - ]; - } - requestPort.send(request); - return await responseQueue.next; - } - - @override - Future dispose() async { - if (_isDisposed) { - return; - } - _isDisposed = true; - requestPort.send(null); - await responseQueue.cancel(); - } -} - -class _InitilizationArgs { - _InitilizationArgs( - this.apiCallPortSendPort, - this.eventPortSendPort, - this.onExitSendPort, - this.provider, - this.argNativeHandles, - ); - - final SendPort apiCallPortSendPort; - final SendPort eventPortSendPort; - final SendPort? onExitSendPort; - final NativeBindingsProvider provider; - final List argNativeHandles; -} - -class InitilizationResult { - InitilizationResult( - this._apiCallPortSendPort, - this.irisApiEngineNativeHandle, - this.extraData, - this._debugIrisCEventHandlerNativeHandle, - this._debugIrisEventHandlerNativeHandle, - ); - - final SendPort _apiCallPortSendPort; - final int irisApiEngineNativeHandle; - - /// Same as [CreateNativeApiEngineResult.extraData] - final Map extraData; - - final int? _debugIrisCEventHandlerNativeHandle; - final int? _debugIrisEventHandlerNativeHandle; -} - -/// Listener when hot restarted. -/// -/// You can release some native resources, such like delete the pointer which is -/// created by ffi. -/// -/// NOTE that: -/// * This listener is only received on debug mode. -/// * You should not comunicate with the [IrisMethodChannel] anymore inside this listener. -/// * You should not do some asynchronous jobs inside this listener. -typedef HotRestartListener = void Function(Object? message); - -class _HotRestartFinalizer { - _HotRestartFinalizer(this.provider) { - assert(() { - _onExitPort = ReceivePort(); - _onExitSubscription = _onExitPort?.listen(_finalize); - - return true; - }()); - } - - final NativeBindingsProvider provider; - - final List _hotRestartListeners = []; - - ReceivePort? _onExitPort; - StreamSubscription? _onExitSubscription; - - SendPort? get onExitSendPort => _onExitPort?.sendPort; - - bool _isFinalize = false; - - int? _debugIrisApiEngineNativeHandle; - set debugIrisApiEngineNativeHandle(int value) { - _debugIrisApiEngineNativeHandle = value; - } - - int? _debugIrisCEventHandlerNativeHandle; - set debugIrisCEventHandlerNativeHandle(int? value) { - _debugIrisCEventHandlerNativeHandle = value; - } - - int? _debugIrisEventHandlerNativeHandle; - set debugIrisEventHandlerNativeHandle(int? value) { - _debugIrisEventHandlerNativeHandle = value; - } - - // ignore: avoid_annotating_with_dynamic - void _finalize(dynamic msg) { - if (_isFinalize) { - return; - } - _isFinalize = true; - - // We will receive a value of `0` as message when the `IrisMethodChannel.dispose` - // is called normally, which will call the `Isolate.exit(onExitSendPort, 0)` - // to send a value of `0` as a exit response. See `_execute` function for more detail. - if (msg != null && msg == 0) { - return; - } - - for (final listener in _hotRestartListeners.reversed) { - listener(msg); - } - - // When hot restart happen, the `IrisMethodChannel.dispose` function will not - // be called normally, cause the native API engine can not be destroy correctly, - // so we need to release the native resources which create by the - // `NativeBindingDelegate` explicitly. - final nativeBindingDelegate = provider.provideNativeBindingDelegate(); - nativeBindingDelegate.initialize(); - - nativeBindingDelegate.destroyNativeApiEngine( - ffi.Pointer.fromAddress(_debugIrisApiEngineNativeHandle!)); - - calloc.free(ffi.Pointer.fromAddress(_debugIrisCEventHandlerNativeHandle!)); - nativeBindingDelegate.destroyIrisEventHandler( - ffi.Pointer.fromAddress(_debugIrisEventHandlerNativeHandle!)); - - final irisEvent = provider.provideIrisEvent(); - irisEvent.dispose(); - - _onExitSubscription?.cancel(); - } - - VoidCallback addHotRestartListener(HotRestartListener listener) { - assert(() { - final Object? debugCheckForReturnedFuture = listener as dynamic; - if (debugCheckForReturnedFuture is Future) { - throw UnsupportedError( - 'HotRestartListener must be a void method without an `async` keyword.'); - } - return true; - }()); - - _hotRestartListeners.add(listener); - return () { - removeHotRestartListener(listener); - }; - } - - void removeHotRestartListener(HotRestartListener listener) { - _hotRestartListeners.remove(listener); +class IrisMethodChannel { + IrisMethodChannel(this._nativeBindingsProvider) { + _irisMethodChannelInternal = + createIrisMethodChannelInternal(_nativeBindingsProvider); } - void dispose() { - _hotRestartListeners.clear(); - } -} + final PlatformBindingsProvider _nativeBindingsProvider; -class IrisMethodChannel { - IrisMethodChannel(this._nativeBindingsProvider); + late final IrisMethodChannelInternal _irisMethodChannelInternal; - final NativeBindingsProvider _nativeBindingsProvider; + final MethodChannel _channel = const MethodChannel('iris_method_channel'); bool _initilized = false; - late _Messenger messenger; - late StreamSubscription evntSubscription; @visibleForTesting final ScopedObjects scopedEventHandlers = ScopedObjects(); - late int _nativeHandle; - - @visibleForTesting - late Isolate workerIsolate; - late _HotRestartFinalizer _hotRestartFinalizer; - - final MethodChannel _channel = const MethodChannel('iris_method_channel'); - - static Future _execute(_InitilizationArgs args) async { - final SendPort mainApiCallSendPort = args.apiCallPortSendPort; - final SendPort mainEventSendPort = args.eventPortSendPort; - final SendPort? onExitSendPort = args.onExitSendPort; - final NativeBindingsProvider provider = args.provider; - - final List> argsInner = args.argNativeHandles - .map>((e) => ffi.Pointer.fromAddress(e)) - .toList(); - - final apiCallPort = ReceivePort(); - - final nativeBindingDelegate = provider.provideNativeBindingDelegate(); - final irisEvent = provider.provideIrisEvent(); - - final _IrisMethodChannelNative executor = - _IrisMethodChannelNative(nativeBindingDelegate, irisEvent); - final CreateNativeApiEngineResult executorInitilizationResult = - executor.initilize(mainEventSendPort, argsInner); - - int? debugIrisCEventHandlerNativeHandle; - int? debugIrisEventHandlerNativeHandle; - - assert(() { - debugIrisCEventHandlerNativeHandle = - executor.irisCEventHandlerNativeHandle; - debugIrisEventHandlerNativeHandle = executor.irisEventHandlerNativeHandle; - - return true; - }()); - - final InitilizationResult initilizationResponse = InitilizationResult( - apiCallPort.sendPort, - executor.irisApiEngineNativeHandle, - executorInitilizationResult.extraData, - debugIrisCEventHandlerNativeHandle, - debugIrisEventHandlerNativeHandle, - ); - - mainApiCallSendPort.send(initilizationResponse); - - // Wait for messages from the main isolate. - await for (final request in apiCallPort) { - if (request == null) { - // Exit if the main isolate sends a null message, indicating there are no - // more files to read and parse. - break; - } - - assert(request is _Request); - - if (request is _ApiCallRequest) { - final result = executor.invokeMethod(request.methodCall); - - mainApiCallSendPort.send(result); - } else if (request is _ApiCallListRequest) { - final results = []; - for (final methodCall in request.methodCalls) { - final result = executor.invokeMethod(methodCall); - results.add(result); - } - - mainApiCallSendPort.send(results); - } else if (request is _CreateNativeEventHandlerRequest) { - final result = executor.createNativeEventHandler(request.methodCall); - mainApiCallSendPort.send(result); - } else if (request is _CreateNativeEventHandlerListRequest) { - final results = []; - for (final methodCall in request.methodCalls) { - final result = executor.createNativeEventHandler(methodCall); - results.add(result); - } - - mainApiCallSendPort.send(results); - } else if (request is _DestroyNativeEventHandlerRequest) { - final result = executor.destroyNativeEventHandler(request.methodCall); - mainApiCallSendPort.send(result); - } else if (request is _DestroyNativeEventHandlerListRequest) { - final results = []; - for (final methodCall in request.methodCalls) { - final result = executor.destroyNativeEventHandler(methodCall); - results.add(result); - } - - mainApiCallSendPort.send(results); - } - } - - executor.dispose(); - Isolate.exit(onExitSendPort, 0); - } void _setuponDetachedFromEngineListener() { _channel.setMethodCallHandler((call) async { if (call.method == 'onDetachedFromEngine_fromPlatform') { - debugPrint('Receive the onDetachedFromEngine callback, clean the native resources.'); + debugPrint( + 'Receive the onDetachedFromEngine callback, clean the native resources.'); dispose(); return true; } @@ -421,60 +42,12 @@ class IrisMethodChannel { return null; } - final apiCallPort = ReceivePort(); - final eventPort = ReceivePort(); - _setuponDetachedFromEngineListener(); - _hotRestartFinalizer = _HotRestartFinalizer(_nativeBindingsProvider); - - workerIsolate = await Isolate.spawn( - _execute, - _InitilizationArgs( - apiCallPort.sendPort, - eventPort.sendPort, - _hotRestartFinalizer.onExitSendPort, - _nativeBindingsProvider, - args, - ), - onExit: _hotRestartFinalizer.onExitSendPort, - ); - - // Convert the ReceivePort into a StreamQueue to receive messages from the - // spawned isolate using a pull-based interface. Events are stored in this - // queue until they are accessed by `events.next`. - // final events = StreamQueue(p); - final responseQueue = StreamQueue(apiCallPort); - - // The first message from the spawned isolate is a SendPort. This port is - // used to communicate with the spawned isolate. - // SendPort sendPort = await events.next; - final msg = await responseQueue.next; - assert(msg is InitilizationResult); - final initilizationResult = msg as InitilizationResult; - final requestPort = initilizationResult._apiCallPortSendPort; - _nativeHandle = initilizationResult.irisApiEngineNativeHandle; - - assert(() { - _hotRestartFinalizer.debugIrisApiEngineNativeHandle = - initilizationResult.irisApiEngineNativeHandle; - _hotRestartFinalizer.debugIrisCEventHandlerNativeHandle = - initilizationResult._debugIrisCEventHandlerNativeHandle; - _hotRestartFinalizer.debugIrisEventHandlerNativeHandle = - initilizationResult._debugIrisEventHandlerNativeHandle; - - return true; - }()); - - messenger = _Messenger(requestPort, responseQueue); - - evntSubscription = eventPort.listen((message) { - if (!_initilized) { - return; - } - - final eventMessage = IrisEvent.parseMessage(message); + final initilizationResult = + await _irisMethodChannelInternal.initilize(args); + _irisMethodChannelInternal.setIrisEventMessageListener((eventMessage) { bool handled = false; for (final sub in scopedEventHandlers.values) { final scopedObjects = sub as DisposableScopedObjects; @@ -513,8 +86,9 @@ class IrisMethodChannel { irisReturnCode: kDisposedIrisMethodCallReturnCode, data: kDisposedIrisMethodCallData); } + final CallApiResult result = - await messenger.send(_ApiCallRequest(methodCall)); + await _irisMethodChannelInternal.execute(ApiCallRequest(methodCall)); return result; } @@ -529,8 +103,8 @@ class IrisMethodChannel { .toList(); } - final List result = - await messenger.listSend(_ApiCallListRequest(methodCalls)); + final List result = await _irisMethodChannelInternal + .listExecute(ApiCallListRequest(methodCalls)); return result; } @@ -540,11 +114,8 @@ class IrisMethodChannel { return; } _initilized = false; - _hotRestartFinalizer.dispose(); - await scopedEventHandlers.clear(); - await evntSubscription.cancel(); - await messenger.dispose(); + await _irisMethodChannelInternal.dispose(); } Future registerEventHandler( @@ -557,14 +128,14 @@ class IrisMethodChannel { final DisposableScopedObjects subScopedObjects = scopedEventHandlers .putIfAbsent(scopedEvent.scopedKey, () => DisposableScopedObjects()); - final eventKey = _EventHandlerHolderKey( + final eventKey = EventHandlerHolderKey( registerName: scopedEvent.registerName, unregisterName: scopedEvent.unregisterName, ); final EventHandlerHolder holder = subScopedObjects.putIfAbsent( eventKey, () => EventHandlerHolder( - key: _EventHandlerHolderKey( + key: EventHandlerHolderKey( registerName: scopedEvent.registerName, unregisterName: scopedEvent.unregisterName, ), @@ -572,8 +143,9 @@ class IrisMethodChannel { late CallApiResult result; if (holder.getEventHandlers().isEmpty) { - result = await messenger.send(_CreateNativeEventHandlerRequest( - IrisMethodCall(eventKey.registerName, params))); + result = await _irisMethodChannelInternal.execute( + CreateNativeEventHandlerRequest( + IrisMethodCall(eventKey.registerName, params))); final nativeEventHandlerIntPtr = result.data['observerIntPtr']; holder.nativeEventHandlerIntPtr = nativeEventHandlerIntPtr; @@ -596,7 +168,7 @@ class IrisMethodChannel { final DisposableScopedObjects? subScopedObjects = scopedEventHandlers.get(scopedEvent.scopedKey); - final eventKey = _EventHandlerHolderKey( + final eventKey = EventHandlerHolderKey( registerName: scopedEvent.registerName, unregisterName: scopedEvent.unregisterName, ); @@ -604,11 +176,14 @@ class IrisMethodChannel { if (holder != null) { holder.removeEventHandler(scopedEvent.handler); if (holder.getEventHandlers().isEmpty) { - return messenger.send(_DestroyNativeEventHandlerRequest( + return _irisMethodChannelInternal + .execute(DestroyNativeEventHandlerRequest( IrisMethodCall( scopedEvent.unregisterName, params, - rawBufferParams: [BufferParam(holder.nativeEventHandlerIntPtr, 1)], + rawBufferParams: [ + BufferParam(BufferParamHandle(holder.nativeEventHandlerIntPtr), 1) + ], ), )); } @@ -634,13 +209,14 @@ class IrisMethodChannel { holder.key.unregisterName, '', rawBufferParams: [ - BufferParam(holder.nativeEventHandlerIntPtr, 1) + BufferParam( + BufferParamHandle(holder.nativeEventHandlerIntPtr), 1) ], )) .toList(); - await messenger - .listSend(_DestroyNativeEventHandlerListRequest(methodCalls)); + await _irisMethodChannelInternal + .listExecute(DestroyNativeEventHandlerListRequest(methodCalls)); await holder.dispose(); } @@ -650,201 +226,24 @@ class IrisMethodChannel { } } - int getNativeHandle() { + int getApiEngineHandle() { if (!_initilized) { return 0; } - return _nativeHandle; + return _irisMethodChannelInternal.getApiEngineHandle(); } VoidCallback addHotRestartListener(HotRestartListener listener) { - return _hotRestartFinalizer.addHotRestartListener(listener); + return _irisMethodChannelInternal.addHotRestartListener(listener); } void removeHotRestartListener(HotRestartListener listener) { - _hotRestartFinalizer.removeHotRestartListener(listener); - } -} - -abstract class _Request {} - -abstract class _IrisMethodCallRequest implements _Request { - const _IrisMethodCallRequest(this.methodCall); - - final IrisMethodCall methodCall; -} - -abstract class _IrisMethodCallListRequest implements _Request { - const _IrisMethodCallListRequest(this.methodCalls); - - final List methodCalls; -} - -class _ApiCallRequest extends _IrisMethodCallRequest { - const _ApiCallRequest(IrisMethodCall methodCall) : super(methodCall); -} - -// ignore: unused_element -class _ApiCallListRequest extends _IrisMethodCallListRequest { - const _ApiCallListRequest(List methodCalls) - : super(methodCalls); -} - -class _CreateNativeEventHandlerRequest extends _IrisMethodCallRequest { - const _CreateNativeEventHandlerRequest(IrisMethodCall methodCall) - : super(methodCall); -} - -// ignore: unused_element -class _CreateNativeEventHandlerListRequest extends _IrisMethodCallListRequest { - const _CreateNativeEventHandlerListRequest(List methodCalls) - : super(methodCalls); -} - -class _DestroyNativeEventHandlerRequest extends _IrisMethodCallRequest { - const _DestroyNativeEventHandlerRequest(IrisMethodCall methodCall) - : super(methodCall); -} - -class _DestroyNativeEventHandlerListRequest extends _IrisMethodCallListRequest { - const _DestroyNativeEventHandlerListRequest(List methodCalls) - : super(methodCalls); -} - -class _IrisMethodChannelNative { - _IrisMethodChannelNative(this._nativeIrisApiEngineBinding, this._irisEvent); - final NativeBindingDelegate _nativeIrisApiEngineBinding; - - ffi.Pointer? _irisApiEnginePtr; - int get irisApiEngineNativeHandle { - assert(_irisApiEnginePtr != null); - return _irisApiEnginePtr!.address; - } - - final IrisEvent _irisEvent; - ffi.Pointer? _irisCEventHandler; - int get irisCEventHandlerNativeHandle { - assert(_irisCEventHandler != null); - return _irisCEventHandler!.address; - } - - ffi.Pointer? _irisEventHandler; - int get irisEventHandlerNativeHandle { - assert(_irisEventHandler != null); - return _irisEventHandler!.address; - } - - CreateNativeApiEngineResult initilize( - SendPort sendPort, List> args) { - _irisEvent.initialize(); - _irisEvent.registerEventHandler(sendPort); - - _nativeIrisApiEngineBinding.initialize(); - final createResult = - _nativeIrisApiEngineBinding.createNativeApiEngine(args); - _irisApiEnginePtr = createResult.apiEnginePtr; - - _irisCEventHandler = calloc() - ..ref.OnEvent = _irisEvent.onEventPtr.cast(); - - _irisEventHandler = - _nativeIrisApiEngineBinding.createIrisEventHandler(_irisCEventHandler!); - - return createResult; - } - - CallApiResult _invokeMethod(IrisMethodCall methodCall) { - assert(_irisApiEnginePtr != null, 'Make sure initilize() has been called.'); - - return _nativeIrisApiEngineBinding.invokeMethod( - _irisApiEnginePtr!, methodCall); - } - - CallApiResult invokeMethod(IrisMethodCall methodCall) { - return _invokeMethod(methodCall); - } - - void dispose() { - assert(_irisApiEnginePtr != null); - - _nativeIrisApiEngineBinding.destroyNativeApiEngine(_irisApiEnginePtr!); - _irisApiEnginePtr = null; - - _irisEvent.dispose(); - - _nativeIrisApiEngineBinding.destroyIrisEventHandler(_irisEventHandler!); - _irisEventHandler = null; - - calloc.free(_irisCEventHandler!); - _irisCEventHandler = null; - } - - CallApiResult createNativeEventHandler(IrisMethodCall methodCall) { - final eventHandlerIntPtr = _irisEventHandler!.address; - final result = _invokeMethod(IrisMethodCall( - methodCall.funcName, - methodCall.params, - rawBufferParams: [BufferParam(eventHandlerIntPtr, 1)], - )); - result.data['observerIntPtr'] = eventHandlerIntPtr; - return result; - } - - CallApiResult destroyNativeEventHandler(IrisMethodCall methodCall) { - assert(methodCall.rawBufferParams != null); - assert(methodCall.rawBufferParams!.length == 1); - - CallApiResult result; - if (methodCall.funcName.isEmpty) { - result = CallApiResult(irisReturnCode: 0, data: {'result': 0}); - } else { - result = _invokeMethod(methodCall); - } - - return result; + _irisMethodChannelInternal.removeHotRestartListener(listener); } -} - -class BufferParam { - const BufferParam(this.intPtr, this.length); - final int intPtr; - final int length; -} -class ScopedEvent { - const ScopedEvent({ - required this.scopedKey, - required this.registerName, - required this.unregisterName, - // required this.params, - required this.handler, - }); - final TypedScopedKey scopedKey; - final String registerName; - final String unregisterName; - // final String params; - final EventLoopEventHandler handler; -} - -abstract class IrisEventKey { - const IrisEventKey({ - required this.registerName, - required this.unregisterName, - }); - final String registerName; - final String unregisterName; - - @override - bool operator ==(Object other) { - if (other.runtimeType != runtimeType) { - return false; - } - return other is IrisEventKey && - other.registerName == registerName && - other.unregisterName == unregisterName; + @visibleForTesting + IrisMethodChannelInternal getIrisMethodChannelInternal() { + return _irisMethodChannelInternal; } - - @override - int get hashCode => Object.hash(registerName, unregisterName); } diff --git a/lib/src/native_bindings_delegate.dart b/lib/src/native_bindings_delegate.dart deleted file mode 100644 index 5c85ccf..0000000 --- a/lib/src/native_bindings_delegate.dart +++ /dev/null @@ -1,155 +0,0 @@ -import 'dart:convert'; -import 'dart:ffi' as ffi; -import 'dart:isolate'; - -import 'package:ffi/ffi.dart'; -import 'package:flutter/foundation.dart'; -import 'package:iris_method_channel/src/bindings/native_iris_api_common_bindings.dart' - as iris; -import 'package:iris_method_channel/src/iris_event.dart'; -import 'package:iris_method_channel/src/iris_method_channel.dart'; - -// ignore_for_file: public_member_api_docs - -class CreateNativeApiEngineResult { - const CreateNativeApiEngineResult(this.apiEnginePtr, - {this.extraData = const {}}); - final ffi.Pointer apiEnginePtr; - final Map extraData; -} - -/// Unified interface for iris API engine. -/// The [NativeBindingDelegate] is running inside a seperate isolate which is -/// spawned by the main isolate, so you should not share any objects in this class. -abstract class NativeBindingDelegate { - void initialize(); - - CreateNativeApiEngineResult createNativeApiEngine( - List> args); - - CallApiResult invokeMethod( - ffi.Pointer irisApiEnginePtr, - IrisMethodCall methodCall, - ) { - return using((Arena arena) { - final funcName = methodCall.funcName; - final params = methodCall.params; - final buffers = methodCall.buffers; - final rawBufferParams = methodCall.rawBufferParams; - assert(!(buffers != null && rawBufferParams != null)); - - List? bufferParamList = []; - - if (buffers != null) { - for (int i = 0; i < buffers.length; i++) { - final buffer = buffers[i]; - if (buffer.isEmpty) { - bufferParamList.add(const BufferParam(0, 0)); - continue; - } - final ffi.Pointer bufferData = - arena.allocate(buffer.length); - - final pointerList = bufferData.asTypedList(buffer.length); - pointerList.setAll(0, buffer); - - bufferParamList.add(BufferParam(bufferData.address, buffer.length)); - } - } else { - bufferParamList = rawBufferParams; - } - - final ffi.Pointer resultPointer = - arena.allocate(kBasicResultLength); - - final ffi.Pointer funcNamePointer = - funcName.toNativeUtf8(allocator: arena).cast(); - - final ffi.Pointer paramsPointerUtf8 = - params.toNativeUtf8(allocator: arena); - final paramsPointerUtf8Length = paramsPointerUtf8.length; - final ffi.Pointer paramsPointer = paramsPointerUtf8.cast(); - - ffi.Pointer> bufferListPtr; - ffi.Pointer bufferListLengthPtr = ffi.nullptr; - final bufferLengthLength = bufferParamList?.length ?? 0; - - if (bufferParamList != null) { - bufferListPtr = - arena.allocate(bufferParamList.length * ffi.sizeOf()); - - for (int i = 0; i < bufferParamList.length; i++) { - final bufferParam = bufferParamList[i]; - bufferListPtr[i] = ffi.Pointer.fromAddress(bufferParam.intPtr); - } - } else { - bufferListPtr = ffi.nullptr; - bufferListLengthPtr = ffi.nullptr; - } - - try { - final apiParam = arena() - ..ref.event = funcNamePointer - ..ref.data = paramsPointer - ..ref.data_size = paramsPointerUtf8Length - ..ref.result = resultPointer - ..ref.buffer = bufferListPtr - ..ref.length = bufferListLengthPtr - ..ref.buffer_count = bufferLengthLength; - - final irisReturnCode = callApi( - methodCall, - irisApiEnginePtr, - apiParam, - ); - - if (irisReturnCode != 0) { - return CallApiResult(irisReturnCode: irisReturnCode, data: const {}); - } - - final result = resultPointer.cast().toDartString(); - - final resultMap = Map.from(jsonDecode(result)); - - return CallApiResult( - irisReturnCode: irisReturnCode, - data: resultMap, - rawData: result, - ); - } catch (e) { - debugPrint( - '[_ApiCallExecutor] $funcName, params: $params\nerror: $e'); - return CallApiResult(irisReturnCode: -1, data: const {}); - } - }); - } - - int callApi( - IrisMethodCall methodCall, - ffi.Pointer apiEnginePtr, - ffi.Pointer param, - ); - - ffi.Pointer createIrisEventHandler( - ffi.Pointer eventHandler, - ); - - void destroyIrisEventHandler( - ffi.Pointer handler, - ); - - void destroyNativeApiEngine(ffi.Pointer apiEnginePtr); -} - -/// A provider for provide the ffi bindings of native implementation(such like -/// [NativeBindingDelegate], [IrisEvent]), which is passed to the isolate, you -/// should not sotre any objects with type that [SendPort] not allowed. -abstract class NativeBindingsProvider { - /// Provide the implementation of [NativeBindingDelegate]. - NativeBindingDelegate provideNativeBindingDelegate(); - - /// Provide the implementation of [IrisEvent]. - IrisEvent provideIrisEvent() { - return IrisEvent(); - } -} diff --git a/lib/src/bindings/native_iris_api_common_bindings.dart b/lib/src/platform/io/bindings/native_iris_api_common_bindings.dart similarity index 91% rename from lib/src/bindings/native_iris_api_common_bindings.dart rename to lib/src/platform/io/bindings/native_iris_api_common_bindings.dart index 542acd8..d698cf8 100644 --- a/lib/src/bindings/native_iris_api_common_bindings.dart +++ b/lib/src/platform/io/bindings/native_iris_api_common_bindings.dart @@ -1,4 +1,4 @@ -// ignore_for_file: camel_case_types, non_constant_identifier_names +// ignore_for_file: camel_case_types, non_constant_identifier_names, public_member_api_docs import 'dart:ffi' as ffi; @@ -20,7 +20,7 @@ class ApiParam extends ffi.Struct { external int buffer_count; } -typedef IrisEventHandlerHandle = ffi.Pointer; +// typedef IrisEventHandlerHandle = ffi.Pointer; class IrisCEventHandler extends ffi.Struct { external Func_Event OnEvent; diff --git a/lib/src/bindings/native_iris_event_bindings.dart b/lib/src/platform/io/bindings/native_iris_event_bindings.dart similarity index 99% rename from lib/src/bindings/native_iris_event_bindings.dart rename to lib/src/platform/io/bindings/native_iris_event_bindings.dart index f9eb7b6..b30604f 100644 --- a/lib/src/bindings/native_iris_event_bindings.dart +++ b/lib/src/platform/io/bindings/native_iris_event_bindings.dart @@ -1,4 +1,4 @@ -// ignore_for_file: camel_case_types, non_constant_identifier_names +// ignore_for_file: camel_case_types, non_constant_identifier_names, public_member_api_docs, sort_constructors_first // AUTO GENERATED FILE, DO NOT EDIT. // diff --git a/lib/src/platform/io/iris_event_io.dart b/lib/src/platform/io/iris_event_io.dart new file mode 100644 index 0000000..bc39007 --- /dev/null +++ b/lib/src/platform/io/iris_event_io.dart @@ -0,0 +1,54 @@ +import 'dart:ffi' as ffi; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:iris_method_channel/src/platform/io/bindings/native_iris_event_bindings.dart'; +import 'package:iris_method_channel/src/platform/iris_event_interface.dart'; + +const _libName = 'iris_method_channel'; + +ffi.DynamicLibrary _loadLib() { + if (Platform.isWindows) { + return ffi.DynamicLibrary.open('$_libName.dll'); + } + + if (Platform.isAndroid) { + return ffi.DynamicLibrary.open('lib$_libName.so'); + } + + return ffi.DynamicLibrary.process(); +} + +/// [IrisEvent] implementation of `dart:io` +class IrisEventIO implements IrisEvent { + /// Construct [IrisEventIO] + IrisEventIO() { + _nativeIrisEventBinding = NativeIrisEventBinding(_loadLib()); + } + + late final NativeIrisEventBinding _nativeIrisEventBinding; + + /// Initialize the [IrisEvent], which call `InitDartApiDL` directly + void initialize() { + _nativeIrisEventBinding.InitDartApiDL(ffi.NativeApi.initializeApiDLData); + } + + /// Register dart [SendPort] to send the message from native + void registerEventHandler(SendPort sendPort) { + _nativeIrisEventBinding.RegisterDartPort(sendPort.nativePort); + } + + /// Unregister dart [SendPort] which used to send the message from native + void unregisterEventHandler(SendPort sendPort) { + _nativeIrisEventBinding.UnregisterDartPort(sendPort.nativePort); + } + + /// Clean up native resources + void dispose() { + _nativeIrisEventBinding.Dispose(); + } + + /// Get the onEvent function pointer from C + ffi.Pointer)>> + get onEventPtr => _nativeIrisEventBinding.addresses.OnEvent; +} diff --git a/lib/src/platform/io/iris_method_channel_internal_io.dart b/lib/src/platform/io/iris_method_channel_internal_io.dart new file mode 100644 index 0000000..380bfb5 --- /dev/null +++ b/lib/src/platform/io/iris_method_channel_internal_io.dart @@ -0,0 +1,615 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi' as ffi; +import 'dart:isolate'; + +import 'package:async/async.dart'; +import 'package:ffi/ffi.dart'; +import 'package:flutter/foundation.dart' + show VoidCallback, debugPrint, visibleForTesting; +import 'package:iris_method_channel/iris_method_channel.dart'; +import 'package:iris_method_channel/src/platform/io/bindings/native_iris_api_common_bindings.dart' + as iris; +import 'package:iris_method_channel/src/platform/io/iris_event_io.dart'; + +// ignore_for_file: public_member_api_docs + +class _Messenger implements DisposableObject { + _Messenger(this.requestPort, this.responseQueue); + final SendPort requestPort; + final StreamQueue responseQueue; + bool _isDisposed = false; + + Future send(Request request) async { + if (_isDisposed) { + return CallApiResult( + irisReturnCode: kDisposedIrisMethodCallReturnCode, + data: kDisposedIrisMethodCallData); + } + requestPort.send(request); + return await responseQueue.next; + } + + Future> listSend(Request request) async { + if (_isDisposed) { + return [ + CallApiResult( + irisReturnCode: kDisposedIrisMethodCallReturnCode, + data: kDisposedIrisMethodCallData) + ]; + } + requestPort.send(request); + return await responseQueue.next; + } + + @override + Future dispose() async { + if (_isDisposed) { + return; + } + _isDisposed = true; + requestPort.send(null); + await responseQueue.cancel(); + } +} + +class _InitilizationArgs { + _InitilizationArgs( + this.apiCallPortSendPort, + this.eventPortSendPort, + this.onExitSendPort, + this.provider, + this.argNativeHandles, + ); + + final SendPort apiCallPortSendPort; + final SendPort eventPortSendPort; + final SendPort? onExitSendPort; + final PlatformBindingsProvider provider; + final List argNativeHandles; +} + +class InitilizationResultIO implements InitilizationResult { + InitilizationResultIO( + this._apiCallPortSendPort, + this.irisApiEngineNativeHandle, + this.extraData, + this._debugIrisCEventHandlerNativeHandle, + this._debugIrisEventHandlerNativeHandle, + ); + + final SendPort _apiCallPortSendPort; + final int irisApiEngineNativeHandle; + + /// Same as [CreateApiEngineResult.extraData] + final Map extraData; + + final int? _debugIrisCEventHandlerNativeHandle; + final int? _debugIrisEventHandlerNativeHandle; +} + +class _HotRestartFinalizer { + _HotRestartFinalizer(this.provider) { + assert(() { + _onExitPort = ReceivePort(); + _onExitSubscription = _onExitPort?.listen(_finalize); + + return true; + }()); + } + + final PlatformBindingsProvider provider; + + final List _hotRestartListeners = []; + + ReceivePort? _onExitPort; + StreamSubscription? _onExitSubscription; + + SendPort? get onExitSendPort => _onExitPort?.sendPort; + + bool _isFinalize = false; + + int? _debugIrisApiEngineNativeHandle; + set debugIrisApiEngineNativeHandle(int value) { + _debugIrisApiEngineNativeHandle = value; + } + + int? _debugIrisCEventHandlerNativeHandle; + set debugIrisCEventHandlerNativeHandle(int? value) { + _debugIrisCEventHandlerNativeHandle = value; + } + + int? _debugIrisEventHandlerNativeHandle; + set debugIrisEventHandlerNativeHandle(int? value) { + _debugIrisEventHandlerNativeHandle = value; + } + + // ignore: avoid_annotating_with_dynamic + void _finalize(dynamic msg) { + if (_isFinalize) { + return; + } + _isFinalize = true; + + // We will receive a value of `0` as message when the `IrisMethodChannel.dispose` + // is called normally, which will call the `Isolate.exit(onExitSendPort, 0)` + // to send a value of `0` as a exit response. See `_execute` function for more detail. + if (msg != null && msg == 0) { + return; + } + + for (final listener in _hotRestartListeners.reversed) { + listener(msg); + } + + // When hot restart happen, the `IrisMethodChannel.dispose` function will not + // be called normally, cause the native API engine can not be destroy correctly, + // so we need to release the native resources which create by the + // `NativeBindingDelegate` explicitly. + final nativeBindingDelegate = provider.provideNativeBindingDelegate(); + nativeBindingDelegate.initialize(); + + nativeBindingDelegate.destroyNativeApiEngine(IrisApiEngineHandle( + ffi.Pointer.fromAddress(_debugIrisApiEngineNativeHandle!))); + + calloc.free(ffi.Pointer.fromAddress(_debugIrisCEventHandlerNativeHandle!)); + nativeBindingDelegate.destroyIrisEventHandler(IrisEventHandlerHandle( + ffi.Pointer.fromAddress(_debugIrisEventHandlerNativeHandle!))); + + assert(provider.provideIrisEvent() != null); + final irisEvent = provider.provideIrisEvent()! as IrisEventIO; + irisEvent.dispose(); + + _onExitSubscription?.cancel(); + } + + VoidCallback addHotRestartListener(HotRestartListener listener) { + assert(() { + final Object? debugCheckForReturnedFuture = listener as dynamic; + if (debugCheckForReturnedFuture is Future) { + throw UnsupportedError( + 'HotRestartListener must be a void method without an `async` keyword.'); + } + return true; + }()); + + _hotRestartListeners.add(listener); + return () { + removeHotRestartListener(listener); + }; + } + + void removeHotRestartListener(HotRestartListener listener) { + _hotRestartListeners.remove(listener); + } + + void dispose() { + _hotRestartListeners.clear(); + } +} + +/// Extension functions of `PlatformBindingsDelegateInterfaceIO` +extension PlatformBindingsDelegateInterfaceIOExt + on PlatformBindingsDelegateInterface { + CallApiResult invokeMethod( + IrisApiEngineHandle irisApiEnginePtr, + IrisMethodCall methodCall, + ) { + return using((Arena arena) { + final funcName = methodCall.funcName; + final params = methodCall.params; + final buffers = methodCall.buffers; + final rawBufferParams = methodCall.rawBufferParams; + assert(!(buffers != null && rawBufferParams != null)); + + List? bufferParamList = []; + + if (buffers != null) { + for (int i = 0; i < buffers.length; i++) { + final buffer = buffers[i]; + if (buffer.isEmpty) { + bufferParamList.add(const BufferParam(BufferParamHandle(0), 0)); + continue; + } + final ffi.Pointer bufferData = + arena.allocate(buffer.length); + + final pointerList = bufferData.asTypedList(buffer.length); + pointerList.setAll(0, buffer); + + bufferParamList.add(BufferParam( + BufferParamHandle(bufferData.address), buffer.length)); + } + } else { + bufferParamList = rawBufferParams; + } + + final ffi.Pointer resultPointer = + arena.allocate(kBasicResultLength); + + final ffi.Pointer funcNamePointer = + funcName.toNativeUtf8(allocator: arena).cast(); + + final ffi.Pointer paramsPointerUtf8 = + params.toNativeUtf8(allocator: arena); + final paramsPointerUtf8Length = paramsPointerUtf8.length; + final ffi.Pointer paramsPointer = paramsPointerUtf8.cast(); + + ffi.Pointer> bufferListPtr; + ffi.Pointer bufferListLengthPtr = ffi.nullptr; + final bufferLengthLength = bufferParamList?.length ?? 0; + + if (bufferParamList != null) { + bufferListPtr = + arena.allocate(bufferParamList.length * ffi.sizeOf()); + + for (int i = 0; i < bufferParamList.length; i++) { + final bufferParam = bufferParamList[i]; + bufferListPtr[i] = + ffi.Pointer.fromAddress(bufferParam.intPtr() as int); + } + } else { + bufferListPtr = ffi.nullptr; + bufferListLengthPtr = ffi.nullptr; + } + + try { + final apiParam = arena() + ..ref.event = funcNamePointer + ..ref.data = paramsPointer + ..ref.data_size = paramsPointerUtf8Length + ..ref.result = resultPointer + ..ref.buffer = bufferListPtr + ..ref.length = bufferListLengthPtr + ..ref.buffer_count = bufferLengthLength; + + final irisReturnCode = callApi( + methodCall, + irisApiEnginePtr, + IrisApiParamHandle(apiParam), + ); + + if (irisReturnCode != 0) { + return CallApiResult(irisReturnCode: irisReturnCode, data: const {}); + } + + final result = resultPointer.cast().toDartString(); + + final resultMap = Map.from(jsonDecode(result)); + + return CallApiResult( + irisReturnCode: irisReturnCode, + data: resultMap, + rawData: result, + ); + } catch (e) { + debugPrint('[_ApiCallExecutor] $funcName, params: $params\nerror: $e'); + return CallApiResult(irisReturnCode: -1, data: const {}); + } + }); + } +} + +class _IrisMethodChannelNative { + _IrisMethodChannelNative(this._nativeIrisApiEngineBinding, this._irisEvent); + final PlatformBindingsDelegateInterface _nativeIrisApiEngineBinding; + + ffi.Pointer? _irisApiEnginePtr; + int get irisApiEngineNativeHandle { + assert(_irisApiEnginePtr != null); + return _irisApiEnginePtr!.address; + } + + final IrisEventIO _irisEvent; + ffi.Pointer? _irisCEventHandler; + int get irisCEventHandlerNativeHandle { + assert(_irisCEventHandler != null); + return _irisCEventHandler!.address; + } + + ffi.Pointer? _irisEventHandler; + int get irisEventHandlerNativeHandle { + assert(_irisEventHandler != null); + return _irisEventHandler!.address; + } + + CreateApiEngineResult initilize( + SendPort sendPort, List> args) { + _irisEvent.initialize(); + _irisEvent.registerEventHandler(sendPort); + + _nativeIrisApiEngineBinding.initialize(); + final createResult = _nativeIrisApiEngineBinding.createApiEngine(args); + _irisApiEnginePtr = createResult.apiEnginePtr() as ffi.Pointer?; + + _irisCEventHandler = calloc() + ..ref.OnEvent = _irisEvent.onEventPtr.cast(); + + _irisEventHandler = _nativeIrisApiEngineBinding.createIrisEventHandler( + IrisCEventHandlerHandle(_irisCEventHandler!))() + as ffi.Pointer?; + + return createResult; + } + + CallApiResult _invokeMethod(IrisMethodCall methodCall) { + assert(_irisApiEnginePtr != null, 'Make sure initilize() has been called.'); + + return _nativeIrisApiEngineBinding.invokeMethod( + IrisApiEngineHandle(_irisApiEnginePtr!), methodCall); + } + + CallApiResult invokeMethod(IrisMethodCall methodCall) { + return _invokeMethod(methodCall); + } + + void dispose() { + assert(_irisApiEnginePtr != null); + + _nativeIrisApiEngineBinding + .destroyNativeApiEngine(IrisApiEngineHandle(_irisApiEnginePtr!)); + _irisApiEnginePtr = null; + + _irisEvent.dispose(); + + _nativeIrisApiEngineBinding + .destroyIrisEventHandler(IrisEventHandlerHandle(_irisEventHandler!)); + _irisEventHandler = null; + + calloc.free(_irisCEventHandler!); + _irisCEventHandler = null; + } + + CallApiResult createNativeEventHandler(IrisMethodCall methodCall) { + final eventHandlerIntPtr = _irisEventHandler!.address; + final result = _invokeMethod(IrisMethodCall( + methodCall.funcName, + methodCall.params, + rawBufferParams: [BufferParam(BufferParamHandle(eventHandlerIntPtr), 1)], + )); + result.data['observerIntPtr'] = eventHandlerIntPtr; + return result; + } + + CallApiResult destroyNativeEventHandler(IrisMethodCall methodCall) { + assert(methodCall.rawBufferParams != null); + assert(methodCall.rawBufferParams!.length == 1); + + CallApiResult result; + if (methodCall.funcName.isEmpty) { + result = CallApiResult(irisReturnCode: 0, data: {'result': 0}); + } else { + result = _invokeMethod(methodCall); + } + + return result; + } +} + +class IrisMethodChannelInternalIO implements IrisMethodChannelInternal { + IrisMethodChannelInternalIO(this._nativeBindingsProvider); + + final PlatformBindingsProvider _nativeBindingsProvider; + + bool _initilized = false; + late _Messenger _messenger; + late StreamSubscription _evntSubscription; + // @visibleForTesting + // final ScopedObjects scopedEventHandlers = ScopedObjects(); + late int _nativeHandle; + + IrisEventMessageListener? _irisEventMessageListener; + + @visibleForTesting + late Isolate workerIsolate; + late _HotRestartFinalizer _hotRestartFinalizer; + + static Future _execute(_InitilizationArgs args) async { + final SendPort mainApiCallSendPort = args.apiCallPortSendPort; + final SendPort mainEventSendPort = args.eventPortSendPort; + final SendPort? onExitSendPort = args.onExitSendPort; + final PlatformBindingsProvider provider = args.provider; + + final List> argsInner = args.argNativeHandles + .map>((e) => ffi.Pointer.fromAddress(e)) + .toList(); + + final apiCallPort = ReceivePort(); + + final nativeBindingDelegate = provider.provideNativeBindingDelegate(); + + final IrisEvent irisEvent = provider.provideIrisEvent() ?? IrisEventIO(); + // assert(irisEvent != null); + + // final irisEvent = provider.provideIrisEvent(); + + final _IrisMethodChannelNative executor = _IrisMethodChannelNative( + nativeBindingDelegate, irisEvent as IrisEventIO); + final CreateApiEngineResult executorInitilizationResult = + executor.initilize(mainEventSendPort, argsInner); + + int? debugIrisCEventHandlerNativeHandle; + int? debugIrisEventHandlerNativeHandle; + + assert(() { + debugIrisCEventHandlerNativeHandle = + executor.irisCEventHandlerNativeHandle; + debugIrisEventHandlerNativeHandle = executor.irisEventHandlerNativeHandle; + + return true; + }()); + + final InitilizationResult initilizationResponse = InitilizationResultIO( + apiCallPort.sendPort, + executor.irisApiEngineNativeHandle, + executorInitilizationResult.extraData, + debugIrisCEventHandlerNativeHandle, + debugIrisEventHandlerNativeHandle, + ); + + mainApiCallSendPort.send(initilizationResponse); + + // Wait for messages from the main isolate. + await for (final request in apiCallPort) { + if (request == null) { + // Exit if the main isolate sends a null message, indicating there are no + // more files to read and parse. + break; + } + + assert(request is Request); + + if (request is ApiCallRequest) { + final result = executor.invokeMethod(request.methodCall); + + mainApiCallSendPort.send(result); + } else if (request is ApiCallListRequest) { + final results = []; + for (final methodCall in request.methodCalls) { + final result = executor.invokeMethod(methodCall); + results.add(result); + } + + mainApiCallSendPort.send(results); + } else if (request is CreateNativeEventHandlerRequest) { + final result = executor.createNativeEventHandler(request.methodCall); + mainApiCallSendPort.send(result); + } else if (request is CreateNativeEventHandlerListRequest) { + final results = []; + for (final methodCall in request.methodCalls) { + final result = executor.createNativeEventHandler(methodCall); + results.add(result); + } + + mainApiCallSendPort.send(results); + } else if (request is DestroyNativeEventHandlerRequest) { + final result = executor.destroyNativeEventHandler(request.methodCall); + mainApiCallSendPort.send(result); + } else if (request is DestroyNativeEventHandlerListRequest) { + final results = []; + for (final methodCall in request.methodCalls) { + final result = executor.destroyNativeEventHandler(methodCall); + results.add(result); + } + + mainApiCallSendPort.send(results); + } + } + + executor.dispose(); + Isolate.exit(onExitSendPort, 0); + } + + @override + Future initilize(List args) async { + if (_initilized) { + return null; + } + + final apiCallPort = ReceivePort(); + final eventPort = ReceivePort(); + + _hotRestartFinalizer = _HotRestartFinalizer(_nativeBindingsProvider); + + workerIsolate = await Isolate.spawn( + _execute, + _InitilizationArgs( + apiCallPort.sendPort, + eventPort.sendPort, + _hotRestartFinalizer.onExitSendPort, + _nativeBindingsProvider, + args, + ), + onExit: _hotRestartFinalizer.onExitSendPort, + ); + + // Convert the ReceivePort into a StreamQueue to receive messages from the + // spawned isolate using a pull-based interface. Events are stored in this + // queue until they are accessed by `events.next`. + // final events = StreamQueue(p); + final responseQueue = StreamQueue(apiCallPort); + + // The first message from the spawned isolate is a SendPort. This port is + // used to communicate with the spawned isolate. + // SendPort sendPort = await events.next; + final msg = await responseQueue.next; + assert(msg is InitilizationResult); + final initilizationResult = msg as InitilizationResultIO; + final requestPort = initilizationResult._apiCallPortSendPort; + _nativeHandle = initilizationResult.irisApiEngineNativeHandle; + + assert(() { + _hotRestartFinalizer.debugIrisApiEngineNativeHandle = + initilizationResult.irisApiEngineNativeHandle; + _hotRestartFinalizer.debugIrisCEventHandlerNativeHandle = + initilizationResult._debugIrisCEventHandlerNativeHandle; + _hotRestartFinalizer.debugIrisEventHandlerNativeHandle = + initilizationResult._debugIrisEventHandlerNativeHandle; + + return true; + }()); + + _messenger = _Messenger(requestPort, responseQueue); + + _evntSubscription = eventPort.listen((message) { + if (!_initilized) { + return; + } + + final eventMessage = parseMessage(message); + + _irisEventMessageListener?.call(eventMessage); + }); + + _initilized = true; + + return initilizationResult; + } + + @override + Future dispose() async { + if (!_initilized) { + return; + } + _initilized = false; + _irisEventMessageListener = null; + _hotRestartFinalizer.dispose(); + await _evntSubscription.cancel(); + + await _messenger.dispose(); + } + + @override + VoidCallback addHotRestartListener(HotRestartListener listener) { + return _hotRestartFinalizer.addHotRestartListener(listener); + } + + @override + void removeHotRestartListener(HotRestartListener listener) { + _hotRestartFinalizer.removeHotRestartListener(listener); + } + + @override + Future execute(Request request) { + return _messenger.send(request); + } + + @override + int getApiEngineHandle() { + if (!_initilized) { + return 0; + } + + return _nativeHandle; + } + + @override + Future> listExecute(Request request) { + return _messenger.listSend(request); + } + + @override + void setIrisEventMessageListener(IrisEventMessageListener listener) { + _irisEventMessageListener = listener; + } +} diff --git a/lib/src/platform/io/utils_actual_io.dart b/lib/src/platform/io/utils_actual_io.dart new file mode 100644 index 0000000..743710c --- /dev/null +++ b/lib/src/platform/io/utils_actual_io.dart @@ -0,0 +1,10 @@ +import 'dart:ffi' as ffi; +import 'dart:typed_data'; + +// ignore_for_file: public_member_api_docs + +Uint8List uint8ListFromPtr(int intPtr, int length) { + final ptr = ffi.Pointer.fromAddress(intPtr); + final memoryList = ptr.asTypedList(length); + return Uint8List.fromList(memoryList); +} diff --git a/lib/src/platform/iris_event_interface.dart b/lib/src/platform/iris_event_interface.dart new file mode 100644 index 0000000..90c6002 --- /dev/null +++ b/lib/src/platform/iris_event_interface.dart @@ -0,0 +1,57 @@ +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart' show protected; + +// ignore_for_file: public_member_api_docs + +/// Iris event handler interface +abstract class EventLoopEventHandler { + /// Callback when received events + bool handleEvent( + String eventName, String eventData, List buffers) { + return handleEventInternal(eventName, eventData, buffers); + } + + @protected + bool handleEventInternal( + String eventName, String eventData, List buffers); +} + +/// Object to hold the iris event infos +class IrisEventMessage { + /// Construct [IrisEventMessage] + const IrisEventMessage(this.event, this.data, this.buffers); + + /// The event name + final String event; + + /// The json data + final String data; + + /// Byte buffers + final List buffers; +} + +/// Parse message to [IrisEventMessage] object +// ignore: avoid_annotating_with_dynamic +IrisEventMessage parseMessage(dynamic message) { + final dataList = List.from(message); + final String event = dataList[0]; + String data = dataList[1] as String; + if (data.isEmpty) { + data = '{}'; + } + + String res = dataList[1] as String; + if (res.isEmpty) { + res = '{}'; + } + final List buffers = + dataList.length == 3 ? List.from(dataList[2]) : []; + + return IrisEventMessage(event, data, buffers); +} + +typedef IrisEventMessageListener = void Function(IrisEventMessage message); + +abstract class IrisEvent {} diff --git a/lib/src/platform/iris_method_channel_interface.dart b/lib/src/platform/iris_method_channel_interface.dart new file mode 100644 index 0000000..92f18b8 --- /dev/null +++ b/lib/src/platform/iris_method_channel_interface.dart @@ -0,0 +1,202 @@ +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart' show VoidCallback, SynchronousFuture; +import 'package:iris_method_channel/src/iris_handles.dart'; +import 'package:iris_method_channel/src/platform/iris_event_interface.dart'; +import 'package:iris_method_channel/src/scoped_objects.dart'; + +// ignore_for_file: public_member_api_docs + +const int kBasicResultLength = 64 * 1024; +const int kDisposedIrisMethodCallReturnCode = 1000; +const Map kDisposedIrisMethodCallData = {'result': 0}; + +class BufferParam { + const BufferParam(this.intPtr, this.length); + final BufferParamHandle intPtr; + final int length; +} + +class CallApiResult { + CallApiResult( + {required this.irisReturnCode, required this.data, this.rawData = ''}); + + final int irisReturnCode; + + final Map data; + + // TODO(littlegnal): Remove rawData after EP-253 landed. + final String rawData; +} + +class IrisMethodCall { + const IrisMethodCall(this.funcName, this.params, + {this.buffers, this.rawBufferParams}); + final String funcName; + final String params; + final List? buffers; + final List? rawBufferParams; +} + +abstract class InitilizationResult {} + +abstract class Request {} + +abstract class IrisMethodCallRequest implements Request { + const IrisMethodCallRequest(this.methodCall); + + final IrisMethodCall methodCall; +} + +abstract class IrisMethodCallListRequest implements Request { + const IrisMethodCallListRequest(this.methodCalls); + + final List methodCalls; +} + +class ApiCallRequest extends IrisMethodCallRequest { + const ApiCallRequest(IrisMethodCall methodCall) : super(methodCall); +} + +// ignore: unused_element +class ApiCallListRequest extends IrisMethodCallListRequest { + const ApiCallListRequest(List methodCalls) + : super(methodCalls); +} + +class CreateNativeEventHandlerRequest extends IrisMethodCallRequest { + const CreateNativeEventHandlerRequest(IrisMethodCall methodCall) + : super(methodCall); +} + +// ignore: unused_element +class CreateNativeEventHandlerListRequest extends IrisMethodCallListRequest { + const CreateNativeEventHandlerListRequest(List methodCalls) + : super(methodCalls); +} + +class DestroyNativeEventHandlerRequest extends IrisMethodCallRequest { + const DestroyNativeEventHandlerRequest(IrisMethodCall methodCall) + : super(methodCall); +} + +class DestroyNativeEventHandlerListRequest extends IrisMethodCallListRequest { + const DestroyNativeEventHandlerListRequest(List methodCalls) + : super(methodCalls); +} + +/// Listener when hot restarted. +/// +/// You can release some native resources, such like delete the pointer which is +/// created by ffi. +/// +/// NOTE that: +/// * This listener is only received on debug mode. +/// * You should not comunicate with the `IrisMethodChannel` anymore inside this listener. +/// * You should not do some asynchronous jobs inside this listener. +typedef HotRestartListener = void Function(Object? message); + +abstract class IrisMethodChannelInternal { + Future initilize(List args); + + // Future invokeMethod(IrisMethodCall methodCall); + + Future execute(Request request); + + Future> listExecute(Request request); + + int getApiEngineHandle(); + + VoidCallback addHotRestartListener(HotRestartListener listener); + + void removeHotRestartListener(HotRestartListener listener); + + void setIrisEventMessageListener(IrisEventMessageListener listener); + + Future dispose(); +} + +class ScopedEvent { + const ScopedEvent({ + required this.scopedKey, + required this.registerName, + required this.unregisterName, + // required this.params, + required this.handler, + }); + final TypedScopedKey scopedKey; + final String registerName; + final String unregisterName; + // final String params; + final EventLoopEventHandler handler; +} + +abstract class IrisEventKey { + const IrisEventKey({ + required this.registerName, + required this.unregisterName, + }); + final String registerName; + final String unregisterName; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is IrisEventKey && + other.registerName == registerName && + other.unregisterName == unregisterName; + } + + @override + int get hashCode => Object.hash(registerName, unregisterName); +} + +class EventHandlerHolder + with ScopedDisposableObjectMixin + implements DisposableObject { + EventHandlerHolder({required this.key}); + final EventHandlerHolderKey key; + final Set _eventHandlers = {}; + + int nativeEventHandlerIntPtr = 0; + + void addEventHandler(EventLoopEventHandler eventHandler) { + _eventHandlers.add(eventHandler); + } + + Future removeEventHandler(EventLoopEventHandler eventHandler) async { + _eventHandlers.remove(eventHandler); + } + + Set getEventHandlers() => _eventHandlers; + + @override + Future dispose() { + _eventHandlers.clear(); + return SynchronousFuture(null); + } +} + +class EventHandlerHolderKey implements ScopedKey { + const EventHandlerHolderKey({ + required this.registerName, + required this.unregisterName, + }); + final String registerName; + final String unregisterName; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is EventHandlerHolderKey && + other.registerName == registerName && + other.unregisterName == unregisterName; + } + + @override + int get hashCode => Object.hash(registerName, unregisterName); +} diff --git a/lib/src/platform/iris_method_channel_internal.dart b/lib/src/platform/iris_method_channel_internal.dart new file mode 100644 index 0000000..a6050cd --- /dev/null +++ b/lib/src/platform/iris_method_channel_internal.dart @@ -0,0 +1,3 @@ +export 'iris_method_channel_internal_expect.dart' + if (dart.library.io) 'iris_method_channel_internal_actual_io.dart' + if (dart.library.js) 'iris_method_channel_internal_actual_web.dart'; diff --git a/lib/src/platform/iris_method_channel_internal_actual_io.dart b/lib/src/platform/iris_method_channel_internal_actual_io.dart new file mode 100644 index 0000000..3af747b --- /dev/null +++ b/lib/src/platform/iris_method_channel_internal_actual_io.dart @@ -0,0 +1,8 @@ +import 'package:iris_method_channel/src/platform/io/iris_method_channel_internal_io.dart'; +import 'package:iris_method_channel/src/platform/iris_method_channel_interface.dart'; +import 'package:iris_method_channel/src/platform/platform_bindings_delegate_interface.dart'; + +/// Create the [IrisMethodChannelInternal] for `dart:io` +IrisMethodChannelInternal createIrisMethodChannelInternal( + PlatformBindingsProvider provider) => + IrisMethodChannelInternalIO(provider); diff --git a/lib/src/platform/iris_method_channel_internal_actual_web.dart b/lib/src/platform/iris_method_channel_internal_actual_web.dart new file mode 100644 index 0000000..eff3365 --- /dev/null +++ b/lib/src/platform/iris_method_channel_internal_actual_web.dart @@ -0,0 +1,8 @@ +import 'package:iris_method_channel/src/platform/iris_method_channel_interface.dart'; +import 'package:iris_method_channel/src/platform/platform_bindings_delegate_interface.dart'; +import 'package:iris_method_channel/src/platform/web/iris_method_channel_internal_web.dart'; + +/// Create the [IrisMethodChannelInternal] for web +IrisMethodChannelInternal createIrisMethodChannelInternal( + PlatformBindingsProvider provider) => + IrisMethodChannelInternalWeb(provider); diff --git a/lib/src/platform/iris_method_channel_internal_expect.dart b/lib/src/platform/iris_method_channel_internal_expect.dart new file mode 100644 index 0000000..955c418 --- /dev/null +++ b/lib/src/platform/iris_method_channel_internal_expect.dart @@ -0,0 +1,10 @@ +import 'package:iris_method_channel/src/platform/iris_method_channel_interface.dart'; +import 'package:iris_method_channel/src/platform/platform_bindings_delegate_interface.dart'; + +/// Stub function for create the [IrisMethodChannelInternal] +/// See implementation: +/// io: `iris_method_channel_internal_actual_io.dart` +/// web: `iris_method_channel_internal_actual_web.dart` +IrisMethodChannelInternal createIrisMethodChannelInternal( + PlatformBindingsProvider provider) => + throw UnimplementedError('Unimplemented'); diff --git a/lib/src/platform/platform_bindings_delegate_interface.dart b/lib/src/platform/platform_bindings_delegate_interface.dart new file mode 100644 index 0000000..d976dea --- /dev/null +++ b/lib/src/platform/platform_bindings_delegate_interface.dart @@ -0,0 +1,56 @@ +import 'package:iris_method_channel/src/iris_handles.dart'; +import 'package:iris_method_channel/src/platform/iris_event_interface.dart'; +import 'package:iris_method_channel/src/platform/iris_method_channel_interface.dart'; + +// ignore_for_file: public_member_api_docs + +class CreateApiEngineResult { + const CreateApiEngineResult(this.apiEnginePtr, {this.extraData = const {}}); + final IrisApiEngineHandle apiEnginePtr; + final Map extraData; +} + +/// Unified interface for iris API engine of different platforms. +/// On IO, the [PlatformBindingsDelegateInterface] is running inside a seperate isolate which is +/// spawned by the main isolate, so you should not share any objects in this class. +abstract class PlatformBindingsDelegateInterface { + void initialize(); + + CreateApiEngineResult createApiEngine(List args); + + int callApi( + IrisMethodCall methodCall, + IrisApiEngineHandle apiEnginePtr, + IrisApiParamHandle param, + ); + + Future callApiAsync( + IrisMethodCall methodCall, + IrisApiEngineHandle apiEnginePtr, + IrisApiParamHandle param, + ); + + IrisEventHandlerHandle createIrisEventHandler( + IrisCEventHandlerHandle eventHandler, + ); + + void destroyIrisEventHandler( + IrisEventHandlerHandle handler, + ); + + void destroyNativeApiEngine(IrisApiEngineHandle apiEnginePtr); +} + +/// Provider class that allow the user passing the custom implemetation of [PlatformBindingsDelegateInterface], +/// and [IrisEvent]. +/// +/// On IO, a provider for provide the ffi bindings of native implementation(such like +/// [PlatformBindingsDelegateInterface], [IrisEvent]), which is passed to the isolate, you +/// should not sotre any objects with type that `SendPort` not allowed. +abstract class PlatformBindingsProvider { + /// Provide the implementation of [PlatformBindingsDelegateInterface]. + PlatformBindingsDelegateInterface provideNativeBindingDelegate(); + + /// Provide the implementation of [IrisEvent]. + IrisEvent? provideIrisEvent() => null; +} diff --git a/lib/src/platform/utils.dart b/lib/src/platform/utils.dart new file mode 100644 index 0000000..3e24fee --- /dev/null +++ b/lib/src/platform/utils.dart @@ -0,0 +1,3 @@ +export 'package:iris_method_channel/src/platform/utils_expect.dart' + if (dart.library.io) 'io/utils_actual_io.dart' + if (dart.library.js) 'web/utils_actual_web.dart'; diff --git a/lib/src/platform/utils_expect.dart b/lib/src/platform/utils_expect.dart new file mode 100644 index 0000000..11bfd27 --- /dev/null +++ b/lib/src/platform/utils_expect.dart @@ -0,0 +1,8 @@ +import 'dart:typed_data'; + +/// Stub function for translate the int ptr to the [Uint8List]. +/// See implementation: +/// io: `io/utils_actual_io.dart` +/// web: `web/utils_actual_web.dart`(Empty implementation) +Uint8List uint8ListFromPtr(int intPtr, int length) => + throw UnimplementedError('Unimplemented'); diff --git a/lib/src/platform/web/bindings/iris_api_common_bindings_js.dart b/lib/src/platform/web/bindings/iris_api_common_bindings_js.dart new file mode 100644 index 0000000..2d9424e --- /dev/null +++ b/lib/src/platform/web/bindings/iris_api_common_bindings_js.dart @@ -0,0 +1,99 @@ +@JS() +library iris_web; + +import 'dart:convert'; + +import 'package:iris_method_channel/src/platform/iris_event_interface.dart'; +import 'package:iris_method_channel/src/platform/iris_method_channel_interface.dart'; +import 'package:js/js.dart'; + +// ignore_for_file: public_member_api_docs, non_constant_identifier_names + +// NOTE: +// For compatibility to dart sdk >= 2.12, we only use the feature that are +// supported in `js: 0.6.3` at this time + +@JS('AgoraWrapper.EventParam') +@anonymous +class EventParam { + // Must have an unnamed factory constructor with named arguments. + external factory EventParam({ + String event, + String data, + int data_size, + String result, + List buffer, + List length, + int buffer_count, + }); + + external String get event; + external String get data; + external int get data_size; + external String get result; + external List get buffer; + external List get length; + external int get buffer_count; +} + +IrisEventMessage toIrisEventMessage(EventParam param) { + return IrisEventMessage(param.event, param.data, []); +} + +typedef ApiParam = EventParam; + +@JS('AgoraWrapper.CallIrisApiResult') +@anonymous +class CallIrisApiResult { + external factory CallIrisApiResult({ + int code, + String data, + }); + + external int get code; + external String get data; +} + +extension CallIrisApiResultExt on CallIrisApiResult { + CallApiResult toCallApiResult() { + return CallApiResult( + irisReturnCode: code, data: jsonDecode(data), rawData: data); + } +} + +typedef IrisCEventHandler = void Function(EventParam param); + +@JS('AgoraWrapper.IrisEventHandlerHandle') +@anonymous +class IrisEventHandlerHandle {} + +@JS('AgoraWrapper.IrisApiEngine') +@anonymous +class IrisApiEngine {} + +@JS('AgoraWrapper.CreateIrisApiEngine') +external IrisApiEngine CreateIrisApiEngine(); + +@JS('AgoraWrapper.DestroyIrisApiEngine') +external int DestroyIrisApiEngine(IrisApiEngine engine_ptr); + +@JS('AgoraWrapper.CallIrisApi') +external int CallIrisApi(IrisApiEngine engine_ptr, ApiParam apiParam); + +@JS('AgoraWrapper.CallIrisApiAsync') +external Future CallIrisApiAsync( + IrisApiEngine engine_ptr, ApiParam apiParam); + +typedef IrisCEventHandlerDartCallback = void Function(EventParam param); + +@JS('AgoraWrapper.CreateIrisEventHandler') +external IrisEventHandlerHandle CreateIrisEventHandler( + IrisCEventHandler event_handler); + +@JS('AgoraWrapper.SetIrisRtcEngineEventHandler') +external IrisEventHandlerHandle SetIrisRtcEngineEventHandler( + IrisApiEngine engine_ptr, IrisEventHandlerHandle event_handler); + +@JS('AgoraWrapper.UnsetIrisRtcEngineEventHandler') +external IrisEventHandlerHandle UnsetIrisRtcEngineEventHandler( + IrisApiEngine engine_ptr, IrisEventHandlerHandle event_handler); diff --git a/lib/src/platform/web/iris_event_web.dart b/lib/src/platform/web/iris_event_web.dart new file mode 100644 index 0000000..62f9644 --- /dev/null +++ b/lib/src/platform/web/iris_event_web.dart @@ -0,0 +1,44 @@ +import 'package:iris_method_channel/src/iris_handles.dart'; +import 'package:iris_method_channel/src/platform/iris_event_interface.dart'; +import 'package:iris_method_channel/src/platform/web/bindings/iris_api_common_bindings_js.dart' + as js; +import 'package:js/js.dart' show allowInterop; + +// ignore_for_file: public_member_api_docs + +class IrisEventWeb implements IrisEvent { + IrisEventWeb(this._irisApiEngine); + + final IrisApiEngineHandle _irisApiEngine; + + IrisEventMessageListener? _irisEventMessageListener; + + js.IrisEventHandlerHandle? _irisEventHandlerJS; + + void initialize() { + final irisCEventHandlerJS = allowInterop(_onEventFromJS); + + _irisEventHandlerJS = js.CreateIrisEventHandler(irisCEventHandlerJS); + js.SetIrisRtcEngineEventHandler( + _irisApiEngine() as js.IrisApiEngine, _irisEventHandlerJS!); + } + + void setIrisEventMessageListener(IrisEventMessageListener? listener) { + _irisEventMessageListener = listener; + } + + void _onEventFromJS(js.EventParam param) { + if (_irisEventMessageListener != null) { + _irisEventMessageListener?.call(js.toIrisEventMessage(param)); + } + } + + /// Clean up native resources + void dispose() { + js.UnsetIrisRtcEngineEventHandler( + _irisApiEngine() as js.IrisApiEngine, _irisEventHandlerJS!); + _irisEventHandlerJS = null; + + _irisEventMessageListener = null; + } +} diff --git a/lib/src/platform/web/iris_method_channel_internal_web.dart b/lib/src/platform/web/iris_method_channel_internal_web.dart new file mode 100644 index 0000000..f18cbb7 --- /dev/null +++ b/lib/src/platform/web/iris_method_channel_internal_web.dart @@ -0,0 +1,103 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart' show VoidCallback, debugPrint; +import 'package:iris_method_channel/iris_method_channel.dart'; +import 'package:iris_method_channel/src/platform/web/iris_event_web.dart'; + +// ignore_for_file: public_member_api_docs + +class InitilizationResultWeb implements InitilizationResult { + InitilizationResultWeb(); +} + +class IrisMethodChannelInternalWeb implements IrisMethodChannelInternal { + IrisMethodChannelInternalWeb(this._nativeBindingsProvider); + + final PlatformBindingsProvider _nativeBindingsProvider; + IrisEventWeb? _irisEventWeb; + IrisApiEngineHandle? _irisApiEngine; + PlatformBindingsDelegateInterface? _platformBindingsDelegate; + + @override + VoidCallback addHotRestartListener(HotRestartListener listener) { + return () {}; + } + + @override + Future dispose() async { + assert(_irisApiEngine != null); + + _irisEventWeb?.dispose(); + _irisEventWeb = null; + + _platformBindingsDelegate?.destroyNativeApiEngine(_irisApiEngine!); + _platformBindingsDelegate = null; + _irisApiEngine = null; + } + + @override + Future execute(Request request) async { + if (request is CreateNativeEventHandlerRequest) { + return CallApiResult(irisReturnCode: 0, data: {'observerIntPtr': 0}); + } else if (request is ApiCallRequest) { + final IrisMethodCall methodCall = request.methodCall; + return _executeMethodCall(methodCall); + } else { + return CallApiResult(irisReturnCode: 0, data: {'result': 0}); + } + } + + Future _executeMethodCall(IrisMethodCall methodCall) async { + // On web, we do not create a `IrisApiParamHandle` directly, but pass the `methodCall` + // to the `callApiAsync` implementation to create their platform specific parameters + // instead. + final ret = await _platformBindingsDelegate! + .callApiAsync(methodCall, _irisApiEngine!, const IrisApiParamHandle(0)); + + return ret; + } + + @override + int getApiEngineHandle() { + return 0; + } + + @override + Future initilize(List args) async { + _platformBindingsDelegate = + _nativeBindingsProvider.provideNativeBindingDelegate(); + final createApiEngineResult = + _platformBindingsDelegate!.createApiEngine(args); + _irisApiEngine = createApiEngineResult.apiEnginePtr; + + final irisEvent = _nativeBindingsProvider.provideIrisEvent() ?? + IrisEventWeb(_irisApiEngine!); + _irisEventWeb = irisEvent as IrisEventWeb; + _irisEventWeb!.initialize(); + + return InitilizationResultWeb(); + } + + @override + Future> listExecute(Request request) async { + final results = []; + if (request is ApiCallListRequest) { + final methodCalls = request.methodCalls; + for (final methodCall in methodCalls) { + final result = await _executeMethodCall(methodCall); + results.add(result); + } + } else if (request is DestroyNativeEventHandlerListRequest) { + debugPrint('[listExecute] Not implemented request: $request'); + } + + return results; + } + + @override + void removeHotRestartListener(HotRestartListener listener) {} + + @override + void setIrisEventMessageListener(IrisEventMessageListener listener) { + _irisEventWeb!.setIrisEventMessageListener(listener); + } +} diff --git a/lib/src/platform/web/utils_actual_web.dart b/lib/src/platform/web/utils_actual_web.dart new file mode 100644 index 0000000..d28244c --- /dev/null +++ b/lib/src/platform/web/utils_actual_web.dart @@ -0,0 +1,8 @@ +import 'dart:typed_data'; + +// ignore_for_file: public_member_api_docs + +/// Empty implementation on web +Uint8List uint8ListFromPtr(int intPtr, int length) { + return Uint8List(0); +} diff --git a/pubspec.yaml b/pubspec.yaml index bb4f72e..6034eed 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,17 +2,22 @@ name: iris_method_channel description: >- iris_method_channel is a method channel that communicate between C/C++(iris) and dart, which is used by Agora Flutter SDKs. -version: 1.2.2 +version: 2.0.0-dev.1 homepage: https://www.agora.io repository: https://github.com/AgoraIO-Extensions/iris_method_channel_flutter/tree/main environment: - sdk: '>=2.14.0 <3.0.0' + sdk: '>=2.14.0 <4.0.0' flutter: '>=2.0.0' dependencies: flutter: sdk: flutter + flutter_web_plugins: + sdk: flutter + ffi: '>=1.1.2' async: ^2.8.2 + js: ^0.6.3 + dev_dependencies: flutter_test: sdk: flutter @@ -31,3 +36,6 @@ flutter: pluginClass: IrisMethodChannelPlugin windows: pluginClass: IrisMethodChannelPluginCApi + web: + pluginClass: IrisMethodChannelWeb + fileName: iris_method_channel_web.dart diff --git a/test/iris_method_channel_test.dart b/test/iris_method_channel_test.dart index 035cbc7..a8bf004 100644 --- a/test/iris_method_channel_test.dart +++ b/test/iris_method_channel_test.dart @@ -1,223 +1,13 @@ import 'dart:convert'; -import 'dart:ffi' as ffi; -import 'dart:isolate'; import 'dart:typed_data'; -import 'package:ffi/ffi.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/services.dart' show StandardMethodCodec, MethodCall; import 'package:flutter_test/flutter_test.dart'; -import 'package:iris_method_channel/src/bindings/native_iris_api_common_bindings.dart'; -import 'package:iris_method_channel/src/bindings/native_iris_event_bindings.dart' - as iris_event; -import 'package:iris_method_channel/src/iris_event.dart'; -import 'package:iris_method_channel/src/iris_method_channel.dart'; -import 'package:iris_method_channel/src/native_bindings_delegate.dart'; -import 'package:iris_method_channel/src/scoped_objects.dart'; - -class _ApiParam { - _ApiParam(this.event, this.data); - final String event; - final String data; -} - -class _CallApiRecord { - _CallApiRecord(this.methodCall, this.apiParam); - final IrisMethodCall methodCall; - final _ApiParam apiParam; -} - -class _FakeNativeBindingDelegateMessenger { - _FakeNativeBindingDelegateMessenger() { - apiCallPort.listen((message) { - assert(message is _CallApiRecord); - callApiRecords.add(message); - }); - } - final apiCallPort = ReceivePort(); - final callApiRecords = <_CallApiRecord>[]; - - SendPort getSendPort() => apiCallPort.sendPort; -} - -class _FakeNativeBindingDelegate extends NativeBindingDelegate { - _FakeNativeBindingDelegate(this.apiCallPortSendPort); - - final SendPort apiCallPortSendPort; - - void _response(ffi.Pointer param, Map result) { - using((Arena arena) { - final ffi.Pointer resultMapPointerUtf8 = - jsonEncode(result).toNativeUtf8(allocator: arena); - final ffi.Pointer resultMapPointerInt8 = - resultMapPointerUtf8.cast(); - - for (int i = 0; i < kBasicResultLength; i++) { - if (i >= resultMapPointerUtf8.length) { - break; - } - - param.ref.result[i] = resultMapPointerInt8[i]; - } - }); - } - - @override - int callApi(IrisMethodCall methodCall, ffi.Pointer apiEnginePtr, - ffi.Pointer param) { - final record = _CallApiRecord( - methodCall, - _ApiParam( - param.ref.event.cast().toDartString(), - param.ref.data.cast().toDartString(), - ), - ); - apiCallPortSendPort.send(record); - - _response(param, {}); - - return 0; - } - - @override - ffi.Pointer createIrisEventHandler( - ffi.Pointer eventHandler) { - final record = _CallApiRecord( - const IrisMethodCall('createIrisEventHandler', '{}'), - _ApiParam( - 'createIrisEventHandler', - '{}', - ), - ); - apiCallPortSendPort.send(record); - return ffi.Pointer.fromAddress(123456); - } - - @override - CreateNativeApiEngineResult createNativeApiEngine( - List> args) { - return CreateNativeApiEngineResult( - ffi.Pointer.fromAddress(100), - extraData: {'extra_handle': 1000}, - ); - } - - @override - void destroyIrisEventHandler(ffi.Pointer handler) { - final record = _CallApiRecord( - const IrisMethodCall('destroyIrisEventHandler', '{}'), - _ApiParam( - 'destroyIrisEventHandler', - '{}', - ), - ); - apiCallPortSendPort.send(record); - } - - @override - void destroyNativeApiEngine(ffi.Pointer apiEnginePtr) { - final record = _CallApiRecord( - const IrisMethodCall('destroyNativeApiEngine', '{}'), - _ApiParam( - 'destroyNativeApiEngine', - '{}', - ), - ); - apiCallPortSendPort.send(record); - } - - @override - void initialize() { - final record = _CallApiRecord( - const IrisMethodCall('initialize', '{}'), - _ApiParam( - 'initialize', - '{}', - ), - ); - apiCallPortSendPort.send(record); - } -} - -class _FakeIrisEvent implements IrisEvent { - _FakeIrisEvent(this.apiCallPortSendPort); - - final SendPort apiCallPortSendPort; - - @override - void initialize() { - final record = _CallApiRecord( - const IrisMethodCall('IrisEvent_initialize', '{}'), - _ApiParam( - 'IrisEvent_initialize', - '{}', - ), - ); - apiCallPortSendPort.send(record); - } - - @override - void registerEventHandler(SendPort sendPort) { - final record = _CallApiRecord( - const IrisMethodCall('IrisEvent_registerEventHandler', '{}'), - _ApiParam( - 'IrisEvent_registerEventHandler', - '{}', - ), - ); - apiCallPortSendPort.send(record); - } - - @override - void unregisterEventHandler(SendPort sendPort) { - final record = _CallApiRecord( - const IrisMethodCall('IrisEvent_unregisterEventHandler', '{}'), - _ApiParam( - 'IrisEvent_unregisterEventHandler', - '{}', - ), - ); - apiCallPortSendPort.send(record); - } - - @override - void dispose() { - final record = _CallApiRecord( - const IrisMethodCall('IrisEvent_dispose', '{}'), - _ApiParam( - 'IrisEvent_dispose', - '{}', - ), - ); - apiCallPortSendPort.send(record); - } - - @override - ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer p1)>> - get onEventPtr => ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer p1)>>.fromAddress(0); -} +import 'package:iris_method_channel/iris_method_channel.dart'; -class _FakeNativeBindingDelegateProvider extends NativeBindingsProvider { - _FakeNativeBindingDelegateProvider( - this.nativeBindingDelegate, this.irisEvent); - - final NativeBindingDelegate nativeBindingDelegate; - final IrisEvent irisEvent; - - @override - NativeBindingDelegate provideNativeBindingDelegate() { - return nativeBindingDelegate; - } - - @override - IrisEvent provideIrisEvent() { - return irisEvent; - } -} +import 'platform/platform_cases.dart'; +import 'platform/platform_tester.dart'; class _TestEventLoopEventHandler extends EventLoopEventHandler { @override @@ -230,39 +20,18 @@ class _TestEventLoopEventHandler extends EventLoopEventHandler { void main() { final binding = TestWidgetsFlutterBinding.ensureInitialized(); - late _FakeNativeBindingDelegateMessenger messenger; - late NativeBindingsProvider nativeBindingsProvider; - late IrisMethodChannel irisMethodChannel; + platformCases(); - setUp(() { - messenger = _FakeNativeBindingDelegateMessenger(); - final _FakeNativeBindingDelegate nativeBindingDelegate = - _FakeNativeBindingDelegate(messenger.getSendPort()); - final _FakeIrisEvent irisEvent = _FakeIrisEvent(messenger.getSendPort()); - nativeBindingsProvider = - _FakeNativeBindingDelegateProvider(nativeBindingDelegate, irisEvent); - irisMethodChannel = IrisMethodChannel(nativeBindingsProvider); - }); + late CallApiRecorderInterface messenger; - group('Get InitilizationResult', () { - test('able to get InitilizationResult from initilize', () async { - final InitilizationResult? result = await irisMethodChannel.initilize([]); - expect(result, isNotNull); - - expect(result!.irisApiEngineNativeHandle, 100); - expect(result.extraData, {'extra_handle': 1000}); - - await irisMethodChannel.dispose(); - }); - - test('get null InitilizationResult if initilize multiple times', () async { - await irisMethodChannel.initilize([]); + late IrisMethodChannel irisMethodChannel; - final InitilizationResult? result = await irisMethodChannel.initilize([]); - expect(result, isNull); + late PlatformTesterInterface platformTester; - await irisMethodChannel.dispose(); - }); + setUp(() { + platformTester = getPlatformTester(); + irisMethodChannel = platformTester.getIrisMethodChannel(); + messenger = platformTester.getCallApiRecorder(); }); test( @@ -328,96 +97,111 @@ void main() { await irisMethodChannel.dispose(); }); - test('registerEventHandler', () async { - await irisMethodChannel.initilize([]); - - const key = TypedScopedKey(_TestEventLoopEventHandler); - final eventHandler = _TestEventLoopEventHandler(); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler', - unregisterName: 'unregisterEventHandler', - handler: eventHandler), - jsonEncode({})); - - final DisposableScopedObjects subScopedObjects = - irisMethodChannel.scopedEventHandlers.get(key)!; - expect(subScopedObjects.keys.length, 1); + test( + 'registerEventHandler', + () async { + await irisMethodChannel.initilize([]); - final holder = subScopedObjects.values.elementAt(0) as EventHandlerHolder; + const key = TypedScopedKey(_TestEventLoopEventHandler); + final eventHandler = _TestEventLoopEventHandler(); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler', + unregisterName: 'unregisterEventHandler', + handler: eventHandler), + jsonEncode({})); - expect(holder.nativeEventHandlerIntPtr, 123456); + final DisposableScopedObjects subScopedObjects = + irisMethodChannel.scopedEventHandlers.get(key)!; + expect(subScopedObjects.keys.length, 1); - expect(holder.getEventHandlers().length, 1); - expect(holder.getEventHandlers().elementAt(0), eventHandler); + final holder = subScopedObjects.values.elementAt(0) as EventHandlerHolder; - final registerEventHandlerCallRecord = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'registerEventHandler'); - expect(registerEventHandlerCallRecord.length, 1); + expect(holder.nativeEventHandlerIntPtr, 123456); - await irisMethodChannel.dispose(); - }); + expect(holder.getEventHandlers().length, 1); + expect(holder.getEventHandlers().elementAt(0), eventHandler); - test('unregisterEventHandler', () async { - await irisMethodChannel.initilize([]); + final registerEventHandlerCallRecord = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'registerEventHandler'); + expect(registerEventHandlerCallRecord.length, 1); - const key = TypedScopedKey(_TestEventLoopEventHandler); - final eventHandler = _TestEventLoopEventHandler(); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler', - unregisterName: 'unregisterEventHandler', - handler: eventHandler), - jsonEncode({})); - await irisMethodChannel.unregisterEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler', - unregisterName: 'unregisterEventHandler', - handler: eventHandler), - jsonEncode({})); - - final DisposableScopedObjects subScopedObjects = - irisMethodChannel.scopedEventHandlers.get(key)!; - expect(subScopedObjects.keys.length, 1); - - final EventHandlerHolder holder = - subScopedObjects.values.elementAt(0) as EventHandlerHolder; - expect(holder.getEventHandlers().length, 0); - - final registerEventHandlerCallRecord = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'unregisterEventHandler'); - expect(registerEventHandlerCallRecord.length, 1); + await irisMethodChannel.dispose(); + }, + // On web, the `PlatformBindingsDelegateInterface`'s APIs not be called at all + skip: kIsWeb, + ); - await irisMethodChannel.dispose(); - }); + test( + 'unregisterEventHandler', + () async { + await irisMethodChannel.initilize([]); - test('unregisterEventHandlers', () async { - await irisMethodChannel.initilize([]); + const key = TypedScopedKey(_TestEventLoopEventHandler); + final eventHandler = _TestEventLoopEventHandler(); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler', + unregisterName: 'unregisterEventHandler', + handler: eventHandler), + jsonEncode({})); + await irisMethodChannel.unregisterEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler', + unregisterName: 'unregisterEventHandler', + handler: eventHandler), + jsonEncode({})); + + final DisposableScopedObjects subScopedObjects = + irisMethodChannel.scopedEventHandlers.get(key)!; + expect(subScopedObjects.keys.length, 1); + + final EventHandlerHolder holder = + subScopedObjects.values.elementAt(0) as EventHandlerHolder; + expect(holder.getEventHandlers().length, 0); + + final registerEventHandlerCallRecord = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'unregisterEventHandler'); + expect(registerEventHandlerCallRecord.length, 1); - const key = TypedScopedKey(_TestEventLoopEventHandler); - final eventHandler = _TestEventLoopEventHandler(); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler', - unregisterName: 'unregisterEventHandler', - handler: eventHandler), - jsonEncode({})); - await irisMethodChannel.unregisterEventHandlers(key); + await irisMethodChannel.dispose(); + }, + // On web, the `PlatformBindingsDelegateInterface`'s APIs not be called at all + skip: kIsWeb, + ); - final DisposableScopedObjects? subScopedObjects = - irisMethodChannel.scopedEventHandlers.get(key); - expect(subScopedObjects, isNull); + test( + 'unregisterEventHandlers', + () async { + await irisMethodChannel.initilize([]); - final registerEventHandlerCallRecord = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'unregisterEventHandler'); - expect(registerEventHandlerCallRecord.length, 1); + const key = TypedScopedKey(_TestEventLoopEventHandler); + final eventHandler = _TestEventLoopEventHandler(); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler', + unregisterName: 'unregisterEventHandler', + handler: eventHandler), + jsonEncode({})); + await irisMethodChannel.unregisterEventHandlers(key); + + final DisposableScopedObjects? subScopedObjects = + irisMethodChannel.scopedEventHandlers.get(key); + expect(subScopedObjects, isNull); + + final registerEventHandlerCallRecord = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'unregisterEventHandler'); + expect(registerEventHandlerCallRecord.length, 1); - await irisMethodChannel.dispose(); - }); + await irisMethodChannel.dispose(); + }, + // On web, the `PlatformBindingsDelegateInterface`'s APIs not be called at all + skip: kIsWeb, + ); test('disposed', () async { await irisMethodChannel.initilize([]); @@ -428,9 +212,12 @@ void main() { .where((e) => e.methodCall.funcName == 'destroyNativeApiEngine'); expect(callRecord1.length, 1); - final callRecord2 = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'destroyIrisEventHandler'); - expect(callRecord2.length, 1); + // On web, we do not call the `destroyIrisEventHandler` + if (!kIsWeb) { + final callRecord2 = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'destroyIrisEventHandler'); + expect(callRecord2.length, 1); + } }); test('disposed multiple times', () async { @@ -443,9 +230,12 @@ void main() { .where((e) => e.methodCall.funcName == 'destroyNativeApiEngine'); expect(callRecord1.length, 1); - final callRecord2 = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'destroyIrisEventHandler'); - expect(callRecord2.length, 1); + // On web, we do not call the `destroyIrisEventHandler` + if (!kIsWeb) { + final callRecord2 = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'destroyIrisEventHandler'); + expect(callRecord2.length, 1); + } }); test('disposed after receive onDetachedFromEngine_fromPlatform', () async { @@ -469,9 +259,12 @@ void main() { .where((e) => e.methodCall.funcName == 'destroyNativeApiEngine'); expect(callRecord1.length, 1); - final callRecord2 = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'destroyIrisEventHandler'); - expect(callRecord2.length, 1); + // On web, we do not call the `destroyIrisEventHandler` + if (!kIsWeb) { + final callRecord2 = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'destroyIrisEventHandler'); + expect(callRecord2.length, 1); + } }); test('invokeMethod after disposed', () async { @@ -512,28 +305,33 @@ void main() { expect(callApiResult[1].data, kDisposedIrisMethodCallData); }); - test('registerEventHandler after disposed', () async { - await irisMethodChannel.initilize([]); - await irisMethodChannel.dispose(); - - const key = TypedScopedKey(_TestEventLoopEventHandler); - final eventHandler = _TestEventLoopEventHandler(); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler', - unregisterName: 'unregisterEventHandler', - handler: eventHandler), - jsonEncode({})); + test( + 'registerEventHandler after disposed', + () async { + await irisMethodChannel.initilize([]); + await irisMethodChannel.dispose(); - final DisposableScopedObjects? subScopedObjects = - irisMethodChannel.scopedEventHandlers.get(key); - expect(subScopedObjects, isNull); - - final registerEventHandlerCallRecord = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'registerEventHandler'); - expect(registerEventHandlerCallRecord.length, 0); - }); + const key = TypedScopedKey(_TestEventLoopEventHandler); + final eventHandler = _TestEventLoopEventHandler(); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler', + unregisterName: 'unregisterEventHandler', + handler: eventHandler), + jsonEncode({})); + + final DisposableScopedObjects? subScopedObjects = + irisMethodChannel.scopedEventHandlers.get(key); + expect(subScopedObjects, isNull); + + final registerEventHandlerCallRecord = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'registerEventHandler'); + expect(registerEventHandlerCallRecord.length, 0); + }, + // On web, the `PlatformBindingsDelegateInterface`'s APIs not be called at all + skip: kIsWeb, + ); test('unregisterEventHandler after disposed', () async { await irisMethodChannel.initilize([]); @@ -589,479 +387,445 @@ void main() { expect(registerEventHandlerCallRecord.length, 0); }); - test('registerEventHandler 2 times', () async { - await irisMethodChannel.initilize([]); - - const key = TypedScopedKey(_TestEventLoopEventHandler); - final eventHandler1 = _TestEventLoopEventHandler(); - final eventHandler2 = _TestEventLoopEventHandler(); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler', - unregisterName: 'unregisterEventHandler', - handler: eventHandler1), - jsonEncode({})); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler', - unregisterName: 'unregisterEventHandler', - handler: eventHandler2), - jsonEncode({})); - - final DisposableScopedObjects subScopedObjects = - irisMethodChannel.scopedEventHandlers.get(key)!; - expect(subScopedObjects.keys.length, 1); - - final holder = subScopedObjects.values.elementAt(0) as EventHandlerHolder; - - expect(holder.nativeEventHandlerIntPtr, 123456); - - expect(holder.getEventHandlers().length, 2); - expect(holder.getEventHandlers().elementAt(0), eventHandler1); - expect(holder.getEventHandlers().elementAt(1), eventHandler2); - - final registerEventHandlerCallRecord = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'registerEventHandler'); - expect(registerEventHandlerCallRecord.length, 1); - - await irisMethodChannel.dispose(); - }); - - test('registerEventHandler 2 times with different registerName', () async { - await irisMethodChannel.initilize([]); - - const key = TypedScopedKey(_TestEventLoopEventHandler); - final eventHandler1 = _TestEventLoopEventHandler(); - final eventHandler2 = _TestEventLoopEventHandler(); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler', - unregisterName: 'unregisterEventHandler', - handler: eventHandler1), - jsonEncode({})); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler1', - unregisterName: 'unregisterEventHandler1', - handler: eventHandler2), - jsonEncode({})); - - final DisposableScopedObjects subScopedObjects = - irisMethodChannel.scopedEventHandlers.get(key)!; - expect(subScopedObjects.keys.length, 2); - - final holder = subScopedObjects.values.elementAt(0) as EventHandlerHolder; - expect(holder.nativeEventHandlerIntPtr, 123456); - - expect(holder.getEventHandlers().length, 1); - expect(holder.getEventHandlers().elementAt(0), eventHandler1); - - final holder2 = subScopedObjects.values.elementAt(1) as EventHandlerHolder; - expect(holder2.nativeEventHandlerIntPtr, 123456); - - expect(holder2.getEventHandlers().length, 1); - expect(holder2.getEventHandlers().elementAt(0), eventHandler2); - - final registerEventHandlerCallRecord = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'registerEventHandler'); - expect(registerEventHandlerCallRecord.length, 1); - - final registerEventHandlerCallRecord1 = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'registerEventHandler1'); - expect(registerEventHandlerCallRecord1.length, 1); - - await irisMethodChannel.dispose(); - }); - - test('registerEventHandler 2 times, then unregisterEventHandler', () async { - await irisMethodChannel.initilize([]); - - const key = TypedScopedKey(_TestEventLoopEventHandler); - final eventHandler1 = _TestEventLoopEventHandler(); - final eventHandler2 = _TestEventLoopEventHandler(); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler', - unregisterName: 'unregisterEventHandler', - handler: eventHandler1), - jsonEncode({})); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler', - unregisterName: 'unregisterEventHandler', - handler: eventHandler2), - jsonEncode({})); - - await irisMethodChannel.unregisterEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler', - unregisterName: 'unregisterEventHandler', - handler: eventHandler2), - jsonEncode({})); - - final DisposableScopedObjects subScopedObjects = - irisMethodChannel.scopedEventHandlers.get(key)!; - expect(subScopedObjects.keys.length, 1); - - final holder = subScopedObjects.values.elementAt(0) as EventHandlerHolder; - - expect(holder.nativeEventHandlerIntPtr, 123456); - - expect(holder.getEventHandlers().length, 1); - expect(holder.getEventHandlers().elementAt(0), eventHandler1); - - final unregisterEventHandlerCallRecord = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'unregisterEventHandler'); - expect(unregisterEventHandlerCallRecord.length, 0); - - await irisMethodChannel.dispose(); - }); - test( - 'registerEventHandler 2 times with different registerName, then unregisterEventHandler', - () async { - await irisMethodChannel.initilize([]); - - const key = TypedScopedKey(_TestEventLoopEventHandler); - final eventHandler1 = _TestEventLoopEventHandler(); - final eventHandler2 = _TestEventLoopEventHandler(); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler', - unregisterName: 'unregisterEventHandler', - handler: eventHandler1), - jsonEncode({})); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler1', - unregisterName: 'unregisterEventHandler1', - handler: eventHandler2), - jsonEncode({})); - - await irisMethodChannel.unregisterEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler1', - unregisterName: 'unregisterEventHandler1', - handler: eventHandler2), - jsonEncode({})); - - final DisposableScopedObjects subScopedObjects = - irisMethodChannel.scopedEventHandlers.get(key)!; - expect(subScopedObjects.keys.length, 2); - - final holder = subScopedObjects.values.elementAt(0) as EventHandlerHolder; - - expect(holder.nativeEventHandlerIntPtr, 123456); - - expect(holder.getEventHandlers().length, 1); - expect(holder.getEventHandlers().elementAt(0), eventHandler1); - - final holder2 = subScopedObjects.values.elementAt(1) as EventHandlerHolder; - - expect(holder2.getEventHandlers().length, 0); + 'registerEventHandler 2 times', + () async { + await irisMethodChannel.initilize([]); - final unregisterEventHandlerCallRecord = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'unregisterEventHandler1'); - expect(unregisterEventHandlerCallRecord.length, 1); + const key = TypedScopedKey(_TestEventLoopEventHandler); + final eventHandler1 = _TestEventLoopEventHandler(); + final eventHandler2 = _TestEventLoopEventHandler(); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler', + unregisterName: 'unregisterEventHandler', + handler: eventHandler1), + jsonEncode({})); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler', + unregisterName: 'unregisterEventHandler', + handler: eventHandler2), + jsonEncode({})); + + final DisposableScopedObjects subScopedObjects = + irisMethodChannel.scopedEventHandlers.get(key)!; + expect(subScopedObjects.keys.length, 1); + + final holder = subScopedObjects.values.elementAt(0) as EventHandlerHolder; + + expect(holder.nativeEventHandlerIntPtr, 123456); + + expect(holder.getEventHandlers().length, 2); + expect(holder.getEventHandlers().elementAt(0), eventHandler1); + expect(holder.getEventHandlers().elementAt(1), eventHandler2); + + final registerEventHandlerCallRecord = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'registerEventHandler'); + expect(registerEventHandlerCallRecord.length, 1); - await irisMethodChannel.dispose(); - }); + await irisMethodChannel.dispose(); + }, + // On web, the `PlatformBindingsDelegateInterface`'s APIs not be called at all + skip: kIsWeb, + ); test( - 'registerEventHandler 2 times with different registerName, then unregisterEventHandler, then unregisterEventHandlers', - () async { - await irisMethodChannel.initilize([]); - - const key = TypedScopedKey(_TestEventLoopEventHandler); - final eventHandler1 = _TestEventLoopEventHandler(); - final eventHandler2 = _TestEventLoopEventHandler(); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler', - unregisterName: 'unregisterEventHandler', - handler: eventHandler1), - jsonEncode({})); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler1', - unregisterName: 'unregisterEventHandler1', - handler: eventHandler2), - jsonEncode({})); - - await irisMethodChannel.unregisterEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler1', - unregisterName: 'unregisterEventHandler1', - handler: eventHandler2), - jsonEncode({})); - - await irisMethodChannel.unregisterEventHandlers(key); - - final DisposableScopedObjects? subScopedObjects = - irisMethodChannel.scopedEventHandlers.get(key); - expect(subScopedObjects, isNull); - - final unregisterEventHandlerCallRecord = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'unregisterEventHandler'); - expect(unregisterEventHandlerCallRecord.length, 1); + 'registerEventHandler 2 times with different registerName', + () async { + await irisMethodChannel.initilize([]); - final unregisterEventHandlerCallRecord1 = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'unregisterEventHandler1'); - expect(unregisterEventHandlerCallRecord1.length, 1); + const key = TypedScopedKey(_TestEventLoopEventHandler); + final eventHandler1 = _TestEventLoopEventHandler(); + final eventHandler2 = _TestEventLoopEventHandler(); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler', + unregisterName: 'unregisterEventHandler', + handler: eventHandler1), + jsonEncode({})); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler1', + unregisterName: 'unregisterEventHandler1', + handler: eventHandler2), + jsonEncode({})); + + final DisposableScopedObjects subScopedObjects = + irisMethodChannel.scopedEventHandlers.get(key)!; + expect(subScopedObjects.keys.length, 2); + + final holder = subScopedObjects.values.elementAt(0) as EventHandlerHolder; + expect(holder.nativeEventHandlerIntPtr, 123456); + + expect(holder.getEventHandlers().length, 1); + expect(holder.getEventHandlers().elementAt(0), eventHandler1); + + final holder2 = + subScopedObjects.values.elementAt(1) as EventHandlerHolder; + expect(holder2.nativeEventHandlerIntPtr, 123456); + + expect(holder2.getEventHandlers().length, 1); + expect(holder2.getEventHandlers().elementAt(0), eventHandler2); + + final registerEventHandlerCallRecord = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'registerEventHandler'); + expect(registerEventHandlerCallRecord.length, 1); + + final registerEventHandlerCallRecord1 = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'registerEventHandler1'); + expect(registerEventHandlerCallRecord1.length, 1); - await irisMethodChannel.dispose(); - }); + await irisMethodChannel.dispose(); + }, + // On web, the `PlatformBindingsDelegateInterface`'s APIs not be called at all + skip: kIsWeb, + ); test( - 'registerEventHandler 2 times with different registerName, then unregisterEventHandler without await, then unregisterEventHandlers', - () async { - await irisMethodChannel.initilize([]); - - const key = TypedScopedKey(_TestEventLoopEventHandler); - final eventHandler1 = _TestEventLoopEventHandler(); - final eventHandler2 = _TestEventLoopEventHandler(); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler', - unregisterName: 'unregisterEventHandler', - handler: eventHandler1), - jsonEncode({})); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler1', - unregisterName: 'unregisterEventHandler1', - handler: eventHandler2), - jsonEncode({})); - - irisMethodChannel.unregisterEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler1', - unregisterName: 'unregisterEventHandler1', - handler: eventHandler2), - jsonEncode({})); - - await irisMethodChannel.unregisterEventHandlers(key); - - final DisposableScopedObjects? subScopedObjects = - irisMethodChannel.scopedEventHandlers.get(key); - expect(subScopedObjects, isNull); - - final unregisterEventHandlerCallRecord = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'unregisterEventHandler'); - expect(unregisterEventHandlerCallRecord.length, 1); + 'registerEventHandler 2 times, then unregisterEventHandler', + () async { + await irisMethodChannel.initilize([]); - final unregisterEventHandlerCallRecord1 = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'unregisterEventHandler1'); - expect(unregisterEventHandlerCallRecord1.length, 1); + const key = TypedScopedKey(_TestEventLoopEventHandler); + final eventHandler1 = _TestEventLoopEventHandler(); + final eventHandler2 = _TestEventLoopEventHandler(); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler', + unregisterName: 'unregisterEventHandler', + handler: eventHandler1), + jsonEncode({})); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler', + unregisterName: 'unregisterEventHandler', + handler: eventHandler2), + jsonEncode({})); + + await irisMethodChannel.unregisterEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler', + unregisterName: 'unregisterEventHandler', + handler: eventHandler2), + jsonEncode({})); + + final DisposableScopedObjects subScopedObjects = + irisMethodChannel.scopedEventHandlers.get(key)!; + expect(subScopedObjects.keys.length, 1); + + final holder = subScopedObjects.values.elementAt(0) as EventHandlerHolder; + + expect(holder.nativeEventHandlerIntPtr, 123456); + + expect(holder.getEventHandlers().length, 1); + expect(holder.getEventHandlers().elementAt(0), eventHandler1); + + final unregisterEventHandlerCallRecord = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'unregisterEventHandler'); + expect(unregisterEventHandlerCallRecord.length, 0); - await irisMethodChannel.dispose(); - }); + await irisMethodChannel.dispose(); + }, + // On web, the `PlatformBindingsDelegateInterface`'s APIs not be called at all + skip: kIsWeb, + ); test( - 'registerEventHandler 2 times with different registerName, then unregisterEventHandler without await, then unregisterEventHandlers without await', - () async { - await irisMethodChannel.initilize([]); - - const key = TypedScopedKey(_TestEventLoopEventHandler); - final eventHandler1 = _TestEventLoopEventHandler(); - final eventHandler2 = _TestEventLoopEventHandler(); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler', - unregisterName: 'unregisterEventHandler', - handler: eventHandler1), - jsonEncode({})); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler1', - unregisterName: 'unregisterEventHandler1', - handler: eventHandler2), - jsonEncode({})); - - irisMethodChannel.unregisterEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler1', - unregisterName: 'unregisterEventHandler1', - handler: eventHandler2), - jsonEncode({})); - - irisMethodChannel.unregisterEventHandlers(key); - - // Wait for `unregisterEventHandler/unregisterEventHandlers` completed. - await Future.delayed(const Duration(milliseconds: 500)); - - final unregisterEventHandlerCallRecord = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'unregisterEventHandler'); - expect(unregisterEventHandlerCallRecord.length, 1); - - final unregisterEventHandlerCallRecord1 = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'unregisterEventHandler1'); - expect(unregisterEventHandlerCallRecord1.length, 1); - - await irisMethodChannel.dispose(); - }); - - test('registerEventHandler 2 times, then unregisterEventHandlers', () async { - await irisMethodChannel.initilize([]); - - const key = TypedScopedKey(_TestEventLoopEventHandler); - final eventHandler1 = _TestEventLoopEventHandler(); - final eventHandler2 = _TestEventLoopEventHandler(); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler', - unregisterName: 'unregisterEventHandler', - handler: eventHandler1), - jsonEncode({})); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler', - unregisterName: 'unregisterEventHandler', - handler: eventHandler2), - jsonEncode({})); - - await irisMethodChannel.unregisterEventHandlers(key); - - final DisposableScopedObjects? subScopedObjects = - irisMethodChannel.scopedEventHandlers.get(key); - expect(subScopedObjects, isNull); + 'registerEventHandler 2 times with different registerName, then unregisterEventHandler', + () async { + await irisMethodChannel.initilize([]); - final registerEventHandlerCallRecord = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'unregisterEventHandler'); - expect(registerEventHandlerCallRecord.length, 2); + const key = TypedScopedKey(_TestEventLoopEventHandler); + final eventHandler1 = _TestEventLoopEventHandler(); + final eventHandler2 = _TestEventLoopEventHandler(); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler', + unregisterName: 'unregisterEventHandler', + handler: eventHandler1), + jsonEncode({})); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler1', + unregisterName: 'unregisterEventHandler1', + handler: eventHandler2), + jsonEncode({})); + + await irisMethodChannel.unregisterEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler1', + unregisterName: 'unregisterEventHandler1', + handler: eventHandler2), + jsonEncode({})); + + final DisposableScopedObjects subScopedObjects = + irisMethodChannel.scopedEventHandlers.get(key)!; + expect(subScopedObjects.keys.length, 2); + + final holder = subScopedObjects.values.elementAt(0) as EventHandlerHolder; + + expect(holder.nativeEventHandlerIntPtr, 123456); + + expect(holder.getEventHandlers().length, 1); + expect(holder.getEventHandlers().elementAt(0), eventHandler1); + + final holder2 = + subScopedObjects.values.elementAt(1) as EventHandlerHolder; + + expect(holder2.getEventHandlers().length, 0); + + final unregisterEventHandlerCallRecord = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'unregisterEventHandler1'); + expect(unregisterEventHandlerCallRecord.length, 1); - await irisMethodChannel.dispose(); - }); + await irisMethodChannel.dispose(); + }, + // On web, the `PlatformBindingsDelegateInterface`'s APIs not be called at all + skip: kIsWeb, + ); test( - 'registerEventHandler 2 times with different registerName, then unregisterEventHandlers', - () async { - await irisMethodChannel.initilize([]); - - const key = TypedScopedKey(_TestEventLoopEventHandler); - final eventHandler1 = _TestEventLoopEventHandler(); - final eventHandler2 = _TestEventLoopEventHandler(); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler', - unregisterName: 'unregisterEventHandler', - handler: eventHandler1), - jsonEncode({})); - await irisMethodChannel.registerEventHandler( - ScopedEvent( - scopedKey: key, - registerName: 'registerEventHandler1', - unregisterName: 'unregisterEventHandler1', - handler: eventHandler2), - jsonEncode({})); - - await irisMethodChannel.unregisterEventHandlers(key); - - final DisposableScopedObjects? subScopedObjects = - irisMethodChannel.scopedEventHandlers.get(key); - expect(subScopedObjects, isNull); - - final registerEventHandlerCallRecord = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'unregisterEventHandler'); - expect(registerEventHandlerCallRecord.length, 1); - - final registerEventHandlerCallRecord1 = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'unregisterEventHandler1'); - expect(registerEventHandlerCallRecord1.length, 1); - - await irisMethodChannel.dispose(); - }); - - test('Should clean native resources when hot restart happen', () async { - await irisMethodChannel.initilize([]); - - irisMethodChannel.workerIsolate.kill(priority: Isolate.immediate); - // Delayed 1 second to ensure `irisMethodChannel.workerIsolate.kill` done - await Future.delayed(const Duration(seconds: 1)); - - final destroyNativeApiEngineCallRecord = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'destroyNativeApiEngine'); - expect(destroyNativeApiEngineCallRecord.length, 1); + 'registerEventHandler 2 times with different registerName, then unregisterEventHandler, then unregisterEventHandlers', + () async { + await irisMethodChannel.initilize([]); - final destroyIrisEventHandlerCallRecord = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'destroyIrisEventHandler'); - expect(destroyIrisEventHandlerCallRecord.length, 1); + const key = TypedScopedKey(_TestEventLoopEventHandler); + final eventHandler1 = _TestEventLoopEventHandler(); + final eventHandler2 = _TestEventLoopEventHandler(); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler', + unregisterName: 'unregisterEventHandler', + handler: eventHandler1), + jsonEncode({})); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler1', + unregisterName: 'unregisterEventHandler1', + handler: eventHandler2), + jsonEncode({})); + + await irisMethodChannel.unregisterEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler1', + unregisterName: 'unregisterEventHandler1', + handler: eventHandler2), + jsonEncode({})); + + await irisMethodChannel.unregisterEventHandlers(key); + + final DisposableScopedObjects? subScopedObjects = + irisMethodChannel.scopedEventHandlers.get(key); + expect(subScopedObjects, isNull); + + final unregisterEventHandlerCallRecord = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'unregisterEventHandler'); + expect(unregisterEventHandlerCallRecord.length, 1); + + final unregisterEventHandlerCallRecord1 = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'unregisterEventHandler1'); + expect(unregisterEventHandlerCallRecord1.length, 1); - final irisEventDisposeCallRecord = messenger.callApiRecords - .where((e) => e.methodCall.funcName == 'IrisEvent_dispose'); - expect(irisEventDisposeCallRecord.length, 1); - }); + await irisMethodChannel.dispose(); + }, + // On web, the `PlatformBindingsDelegateInterface`'s APIs not be called at all + skip: kIsWeb, + ); - test('addHotRestartListener', () async { - await irisMethodChannel.initilize([]); + test( + 'registerEventHandler 2 times with different registerName, then unregisterEventHandler without await, then unregisterEventHandlers', + () async { + await irisMethodChannel.initilize([]); - bool hotRestartListenerCalled = false; - irisMethodChannel.addHotRestartListener((message) { - hotRestartListenerCalled = true; - }); - irisMethodChannel.workerIsolate.kill(priority: Isolate.immediate); + const key = TypedScopedKey(_TestEventLoopEventHandler); + final eventHandler1 = _TestEventLoopEventHandler(); + final eventHandler2 = _TestEventLoopEventHandler(); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler', + unregisterName: 'unregisterEventHandler', + handler: eventHandler1), + jsonEncode({})); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler1', + unregisterName: 'unregisterEventHandler1', + handler: eventHandler2), + jsonEncode({})); + + irisMethodChannel.unregisterEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler1', + unregisterName: 'unregisterEventHandler1', + handler: eventHandler2), + jsonEncode({})); + + await irisMethodChannel.unregisterEventHandlers(key); + + final DisposableScopedObjects? subScopedObjects = + irisMethodChannel.scopedEventHandlers.get(key); + expect(subScopedObjects, isNull); + + final unregisterEventHandlerCallRecord = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'unregisterEventHandler'); + expect(unregisterEventHandlerCallRecord.length, 1); + + final unregisterEventHandlerCallRecord1 = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'unregisterEventHandler1'); + expect(unregisterEventHandlerCallRecord1.length, 1); - // Delayed 1 second to ensure `irisMethodChannel.workerIsolate.kill` done - await Future.delayed(const Duration(seconds: 1)); + await irisMethodChannel.dispose(); + }, + // On web, the `PlatformBindingsDelegateInterface`'s APIs not be called at all + skip: kIsWeb, + ); - expect(hotRestartListenerCalled, true); - }); + test( + 'registerEventHandler 2 times with different registerName, then unregisterEventHandler without await, then unregisterEventHandlers without await', + () async { + await irisMethodChannel.initilize([]); - test('removeHotRestartListener', () async { - await irisMethodChannel.initilize([]); + const key = TypedScopedKey(_TestEventLoopEventHandler); + final eventHandler1 = _TestEventLoopEventHandler(); + final eventHandler2 = _TestEventLoopEventHandler(); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler', + unregisterName: 'unregisterEventHandler', + handler: eventHandler1), + jsonEncode({})); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler1', + unregisterName: 'unregisterEventHandler1', + handler: eventHandler2), + jsonEncode({})); + + irisMethodChannel.unregisterEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler1', + unregisterName: 'unregisterEventHandler1', + handler: eventHandler2), + jsonEncode({})); + + irisMethodChannel.unregisterEventHandlers(key); + + // Wait for `unregisterEventHandler/unregisterEventHandlers` completed. + await Future.delayed(const Duration(milliseconds: 500)); + + final unregisterEventHandlerCallRecord = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'unregisterEventHandler'); + expect(unregisterEventHandlerCallRecord.length, 1); + + final unregisterEventHandlerCallRecord1 = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'unregisterEventHandler1'); + expect(unregisterEventHandlerCallRecord1.length, 1); - bool hotRestartListenerCalled = false; - // ignore: prefer_function_declarations_over_variables - final listener = (message) { - hotRestartListenerCalled = true; - }; - irisMethodChannel.addHotRestartListener(listener); - irisMethodChannel.removeHotRestartListener(listener); - irisMethodChannel.workerIsolate.kill(priority: Isolate.immediate); + await irisMethodChannel.dispose(); + }, + // On web, the `PlatformBindingsDelegateInterface`'s APIs not be called at all + skip: kIsWeb, + ); - // Delayed 1 second to ensure `irisMethodChannel.workerIsolate.kill` done - await Future.delayed(const Duration(seconds: 1)); + test( + 'registerEventHandler 2 times, then unregisterEventHandlers', + () async { + await irisMethodChannel.initilize([]); - expect(hotRestartListenerCalled, false); - }); + const key = TypedScopedKey(_TestEventLoopEventHandler); + final eventHandler1 = _TestEventLoopEventHandler(); + final eventHandler2 = _TestEventLoopEventHandler(); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler', + unregisterName: 'unregisterEventHandler', + handler: eventHandler1), + jsonEncode({})); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler', + unregisterName: 'unregisterEventHandler', + handler: eventHandler2), + jsonEncode({})); + + await irisMethodChannel.unregisterEventHandlers(key); + + final DisposableScopedObjects? subScopedObjects = + irisMethodChannel.scopedEventHandlers.get(key); + expect(subScopedObjects, isNull); + + final registerEventHandlerCallRecord = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'unregisterEventHandler'); + expect(registerEventHandlerCallRecord.length, 2); - test('removeHotRestartListener through returned VoidCallback', () async { - await irisMethodChannel.initilize([]); + await irisMethodChannel.dispose(); + }, + // On web, the `PlatformBindingsDelegateInterface`'s APIs not be called at all + skip: kIsWeb, + ); - bool hotRestartListenerCalled = false; - // ignore: prefer_function_declarations_over_variables - final listener = (message) { - hotRestartListenerCalled = true; - }; - final removeListener = irisMethodChannel.addHotRestartListener(listener); - removeListener(); - irisMethodChannel.workerIsolate.kill(priority: Isolate.immediate); + test( + 'registerEventHandler 2 times with different registerName, then unregisterEventHandlers', + () async { + await irisMethodChannel.initilize([]); - // Delayed 1 second to ensure `irisMethodChannel.workerIsolate.kill` done - await Future.delayed(const Duration(seconds: 1)); + const key = TypedScopedKey(_TestEventLoopEventHandler); + final eventHandler1 = _TestEventLoopEventHandler(); + final eventHandler2 = _TestEventLoopEventHandler(); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler', + unregisterName: 'unregisterEventHandler', + handler: eventHandler1), + jsonEncode({})); + await irisMethodChannel.registerEventHandler( + ScopedEvent( + scopedKey: key, + registerName: 'registerEventHandler1', + unregisterName: 'unregisterEventHandler1', + handler: eventHandler2), + jsonEncode({})); + + await irisMethodChannel.unregisterEventHandlers(key); + + final DisposableScopedObjects? subScopedObjects = + irisMethodChannel.scopedEventHandlers.get(key); + expect(subScopedObjects, isNull); + + final registerEventHandlerCallRecord = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'unregisterEventHandler'); + expect(registerEventHandlerCallRecord.length, 1); + + final registerEventHandlerCallRecord1 = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'unregisterEventHandler1'); + expect(registerEventHandlerCallRecord1.length, 1); - expect(hotRestartListenerCalled, false); - }); + await irisMethodChannel.dispose(); + }, + // On web, the `PlatformBindingsDelegateInterface`'s APIs not be called at all + skip: kIsWeb, + ); } diff --git a/test/platform/fake/fake_iris_api_common_bindings_js.dart b/test/platform/fake/fake_iris_api_common_bindings_js.dart new file mode 100644 index 0000000..1373636 --- /dev/null +++ b/test/platform/fake/fake_iris_api_common_bindings_js.dart @@ -0,0 +1,7 @@ +// The Dart class must have `@JSExport` on it or one of its instance members. +import 'package:js/js.dart'; + +@JSExport() +class FakeIrisApiEngineJS { + FakeIrisApiEngineJS(); +} diff --git a/test/platform/fake/fake_platform_binding_delegate_io.dart b/test/platform/fake/fake_platform_binding_delegate_io.dart new file mode 100644 index 0000000..4fdaca0 --- /dev/null +++ b/test/platform/fake/fake_platform_binding_delegate_io.dart @@ -0,0 +1,247 @@ +import 'dart:convert'; +import 'dart:ffi' as ffi; +import 'dart:isolate'; + +import 'package:ffi/ffi.dart'; +import 'package:iris_method_channel/iris_method_channel.dart'; +import 'package:iris_method_channel/src/platform/io/bindings/native_iris_api_common_bindings.dart' + as iris; +import 'package:iris_method_channel/src/platform/io/bindings/native_iris_event_bindings.dart' + as iris_event; +import 'package:iris_method_channel/src/platform/io/iris_event_io.dart'; + +import 'platform_tester_interface.dart'; + +class _FakeNativeBindingDelegateMessenger implements CallApiRecorderInterface { + _FakeNativeBindingDelegateMessenger() { + apiCallPort.listen((message) { + assert(message is CallApiRecord); + callApiRecords.add(message); + }); + } + final apiCallPort = ReceivePort(); + final _callApiRecords = []; + + SendPort getSendPort() => apiCallPort.sendPort; + + @override + List get callApiRecords { + return _callApiRecords; + } +} + +class FakeNativeBindingDelegate extends PlatformBindingsDelegateInterface { + FakeNativeBindingDelegate(this.apiCallPortSendPort); + + final SendPort apiCallPortSendPort; + + void _response(ffi.Pointer param, Map result) { + using((Arena arena) { + final ffi.Pointer resultMapPointerUtf8 = + jsonEncode(result).toNativeUtf8(allocator: arena); + final ffi.Pointer resultMapPointerInt8 = + resultMapPointerUtf8.cast(); + + for (int i = 0; i < kBasicResultLength; i++) { + if (i >= resultMapPointerUtf8.length) { + break; + } + + param.ref.result[i] = resultMapPointerInt8[i]; + } + }); + } + + @override + int callApi( + IrisMethodCall methodCall, + IrisApiEngineHandle apiEnginePtr, + IrisApiParamHandle param, + ) { + final theParam = param() as ffi.Pointer; + final record = CallApiRecord( + methodCall, + CallApiRecordApiParam( + theParam.ref.event.cast().toDartString(), + theParam.ref.data.cast().toDartString(), + ), + ); + apiCallPortSendPort.send(record); + + _response(theParam, {}); + + return 0; + } + + @override + IrisEventHandlerHandle createIrisEventHandler( + IrisCEventHandlerHandle eventHandler, + ) { + final record = CallApiRecord( + const IrisMethodCall('createIrisEventHandler', '{}'), + CallApiRecordApiParam( + 'createIrisEventHandler', + '{}', + ), + ); + apiCallPortSendPort.send(record); + return IrisEventHandlerHandle(ffi.Pointer.fromAddress(123456)); + } + + @override + CreateApiEngineResult createApiEngine(List args) { + return CreateApiEngineResult( + IrisApiEngineHandle(ffi.Pointer.fromAddress(100)), + extraData: {'extra_handle': 1000}, + ); + } + + @override + void destroyIrisEventHandler( + IrisEventHandlerHandle handler, + ) { + final record = CallApiRecord( + const IrisMethodCall('destroyIrisEventHandler', '{}'), + CallApiRecordApiParam( + 'destroyIrisEventHandler', + '{}', + ), + ); + apiCallPortSendPort.send(record); + } + + @override + void destroyNativeApiEngine(IrisApiEngineHandle apiEnginePtr) { + final record = CallApiRecord( + const IrisMethodCall('destroyNativeApiEngine', '{}'), + CallApiRecordApiParam( + 'destroyNativeApiEngine', + '{}', + ), + ); + apiCallPortSendPort.send(record); + } + + @override + void initialize() { + final record = CallApiRecord( + const IrisMethodCall('initialize', '{}'), + CallApiRecordApiParam( + 'initialize', + '{}', + ), + ); + apiCallPortSendPort.send(record); + } + + @override + Future callApiAsync(IrisMethodCall methodCall, + IrisApiEngineHandle apiEnginePtr, IrisApiParamHandle param) async { + return CallApiResult(irisReturnCode: 0, data: {}); + } +} + +class _FakeIrisEvent implements IrisEventIO { + _FakeIrisEvent(this.apiCallPortSendPort); + + final SendPort apiCallPortSendPort; + + @override + void initialize() { + final record = CallApiRecord( + const IrisMethodCall('IrisEvent_initialize', '{}'), + CallApiRecordApiParam( + 'IrisEvent_initialize', + '{}', + ), + ); + apiCallPortSendPort.send(record); + } + + @override + void registerEventHandler(SendPort sendPort) { + final record = CallApiRecord( + const IrisMethodCall('IrisEvent_registerEventHandler', '{}'), + CallApiRecordApiParam( + 'IrisEvent_registerEventHandler', + '{}', + ), + ); + apiCallPortSendPort.send(record); + } + + @override + void unregisterEventHandler(SendPort sendPort) { + final record = CallApiRecord( + const IrisMethodCall('IrisEvent_unregisterEventHandler', '{}'), + CallApiRecordApiParam( + 'IrisEvent_unregisterEventHandler', + '{}', + ), + ); + apiCallPortSendPort.send(record); + } + + @override + void dispose() { + final record = CallApiRecord( + const IrisMethodCall('IrisEvent_dispose', '{}'), + CallApiRecordApiParam( + 'IrisEvent_dispose', + '{}', + ), + ); + apiCallPortSendPort.send(record); + } + + @override + ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer p1)>> + get onEventPtr => ffi.Pointer< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer p1)>>.fromAddress(0); +} + +class FakeNativeBindingDelegateProvider extends PlatformBindingsProvider { + FakeNativeBindingDelegateProvider(this.nativeBindingDelegate, this.irisEvent); + + final PlatformBindingsDelegateInterface nativeBindingDelegate; + final IrisEvent irisEvent; + + @override + PlatformBindingsDelegateInterface provideNativeBindingDelegate() { + return nativeBindingDelegate; + } + + @override + IrisEvent provideIrisEvent() { + return irisEvent; + } +} + +class PlatformTesterInterfaceIO implements PlatformTesterInterface { + PlatformTesterInterfaceIO() { + messenger = _FakeNativeBindingDelegateMessenger(); + final FakeNativeBindingDelegate nativeBindingDelegate = + FakeNativeBindingDelegate(messenger.getSendPort()); + final _FakeIrisEvent irisEvent = _FakeIrisEvent(messenger.getSendPort()); + final nativeBindingsProvider = + FakeNativeBindingDelegateProvider(nativeBindingDelegate, irisEvent); + irisMethodChannel = IrisMethodChannel(nativeBindingsProvider); + } + + late _FakeNativeBindingDelegateMessenger messenger; + late IrisMethodChannel irisMethodChannel; + + @override + CallApiRecorderInterface getCallApiRecorder() { + return messenger; + } + + @override + IrisMethodChannel getIrisMethodChannel() { + return irisMethodChannel; + } +} diff --git a/test/platform/fake/fake_platform_binding_delegate_web.dart b/test/platform/fake/fake_platform_binding_delegate_web.dart new file mode 100644 index 0000000..bd44c95 --- /dev/null +++ b/test/platform/fake/fake_platform_binding_delegate_web.dart @@ -0,0 +1,173 @@ +import 'dart:js' as js; + +import 'package:iris_method_channel/iris_method_channel.dart'; +import 'package:iris_method_channel/src/platform/web/iris_event_web.dart'; + +import 'platform_tester_interface.dart'; + +class FakeTypeWeb {} + +class _FakeNativeBindingDelegateMessenger implements CallApiRecorderInterface { + _FakeNativeBindingDelegateMessenger(); + final _callApiRecords = []; + + @override + List get callApiRecords { + return _callApiRecords; + } + + void addCallApiRecord(CallApiRecord record) { + _callApiRecords.add(record); + } +} + +class FakeNativeBindingDelegate extends PlatformBindingsDelegateInterface { + FakeNativeBindingDelegate(this.messenger); + + final _FakeNativeBindingDelegateMessenger messenger; + + @override + int callApi( + IrisMethodCall methodCall, + IrisApiEngineHandle apiEnginePtr, + IrisApiParamHandle param, + ) { + throw UnimplementedError('Not implemented on web.'); + } + + @override + IrisEventHandlerHandle createIrisEventHandler( + IrisCEventHandlerHandle eventHandler, + ) { + final record = CallApiRecord( + const IrisMethodCall('createIrisEventHandler', '{}'), + CallApiRecordApiParam( + 'createIrisEventHandler', + '{}', + ), + ); + messenger.addCallApiRecord(record); + + return IrisEventHandlerHandle(FakeTypeWeb()); + } + + @override + CreateApiEngineResult createApiEngine(List args) { + return CreateApiEngineResult( + IrisApiEngineHandle(FakeTypeWeb()), + extraData: {'extra_handle': 1000}, + ); + } + + @override + void destroyIrisEventHandler( + IrisEventHandlerHandle handler, + ) { + final record = CallApiRecord( + const IrisMethodCall('destroyIrisEventHandler', '{}'), + CallApiRecordApiParam( + 'destroyIrisEventHandler', + '{}', + ), + ); + messenger.addCallApiRecord(record); + } + + @override + void destroyNativeApiEngine(IrisApiEngineHandle apiEnginePtr) { + final record = CallApiRecord( + const IrisMethodCall('destroyNativeApiEngine', '{}'), + CallApiRecordApiParam( + 'destroyNativeApiEngine', + '{}', + ), + ); + messenger.addCallApiRecord(record); + } + + @override + void initialize() { + final record = CallApiRecord( + const IrisMethodCall('initialize', '{}'), + CallApiRecordApiParam( + 'initialize', + '{}', + ), + ); + messenger.addCallApiRecord(record); + } + + @override + Future callApiAsync(IrisMethodCall methodCall, + IrisApiEngineHandle apiEnginePtr, IrisApiParamHandle param) async { + final record = CallApiRecord( + methodCall, + CallApiRecordApiParam( + methodCall.funcName, + methodCall.params, + ), + ); + messenger.addCallApiRecord(record); + + return CallApiResult(irisReturnCode: 0, data: {}); + } +} + +class _FakeIrisEvent implements IrisEventWeb { + _FakeIrisEvent(); + + @override + void dispose() {} + + @override + void initialize() {} + + @override + void setIrisEventMessageListener(IrisEventMessageListener? listener) {} +} + +class FakeNativeBindingDelegateProvider extends PlatformBindingsProvider { + FakeNativeBindingDelegateProvider(this.nativeBindingDelegate, this.irisEvent); + + final PlatformBindingsDelegateInterface nativeBindingDelegate; + final IrisEvent irisEvent; + + @override + PlatformBindingsDelegateInterface provideNativeBindingDelegate() { + return nativeBindingDelegate; + } + + @override + IrisEvent provideIrisEvent() { + return irisEvent; + } +} + +class EventParamFake {} + +class PlatformTesterInterfaceWeb implements PlatformTesterInterface { + PlatformTesterInterfaceWeb() { + js.context['EventParam'] = EventParamFake(); + + messenger = _FakeNativeBindingDelegateMessenger(); + final FakeNativeBindingDelegate nativeBindingDelegate = + FakeNativeBindingDelegate(messenger); + final _FakeIrisEvent irisEvent = _FakeIrisEvent(); + final nativeBindingsProvider = + FakeNativeBindingDelegateProvider(nativeBindingDelegate, irisEvent); + irisMethodChannel = IrisMethodChannel(nativeBindingsProvider); + } + + late _FakeNativeBindingDelegateMessenger messenger; + late IrisMethodChannel irisMethodChannel; + + @override + CallApiRecorderInterface getCallApiRecorder() { + return messenger; + } + + @override + IrisMethodChannel getIrisMethodChannel() { + return irisMethodChannel; + } +} diff --git a/test/platform/fake/platform_tester_interface.dart b/test/platform/fake/platform_tester_interface.dart new file mode 100644 index 0000000..058c78c --- /dev/null +++ b/test/platform/fake/platform_tester_interface.dart @@ -0,0 +1,23 @@ +import 'package:iris_method_channel/iris_method_channel.dart'; + +class CallApiRecordApiParam { + CallApiRecordApiParam(this.event, this.data); + final String event; + final String data; +} + +class CallApiRecord { + CallApiRecord(this.methodCall, this.apiParam); + final IrisMethodCall methodCall; + final CallApiRecordApiParam apiParam; +} + +abstract class CallApiRecorderInterface { + List get callApiRecords; +} + +abstract class PlatformTesterInterface { + IrisMethodChannel getIrisMethodChannel(); + + CallApiRecorderInterface getCallApiRecorder(); +} diff --git a/test/platform/platform_cases.dart b/test/platform/platform_cases.dart new file mode 100644 index 0000000..519d0e2 --- /dev/null +++ b/test/platform/platform_cases.dart @@ -0,0 +1,3 @@ +export 'platform_cases_expect.dart' + if (dart.library.io) 'platform_cases_actual_io.dart' + if (dart.library.html) 'platform_cases_actual_web.dart'; diff --git a/test/platform/platform_cases_actual_io.dart b/test/platform/platform_cases_actual_io.dart new file mode 100644 index 0000000..fe8b2e0 --- /dev/null +++ b/test/platform/platform_cases_actual_io.dart @@ -0,0 +1,119 @@ +import 'dart:isolate'; + +import 'package:iris_method_channel/iris_method_channel.dart'; +import 'package:iris_method_channel/src/platform/io/iris_method_channel_internal_io.dart'; +import 'package:test/test.dart'; + +import 'platform_tester.dart'; + +void platformCases() { + group('Get InitilizationResult', () { + late CallApiRecorderInterface messenger; + + late IrisMethodChannel irisMethodChannel; + + late IrisMethodChannelInternalIO irisMethodChannelInternal; + + late PlatformTesterInterface platformTester; + + setUp(() { + platformTester = getPlatformTester(); + irisMethodChannel = platformTester.getIrisMethodChannel(); + irisMethodChannelInternal = irisMethodChannel + .getIrisMethodChannelInternal() as IrisMethodChannelInternalIO; + messenger = platformTester.getCallApiRecorder(); + }); + + test('able to get InitilizationResult from initilize', () async { + final InitilizationResult? result = await irisMethodChannel.initilize([]); + expect(result, isNotNull); + + final resultIO = result! as InitilizationResultIO; + + expect(resultIO.irisApiEngineNativeHandle, 100); + expect(resultIO.extraData, {'extra_handle': 1000}); + + await irisMethodChannel.dispose(); + }); + + test('get null InitilizationResult if initilize multiple times', () async { + await irisMethodChannel.initilize([]); + + final InitilizationResult? result = await irisMethodChannel.initilize([]); + expect(result, isNull); + + await irisMethodChannel.dispose(); + }); + + test('Should clean native resources when hot restart happen', () async { + await irisMethodChannel.initilize([]); + + irisMethodChannelInternal.workerIsolate.kill(priority: Isolate.immediate); + // Delayed 1 second to ensure `irisMethodChannel.workerIsolate.kill` done + await Future.delayed(const Duration(seconds: 1)); + + final destroyNativeApiEngineCallRecord = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'destroyNativeApiEngine'); + expect(destroyNativeApiEngineCallRecord.length, 1); + + final destroyIrisEventHandlerCallRecord = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'destroyIrisEventHandler'); + expect(destroyIrisEventHandlerCallRecord.length, 1); + + final irisEventDisposeCallRecord = messenger.callApiRecords + .where((e) => e.methodCall.funcName == 'IrisEvent_dispose'); + expect(irisEventDisposeCallRecord.length, 1); + }); + + test('addHotRestartListener', () async { + await irisMethodChannel.initilize([]); + + bool hotRestartListenerCalled = false; + irisMethodChannel.addHotRestartListener((message) { + hotRestartListenerCalled = true; + }); + irisMethodChannelInternal.workerIsolate.kill(priority: Isolate.immediate); + + // Delayed 1 second to ensure `irisMethodChannel.workerIsolate.kill` done + await Future.delayed(const Duration(seconds: 1)); + + expect(hotRestartListenerCalled, true); + }); + + test('removeHotRestartListener', () async { + await irisMethodChannel.initilize([]); + + bool hotRestartListenerCalled = false; + // ignore: prefer_function_declarations_over_variables + final listener = (message) { + hotRestartListenerCalled = true; + }; + irisMethodChannel.addHotRestartListener(listener); + irisMethodChannel.removeHotRestartListener(listener); + irisMethodChannelInternal.workerIsolate.kill(priority: Isolate.immediate); + + // Delayed 1 second to ensure `irisMethodChannel.workerIsolate.kill` done + await Future.delayed(const Duration(seconds: 1)); + + expect(hotRestartListenerCalled, false); + }); + + test('removeHotRestartListener through returned VoidCallback', () async { + await irisMethodChannel.initilize([]); + + bool hotRestartListenerCalled = false; + // ignore: prefer_function_declarations_over_variables + final listener = (message) { + hotRestartListenerCalled = true; + }; + final removeListener = irisMethodChannel.addHotRestartListener(listener); + removeListener(); + irisMethodChannelInternal.workerIsolate.kill(priority: Isolate.immediate); + + // Delayed 1 second to ensure `irisMethodChannel.workerIsolate.kill` done + await Future.delayed(const Duration(seconds: 1)); + + expect(hotRestartListenerCalled, false); + }); + }); +} diff --git a/test/platform/platform_cases_actual_web.dart b/test/platform/platform_cases_actual_web.dart new file mode 100644 index 0000000..7a7d31e --- /dev/null +++ b/test/platform/platform_cases_actual_web.dart @@ -0,0 +1,3 @@ +void platformCases() { + // Empty +} diff --git a/test/platform/platform_cases_expect.dart b/test/platform/platform_cases_expect.dart new file mode 100644 index 0000000..92097d3 --- /dev/null +++ b/test/platform/platform_cases_expect.dart @@ -0,0 +1,5 @@ +/// Stub function for platform specific test cases. +/// See implemetation: +/// io: `platform_cases_actual_io.dart` +/// web: `platform_cases_actual_web.dart` +void platformCases() => throw UnimplementedError('Unimplemented'); diff --git a/test/platform/platform_tester.dart b/test/platform/platform_tester.dart new file mode 100644 index 0000000..081ba1c --- /dev/null +++ b/test/platform/platform_tester.dart @@ -0,0 +1,5 @@ +export 'fake/platform_tester_interface.dart'; + +export 'platform_tester_expect.dart' + if (dart.library.io) 'platform_tester_actual_io.dart' + if (dart.library.js) 'platform_tester_actual_web.dart'; diff --git a/test/platform/platform_tester_actual_io.dart b/test/platform/platform_tester_actual_io.dart new file mode 100644 index 0000000..c6112d8 --- /dev/null +++ b/test/platform/platform_tester_actual_io.dart @@ -0,0 +1,5 @@ +import 'fake/fake_platform_binding_delegate_io.dart'; +import 'fake/platform_tester_interface.dart'; + +/// Implementation of create the [PlatformTesterInterface] of `dart:io` +PlatformTesterInterface getPlatformTester() => PlatformTesterInterfaceIO(); diff --git a/test/platform/platform_tester_actual_web.dart b/test/platform/platform_tester_actual_web.dart new file mode 100644 index 0000000..14dc5ff --- /dev/null +++ b/test/platform/platform_tester_actual_web.dart @@ -0,0 +1,5 @@ +import 'fake/fake_platform_binding_delegate_web.dart'; +import 'fake/platform_tester_interface.dart'; + +/// Implementation of create the [PlatformTesterInterface] on web +PlatformTesterInterface getPlatformTester() => PlatformTesterInterfaceWeb(); diff --git a/test/platform/platform_tester_expect.dart b/test/platform/platform_tester_expect.dart new file mode 100644 index 0000000..0be7609 --- /dev/null +++ b/test/platform/platform_tester_expect.dart @@ -0,0 +1,8 @@ +import 'fake/platform_tester_interface.dart'; + +/// Stub function for create the platform specific [PlatformTesterInterface]. +/// See implemetation: +/// io: `platform_tester_actual_io.dart` +/// web: `platform_tester_actual_web.dart` +PlatformTesterInterface getPlatformTester() => + throw UnimplementedError('Unimplemented');