Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
git-elliot opened this issue Apr 17, 2023 · 13 comments

Comments

@git-elliot
Copy link

git-elliot commented Apr 17, 2023

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
Copy link
Collaborator

Can you provide a reproducible sample project?

@git-elliot
Copy link
Author

git-elliot commented May 10, 2023

@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
Copy link

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

@virtualzeta
Copy link

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
Copy link

@nmfisher @rmawatson Any update on this?

@nmfisher
Copy link
Collaborator

@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
Copy link

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');
  });
}
  1. Write a simple Dart class:
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
Copy link
Collaborator

What do you ultimately want to send via your SendPort?

@definitelyme
Copy link

definitelyme commented Nov 13, 2023

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
Copy link
Collaborator

nmfisher commented Nov 13, 2023

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
Copy link

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
Copy link
Collaborator

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
Copy link

vetemaa commented Nov 28, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants