rustwasm / wasm-bindgen

Facilitating high-level interactions between Wasm modules and JavaScript
https://rustwasm.github.io/docs/wasm-bindgen/
Apache License 2.0
7.63k stars 1.05k forks source link

Support public custom struct in custom struct #1464

Open dtysky opened 5 years ago

dtysky commented 5 years ago

Motivation

Example:

/* ---------------- Vector3 ---------------- */
#[wasm_bindgen]
pub struct Vector3 {
  value: (f64, f64, f64),
  pub is_dirty: bool
}

#[wasm_bindgen]
impl Vector3 {
  #[wasm_bindgen(constructor)]
  pub fn new(x: f64, y: f64, z: f64) -> Vector3 {
    Vector3{value: (x, y, z), is_dirty: false}
  }

  #[wasm_bindgen(method)]
  pub fn zero() -> Vector3 {
    return Vector3::new(0., 0., 0.);
  }

  #[wasm_bindgen(method)]
  pub fn one() -> Vector3 {
    return Vector3::new(1., 1., 1.);
  }

  #[wasm_bindgen(method)]
  pub fn set(&mut self, x: f64, y: f64, z: f64) {
    self.value.0 = x;
    self.value.1 = y;
    self.value.2 = z;
    self.is_dirty = true;
  }
}

/* ---------------- Transform ---------------- */
#[wasm_bindgen]
pub struct Transform {
  test: Cell<Vector3>
}

#[wasm_bindgen]
impl Transform {
  #[wasm_bindgen(constructor)]
  pub fn new() -> Transform {
    Transform{
      test: Cell::new(Vector3::zero())
    }
  }
}

In these code we define a struct Vector and use it as a member position in another struct Transform, there is no way to pass refer of position to javascript by 'wasm_bindgen' attribute.

But It's really a common scene, it had took me lots of time, at last i found this problem could be solved on pure javascript side, see 'Proposed Solution'.

Proposed Solution

After some trials, I wrote following code to solve this problem:

#[wasm_bindgen]
impl Transform {
  #[wasm_bindgen(method, js_name="getTestPtr")]
  pub fn get_test_ptr(&mut self) -> *mut Vector3 {
    self.test.get_mut()
  }
}
// javascript file
export class Transform {

    free() {
        const ptr = this.ptr;
        this.ptr = 0;
        freeTransform(ptr);
    }

    /**
    * @returns {}
    */
    constructor() {
        this.ptr = wasm.transform_new();
        // after constructor, create an member `test`
        this.test = Vector3.__wrap(this.getTestPtr());
    }

    getTestPtr() {
        return wasm.transform_getTestPtr(this.ptr);
    }
}
// ts header file
export class Transform {
  test: Vector3;
  free(): void;
  constructor();
  getTestPtr(): number;
}
// application file
import('../pkg/paean').then(paean => {
  const transform = new this.paean.Transform();
  // it works
  transform.test.set(2, 2, 2);
  // it works too
  console.log(transform.test.x(), transform.test.y(), transform.test.z());
});

Additional Context

So, is it possible to support using public custom struct in custom struct or returning rust object's refer to javascript like these:

#[wasm_bindgen]
pub struct Transform {
    pub test: Cell<Vector3>
}

// or
#[wasm_bindgen]
impl Transform {
  #[wasm_bindgen(method, js_name="getTest")]
  pub fn get_test(&mut self) -> &Vector3 {
    self.test.get_mut()
  }
}

Thanks.

alexcrichton commented 5 years ago

Thanks for the report! Unfortunately this is quite difficult to support in wasm-bindgen due to the nature of the GC in JS. In your proposed solution, for example, the Vector3 cannot live longer than Transform but there's nothing preventing that and it allows for a use-after-free.

It's possible for us to tie all these lifetimes together somewhat but then borrowing also gets really tricky. For example storing Vector3 in Rust and having a handle to it in JS is actually quite different, in Rust there's no RefCell but with the JS handle there's a hidden RefCell.

I think this may be possible to do in the long run if we get really creative, but today there's no clear, safe, and workable method to implement this.

dtysky commented 5 years ago

@alexcrichton

Thanks for your detailed reply, I can understand what you are worry about, but these problems really confuse me, for example, what is the best way to change the position of Camera and Mesh :

#[wasm_bindgen]
pub struct Transform {
  anchor: Cell<Vector3>,
  position: Cell<Vector3>,
  rotation: Cell<Vector3>,
  scale: Cell<Vector3>,
  quaternion: Cell<Quaternion>,
  world_matrix: Cell<Matrix4>
}

#[wasm_bindgen]
pub struct Camera {
  transform: Cell<Transform>,
  fov: f64,
  aspect: f64,
  near: f64,
  far: f64,
  view_matrix: Cell<Matrix4>,
  projection_matrix: Cell<Matrix4>
}

#[wasm_bindgen]
pub struct Mesh {
  transform: Cell<Transform>,
  vertices: Cell<Vec<Vector3>>,
  colors: Cell<Vec<Vector3>>,
}

wasm_bindgen does not support trait now, and rust does not support extends, so I have to write many functions for each struct which has Transform using ugly procedure-oriented way:

#[wasm_bindgen]
impl Transform {
  // could not return refer
  #[wasm_bindgen(method, js_name="getPosition")]
  pub fn get_position(&mut self) -> Vector3 {
    self.position.get_mut().clone()
  }

  #[wasm_bindgen(method, js_name="setPosition")]
  pub fn set_position(&mut self, value: &Vector3) {
    self.position.get_mut().copy(value)
  }
}

#[wasm_bindgen]
pub fn set_camera_position(camera: &mut Camera, vector: &Vector3) {
  // `position` must be private, wasm_bindgen does not support public struct in struct
  camera.transform.get_mut().set_position(vector);
}

// could not return refer
#[wasm_bindgen]
pub fn get_camera_position(camera: &mut Camera) -> Vector3 {
  // a hidden clone
  camera.transform.get_mut().get_position()
}

emmm... Of course these problems could be solved by some design patterns like "ECS" , but those solutions are not universal and will make lot of developers give up.

So could we give developers a selection to do these if they can control the risk of memory themselves?

alexcrichton commented 5 years ago

I definitely agree that we want to solve this somehow! I'm just not sure personally how best to do so, so I'm not sure how to make progress on this.