J-Cake
J-Cake

Reputation: 1618

Creating a factory function in Rust

I'm just starting out learning Rust, and I've hit a bit of a roadblock;

I'm trying to create a function that initialises the rusty_v8 library. They've given the following code get set up with:

use rusty_v8 as v8;

let platform = v8::new_default_platform().unwrap();
v8::V8::initialize_platform(platform);
v8::V8::initialize();

let isolate = &mut v8::Isolate::new(Default::default());

let scope = &mut v8::HandleScope::new(isolate);
let context = v8::Context::new(scope);
let scope = &mut v8::ContextScope::new(scope, context);

let code = v8::String::new(scope, "'Hello' + ' World!'").unwrap();
println!("javascript code: {}", code.to_rust_string_lossy(scope));

let script = v8::Script::compile(scope, code, None).unwrap();
let result = script.run(scope).unwrap();
let result = result.to_string(scope).unwrap();
println!("result: {}", result.to_rust_string_lossy(scope));

Now, I've set myself the challenge of making this reusable. I want to be able to call an init function of some sort, which returns a v8::Scope object, that I can use to execute v8::Script objects. I've managed to create this function:

pub(crate) fn init<'a>() -> v8::ContextScope<'a, v8::HandleScope<'a, v8::Context>> {
    let platform = v8::new_default_platform().unwrap();
    v8::V8::initialize_platform(platform);
    v8::V8::initialize();

    let mut isolate = v8::Isolate::new(Default::default());
    let mut scope = v8::HandleScope::new(&mut isolate);

    let context = v8::Context::new(&mut scope);
    return v8::ContextScope::new(&mut scope, context);
}

So far, I understand how the code should work and why it doesn't. The compiler says for return statement: returns a value referencing data owned by the current function. This makes sense to me, the isolate and scope variables are created within this function. But if I want to use this function as a factory, in other words, to make this function construct a ContextScope object, the isolate and scope objects have to be kept alive. How would I achieve this?

Upvotes: 0

Views: 461

Answers (1)

Kevin Reid
Kevin Reid

Reputation: 43773

The default rules of Rust do not allow you to write the function you want to write.

Whenever you see a type which has a lifetime in it, like HandleScope<'s>, you should understand this to mean that instances of this type are temporary (with the usual exception for the lifetime being 'static, which does not apply here) and will usually exist within the scope of a stack frame. In order to return such a type, a function must be passed an instance of whatever it borrows.

The intention of the library in this case is that you should follow stack frames: HandleScope's documentation says "A stack-allocated class that governs a number of local handles." The exact wording is nonsense — you're free to move a HandleScope into a Box and thereby make it heap-allocated — but evidently they expect you to use it in a stack-oriented fashion.

The simplest way to do this is to modify your function to accept a function to run within the scope:

pub(crate) fn init<F, R>(f: F) -> R
where
    for<'a> F: FnOnce(v8::ContextScope<'a, v8::HandleScope<'a, v8::Context>>) -> R,
{
    let platform = v8::new_default_platform().unwrap();
    v8::V8::initialize_platform(platform);
    v8::V8::initialize();

    let mut isolate = v8::Isolate::new(Default::default());
    let mut scope = v8::HandleScope::new(&mut isolate);

    let context = v8::Context::new(&mut scope);
    let cscope = v8::ContextScope::new(&mut scope, context);

    f(cscope)
}

(Note: This seems like it will not compile because you have overlapping borrows of scope. I'm not familiar with rusty_v8 so I don't know why their example looks like that and whether there's an error or not.)

The more complex approach is to construct a “self-referential struct” which can contain all of the objects needed, while they refer to each other. This is unsafe if done directly (since the struct could be moved) but it can be managed using the ouroboros crate which provides the necessary mechanism in a memory-safe fashion. This still imposes the constraint that you have to refer to the data from within a closure, but you can do it repeatedly rather than only once.


However, it is likely that you should make your factory narrower: stick to returning a v8::Isolate, and make the rest part of your "eval" rather than "init" procedures. That seems likely to be closer to the intended use of the library.

Upvotes: 1

Related Questions