David Alsh
David Alsh•11mo ago

How to call a JS function from Rust?

Hi, I'd like to call a JS function from Rust but I can't seem to find an example that does so. I got this far with the examples and blog tutorials.
// index.js
function add(a, b) {
return a + b
}

// Do I need these?
// globalThis.add = add
// export { add }
// index.js
function add(a, b) {
return a + b
}

// Do I need these?
// globalThis.add = add
// export { add }
// main.rs
async fn main_async() {
let main_module = resolve_path(
"./index.js",
env::current_dir().unwrap().join("js").as_path()
).unwrap();

let mut js_runtime = JsRuntime::new(RuntimeOptions {
module_loader: Some(Rc::new(FsModuleLoader)),
..Default::default()
});

let mod_id = js_runtime.load_main_module(&main_module, None).await.unwrap();

let result = js_runtime.mod_evaluate(mod_id);

js_runtime.run_event_loop(PollEventLoopOptions::default()).await.unwrap();

result.await.unwrap();
}

fn main() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(main_async())
}
// main.rs
async fn main_async() {
let main_module = resolve_path(
"./index.js",
env::current_dir().unwrap().join("js").as_path()
).unwrap();

let mut js_runtime = JsRuntime::new(RuntimeOptions {
module_loader: Some(Rc::new(FsModuleLoader)),
..Default::default()
});

let mod_id = js_runtime.load_main_module(&main_module, None).await.unwrap();

let result = js_runtime.mod_evaluate(mod_id);

js_runtime.run_event_loop(PollEventLoopOptions::default()).await.unwrap();

result.await.unwrap();
}

fn main() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(main_async())
}
How can I call add(1, 2) from rust?
11 Replies
Deno AI Helper
Deno AI Helper•11mo ago
I wasn't able to find the answer to your question in the official Deno documentation. However, based on the code snippets you've provided, I can offer some general advice. In your JavaScript code, if you want to make the add function available globally, you can assign it to globalThis:
// index.js
function add(a, b) {
return a + b;
}

globalThis.add = add;
// index.js
function add(a, b) {
return a + b;
}

