Skip to content

Instantly share code, notes, and snippets.

@anselm
Created October 26, 2021 03:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save anselm/f116362d3af82647b68e04906843a94f to your computer and use it in GitHub Desktop.
Save anselm/f116362d3af82647b68e04906843a94f to your computer and use it in GitHub Desktop.
Calling Rust from Rusty V8 Javascript in a useful way
/*
This is a quick recipe for how to have a rust application invoke a javascript method using rusty v8 and do something useful.
The rusty v8 source code does show how to do a simpler version of calling rust from javascript in their examples folder - however I wanted an example that ferried some state through javascript back to Rust. In this way javascript can be used as a scripting layer with native code having enough context and information to do real work. Probably another way to do this would be to use a global pointer.
Here are some useful links:
https://github.com/denoland/rusty_v8/blob/main/examples/hello_world.rs
https://github.com/denoland/rusty_v8/blob/main/tests/test_api.rs
https://docs.rs/rusty_v8/0.32.0/rusty_v8/
https://github.com/danbev/learning-v8
Your Cargo.toml should be something like this:
[dependencies]
crossbeam = "0.8.1"
rusty_v8 = "0.32.0"
*/
// debugging laziness
#![allow(dead_code)]
#![allow(unused)]
#![allow(unused_variables)]
// pull in the tools for this example
use std::thread;
use crossbeam::channel::*;
use rusty_v8 as v8;
// declare a flavor of crossbeam channel
pub type U32Sender = Sender<u32>;
// ...
fn main() {
// I am going to pass this channel through the javascript engine and back into rust so that I can pass myself a message
let (u32sender,u32receiver) = unbounded::<u32>();
// Proof that this works is that this code below will print something to the console
thread::spawn(move || {
while let Ok(val) = u32receiver.recv() {
println!("watcher: received {}",val)
}
});
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// start v8 and a context/scope thingie
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Initialize V8
let platform = v8::new_default_platform(0, false).make_shared();
v8::V8::initialize_platform(platform);
v8::V8::initialize();
// An isolate is a runtime or something
let isolate = &mut v8::Isolate::new(Default::default()); // v8::CreateParams::default() also works?
// create a stack allocated handle scope ... the language word choices here are pretty nebulous and badly chosen
let handle = &mut v8::HandleScope::new(isolate);
// a "context"...
let context = v8::Context::new(handle);
// A "scope" in a context...
let scope = &mut v8::ContextScope::new(handle, context);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// build an "object" that will become the global state, and stuff some callbacks into it
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// first make an "object template" - defining a capability to instance a javascript object such as a "const obj = {}"
let myglobals = v8::ObjectTemplate::new(scope);
// variable instances can be added to the somewhat abstract object template - but cannot be read back out so easily
myglobals.set( v8::String::new(scope,"myvariable").unwrap().into(), v8::String::new(scope,"wheee").unwrap().into());
// there is a convenient concept of an internal; but you do have to pre-allocate the number of slots
// https://stackoverflow.com/questions/16600735/what-is-an-internal-field-count-and-what-is-setinternalfieldcount-used-for
// https://v8.dev/docs/embed
myglobals.set_internal_field_count(1);
// add a function - it is declared seperately for simplicity
myglobals.set( v8::String::new(scope,"invoke_a_colony_of_rabbits").unwrap().into(), v8::FunctionTemplate::new(scope,acolonyofrabbits_callback).into() );
// there is a bit of promotion of this object to become the global scope
let context = v8::Context::new_from_template(scope, myglobals);
let scope = &mut v8::ContextScope::new(scope, context);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// stuff a back pointer to u32sender into the javascript layer so that it is visible to the callback
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// we're going to want to probably move the variable out of the stack
// in rust parlance this moves the artifact to the heap along with a nominal concept of ownership
let boxed_sender = Box::<U32Sender>::new(u32sender);
// go about getting a raw pointer to the thing
let boxed_ptr : *mut U32Sender = Box::into_raw(boxed_sender);
// and explicitly more exactly cast raw ptr as a raw 'unknown' pointer because rust
let raw_ptr = boxed_ptr as *mut std::ffi::c_void;
// wrap that in a v8 compatible 'external'
let ext = v8::External::new(scope,raw_ptr);
// stuff external an internal field area - no idea what "into" means really...
context.global(scope).set_internal_field(0,ext.into());
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// run the javascript
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
let javascript = "
invoke_a_colony_of_rabbits(2001);
invoke_a_colony_of_rabbits(2021);
invoke_a_colony_of_rabbits(31459);
invoke_a_colony_of_rabbits(46692);
myvariable;
";
let code = v8::String::new(scope,javascript).unwrap();
// run it
let script = v8::Script::compile(scope, code, None).unwrap();
let result = script.run(scope).unwrap();
// print results
let result = result.to_string(scope).unwrap();
println!("scripting: {}", result.to_rust_string_lossy(scope));
}
// build and register a callback in the javascript object
fn acolonyofrabbits_callback( scope: &mut v8::HandleScope, args: v8::FunctionCallbackArguments, mut _retval: v8::ReturnValue ) {
// get arguments
let message = args.get(0).to_string(scope).unwrap().to_rust_string_lossy(scope);
let myu32: u32 = message.parse().unwrap();
// get context from the scope
let context = scope.get_current_context();
// get external from internal - preemptively unwrap because we believe it exists (ignore Some/None)
let ext = context.global(scope).get_internal_field(scope,0).unwrap();
// cast it back
let ext = unsafe { v8::Local::<v8::External>::cast(ext) };
// get it as a c pointer again
let raw_ptr : *mut std::ffi::c_void = ext.value();
let raw_ptr2 = raw_ptr as *mut U32Sender;
// go back up to being a boxed u32sender
let recovered = unsafe { Box::<U32Sender>::from_raw( raw_ptr2 ) };
// send it a message as a test
recovered.send(myu32);
// kick up the reference count... TODO fix
let boxed_ptr : *mut U32Sender = Box::into_raw(recovered);
// return results
_retval.set(v8::Integer::new(scope, myu32 as i32).into());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment