neon-bindings / neon

Rust bindings for writing safe and fast native Node.js modules.
https://www.neon-bindings.com/
Apache License 2.0
8.05k stars 284 forks source link

How to bind `HashSet` #920

Closed xiBread closed 2 years ago

xiBread commented 2 years ago

I'm still a beginner at Rust, so I'm trying to do something relatively simple and bind some of the HashSet methods. However, I'm getting stumped by an error when implementing an add function for insert.

struct Set {
    set: HashSet<String>,
}

impl Finalize for Set {}

impl Set {
    fn new() -> Self {
        Self {
            set: HashSet::new(),
        }
    }

    fn js_new(mut ctx: FunctionContext) -> JsResult<JsBox<Set>> {
        let set = Set::new();

        Ok(ctx.boxed(set))
    }

    fn js_add(mut ctx: FunctionContext) -> JsResult<JsBoolean> {
        let mut this = ctx.this().downcast_or_throw::<JsBox<Set>, _>(&mut ctx)?;
        let value = ctx.argument::<JsString>(0)?.value(&mut ctx);

        let res = this.set.insert(value.to_string());
        // cannot borrow data in dereference of `neon::prelude::JsBox<Set>` as mutable
        // trait `DerefMut` is required to modify through a dereference, but it is not implemented for `neon::prelude::JsBox<Set>`

        Ok(ctx.boolean(res))
    }
}

I realize this is most likely a mistake with my code and not the library, but I would appreciate any pointers as to how to fix this. Thanks! 🙂

kjvalencik commented 2 years ago

Hello, @xiBread! This is a good question.

Rust Background

A quick summary of Rust references:

  1. There can be any number of immutable/shared (&) references at one time
  2. When there is a mutable/exclusive (&mut) reference, no other references can be held.

These rules are critical to Rust's safety guarantees.

Neon Background

Neon only gives shared references (immutable, &) to values in a JsBox. Why is that? JavaScript and Rust have very different ownership rules. In JavaScript, it's possible to have lots of different aliased mutable references to the same data.

const data = {};
const a = data;
const b = data;

a.a = 1;
b.b = 2;

console.log(data);
// { a: 1, b: 2 }

Writing this code in Rust would be a compile error. Since there could be any number of handles (references) to a JsBox, it would be unsound to give a mutable reference. Consider the following:

fn example(mut ctx: FunctionContext) -> JsResult<JsUndefined> {
    let mut a = cx.argument::<JsBox<Set>>(0)?;
    let mut b = cx.argument::<JsBox<Set>>(1)?;

    Ok(cx.undefined())
}
const box = addon.new_box();

addon.example(box, box);

In this example, both a and b point to the same box! If we could take a mutable reference to each, it would be invalid.

Solution

How do we solve this? Rust has a concept called interior mutability. When using interior mutability, borrow checking is deferred to runtime instead of at compile time. The most common example of interior mutability is a Mutex.

Instead of storing a JsBox<Set>, store a JsBox<RefCell<Set>>. A RefCell performs dynamic borrow checking. It only works for single threaded code, but JavaScript is single threaded! It follows the same Rust borrow checking rules that happen at compile time, except at runtime. Unlimited shared references and only a single exclusive reference.

There is an example of how to use it in the Neon JsBox docs.

Let me know if this clears things up!

xiBread commented 2 years ago

This does clear things up, thank you for the detailed answer!

kjvalencik commented 2 years ago

I should add since you have a wrapper, you could also push the RefCell internally to keep it as a nice JsBox<Set>.

struct Set {
    set: RefCell<HashSet<String>>,
}

let this = ctx.this().downcast_or_throw::<JsBox<Set>, _>(&mut ctx)?;

this.set.borrow_mut().insert(String::from("Hello, World!"));

One way isn't strictly better than the other. It mostly depends how you would like to structure your code.