Search code examples
rustdenoembedded-v8

Code runs in main but fails with "cannot borrow as mutable more than once at a time" when extracted to function


I'd like to execute a small JavaScript snippet using the deno_core crate, get the result object, get the test property and execute it as a function.

The following code works when everything is written in the main, but it fails when I try to extract it as a function.

How to avoid situations like this / why does it work in main / how to extract it correctly (if it's possible to extract this as one function)?

use deno_core::v8;
use deno_core::JsRuntime;

fn main() -> anyhow::Result<()> {
    let mut runtime = JsRuntime::new(Default::default());
    let executed = runtime.execute_script("name", "({ test: (a, b) => { console.log(a + b) } })")?;
    let scope = &mut runtime.handle_scope();
    let local_executed = v8::Local::new(scope, executed);
    let result_obj = v8::Local::<v8::Object>::try_from(local_executed)?;
    let param1 = v8::String::new(scope, "Hello").unwrap();
    let param2 = v8::String::new(scope, "World").unwrap();
    let args = vec![param1.into(), param2.into()];

    let fn_name: v8::Local<v8::String> = v8::String::new(scope, "my_function").unwrap();
    result_obj.get(scope, fn_name.into())
        .map(|value| v8::Local::<v8::Function>::try_from(value))
        .unwrap()?
        .call(scope, local_executed, &args);

    // If I call get_fn and call the result v8::Function here like this, it fails to compile:
    //
    // get_fn(scope, result_obj).call(scope, local_executed, &args);
    //        -----              ---- ^^^^^ second mutable borrow occurs here
    //        |                  |
    //        |                  first borrow later used by call
    //        first mutable borrow occurs here
    //
    // error[E0499]: cannot borrow `*scope` as mutable more than once at a time

    Ok(())
}

fn get_fn<'a>(scope: &'a mut v8::HandleScope, object: v8::Local<'a, v8::Object>) -> v8::Local<'a, v8::Function> {
    let fn_name = v8::String::new(scope, "test").unwrap();
    object.get(scope, fn_name.into())
        .map(|value| v8::Local::<v8::Function>::try_from(value))
        .unwrap()
        .unwrap()
}


Solution

  • The reason why it is complaining is that you've bounded the lifetime of the output of the function to the lifetime of the reference to the scope, instead of the lifetime of the scope itself.

    The solution is to change &'a mut v8::HandleScope to &mut v8::HandleScope<'a> in the function signature.

    Here's my overly detailed explanation:

    Firstly, v8::HandleScope is a type with a lifetime parameter. So, it's possible to write v8::HandleScope<'s>. Writing just v8::HandleScope is called eliding the lifetime parameter, and this just tells the compiler that we aren't going to use it, and avoids having to give it a name only to never use it.

    So, when we define the function signature to take a value of type &'a mut v8::HandleScope, we're bounding 'a to be the lifetime of this reference to the scope, and then eliding the lifetime of the scope itself. Now all of these other functions that want &mut HandleScope such as v8::String::new and v8::Local::get have a lifetime parameter 's and they're looking for a &mut v8::HandleScope<'s>. And then, the values they return are bouded by 's not to outlive the scope in which they belong. What this means is that the lifetime of the v8::Local being returned by this function is actually the lifetime of the HandleScope, which we're eliding. From now on, I'm going to refer this elided lifetime as 's. So we might wonder, how come this function even compiles if we've said we were going to return a Local<'a> but then we're actually returning a Local<'s> ? The reason why this is allowed is that the compiler is rather clever, and knows that since we're holding a &'a to the HandleScope, then that HandleScope<'s> must live longer than the &'a to it, because references cannot outlive the object they point to, so therefore this proves that 's is longer than 'a, that is to say, if an object is valid for 's, then it is valid for 'a. So since the requirement to be a Local<'a> is that it is a Local which is valid for the lifetime 'a, our Local<'s> which is valid for 's, can coerce into a Local<'a> because we do have the guarantee that it is valid for 'a. But of course, we didn't really want to restrict the lifetime of this Local in this way. We've inadvertently tied the lifetime of the Local to the lifetime of our borrow of the scope which means that the borrow checker will still consider the scope to be borrowed even after the function returns. What we've essentially said is that this local is only valid as long as the reference to the scope remains unmodified, but this is not actually true. It's true that the local is for sure valid while the reference lives, but it's also valid after that specifically until the scope is dropped. That's the lifetime 's. So all we have to do is change the signature of the function to:

    fn get_fn<'s>(scope: &mut v8::HandleScope<'s>, object: v8::Local<'s, v8::Object>) -> v8::Local<'s, v8::Function>
    

    And then we won't be making any weird overly tight requirements on the lifetimes, and you can successfully call get_fn(scope, result_obj).call(scope, local_executed, &args) in main.