Node-addon-api: `Error: Invalid argument` when calling Napi::Function

Created on 29 Oct 2017  ·  14Comments  ·  Source: nodejs/node-addon-api

I'm working on a native EventEmitter, and i'm getting invalid argument when calling the handlers in a loop, but nowhere else. see the comments in the screenshots for more info.


Most helpful comment

@devsnek I might be off here, but seems to me like you keep the functions in your class members after the javascript scope that contained them (the scope of the On() function) already returned, and the local reference you had is invalidated.

In order to keep values after the local scope returns you will need to create a reference, which for convinience has an Napi::FunctionReference class you can use:
https://nodejs.github.io/node-addon-api/class_napi_1_1_function_reference.html

Notice that even the Napi::String values you used for keys are invalidated so you better just use std::string for your map keys.

You should still make a copy of the vector before calling the handlers in case the handlers will decide to call On() or RemoveListener() which will change the vector during the iteration, but then you can just take out the value from the ref - see below.

This code seems to do what you want.

addon.cpp:

#include <iostream>
#include <vector>
#include <map>
#include <napi.h>

class Emitter : public Napi::ObjectWrap<Emitter>
{
private:
    typedef std::vector<Napi::FunctionReference> Handlers;
    typedef std::map<std::string, Handlers> HandlersMap;
    HandlersMap handlers_map;

public:
    static void
    Init(Napi::Env env, Napi::Object exports)
    {
        exports["Emitter"] = DefineClass(
            env,
            "Emitter",
            {
                InstanceMethod("on", &Emitter::on),
                InstanceMethod("emit", &Emitter::emit),
            }
        );
    }

    Emitter(const Napi::CallbackInfo &info) : Napi::ObjectWrap<Emitter>(info) {}

    void
    on(const Napi::CallbackInfo &info)
    {
        Napi::String event = info[0].As<Napi::String>();
        Napi::Function func = info[1].As<Napi::Function>();
        handlers_map[event].push_back(
            Napi::Reference<Napi::Function>::New(func, 1)
        );
    }

    void
    emit(const Napi::CallbackInfo &info)
    {
        Napi::String event = info[0].As<Napi::String>();
        std::vector<Napi::Function> list;
        for (Napi::FunctionReference &ref : handlers_map[event]) {
            list.push_back(ref.Value());
        }
        for (Napi::Function &func : list) {
            func.Call({});
        }
    }
};

Napi::Object
addon(Napi::Env env, Napi::Object exports)
{
    Emitter::Init(env, exports);
    return exports;
}

NODE_API_MODULE(addon, addon)

test.js:

'use strict';

const { Emitter } = require('./build/Release/addon');

const e = new Emitter();
e.on('lala', () => console.log('LALA1'));
e.on('lala', () => console.log('LALA2'));
e.on('lala', () => console.log('LALA3'));
e.emit('lala');

Output:

$ node test.js 
LALA1
LALA2
LALA3
(node:13429) Warning: N-API is an experimental feature and could change at any time.

All 14 comments

Can you post a more complete code example? It's impossible to tell what the issue is right now.

Don't know if it's the cause but by the looks of it, the once and every collections can get mutated while they're being iterated over (loop calls into JS, JS can call mutator methods.)

@bnoordhuis so is this a napi issue or should i close this

I'd close it unless you've conclusively established it's a napi issue.

@bnoordhuis i guess this is out of scope of the issue, but do you have any knowledge of how to fix this? i'm not very experienced with any of the addon apis

Iterate over a copy of the list. The built-in EventEmitter does the same thing.

@bnoordhuis i tried making a copy but it has the same error, i'm hopeful i just did this completely wrong

    std::vector<Napi::Function> copy(this->every[name].begin(), this->every[name].end());
    for (Napi::Function const& handler: copy)
      handler.Call(callArgs); // same error as before

after talking with my friend who actually knows c++ i'm gonna repoen this

UB how and where?

@devsnek I might be off here, but seems to me like you keep the functions in your class members after the javascript scope that contained them (the scope of the On() function) already returned, and the local reference you had is invalidated.

In order to keep values after the local scope returns you will need to create a reference, which for convinience has an Napi::FunctionReference class you can use:
https://nodejs.github.io/node-addon-api/class_napi_1_1_function_reference.html

Notice that even the Napi::String values you used for keys are invalidated so you better just use std::string for your map keys.

You should still make a copy of the vector before calling the handlers in case the handlers will decide to call On() or RemoveListener() which will change the vector during the iteration, but then you can just take out the value from the ref - see below.

This code seems to do what you want.

addon.cpp:

#include <iostream>
#include <vector>
#include <map>
#include <napi.h>

class Emitter : public Napi::ObjectWrap<Emitter>
{
private:
    typedef std::vector<Napi::FunctionReference> Handlers;
    typedef std::map<std::string, Handlers> HandlersMap;
    HandlersMap handlers_map;

public:
    static void
    Init(Napi::Env env, Napi::Object exports)
    {
        exports["Emitter"] = DefineClass(
            env,
            "Emitter",
            {
                InstanceMethod("on", &Emitter::on),
                InstanceMethod("emit", &Emitter::emit),
            }
        );
    }

    Emitter(const Napi::CallbackInfo &info) : Napi::ObjectWrap<Emitter>(info) {}

    void
    on(const Napi::CallbackInfo &info)
    {
        Napi::String event = info[0].As<Napi::String>();
        Napi::Function func = info[1].As<Napi::Function>();
        handlers_map[event].push_back(
            Napi::Reference<Napi::Function>::New(func, 1)
        );
    }

    void
    emit(const Napi::CallbackInfo &info)
    {
        Napi::String event = info[0].As<Napi::String>();
        std::vector<Napi::Function> list;
        for (Napi::FunctionReference &ref : handlers_map[event]) {
            list.push_back(ref.Value());
        }
        for (Napi::Function &func : list) {
            func.Call({});
        }
    }
};

Napi::Object
addon(Napi::Env env, Napi::Object exports)
{
    Emitter::Init(env, exports);
    return exports;
}

NODE_API_MODULE(addon, addon)

test.js:

'use strict';

const { Emitter } = require('./build/Release/addon');

const e = new Emitter();
e.on('lala', () => console.log('LALA1'));
e.on('lala', () => console.log('LALA2'));
e.on('lala', () => console.log('LALA3'));
e.emit('lala');

Output:

$ node test.js 
LALA1
LALA2
LALA3
(node:13429) Warning: N-API is an experimental feature and could change at any time.

@bnoordhuis I think this issue can be closed. Hope that helps.

@guymguym thanks for providing a detailed answer.

how to pass arguments to Call() properly?

Was this page helpful?
0 / 5 - 0 ratings