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

[ffi/isolate] Support passing/receiving Pointers to/from Isolates. #50457

Closed
modulovalue opened this issue Nov 14, 2022 · 18 comments
Closed

[ffi/isolate] Support passing/receiving Pointers to/from Isolates. #50457

modulovalue opened this issue Nov 14, 2022 · 18 comments
Labels
area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. library-ffi

Comments

@modulovalue
Copy link
Contributor

Consider the following example:

import 'dart:async';
import 'dart:ffi';
import 'dart:isolate';
import 'package:ffi/ffi.dart';

void main() async {
  final pointer = malloc.allocate<Uint8>(1);
  print(
    await asyncCompute<Pointer, int>(
      fn: fn,
      input: pointer,
    ),
  );
}

int fn(
  final Pointer p,
) {
  return p.address;
}

Future<O> asyncCompute<I, O>({
  required final FutureOr<O> Function(I) fn,
  required final I input,
}) async {
  final receivePort = ReceivePort();
  final inbox = StreamIterator<dynamic>(
    receivePort,
  );
  await _IsolateMessage(
    sendPort: receivePort.sendPort,
    fn: fn,
    input: input,
  ).send();
  final movedNext = await inbox.moveNext();
  assert(
    movedNext,
    "Call to moveNext is expected to return true.",
  );
  final typedResult = inbox.current as O;
  receivePort.close();
  await inbox.cancel();
  return typedResult;
}

class _IsolateMessage<I, O> {
  final SendPort sendPort;
  final FutureOr<O> Function(I) fn;
  final I input;

  const _IsolateMessage({
    required final this.sendPort,
    required final this.fn,
    required final this.input,
  });

  Future<Isolate> send() {
    return Isolate.spawn<_IsolateMessage<I, O>>(
      (final message) {
        Isolate.exit(
          message.sendPort,
          message.fn(
            message.input,
          ),
        );
      },
      this,
    );
  }
}

Running it throws the following error:

mvs-iMac:state mv$ ~/Downloads/dart-sdk/bin/dart --version
Dart SDK version: 2.19.0-255.2.beta (beta) (Tue Oct 4 13:45:53 2022 +0200) on "macos_x64"
mvs-iMac:state mv$ ~/Downloads/dart-sdk/bin/dart pointer.dart 
Unhandled exception:
Invalid argument(s): Illegal argument in isolate message: (object is a Pointer)
#0      Isolate._spawnFunction (dart:isolate-patch/isolate_patch.dart:399:25)
#1      Isolate.spawn (dart:isolate-patch/isolate_patch.dart:379:7)
#2      _IsolateMessage.send (.../pointer.dart:58:20)
#3      asyncCompute (.../pointer.dart:34:5)
#4      main (.../pointer.dart:9:11)
#5      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:297:19)
#6      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12)

It looks like moving Pointers through isolates isn't supported.

One workaround is to read and pass the integer address itself, and to create a pointer manually from that address in the target isolate. However, this is highly inconvenient, as one has to architect his models around integer addresses and not Pointers if he wants to be able to send the models itself across isolates.

I intend to share native memory across isolates and it would be great for these use cases if this restriction could be lifted. cc: @mraleph

@vsmenon vsmenon added area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. library-ffi labels Nov 14, 2022
@mraleph
Copy link
Member

mraleph commented Nov 14, 2022

I think the main reason we decided to disable this is to catch situations when users accidentally send a pointer to another isolate without realising that they need to do something special around lifetime. (e.g. if you send a pointer to some reference counted resource to another isolate you might need to increment reference count and install a finaliser on the receiving end, etc.).

/cc @dcharkes

@dcharkes
Copy link
Contributor

I think the main reason we decided to disable this is to catch situations when users accidentally send a pointer to another isolate without realising that they need to do something special around lifetime. (e.g. if you send a pointer to some reference counted resource to another isolate you might need to increment reference count and install a finaliser on the receiving end, etc.).

/cc @dcharkes

Correct, this is why we decided that.

I did actually make a prototype for supporting sending Finalizers in send-and-exit: https://dart-review.git.corp.google.com/c/sdk/+/235141

We could consider adding support for sending NativeFinalizers in send-and-exit and allowing sending Finalizables and Pointers over.

@modulovalue what is your native-memory/native-resource management strategy?
(I'd imagine that if your model includes native finalizers to clean up your model implements Finalizable. However, since you didn't mention it, you might not be using finalizers at all.)

@modulovalue
Copy link
Contributor Author

modulovalue commented Nov 14, 2022

what is your native-memory/native-resource management strategy?

I plan to manage reference counters on my own.
Edit: my description below is confusing. Here is a diagram #50452 (comment).

