Created
October 26, 2021 03:42
-
-
Save anselm/f116362d3af82647b68e04906843a94f to your computer and use it in GitHub Desktop.
Calling Rust from Rusty V8 Javascript in a useful way
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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