Node: No way to extract arguments by index in N-API modules?

Created on 21 Apr 2019  路  6Comments  路  Source: nodejs/node

Is your feature request related to a problem? Please describe.
In the V8 API, arbitrary arguments are accessible via the instance method v8::FunctionCallbackInfo<T>::operator[](int i). But this doesn't appear exposed through the API, and this can be useful when writing native extensions.

Describe the solution you'd like
Something like napi_get_argument(napi_env, napi_callback_info, size_t, napi_value *result) that would be direct sugar over that. It would return napi_ok normally and napi_invalid_arg if the index is out of range.

Describe alternatives you've considered
I briefly considered the alternative of mallocing the arguments and returning them that way, but I felt it didn't mesh as well with the non-allocating nature of the rest of the API.

n-api question

Most helpful comment

@isiahmeadows IMO this is indeed the preferred method for iterating over arguments.

All 6 comments

napi itself is a c api, and you can't override indexing in c. However, if you use the C++ wrapper, we provide this: https://github.com/nodejs/node-addon-api/blob/36863f087b1f879184ae64ebfef9c19e07c4ae0d/napi.h#L1352

if you just want an array of arguments in c, napi_get_cb_info is pretty much already that api.

@devsnek I'm not referring to the operator, but its underlying functionality. To put it another way, I'm just wanting to know if you can somehow iterate the list of arguments itself through N-API.

Now that I think about it, you could do it via napi_get_cb_info, but it'd take two successive calls and you'd have to allocate the arguments manually:

napi_status status;
size_t argc;
napi_value *argv;

// Get the argument count
napi_get_cb_info(env, cbinfo, &argc, NULL, NULL, NULL);

// Get the arguments themselves
argv = new napi_value[argc];
napi_get_cb_info(env, cbinfo, &argc, argv, NULL, NULL);

I was just wondering/hoping there was a way of doing this that didn't require allocating yet another chunk of memory when the engine has already allocated the arguments list somewhere to expose it to you (in this case, copied from the stack to expose to you via an also-allocated v8::FunctionCallbackInfo<v8::Value> *).

@isiahmeadows although the engine certainly makes it look like the arguments are in a neat array, that array cannot simply be cast to a plain C array so as to avoid copying. We cannot assume that the engine stores arguments contiguously. Thus, the only way to get the arguments into a N-API callback is via napi_get_cb_info(), which has no choice but to iterate over the engine's structure and fill in the C array with napi_values as it does so.

The reason we have a single API to retrieve all the callback info is because each API call can be expensive for such a basic operation. It is also most often the case that bindings accept a pre-determined number of arguments so most often you need not call the function twice. Doing things like variable number of arguments and argument juggling can be done much faster in pure JS.

@gabrielschulhof

although the engine certainly makes it look like the arguments are in a neat array, that array cannot simply be cast to a plain C array so as to avoid copying.

That wasn't my intent - I was citing the existence of a V8 API. I suggested something like status = napi_get_argument(env, cbinfo, index, &result) in the original bug. Using my proposal, it'd work like this:

napi_status status;
// Get arguments length
size_t argc;
status = napi_get_cb_info(env, cbinfo, &argc, NULL, NULL, NULL);
assert(status == napi_ok);

// Iterate in loop
for (size_t i = 0; i < argc; i++) {
    napi_value current = argv[i];
    status = napi_get_argument(env, cbinfo, index, &current)
    assert(status == napi_ok);
    // ...
}


Internally, napi_get_argument might be implemented like this:

// Add a new abstract method and implement it in the various subclasses
class CallbackWrapper {
 public:
  virtual bool ArgAt(size_t index, napi_value *result) = 0;
};

class FunctionCallbackWrapper {
 public:
  /*virtual*/
  bool ArgAt(size_t index, napi_value *result) override {
    if (i >= _args_length) return false;
    *result = v8impl::JsValueFromV8LocalValue(_cbinfo[i]);
    return true;
  }
};

class GetterCallbackWrapper {
 public:
  /*virtual*/
  bool ArgAt(size_t index, napi_value *result) override {
    return false;
  }
};

class SetterCallbackWrapper
    : public CallbackWrapperBase<v8::PropertyCallbackInfo<void>,
                                 &CallbackBundle::setter> {
 public:
  /*virtual*/
  bool ArgAt(size_t index, napi_value *result) override {
    if (i > 0) return false;
    *result = v8impl::JsValueFromV8LocalValue(_value);
    return true;
  }
};

// And the call itself
napi_status napi_get_argument(
    napi_env env,
    napi_callback_info cbinfo,
    size_t index,
    napi_value* result) {
  CHECK_ENV(env);
  CHECK_ARG(env, cbinfo);
  CHECK_ARG(env, result);

  v8impl::CallbackWrapper* info =
      reinterpret_cast<v8impl::CallbackWrapper*>(cbinfo);

  RETURN_STATUS_IF_FALSE(env, info->ArgAt(index, result), napi_invalid_arg);
  return napi_clear_last_error(env);
}

The reason we have a single API to retrieve all the callback info is because each API call can be expensive for such a basic operation. It is also most often the case that bindings accept a pre-determined number of arguments so most often you need not call the function twice. Doing things like variable number of arguments and argument juggling can be done much faster in pure JS.

So is the preferred method to iterate passed-in parameters to do this?

napi_status status;
// Get arguments length
size_t argc;
status = napi_get_cb_info(env, cbinfo, &argc, NULL, NULL, NULL);
assert(status == napi_ok);

// Get array of arguments
napi_value *argv = (napi_value *) malloc(argc * sizeof(uintptr_t));
status = napi_get_cb_info(env, cbinfo, &argc, argv, NULL, NULL);
assert(status == napi_ok);

// Iterate in loop
for (size_t i = 0; i < argc; i++) {
    napi_value current = argv[i];
    // ...
}

@isiahmeadows IMO this is indeed the preferred method for iterating over arguments.

Closed since my question has been answered.

Was this page helpful?
0 / 5 - 0 ratings