neon-bindings/neon

Is it impossible to properly accept results of f.bind(...) from JavaScript?

JohnScience opened this issue · 2 comments

Hello, I'm writing a neon module for electron app. In order to invoke console.log function in renderer context I implemented Window::log in a way that would allow me to do this.

electron-client/main.js:

// ...

class Window extends BrowserWindow {
  //...
  log(msg) {
    this.webContents.send('log', msg);
  }
 // ...
}

// ...

(modulo irrelevant details)

However, when I try to supply the result of binding the first argument of init_app_dir function with Window::log,

electron-client/main.js:

// ...
app.whenReady().then(() => {
  //...
  const win = new Window();
  const init_app_dir_handler = init_app_dir.bind(null, win.log);
  ipcMain.handle('init_app_dir', init_app_dir_handler);
  win.main_loop();
})

and invoke init_app_dir_handler, which the result of binding js_init_app_dir exported as init_app_dir,

// ...
fn js_init_app_dir(mut cx: FunctionContext) -> JsResult<JsNumber> {
    let log = cx.argument::<JsFunction>(0)?;
    let app_data_path = cx.argument::<JsValue>(1)?;
    log
        .call_with(&mut cx)
        .arg(app_data_path)
        // The return type of console.log() is that of the parameter
        .apply::<JsValue,_>(&mut cx)?;
    //let app_data_path = PathBuf::from(app_data_path);
    //Ok(cx.number(init_app_dir(app_data_path)))
    Ok(cx.number(1))
}

#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
    cx.export_function("init_app_dir", js_init_app_dir)?;
    Outcome::export(&mut cx)?;
    Ok(())
}

I get the following error:

Uncaught (in promise) Error: Error invoking remote method 'init_app_dir': TypeError: Cannot read properties of undefined (reading 'webContents')

As far as I understand, the problem is that in

log
        .call_with(&mut cx)
        .arg(app_data_path)

the line .call_with(...) does the following:

impl JsFunction {
    /// Create a [`CallOptions`](function::CallOptions) for calling this function.
    pub fn call_with<'a, C: Context<'a>>(&self, _cx: &C) -> CallOptions<'a> {
        CallOptions {
            this: None,
            // # Safety
            // Only a single context may be used at a time because parent scopes
            // are locked with `&mut self`. Therefore, the lifetime of `CallOptions`
            // will always be the most narrow scope possible.
            callee: Handle::new_internal(unsafe { self.clone() }),
            args: smallvec![],
        }
    }
   // ...
}

i.e. it presumably overwrites this, which presumably must have remained intact. As a result, this.webContents.send(...) is not evaluated properly. I don't know the internals of V8 well anymore, but it seems that another type for results of f.bind(...) needed.

I can see a workaround for this particular case (namely, supplying an instance of Window itself), but it looks like an area where neon can be improved.

In this code, the problem is up related to Neon, native modules of V8 internals.

Neon will operate similar to JavaScript when calling a function created from bind.

In this example, it seems like the issue is because win.log isn't bound to win and causing this to be undefined.

This same problem happens with pure JavaScript. Try calling init_app_dir_handler() directly. It's also possible to reproduce with only the Window.

const win = new Window();
const log = win.log;
log(); // this throws

This is how this typically works in JS and also why it's a frequent source of bugs in the language.

Thank you a lot!