rmawatson / flutter_isolate

Launch an isolate that can use flutter plugins.
MIT License
269 stars 80 forks source link

Unable to send class Objects in sendPort.send when using FlutterIsolate.spawn #137

Open git-elliot opened 1 year ago

git-elliot commented 1 year ago

but can send the same when using Isolate.spawn from dart library.

[VERBOSE-2:dart_vm_initializer.cc(41)] Unhandled Exception: Invalid argument: is a regular instance: Instance of 'ActiveHost'
#0      _SendPort._sendInternal (dart:isolate-patch/isolate_patch.dart:249:43)
#1      _SendPort.send (dart:isolate-patch/isolate_patch.dart:230:5)
#2      HostScannerFlutter._startSearchingDevices.<anonymous closure>.<anonymous closure>.<anonymous closure> (package:network_tools_flutter/src/host_scanner_flutter.dart:94:24)
nmfisher commented 1 year ago

Can you provide a reproducible sample project?

git-elliot commented 1 year ago

@nmfisher clicking on floatingbutton would trigger the code and print the same error but if I replace FlutterIsolate.spawn with Isolate.spawn then there is no such error and code works expected.

import 'dart:isolate';

import 'package:flutter/material.dart';
import 'package:flutter_isolate/flutter_isolate.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

@pragma('vm:entry-point')
Future<void> someFunction(SendPort sendPort) async {
  final port = ReceivePort();
  sendPort.send(port.sendPort);

  await for (int message in port) {
    sendPort.send(Counter(message));
  }
}

class Counter {
  int value;
  Counter(this.value);
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  final receivePort = ReceivePort();

