neon-bindings / neon

Rust bindings for writing safe and fast native Node.js modules.
https://www.neon-bindings.com/
Apache License 2.0
8.01k stars 283 forks source link

Ability to call Javascript methods from Rust threads #197

Closed jugglingcats closed 3 years ago

jugglingcats commented 7 years ago

As discussed on Slack: https://rust-bindings.slack.com/archives/neon/p1489508576491688.

The basic idea is that a Rust addon can create a continuous background thread that notifies Javascript when things happen (event style).

Psuedo Javascript:

addin.run(function() { 
  console.log("got the event!")
}

Psuedo Rust:

fn run(call: Call) -> JsResult<JsString> {
    let scope = call.scope;
    let f = call.arguments.require(scope, 0)?.check::<JsFunction>()?;

    thread::spawn(|| {
        while (true) {
            let args: Vec<Handle<JsNumber>> = vec![];
            match f.call(scope, JsNull::new(), args) {
                _ => { println!("Call to javascript done!"); }
            }
            thread::sleep(1000);
        }
    });
    Ok(JsString::new(scope, "hello node").unwrap())
}

If this was achievable it would be a lot nicer than the C equivalent, an example of which is here: https://gist.github.com/matzoe/11082417.

I imagine there would need to be some kind of wrapper around the call, so maybe not completely transparent as shown above.

jugglingcats commented 7 years ago

I've made a bit of progress with this but am stuck! @dherman perhaps you can help me.

I've managed to invoke a Rust function using uv_async_send (ie. on the libuv default thread). In this method/context it's safe to get the current isolate scope and invoke the Javascript callback, but I don't really understand how to encapsulate this information from the Neon Call object and make use of it.

Am struggling both with Rust basics and the idiomatic Neon way this should work. Any advice/pointers gratefully received. I can't even tell if I'm close or not!!

My working Rust project (so far) is here: https://github.com/jugglingcats/neon-uv/tree/iter1

My working C++ project (which does invoke the js callback) is here: https://github.com/jugglingcats/node-uv/tree/iter1

The C++ project shows the basic approach to converting a Local to Persistent in v8 in order to preserve between the main addon method and the callback, although I don't claim to understand this fully. We need to do something similar in Rust-land. C++ snippet:

    v8::Local<v8::Function> func = v8::Local<v8::Function>::Cast(args[0]);
    auto savedFunc = new v8::Persistent<v8::Function>();
    savedFunc->Reset(isolate, func);
    handle->data = savedFunc;

and (in callback):

    v8::Isolate *isolate = Isolate::GetCurrent();
    HandleScope scope(isolate);

    v8::Persistent<v8::Function> *func = (v8::Persistent<v8::Function> *) handle->data;
    func->Get(isolate)->Call(isolate->GetCurrentContext()->Global(), 0, NULL);
jedireza commented 7 years ago

The C++ project shows the basic approach to converting a Local to Persistent in v8 in order to preserve between the main addon method and the callback [...]

IIRC this is where I left off when attempting async callbacks with Neon. We need a way to persist the callback so that Rust doesn't drop it when the call goes out of scope.

I spoke with @dherman about this (a while back) and we (at the time) thought it was best to not introduce the idea of Persistent handles in the Neon API and instead expose something more specific to async callbacks.

jugglingcats commented 7 years ago

I agree... that's what I meant by finding an idiomatic Neon way of doing it. The Neon developer (hopefully) shouldn't concern themselves with local and persistent.

I wondered if it would be possible to make Call (or it's arguments) Send or Sync. Then they could be passed across the thread boundary. I don't know if this even makes sense as a suggestion though, or how hard it would be!

dherman commented 7 years ago

@jugglingcats I think you have some of the right ideas, but I don't have the complete picture in my head so I can't spell out exactly what needs to be done. I can give you a few thoughts, though:

Unfortunately, it's not safe for another Rust thread to call JS on its same thread -- we can't allow that, because a) the GC isn't multithreaded and the callback closes over data from the main thread, and b) having multiple threads work on arbitrary shared data violates JS's run-to-completion semantics. (Does your C code do that, btw? It's possible I'm missing something but I'd expect that generally to crash or cause strange unspecified behavior.) The only safe thing to do, AFAIK, would be to let the Rust background thread add events onto the main thread's queue, to be dispatched on the main thread.

Similarly, it's not safe to let Call or other data structures like that implement Send or Sync, because again they are pointing to JS data from the main thread.

What I believe we want to do, at least at a hand-wavy level, is bridge between JS's concept of dynamic transferring (and maybe also structured clone) and Rust's ownership. So for example, it should be possible to transfer a typed array to a background Rust thread and then let Rust transfer it back to the callback when it's done doing the work. If we set the types up so that it's a dynamic error to try to send non-transferrable data to the background thread, then we'll be guaranteed to be thread-safe.

Another way of thinking about this is that this is kind of like web workers and postMessage, except that we can probably make the API nicer.

I'd be happy to collaborate with you on this when I have time, btw. Right now I'm trying to get the web site presentable because we're planning on doing some blog posts about using Neon with Electron and how Wire has recently put Neon into production, and I want people to have a good landing experience. After that I have a couple of hacking projects on various parts of Neon that I want to do, but having more powerful ways to do background computation in Rust is a high-priority use case.

jugglingcats commented 7 years ago

Hi @dherman, agree mostly with your analysis. The postMessage analogy is not quite correct though IMO, because with postMessage you are limited strictly to transferring data, whereas within a node addon, you can pass a Javascript callback (function) around. You cannot execute the callback in another thread, but you can pass it around and provided you marshall onto the main node (libuv) thread you can call it later.

The key to passing the callback (and any other node data) around is converting from Local to Persistent. I think this is to avoid the possibility that node's GC cleans up the data after the initial addon function returns. It keeps it in memory until you're done with it.

There are two methods of marshalling onto the main node thread that I'm aware of, with slightly different semantics:

uv_queue_work This is what you most commonly see used. In this case your request goes to the node thread pool. You pass two native callbacks: one for the work itself, and one that's invoked after the work is complete. The worker function is executed in the background and the 'complete' function is called on the main thread (ie. can call a Javascript callback, if appropriate).

This is convenient for ad-hoc long running native tasks because node does the thread pool management.

uv_async_send This simply queues a native callback. In this case your callback runs some time later in the main node thread. Anything done in the native callback blocks the main thread, but it's good if you have your own background thread and just want to notify out via a Javascript callback. This is my use case. I guess it's a bit like setImmediate in Javascript-land.

The C++ example I posted shows the mechanism for uv_async_send, but doesn't actually do any threading. But the structure is correct regarding Local and Persistent handling.

Now for my bit of hand waving...

It seems to me that Rust should have a way to mutate data when sending it across threads. I don't understand the mechanics of this, but something Sync or Send should be able to say "hey, I see I'm being transferred across thread boundary, I need to do xxxxx". If this were possible, you could encapsulate the switch from Local to Persistent in this mechanism, perhaps in the Neon Handle code. Then instead of trying to move the whole Call across, you could pick out any parameters like the Javascript callback and transfer that across. Basically anything that's a Handle.

Appreciate you are busy with other activity. This isn't super-urgent for us: we don't need this feature right away. But am interested to see how it progresses -- I think it could work very sweetly.

Qard commented 7 years ago

Should be possible to contain the response data in a movable object with the callback and just hand it to libuv to execute in the uv_default_loop() thread.

jugglingcats commented 7 years ago

Can you elaborate a little? It is the javascript function handle that needs to be kept around, to be invoked later during the async send native callback (on main thread)

dherman commented 7 years ago

@jugglingcats Are you on the slack today? I'd be happy to chat -- I've been looking into this issue lately.

king6cong commented 6 years ago

Any updates? @jugglingcats @dherman continuous calls into a javascript function from Rust is a feature I looking forward to for a long time :)

Acanguven commented 4 years ago

As #375 merged, what is the roadmap ahead for full implementation?

kjvalencik commented 4 years ago

@Acanguven The feature is currently not stabalized. Currently there is a limitation to the API that may require a breaking change. Specifically, it is not possible to handle Javascript exceptions in the scheduled Rust callback.

One proposal is the implementation of a try/catch feature in Neon, allowing the schedule_with API to remain un-changed. However, the higher level schedule API may be changed to more closely mirror the Task API.

Lastly, the schedule API passes a &mut Context where most neon APIs move ownership of the Context. This is also being evaluated.

Thanks!

tom-haines commented 4 years ago

I have an example I've worked through today based on the test cases for the neon::event::EventHandler (via the event-handler-api). If useful, could push this to neon examples repo ?

ghost commented 4 years ago

@tom-haines can you point me towards your repo? Also is there any future plans for this feature

Brooooooklyn commented 4 years ago

napi-rs could spawn task to libuv thread and call Javascript method from rust thread using Thread Safe Function

kjvalencik commented 3 years ago

This has been implemented with Channel and JoinHandle. https://github.com/neon-bindings/rfcs/pull/32