Notes
Code samples: using JsRuntime
JsRuntime
execute_script
execution takes place on the current global context, so it is possible
to maintain local JS state and invoke this method multiple times.
In general I focused on methods that work on plain old Javascript - passing things directly to V8 - vs giving Typescript to Deno to compile Typescript (and resolve Deno dependencies) for.
execute_script
Executes JS in the global realm. (According to the spec JS needs to be executed in realms, it's somewhat a detail we can ignore)
The most simple way to execute JS via Deno in Rust (thanks, IIFC):
pub async fn do_very_simple(&self) -> String {
let mut runtime = JsRuntime::new(Default::default());
let res = runtime.execute_script_static(
"name_of_the_thing_not_to_be_javascript",
"(function foo() { return \"from the javascript iifc\"})()")
.expect("error");
let str = res.open(runtime.v8_isolate())
.to_rust_string_lossy(&mut runtime.handle_scope());
str
}
the first parameter is a name for the binding that's going on (?) It is NOT ie the name of the function to be called, or Javascript to be executed. Get to that in a minute.
The results from execute_script will be result of the last line of script. (Remember, not all syntax in Javascript return results...)
It is a v8Struct::Value
object which gives us an interface for going from dynamically typed Javascript objects to statically typed Rust objects, with additional functions for operations on the returned object. Better examples of production grade using of the resulting object later....
A slightly more complex and more real world example: putting our IIFC in a seperate file (nobody likes two languages in the same file, looking at you React)
pub async fn do_from_file(&self) -> String {
let mut runtime = JsRuntime::new(Default::default());
let res = runtime.execute_script_static("name_of_the_thing_not_to_be_javascript",
include_str!("file.js"))
.expect("error");
let str = res.open(runtime.v8_isolate())
.to_rust_string_lossy(&mut runtime.handle_scope());
str
}
the include_string function call? It pulls the file in at build time and saves it as a string in the binary, and then at execution time will use that (now static!) string
Q: Calling functions with parameters via execute_script (step one: doing it easily / badly)
... after you've set up your Rust template project....
the contents of index.js is:
function echoInput(input) {
return input;
}
here we are calling it in Rust:
let mut runtime = JsRuntime::new(Default::default());
runtime.execute_script_static("set_var", "var myInput = \"hello from Rust\"")
// we are first adding our variable into the runtime of JS
runtime.execute_script_static("bring in body", include_str!("index.js"));
// and now bringing in the script definition of functions we might want to call....
// then we call it! with the variable we defined earlier as a parameter!
match runtime.execute_script_static("name_of_the_thing_not_to_be_javascript", "echoInput(myInput)") {
Ok(res) => {
print!("finished successfully\n");
res.open(runtime.v8_isolate())
.to_rust_string_lossy(&mut runtime.handle_scope())
}
Err(e) => {
print!("error!");
print!("{}", e);
"no".to_string()
}
}
We handle the result of the execute_script_static
with a pattern match, checking the Result
that comes back. In later examples we'll mostly ignore this, opting for the "panic if there's an error" .expect
. ("expect this will work, or panic with an error message" is how I read that function).
Huh this seems not great, string injecting some parameter into a JavaScript source....
No, it's not really!
Luckily we can define methods in Rust/Deno Core that are callable via Javascript! Let's create a simple reimplentation of a command that appends some extra stuff to a passed in string... in Rust. By default - although this can be changed - these live on Deno.core.ops.
An interesting thing is that this object may be special, I'm not sure if calls to undefined methods here error on the JS side.
#[op]
fn op_test_data_in_out(_: &mut OpState, message: String) -> String {
return message + "... and here's more"
}
technically I think functions decorated with the deno_core::op macro should return a Result< T, Error >
. We'll ignore that here, whatever, String seems to work for our play here.
Let's set up the runtime instance....
let ext = Extension::builder("my_ext")
.ops(vec![
// An op for summing an array of numbers
// The op-layer automatically deserializes inputs
// and serializes the returned Result & value
op_test_data_in_out::decl()
])
.build();
let mut runtime = JsRuntime::new( RuntimeOptions{ extensions: vec![ext], ..Default::default()});
let res = runtime.execute_script_static("name_of_the_thing_not_to_be_javascript",
"(function foo() { return Deno.core.ops.op_test_data_in_out(\"hello from JS\")})()")
.expect("error");
Should return "hello from JS... and here's more".
Here I'm using a standalone function as I don't know the Rust magic to refer to a Rust method (object) in a class (and pass the correct self). But this should be possible.
But ummm what about the event loop / asynchronous Javascript code?
JsRuntime::run_event_loop
is a future that resolves when there are no more async ops pending and users are expected to call it manually after executing script or module.
Source
On the Typescript side (sample.js), if we have a function like so: async function asyncDemo() { return await "hello world after a time" }
On the Rust side....
We can not just execute_script_static give it a "await someAsyncFunction" string, for a number of reasons. The first one being this will give you a Javascript syntax error. You must call the function by itself and deal with the promise object returned by the function (ie: using the promise API directly, not using the syntax sugar provided by await
).
BUT on the Rust side we also need to set ourselves up to handle the eventual return of the Javascript promise. Turns out we need to use Threads and the Tokio Rust asynchronous scheduler (which also provides green threads).
However, as a learner I'm not sure these patterns are in the correct order. (I think so, based on some sample code I've seen laying around, but I'm new to team crab)
let handle = std::thread::spawn(move || {
let tokio_runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
Remember this Tokio thing, it will come into play later. Also note there's going to be two separate things named Runtime here: the JSRuntime and the Tokio Runtime. (And also an OS level thread going on, also explained later).
// load our source script
runtime.execute_script_static("bring in body", include_str!("sample.js"));
let future = async move {
match runtime.execute_script_static("fun", "asyncDemo()") {
Ok(res) => {
let value = res.open(runtime.v8_isolate());
Note that value is a v8::Value object again. Since ECMAScript's async / await support is syntactical sugar over ECMAScript promises, we can validate we have a promise (a bit of runtime type checking never hurt noone)
if (value.is_promise()) {
print!("yes it is a promise\n");
}
But, since it IS a promise, it will resolve sometime later in the future. asynchronously For our example here we'll simplify some larger patterns around channels and just wait around for the value.
// we know it's a promise! Wait for the runtime to resolve the value
// inside the promise (this automatically runs the event loop and
// handles value resolution or oops event loop done)
let promise_result = runtime.resolve_value(res).await;
promise_result.expect("error discovered in promise")
.open(runtime.v8_isolate()).to_rust_string_lossy(&mut runtime.handle_scope())
}
}
Now promise_result is the same we've seen elsewhere, and the result of opening the result will be our resolved promise.
RUST NEWBIE SYNTAX BREAK
In Rust the last statement is returned from a {} block but ONLY if the last statement lacks a semi-colon. Here we take the results of promise_result , get the rust string (lossy) from it, and Rust returns that as the result of the Ok(res) statement... which is the result of the match statement, which is the result of the async move block / saved in the future (which is of type Future.
Note that we've now closed the async block and now we need to wait for the future to be resolved (with the value returned, as described above... but whose type is actually hard to know as humans (listen to what the compiler is telling you the type is, I think you can not (currently) declare the type returned from the async block.)
Note we are now (back) in the same level / scope as we first declared future.
tokio_runtime.block_on(future)
});
handle.join().unwrap()
What we're doing here is we have Tokio block on the future being resolved. Now this is inside that extra thread we've spawned because the Tokio thread driver runs on the main thread and thus it won't let you block that thread.
handle.join().unwrap()
will unwrap the result of the thread, which is the result of the future, which is our string from Node.
Q: wait does execute_script know about ES modules?
A: no. There are functions in DenoCore that handle ES modules, and can run Typescript (not Javascript) through Deno. However, we can avoid those performance costs by using esbuild to transpire Typescript or ES modules to CommonJS syntax.
Dealing with V8 Objects
Sometimes Deno Core is too high level, and you need to interact with V8 objects directly.
Injecting a variable into the runtime (Simple case) - aka "making a global variable"
here's how
let mut scope = runtime.handle_scope();
let variable_context = scope.get_current_context();
let global = variable_context.global(&mut scope);
let myobjectvalue = v8::String::new(&mut scope, "this is the value").unwrap();
let myobjectname = v8::String::new(&mut scope, "Injected").unwrap();
global.set(&mut scope, my_object_name.into(), my_object_value.into());
On the Javascript side, console.log(injected)
will Just Work
This same pattern is used to make larger objects: each thing needs a value, a name for that value and a some kind of place to put that variable. set with the name and the value.
Grabbing a global variable from the context to inject more stuff into it
A great example is on the deno core discussion board.
Downcasting various V8 objects to v8::Value objects
(may just be a Rust newbie issue, but here we go) Everything inherits from v8::Value, but the Rust compiler ?? won't explicitly downcast ??
let str = v8::String::new(&mut scope, "hi").unwrap();
let str: v8::Local<v8::Value> = v8::Local::from(str);
Putting a Vector into a Javascript Array
v8::Array does have a new_with_elements
factory function, but (Rust Newbie) I couldn't figure out how to make the compiler do what I want. It also shows some interesting concepts.
let exclude = vec!["one", "two", "three"];
let exclude_out_array = v8::Array::new(&mut scope, 0);
for i in 0..exclude.len() {
let str = v8::String::new(&mut scope, &exclude[i]).unwrap();
let str: v8::Local<v8::Value> = v8::Local::from(str);
exclude_out_array.set_index(&mut scope, (i as u32), str);
}
Putting a vector into a Javascript array (better)
fn vec_to_vec_of_values<'s>(
scope: &mut v8::HandleScope<'s>,
vec: Vec<String>,
) -> Vec<v8::Local<'s, v8::Value>> {
vec.into_iter()
.map(|s| v8::String::new(scope, s.as_str()).unwrap().into())
.collect()
}
let include_out_array = v8::Array::new_with_elements(&mut scope, vec_to_vec_of_values(&mut scope, my_vector);
Dealing with V8 Objects Attachments (2)
Potentially useful code snippets
macro: put string into object
/*
The reason why we use macros for things that normally an ordinary function would do for
is to avoid errors from Rust about too many mutable borrows. Since every reference to the JSRuntime
acts on either a _mutable_ JSRuntime or other objects with mutablitily, this makes it easy to run into
compile errors with regular functions.
(You could argue that from Rust's perspective what happens in v8 is an implementation detail, or you could argue
that we ARE in fact changing state so the mutability modifer is technically correct.... which is of course the
best kind of correct, especially when it makes other people's lives harder.
Since macros are injected inline there's _technically_ no _actual_ function call going on that would change
the borrow status or lifetime of the JSRuntime variable
*/
#[macro_export]
macro_rules! put_string_into_object {
($scope: expr, $obj: expr, $name: expr, $value: expr) => {{
let key = v8::String::new(&mut $scope, $name).unwrap();
let value = v8::String::new(&mut $scope, $value.as_str()).unwrap();
$obj.set(&mut $scope, key.into(), value.into());
}};
}
Turn a Vector<String> into Vectorv8::Value
// without specifying the lifetime this causes compiler errors
fn vec_to_vec_of_values<'s>(
scope: &mut v8::HandleScope<'s>,
vec: Vec<String>,
) -> Vec<v8::Local<'s, v8::Value>> {
vec.into_iter()
.map(|s| v8::String::new(scope, s.as_str()).unwrap().into())
.collect()
}
let include_out_array = v8::Array::new_with_elements(&mut scope, vec_to_vec_of_values(&mut scope, my_vector);
Resources
Read these to get context to understand the sample code (11)
- Simple walkthough of using JS in Node
- Roll your own JavaScript runtime
- Roll your own JavaScript runtime, pt. 2
- Roll your own JavaScript runtime, pt. 3
- Infrastructure - A Guide to Deno Core
- 5.8 JS Runtime - The Internals of Deno
- Dissecting Deno
- Questions about embedding deno · denoland/deno · Discussion #9595
- javascript - How to understand JS realms
- Interaction with V8 - A Guide to Deno Core
- 5.3 Main program of Deno - The Internals of Deno