denoland / rusty_v8

Rust bindings for the V8 JavaScript engine
https://crates.io/crates/v8
MIT License
3.13k stars 300 forks source link

Question: Global context and callbacks #1534

Closed buchanae closed 1 month ago

buchanae commented 1 month ago

I would like to store some event listeners and call them from rust, such as "cursor moved". I've written code like this:

JS:

const listeners = []

function __invoke(event) {
  for (let l of listeners) {
    if (l.event == event) {
        l.callback()
    }
  }
}

function listen(event, callback) {
  listeners.push({event, callback})
}

function main() {
  listen("CursorMoved", function() {
    log("cursor moved")
  })
}
struct App {
  isolate: v8::OwnedIsolate,
  context: Option<v8::Global<v8::Context>>,
}

impl App {
  fn run_script(&mut self) {
    let mut isolate_scope = v8::HandleScope::new(&mut self.isolate);
    let ctx = v8::Context::new(&mut isolate_scope);
    self.context = Some(v8::Global::new(&mut isolate_scope, ctx));
    let mut scope = v8::ContextScope::new(&mut isolate_scope, ctx);

    // compile and run script
  }

  fn trigger_event(&mut self, event: String) {
        let mut isolate_scope = v8::HandleScope::new(&mut self.isolate);
        let ctx = v8::Local::new(&mut isolate_scope, self.context.as_ref().unwrap());
        let mut scope = v8::ContextScope::new(&mut isolate_scope, ctx);   

        let global = ctx.global(&mut scope);
        let invoke_key = v8::String::new(&mut scope, "__invoke").unwrap().into();
        let invoke: v8::Local<v8::Function> = global.get(&mut scope, invoke_key).unwrap().try_into().unwrap();

        let recv = v8::undefined(&mut scope).into();
        let evstr = v8::String::new(&mut scope, name).unwrap().into();
        invoke.call(&mut scope, recv, &[evstr]);
  }
}

Is this the "right" way to do this? I'm unsure exactly which globals to keep. Is it reasonable to recreate the root HandleScope and ContextScope in trigger_event?

I originally explored other options, such as trying to store a list of listener callback function handles in Rust memory, but I couldn't figure that out, so I ended up defining __invoke and listeners in Javascript. I imagine something like the DOM has a better way though.

Thanks!

bartlomieju commented 1 month ago

Is this the "right" way to do this? I'm unsure exactly which globals to keep. Is it reasonable to recreate the root HandleScope and ContextScope in trigger_event?

Yes. You will have to recreate HandleScope each time you want to invoke a listener. You should be able to skip grabbing the context and ContextScope part though.

You can actually store only the callbacks you are interested in using v8::Global<v8::Function> instead of looking them up from the global scope each time like here:

let global = ctx.global(&mut scope);
let invoke_key = v8::String::new(&mut scope, "__invoke").unwrap().into();
let invoke: v8::Local<v8::Function> = global.get(&mut scope, invoke_key).unwrap().try_into().unwrap();  
buchanae commented 1 month ago

Thanks, good to know I'm on the right track.

You should be able to skip grabbing the context and ContextScope part though.

I'm not sure what you mean. Most functions seem to require a HandleScope<'_, v8::Context>

expected mutable reference `&mut HandleScope<'_, v8::Context>`
   found mutable reference `&mut HandleScope<'_, ()>`
bartlomieju commented 1 month ago

You're right. You can do this instead:

let scope = v8::HandleScope::with_context(isolate, &*self.context)

where self.context is v8::Global<v8::Context>