Dart: Using JsObject and window API in an isolate - dart

I am having some difficulties making use of isolates in Dart. The first problem is I wanted to use dart:js to use a javascript library in one of my isolates. I tried with the following code:
void runCode(SendPort sendPort)
{
print("still ok...");
JsObject object = new JsObject(context['jsCode']);
print("still ok?");
}
void main()
{
ReceivePort receivePort = new ReceivePort();
JsObject object = new JsObject(context['jsCode']);
print("ok so far");
Isolate.spawn(runCode, receivePort.sendPort);
}
The code runs as far as "still ok..." in the runCode function and breaks when I try to use JsObject.
The second problem was I wanted to use the fileSystem API in the isolate. So I tried the following:
void runCode(SendPort sendPort)
{
window.requestFileSystem.then((FileSystem filesytem) => print('ok'));
}
void main()
{
ReceivePort receivePort = new ReceivePort();
Isolate.spawn(runCode, receivePort.sendPort);
}
This second example breaks when I reach the filesystem.
I have read: Dart : Isolate not working when using html import and from here it suggests that dart:html cannot be used in an isolate. Is this the reason why the filesystem API will not work? Is this the same case for dart:js? Or am I completely missing something?
Thanks for any help!

I've read somewhere that only the main thread has access to the DOM, which would cause any other JS action to fail if not in the main thread.

Related

What exactly is a ReceivePort / RawReceivePort? And how to find external method implementations in Dart?