i.e. An Isolate exiting would remove all its references at once, or an isolate could report atomic changes through for example the native finalizer provided by Dart_NewExternalUTF16String. (_Passing around Pointers to native memory intended to be used by external strings was my primary motivation for this issue. The second use case was to share thin wrappers over pointers to Wasmer objects between isolates. Some context around that can be found here dart-archive/wasm#97. _)

@dcharkes
Copy link
Contributor

dcharkes commented Dec 14, 2022

Our Pointer objects are no longer reified. We won't allow attaching finalizers to them. We may just represent them as unboxed integers in Dart.

Given that, it does seem reasonable we allow sending Pointer objects across isolates (since they are immutable, they can also be shared). If the message that is being sent includes a Finalizable we disallow sending the message anyway.

Currently people can still achieve it by taking pointer.address before sending and then re-construct the pointer on the other side.

(There's one caveat: If a message got sent but receiver dies or doesn't handle them, any messages are dropped. This dropping will not cause freeing of any memory (we have support on C side to do that, but not in SendPort api). But this problem seems orthogonal.)

/cc @mraleph @dcharkes

@mkustermann in #50715 (comment)

edit: sgtm

@0xNF
Copy link

0xNF commented Apr 21, 2023

Is this issue considered wontfix? I'd like to pass pointers to an isolate without the ceremony of covertly serializing pointer addresses.

@mkustermann
Copy link
Member

Is this issue considered wontfix? I'd like to pass pointers to an isolate without the ceremony of covertly serializing pointer addresses.

Pointers are already no longer reified (they don't remember their type argument at runtime). We'd like to eventually represent them as simple integers when passing them around.

It's assumed that if the pointer hangs on to native memory which needs freeing / releasing / ... that an enclosing class refers to it and has a finalizer attached. Sending such objects with finalizers is disallowed. But we'll allow sending Pointer objects.

Someone just has to make a CL for it. @dcharkes ?

@0xNF
Copy link

0xNF commented Apr 21, 2023

It's assumed that if the pointer hangs on to native memory which needs freeing / releasing / ... that an enclosing class refers to it and has a finalizer attached. Sending such objects with finalizers is disallowed.

This is actually my explicit usecase. My classes implement NativeFinalizers, and I'm keeping references to them from the main isolate. Is this disallowed because of the memory cloning aspect of isolates? I.e., when the isolate is done the copied objects get GC'd and then the finalizer is triggered, rendering the object held outside the isolate invalid?

@mkustermann
Copy link
Member

Is this disallowed because of the memory cloning aspect of isolates? I.e., when the isolate is done the copied objects get GC'd and then the finalizer is triggered, rendering the object held outside the isolate invalid?

Yes, allowing a clone of a finalizable object would make things problematic: Should the finalizer run when all clones are dead, should it run multiple times / for every clone, does cloning a finalizable need to perform some action (e.g. increasing a refcount), ...

@0xNF
Copy link

0xNF commented Apr 21, 2023

Is there some generally accepted solution for making asynchronous FFI functions? I have potentially long running methods in my underlying libraries and don't want to block the UI on them. I figured isolates was an easy way to do that, but I guess not, due to the finalizer restrictions.

@mkustermann
Copy link
Member

Is there some generally accepted solution for making asynchronous FFI functions? I have potentially long running methods in my underlying libraries and don't want to block the UI on them. I figured isolates was an easy way to do that, but I guess not, due to the finalizer restrictions.

Yes, the most simple way is to spawn an isolate and run a (possibly blocking) ffi call there. Is it possible for you to pass finalizable state via C memory (not dart objects) to the other isolate or make the isolate create this state on it's own?

@dcharkes
Copy link
Contributor

If you do more than 16 isolates, make sure to use what @mkustermann added recently to address:

@0xNF
Copy link

0xNF commented Apr 21, 2023

Yes, the most simple way is to spawn an isolate and run a (possibly blocking) ffi call there. Is it possible for you to pass finalizable state via C memory (not dart objects) to the other isolate or make the isolate create this state on it's own?

I figured it would come to this. It's not ideal, but it's probably the only real solution. Thanks for the help.

@0xNF
Copy link

0xNF commented Apr 22, 2023

We'd like to eventually represent them as simple integers when passing them around.

What are the implications of this? Will the type signatures of Pointer<NativeType> and associates go away at some point in the future, and are there any issues in particular that discuss this?

@dcharkes
Copy link
Contributor

We'd like to eventually represent them as simple integers when passing them around.

What are the implications of this? Will the type signatures of Pointer<NativeType> and associates go away at some point in the future

No, the static types will all stay.

and are there any issues in particular that discuss this?

@brianquinlan
Copy link
Contributor

brianquinlan commented Sep 18, 2023

If Pointer is passable between isolates then I think that ffigened Objective-C classes will also be passable. But the reference counts would be wrong and users can expect crashes.

For cases like that, the generated classes should probably have @pragma('vm:isolate-unsendable') applied or we need some approach of copying finalizer-like things (as @mkustermann mentioned).

@liamappelbe

@mkckr0
Copy link

mkckr0 commented Oct 8, 2023

I also encountered this problem. I find a simple solution, but I can't say whether it may have other problems.

void blockingFunc() async {
  final argPtr = malloc<Char>();
  final argPtrInt = argPtr.address;
  await Isolate.run(() => _bindings.blocking_func(Pointer.fromAddress(argPtrInt));
  malloc.free(argPtr);
}

Can't pass Pointer, then I directly pass the address, which is int.

@nikitadol
Copy link

I made an example of SharedPointer that can be shared between isolates and it will call the native callback when it is no longer needed. Perhaps this will be useful to someone

https://github.com/nikitadol/ffi_isolate_test

@mkustermann
Copy link
Member

As mentioned above in #50457 (comment) we consider Pointers to be simple integers and may actually represent them as such in the future.
=> With eba0e68 landed, we do allow sending Pointers across SendPorts now.

Users should be mindful that using FFI is stepping outside the safe sandbox. As a result one should be careful with how pointers are handled. If they represent a resource that needs freeing, a class implementing Finalizable should encapsulate it. Such finalizables cannot be sent across ports.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-vm Use area-vm for VM related issues, including code coverage, and the AOT and JIT backends. library-ffi
Projects
None yet
Development

No branches or pull requests

9 participants