  Future<void> _incrementCounter() async {
    setState(() {
      _counter++;
    });

    final isolate =
        await FlutterIsolate.spawn(someFunction, receivePort.sendPort);
    receivePort.listen((message) {
      if (message is SendPort) {
        message.send(_counter);
      } else if (message is Counter) {
        print(message.value);
      }
    });

    Future.delayed(const Duration(seconds: 1), () {
      isolate.kill();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
suxinjie commented 1 year ago

I also encountered the same problem, how to solve it, I see the source code supports object sending, which confuses me

virtualzeta commented 1 year ago

I opened this problem but after some testing I realized that I have the same problem indicated here.

The fact that this issue has been open since April makes me think it hasn't been taken into due consideration.

definitelyme commented 11 months ago

@nmfisher @rmawatson Any update on this?

nmfisher commented 11 months ago

@definitelyme as I mentioned in the other linked issue, there's no update as this is fundamental to how we currently spawn isolates. If you explain what you're trying to do in more detail, I might be able to provide a workaround.

definitelyme commented 11 months ago

Thanks for your reply. Basically i want to send objects to a spawned isolate using SendPort.send(), but I get the error Unhandled Exception: Invalid argument: is a regular instance: Instance of [Object]

From flutter's SendPort.send() docs, it says:

If the sender and receiver isolate share the same code (e.g. isolates created via [Isolate.spawn]), the transitive object graph of [message] can contain any object, with the following exceptions:

- Objects with native resources (subclasses of e.g. NativeFieldWrapperClass1). A [Socket] object for example refers internally to objects that have native resources attached and can therefore not be sent.
- [ReceivePort]
- [DynamicLibrary]
- [Finalizable]
- [Finalizer]
- [NativeFinalizer]
- [Pointer]
- [UserTag]
- MirrorReference
Instances of classes that either themselves are marked with @pragma('vm:isolate-unsendable'), extend or implement such classes cannot be sent through the ports.

Apart from those exceptions any object can be sent. Objects that are identified as immutable (e.g. strings) will be shared whereas all other objects will be copied.

(see the last line)

Steps to reproduce:

  1. Init a receive port and spawn an isolate with FlutterIsolate.spawn() passing the SendPort as message

    
    /// Spawn isolate using the FlutterIsolate plugin
    void _spawnFluterIsolatePlugin() async {
    /// Create a [ReceivePort] to send/receive messages from [FlutterIsolate] => [Main Isolate]
    final ReceivePort receivePort = ReceivePort();
    
    IsolateNameServer.registerPortWithName(receivePort.sendPort, 'main_isolate_port');
    
    await FlutterIsolate.spawn(_isolateTask, receivePort.sendPort);
    
    /// Optional: Listen to messages from [FlutterIsolate] ==> [Main Isolate]
    receivePort.listen((message) {
    print('This is the message from FlutterIsolate: $message');
    });
    
    // Wait 3 seconds before sending a message to the [FlutterIsolate]
    Future.delayed(2.seconds, () {
    print('Tried to send message');
    IsolateNameServer.lookupPortByName('flutter_isolate_port')?.send(const IsolateMsg('Hello World!'));
    });
    }

/// Spawn isolate using Isolate.spawn void _spawnNormalIsolate() async { /// Create a [ReceivePort] to send/receive messages from [FlutterIsolate] => [Main Isolate] final ReceivePort receivePort = ReceivePort();

IsolateNameServer.registerPortWithName(receivePort.sendPort, 'main_isolate_port');

await Isolate.spawn(_isolateTask, receivePort.sendPort, errorsAreFatal: true, debugName: '_spawnNormalIsolate');

/// Optional: Listen to messages from [FlutterIsolate] ==> [Main Isolate] receivePort.listen((message) { print('This is the message from FlutterIsolate: $message'); });

// Wait 3 seconds before sending a message to the [FlutterIsolate] Future.delayed(2.seconds, () { print('Tried to send message'); IsolateNameServer.lookupPortByName('flutter_isolate_port')?.send(const IsolateMsg('Hello World!')); }); }

@pragma('vm:entry-point') void _isolateTask(SendPort sendPort) async { /// Create a [ReceivePort] to send/receive messages from [Main Isolate] => [FlutterIsolate] final receivePort = ReceivePort();

IsolateNameServer.registerPortWithName(receivePort.sendPort, 'flutter_isolate_port');

/// Listen to messages from [Main Isolate] ==> [FlutterIsolate] receivePort.listen((msg) async { print('Received a message from main isolate: $msg'); }); }

2. Write a simple Dart class:
```dart
class IsolateMsg {
    final String name;
    const IsolateMsg(this.name);
}
  1. Execute:

    void main() {
    /// ...Flutter initialization
    
    // Try [_spawnNormalIsolate]
    _spawnNormalIsolate(); // This works. Prints: "Received a message from main isolate: Instance of 'IsolateMsg'"
    
    // // Try [_spawnFluterIsolatePlugin]
    // _spawnFluterIsolatePlugin(); // Throws the exception: "[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Invalid argument: is a regular instance reachable via : Instance of 'IsolateMsg'"
    }

    Result:

    Throws: [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Invalid argument: is a regular instance reachable via : Instance of 'IsolateMsg'

Expected Outcome:

Text should be printed to the console: Received a message from main isolate: Instance of 'IsolateMsg'

nmfisher commented 11 months ago

What do you ultimately want to send via your SendPort?

definitelyme commented 11 months ago

I want to send IsolateMsg as an object via my SendPort to FlutterIsolate. This is supported from Flutter 3.0 Can i do this with the FlutterIsolate plugin?

PS: I don't want to use Map, or List

nmfisher commented 11 months ago

Yes but your IsolateMsg just contains a string, and since you can send plain strings via SendPort between FlutterIsolates without a problem, I'm assuming the actual class you want to send is more complicated.

For example, could you serialize your IsolateMsg class to json, send as a plain string then deserialize on the receiving end?

Also, what specifically do you need flutter_isolate for that regular isolates in the newer versions of Flutter can't do?

definitelyme commented 11 months ago

Okay. Even objects like enums etc..

I just want to know why it's possible to do this with isolates created from Isolate.spawn but not FlutterIsolate.spawn. And if there'll be a solution in the future.

nmfisher commented 11 months ago

It's because the current implementation of FlutterIsolate.spawn spawns an isolate that does not share the same code as the main Flutter isolate (basically using spawnUri under the hood). On the documentation for SendPort.send you'll see that that means that you can only send certain primitive types.

Updating the current implementation of FlutterIsolate.spawn to spawn an isolates that shares the same code might be possible. However, I'm reluctant to invest time to explore if that's possible, because the original intended use case for the flutter_isolate plugin (plugins in background isolates) is now supported by Flutter >= 3.7. The only remaining use case seems to be (a) people who can't upgrade yet, or (b) people who need to use dart:ui methods on a background isolate.

Can you explain more about why you still need flutter_isolate, and why you can't achieve what you need to do with simply using Isolate.spawn in Flutter >= 3.7?

vetemaa commented 11 months ago

In my use case, I want to listen to some Firestore collections/documents inside the FlutterIsolate.spawn function. This can not be done using Isolate.spawn due to Unsupported operation: Background isolates do not support setMessageHandler(). Based on the received Firestore data, I want to create a lot of different intertwined objects which I would return to the main thread. Serializing all those objects would be a real headache. Creating the objects in the main thread is not a good option as creating them is computationally expensive. I could use another Isolate for creating the objects, but that's a lot of extra messaging.