I'm currently experimenting with Isolates in dart.
I'm trying to create a wrapper around an Isolate to make it more pleasant to use.
The desired interface is something along the lines:
abstract class BgIsolateInterface {
Future<Response> send<Message, Response>(Message message);
}
I want to have a method that sends a message to the background interface and then return the response to the caller.
To achieve this I figured I have to create a new RawReceivePort or ReceivePort in the send function to reliably get the correct response.
But this would mean I'm essentially creating the port and discarding it. Going against the documentations which states
Opens a long-lived port for receiving messages.
So my questions are:
what exactly are ReceivePorts and RawReceivePorts?
would my use case be valid i.e. have them be created only to read a single response?
should I look at another way of doing things?
Note: Please don't suggest the Flutter compute function as an alternative. I'm looking to do this in a long running isolate so I can share services / state between function calls. I'm just not showing this here to keep the question short.
Thank you very much!!!
Edit #1:
When providing the answer I realised there was also an underling question about how to read the Dart source, more specifically how to find external methods' implementations. That question was added to the title. The original question was just: What exactly is a ReceivePort / RawReceivePort?.
Yesterday, I've searched across the source and I think, I now have the answers. If I'm wrong, anyone more involved with the engine please correct me. This is mostly my speculation.
TLDR:
ReceivePort/RawReceivePorts are essentially int ids with a registered message handler. The SendPort knows to which id i.e. ReceivePort/RawReceivePort it should send the data to.
Yes. But for another use case there is better way.
Change the interface, so we react to states / responses coming from the isolate i.e.
abstract class BgIsolateInterface<Message, Response> {
void send(Message message);
void listen(void Function(Response) onData);
}
Long
#1
I've looked at the implementation and I'm including my findings here also to put a note for my future self on how to actually do this if I ever need to.
First, if we look at the implementation of ReceivePort (comments removed):
abstract class ReceivePort implements Stream<dynamic> {
external factory ReceivePort([String debugName = '']);
external factory ReceivePort.fromRawReceivePort(RawReceivePort rawPort);
StreamSubscription<dynamic> listen(void onData(var message)?,
{Function? onError, void onDone()?, bool? cancelOnError});
void close();
SendPort get sendPort;
}
We can see the external keyword. Now, this means implementation is defined somewhere else. Great! Where?
Let's open the SDK source and look. We are looking for a class definition of the same name i.e. ReceivePort with a #patch annotation. Also it seems the Dart team follows the convention of naming the implementation files for these external methods with the suffix _patch.dart.
We then find the three of these patch files. Two for the js runtime, one for development and one for production, and one file for the native? runtime. Since, I'm not using Dart for the web, the latter is the one I'm interested in.
In the file: sdk/lib/_internal/vm/lib/isolate_patch.dart we see:
#patch
class ReceivePort {
#patch
factory ReceivePort([String debugName = '']) =>
new _ReceivePortImpl(debugName);
#patch
factory ReceivePort.fromRawReceivePort(RawReceivePort rawPort) {
return new _ReceivePortImpl.fromRawReceivePort(rawPort);
}
}
Ok, so the implementation for ReceivePort is actually a library private _ReceivePortImpl class.
Note: As you can see factory methods don't have to return the same class the method is defined in. You just have to return an object that implements or extends it. i.e., has the same contract.
class _ReceivePortImpl extends Stream implements ReceivePort {
_ReceivePortImpl([String debugName = ''])
: this.fromRawReceivePort(new RawReceivePort(null, debugName));
_ReceivePortImpl.fromRawReceivePort(this._rawPort)
: _controller = new StreamController(sync: true) {
_controller.onCancel = close;
_rawPort.handler = _controller.add;
}
SendPort get sendPort {
return _rawPort.sendPort;
}
StreamSubscription listen(void onData(var message)?,
{Function? onError, void onDone()?, bool? cancelOnError}) {
return _controller.stream.listen(onData,
onError: onError, onDone: onDone, cancelOnError: cancelOnError);
}
close() {
_rawPort.close();
_controller.close();
}
final RawReceivePort _rawPort;
final StreamController _controller;
}
Which as we can see is really just a wrapper around a RawReceivePort where the handler is a StreamController.add method. So, what about the RawReceivePort?
If we look at initial file where ReceivePort is defined we again see. It's just one external factory method and an interface for others.
abstract class RawReceivePort {
external factory RawReceivePort([Function? handler, String debugName = '']);
void set handler(Function? newHandler);
SendPort get sendPort;
}
Luckily, its #patch version can also be found in the same place as the ReceivePorts.
#patch
class RawReceivePort {
#patch
factory RawReceivePort([Function? handler, String debugName = '']) {
_RawReceivePortImpl result = new _RawReceivePortImpl(debugName);
result.handler = handler;
return result;
}
}
Ok, again the actual implementation is _RawReceivePortImpl class.
#pragma("vm:entry-point")
class _RawReceivePortImpl implements RawReceivePort {
factory _RawReceivePortImpl(String debugName) {
final port = _RawReceivePortImpl._(debugName);
_portMap[port._get_id()] = <String, dynamic>{
'port': port,
};
return port;
}
#pragma("vm:external-name", "RawReceivePortImpl_factory")
external factory _RawReceivePortImpl._(String debugName);
close() {
_portMap.remove(this._closeInternal());
}
SendPort get sendPort {
return _get_sendport();
}
bool operator ==(var other) {
return (other is _RawReceivePortImpl) &&
(this._get_id() == other._get_id());
}
int get hashCode {
return sendPort.hashCode;
}
#pragma("vm:external-name", "RawReceivePortImpl_get_id")
external int _get_id();
#pragma("vm:external-name", "RawReceivePortImpl_get_sendport")
external SendPort _get_sendport();
#pragma("vm:entry-point", "call")
static _lookupHandler(int id) {
var result = _portMap[id]?['handler'];
return result;
}
#pragma("vm:entry-point", "call")
static _lookupOpenPorts() {
return _portMap.values.map((e) => e['port']).toList();
}
#pragma("vm:entry-point", "call")
static _handleMessage(int id, var message) {
final handler = _portMap[id]?['handler'];
if (handler == null) {
return null;
}
handler(message);
_runPendingImmediateCallback();
return handler;
}
#pragma("vm:external-name", "RawReceivePortImpl_closeInternal")
external int _closeInternal();
#pragma("vm:external-name", "RawReceivePortImpl_setActive")
external _setActive(bool active);
void set handler(Function? value) {
final int id = this._get_id();
if (!_portMap.containsKey(id)) {
_portMap[id] = <String, dynamic>{
'port': this,
};
}
_portMap[id]!['handler'] = value;
}
static final _portMap = <int, Map<String, dynamic>>{};
}
OK, now we're getting somewhere. A lot is going on.
First thing to note are the: #pragma("vm:entry-point"), #pragma("vm:entry-point", "call") and #pragma("vm:external-name", "...") annotations. Docs can be found here.
Oversimplified:
vm:entry-point tells the compiler this class / method will be used from native code.
vm:external-name tells the compiler to invoke a native function which is registered to the name provided by the annotation.
For instance to know the implementation of:
#pragma("vm:external-name", "RawReceivePortImpl_factory")
external factory _RawReceivePortImpl._(String debugName);
We have to look for DEFINE_NATIVE_ENTRY(RawReceivePortImpl_factory. And we find the entry in: runtime/lib/isolate.cc.
DEFINE_NATIVE_ENTRY(RawReceivePortImpl_factory, 0, 2) {
ASSERT(TypeArguments::CheckedHandle(zone, arguments->NativeArgAt(0)).IsNull());
GET_NON_NULL_NATIVE_ARGUMENT(String, debug_name, arguments->NativeArgAt(1));
Dart_Port port_id = PortMap::CreatePort(isolate->message_handler());
return ReceivePort::New(port_id, debug_name, false /* not control port */);
}
We see the port_id is created by PortMap::CreatePort and is of type Dart_Port. Hmmm, and what is a the type definition for Dart_Port.
runtime/include/dart_api.h
typedef int64_t Dart_Port;
OK so the actual internal representation of a RawReceivePort is a signed int stored in 64 bits, and some additional information like the type, state, debug names etc.
Most of the work is then being done in PortMap::CreatePort and other of its methods. I won't go in depth, because quite honestly I don't understand everything.
But from the looks of it the PortMap uses the port_id to point to some additional information + objects. It generates it randomly and makes sure the id is not taken. It also does a lot of different things but let's move on.
When sending a message through SendPort.send, the method essentially calls the registered entry SendPortImpl_sendInternal_ which determines which port to send the information to.
Note: SendPort essentially just points to its ReceivePort and also stores the id of the Isolate where it was created. When posting a message this id is used to determine what kind of objects can be sent through.
The a message is created and passed to PortMap::PostMessage which in turn calls MessageHandler::PostMessage.
There the message is enqueued by a call to MessageQueue::Enqueue. Then a MessageHandlerTask is ran on the ThreadPool.
The MessageHandlerTask essentially just calls the MessageHandler::TaskCallback which eventually calls MessageHandler::HandleMessages.
There the MessageHandler::HandleMessage is called, but this function is implemented by a child class of MessageHandler.
Currently there are two:
IsolateMessageHandler and
NativeMessageHandler.
We are interested in the IsolateMessageHandler.
Looking there we see IsolateMessageHandler::HandleMessage eventually calls DartLibraryCalls::HandleMessage which calls object_store->handle_message_function(). full chain: Thread::Current()->isolate_group()->object_store()->handle_message_function()
The function handle_message_function is defined by the (dynamic?) macro LAZY_ISOLATE(Function, handle_message_function) in runtime/vm/object_store.h.
The property + stores created are used in: runtime/vm/object_store.cc by the: ObjectStore::LazyInitIsolateMembers.
_RawReceivePortImpl is registered to lazily load at the isolate_lib.LookupClassAllowPrivate(Symbols::_RawReceivePortImpl()) call.
As well as, the methods marked with #pragma("vm:entry-point", "call"), including static _handleMessage(int id, var message).
Which is the handler that ->handle_message_function() returns.
Later the DartLibraryCalls::HandleMessage invokes it through DartEntry::InvokeFunction with the parameters port_id and the message.
This calls the _handleMessage function which calls the registered _RawReceivePort.handler.
#2
If we compare the Flutter's compute method implementation. It spins up an Isolate and 3 ReceivePorts for every compute call. If I used compute, I would be spending more resources and loose context between multiple message calls I can have with a long-running Isolate. So for my use case I reason, creating a new ReceivePort everytime I pass a message shouldn't be a problem.
#3
I could use a different approache. But I still wish to have a long running Isolate so I have the flexibility to share context between different calls to the Isolate.
Alternative:
Would be following a bloc / stream style interface and have a method to assign a listener and a method to send or add a message event, and have the calling code listen to the responses received and act accordingly.
i.e. an interface like:
abstract class BgIsolateInterface<Message, Response> {
void send(Message message);
void addListener(void Function(Response) onData);
void removeListener(void Function(Response) onData);
}
the down side is the Message and Response have to be determined when creating the class rather than simply when using the send method like the interface in my question. Also now some other part of the code base has to handle the Response. I prefer to handle everything at the send call site.
Note: The source code of the Dart project is put here for presentation purposes. The live source may change with time. Its distribution and use are governed by their LICENSE.
Also: I'm not C/C++ developer so any interpretation of the C/C++ code may be wrong.
While this answer is long side-steps the questions a little bit, I find it useful to include the steps to search through the Dart source. Personally, I found it difficult initially to find where external functions are defined and what some of the annotation values mean. While these steps could be extracted into a separate question, I think it's useful to keep it here where there was a use case to actually dive deep.
Thank you for reading!

Why doesn't readLineSync works in an Isolate

void func(String dummy) {
String? name = stdin.readLineSync();
print(name);
}
void main(List<String> args) {
Isolate.spawn(func, "Testing");
}
Why doesn't my program prompts a user input ..and waits for me to enter it. Instead, it simply exits. Can someone help me out with an explanation. Not sure where to look.
I did find a similar question posted where they were using a while loop and using readLineSync inside that..and because of single thread of dart ..it wasn't working there.
Dart programs terminates when the main-isolate does not have anymore to do, no events on the event queue, and does not subscribe to any event source which would result in new events being added to the event queue (like ReceivePort or Timer). It does not matter if spawned isolates are still being executed since they will just be killed.
You need to have the main-isolate to do something, or make the main-isolate subscribe to a signal from your spawned isolate using ReceivePort/SendPort as this will prevent the main-isolate from being terminated (since it subscribes to an event source which potentially could add new events on the event queue).
An example of using addOnExitListener on an Isolate object can be seen here:
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
void func(String dummy) {
print('Enter your name:');
final name = stdin.readLineSync();
print('Your name is: $name');
}
Future<void> main(List<String> args) async {
final onExitReceivePort = ReceivePort();
final isolate = await Isolate.spawn(func, "Testing");
isolate.addOnExitListener(
onExitReceivePort.sendPort,
response: 'ReadLineIsolateStopped',
);
// Listen on spawned isolate is stopped event
await for (final onExitEvent in onExitReceivePort) {
print('Got event: $onExitEvent');
onExitReceivePort.close();
}
}
We are here closing the onExitReceivePort as soon we gets one event (since the spawned isolate is then gone) which will stop our main-isolate and the rest of the program.

can you run Isolate.spawn multiple times?

here is a simple code that I use to learn isolate, I spawn twice, but the second spawn does not show anything, any mistake here? Thanks
import 'dart:isolate';
Future<void> main() async {
print('start');
await Isolate.spawn(echo, 'Dart');
await Isolate.spawn(echo, 'Flutter'); // why this 2nd spawn not showing up?
print('end');
}
void echo(msg) {
print(msg);
}
Your program quits before the Isolate has done its job. You can confirm this if you add
await Future.delayed(Duration(seconds: 1));
somewhere towards the end of your program.
Setting up Isolates is often a bit challenging, with all the SendPort stuff etc.

Is it possible to have multiple send and receive ports for Dart Isolates?

Is it possible to open a multiple send and receive ports for the same Isolate in Dart?
E.g. Following code sample would have created two isolates with each having its own send port. However, I was wondering if there is any way to create more than one send/receive ports for the same isolate and choose the receive port to send the message to.
#import('dart:isolate');
echo() {
}
main() {
var sendPort1 = spawnFunction(echo);
var sendPort2 = spawnFunction(echo);
}
You can actually create any number of ReceivePorts, and then as Matt said, any number of SendPorts per ReceivePort.
By default an isolate, including the main isolate, has a ReceivePort created and available through the port getter. This ReceivePort is connected to the SendPort returned from spawnFunction() and spawnUri(). But you can create a new ReceivePort with new ReceivePort(), then you can create as many connected SendPorts as you want via toSendPort(). To use them you send the new SendPort itself along with a message on the original SendPort you either got from spawnFunction(), or via ReceivePort.receive().
By doing this you can set up multiple "channels" between two isolates. I haven't played with it yet to see how this works in practice yet, I've been multiplexing channels via structured messages on one SendPort.
Note that you can create a ReceivePort in any isolate: the parent isolate or the child isolate. So if you want the partent to have two SendPorts to the child, then you need one from spawnFunction() and another that's passed from the child back to the parent in a message.
Here's your example changed to use multiple SendPorts. The steps:
main: Spawn an isolate
main: Send a message with a SendPort so that the isolate can send a message back
echo: Create a second ReceivePort in the isolate
echo: Receive a message in the isolate with a replyTo SendPort
echo: Create a SendPort from the ReceivePort and send it back
main: Receive the message and SendPort from echo
Now main() has two independent SendPorts to the isolate.
#import('dart:isolate');
echo() {
var port2 = new ReceivePort(); // 3
port2.receive((m, p) {
print("two: $m");
});
port.receive((m, p) { // 4
print("one: $m");
p.send("ack", port2.toSendPort()); // 5
});
}
main() {
port.receive((m, sendPort2) { // 6
sendPort2.send("hello 2");
});
var sendPort1 = spawnFunction(echo); // 1
sendPort1.send("hello 1", port.toSendPort()); // 2
}
This prints:
one: hello 1
two: hello 2
Whew!
While I'm not sure about multiple receive ports. You can create multiple send ports per receive port. This functionality is build into the ReceivePort class: ReceivePort.toSendPort
As indicated at the bottom of the help:
It is legal to create several SendPorts from the same ReceivePort.
Hope this helps.
The answer from Justin is basically right, but caused me some trouble because the isolate stopped responding after step 5 has been executed. So here is an updated version that actually worked in my context:
import 'dart:isolate';
echo() {
var port2 = new ReceivePort();
port2.receive((m, p) {
print("two: $m");
});
port.receive((m, p) {
print("one: $m");
p.send(port2.toSendPort());
});
}
main() {
var sendPort1 = spawnFunction(echo);
sendPort1.call("hello 1").then((newPort)=>newPort.send("hello 2"));
}
The major difference is simply that the port is send as message rather than using the replyTo field. This also allows for a more compact code as the other receiving Port is not required.

How to do several asynchronous I/O actions in Dart cleanly (Isolates)?

In Dart, there is a concept of Isolates. I have an application (that I'm experimenting in Dart) that has lots of asynchronous IO where each call (they are database calls) are dependent on the previous one. So I have ended up in a nested callback hell.
I was wondering if Isolates could solve that nested callback soup, but it looks a bit verbose and I'm not sure if it fits it well.
There are also Generators proposed in the next ECMAScript Harmony which could solve these things, but how would you currently do lots of asynchronous IO in Dart in a clean way?
You can use Future's and Completers to chain work together. The following future returns the result of a 'ls' command from a process:
Future<String> fetch(String dir) {
final completer = new Completer();
Process process = new Process.start('ls', [dir]);
process.exitHandler = (int exitCode) {
StringInputStream stringStream = new StringInputStream(process.stdout);
stringStream.dataHandler = () {
String content = stringStream.read();
completer.complete(content);
process.close();
};
};
process.errorHandler = (var error) {
completer.completeException(error);
return true;
};
return completer.future;
};
which you can then chain together like this:
fetch('/').then((val) => fetch("/usr").then((val) => fetch("/tmp")));
Not the most pretty solution but this is what I get by with now.

Resources