globalThis.add = add;
This will make the add function accessible as a global function, which you can then attempt to call from your Rust code. In your Rust code, you are setting up a JsRuntime and loading a module, but you are not showing how you are trying to call the add function. To call a JavaScript function from Rust, you would typically use the JsRuntime to evaluate a script that calls the function. However, the specifics of how to do this depend on the APIs provided by the Rust library you are using for JavaScript integration (which seems to be Deno's runtime in this case). Please note that the advice provided here is not based on the Deno documentation, and you should double-check the accuracy of this advice. If you are using Deno's runtime or another library for embedding JavaScript in Rust, you should consult the documentation or community resources for that specific library to find the best way to call JavaScript functions from Rust.
1954
1954•11mo ago
i am also having this issue. i wasn to use the call_with_args method but i'm not sure how to get a v8::Function from my runtime (or scope, or wherever those live)
jeff.hykin
jeff.hykin•11mo ago
Based on this I think it would look something like this (replace main with "add")
// use deno_core::*;

let mut scope = js_runtime.handle_scope();
let main_fn_key = v8::String::new(&mut scope, "main").unwrap();
let main_fn_local: v8::Local<v8::Function> = ns
.open(&mut scope)
.get(&mut scope, main_fn_key.into())
.unwrap()
.try_into()?;

let main_fn = v8::Global::new(&mut scope, main_fn_local);
let mut args: vec::Vec<v8::Global<v8::Value>> = vec::Vec::new();

// [[ fill out args here ]]

js_runtime.call_with_args(&main_fn, &args).await
// use deno_core::*;

let mut scope = js_runtime.handle_scope();
let main_fn_key = v8::String::new(&mut scope, "main").unwrap();
let main_fn_local: v8::Local<v8::Function> = ns
.open(&mut scope)
.get(&mut scope, main_fn_key.into())
.unwrap()
.try_into()?;

let main_fn = v8::Global::new(&mut scope, main_fn_local);
let mut args: vec::Vec<v8::Global<v8::Value>> = vec::Vec::new();

// [[ fill out args here ]]

js_runtime.call_with_args(&main_fn, &args).await
I imagine you would need to do globalThis.add = add
GitHub
senc/src/engine.rs at a6e220d1d8a6418014b0c048513bb0d7e98c46aa · fe...
Hermetic runtime for TypeScript optimized for generating configuration files (including IaC). - fensak-io/senc
Mike Wilkerson
Mike Wilkerson•9mo ago
I'm trying to do the same thing and have got this working up to the point of invoking call_with_args. Now the problem is with the borrow checker. That call_with_args requires a mutable borrow of the js_runtime. But it's already been mutably borrowed up here:
let mut scope = js_runtime.handle_scope();
let mut scope = js_runtime.handle_scope();
Any suggestions for how to resolve that? Oh, and related, the docs for call_with_args say:
The event loop must be polled seperately for this future to resolve. If the event loop is not polled, the future will never make progress.
I verified that, because when I successfully invoke the function, it hangs forever, not making progress. But calling run_event_loop also involves a mutable borrow of js_runtime. So it results in the same complaint from the borrow checker.
NewWars
NewWars•9mo ago
If you get to know a solution let me know, i'm also looking for it
Mike Wilkerson
Mike Wilkerson•9mo ago
For now, I got it to build and run by enclosing the first mutable borrow within a lexical scope and having it return the Future from the call_with_args. Then in a second lexical scope, do the run_event_loop. Like this:
let fut = {
let mut scope = worker.js_runtime.handle_scope();
// ...
JsRuntime::scoped_call_with_args(&mut scope, &fn_global, &args)
};

let string_value = {
worker.run_event_loop(false).await?;
let result = fut.await?;
let mut scope = worker.js_runtime.handle_scope();
let local = deno_core::v8::Local::new(&mut scope, result);
// The function I'm calling returns a `String`.
deno_core::serde_v8::from_v8::<String>(&mut scope, local)?
};
let fut = {
let mut scope = worker.js_runtime.handle_scope();
// ...
JsRuntime::scoped_call_with_args(&mut scope, &fn_global, &args)
};

let string_value = {
worker.run_event_loop(false).await?;
let result = fut.await?;
let mut scope = worker.js_runtime.handle_scope();
let local = deno_core::v8::Local::new(&mut scope, result);
// The function I'm calling returns a `String`.
deno_core::serde_v8::from_v8::<String>(&mut scope, local)?
};
This does get the result I want. But I don't yet understand what's happening with scopes well enough to reason about the fact that a separate scope is used in the second block. It seems like that might be a problem, but I'm not sure what that would be. Still learning...
NewWars
NewWars•9mo ago
That's certainly odd, if i find how to make it less odd i'll tell you @Mike I haven't read all the code in the gists yet, but this issue seems to have answers https://github.com/denoland/deno_core/issues/515 seems to be the author of this discord thread and post even
Mike Wilkerson
Mike Wilkerson•9mo ago
Ok, it looks to me like what I did with lexical scoping is the same solution as what's in those gists. Only those are cleaner overall. The final gist has this:
let request = {
let mut scope = &mut runtime.handle_scope();
let request = serde_v8::to_v8(scope, "asdsd").unwrap();
Global::new(&mut scope, request)
};
let call = runtime.call_with_args(&function, &[request]);
let request = {
let mut scope = &mut runtime.handle_scope();
let request = serde_v8::to_v8(scope, "asdsd").unwrap();
Global::new(&mut scope, request)
};
let call = runtime.call_with_args(&function, &[request]);
The implementation of call_with_args is where the second mutable borrow happens: it also invokes handle_scope. I'd pulled that apart because I thought (not yet understanding scopes), that it should be the same scope used in both places. So I could clean mine up to look more like that, but in principle, both have these two features: 1. using lexical scopes to fix the double mutable borrow 2. invoking handle_scope() twice I think I want to consider that confirmation ...?
NewWars
NewWars•9mo ago
I guess so 😄
Mike Wilkerson
Mike Wilkerson•9mo ago
Something that's still bugging me about this is memory management. call_with_args requires that the arguments are &[Global<Value>]. And in the example code, getting a global seems to involve: 1. creating a local 2. promoting it to global But does that then mean it's not eligible for garbage collection? In my case, this function calling would happen many times in a long-running process, where the range of possible args is unlimited. So if every invocation of the function involves creating globals that won't be garbage collected, doesn't that mean memory leak?
bartlomieju
bartlomieju•9mo ago
Once these values are dropped (eg after loop iteration) then there will be no longer a global and they can be GCed