Skip to content

Commit

Permalink
src: add templated function factories
Browse files Browse the repository at this point in the history
These variants of `Napi::Function::New` accept the callback as a
template parameter rather than a function parameter. This allows us to
perform the binding without additional heap-allocation of the function
callback data.

PR-URL: nodejs/node-addon-api#608
Reviewed-By: Nicola Del Gobbo <[email protected]>
Reviewed-By: Chengzhong Wu <[email protected]>
Reviewed-By: Michael Dawson <[email protected]>
  • Loading branch information
kevindavies8 committed Dec 14, 2019
1 parent 8289a28 commit d7779b8
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 19 deletions.
103 changes: 102 additions & 1 deletion doc/function.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Value Fn(const CallbackInfo& info) {
}

Object Init(Env env, Object exports) {
exports.Set(String::New(env, "fn"), Function::New(env, Fn));
exports.Set(String::New(env, "fn"), Function::New<Fn>(env));
}

NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)
Expand All @@ -47,6 +47,27 @@ and in general in situations which don't have an existing JavaScript function on
the stack. The `Call` method is used when there is already a JavaScript function
on the stack (for example when running a native method called from JavaScript).

## Type definitions

### Napi::Function::VoidCallback

This is the type describing a callback returning `void` that will be invoked
from JavaScript.

```cpp
typedef void (*VoidCallback)(const Napi::CallbackInfo& info);
```
### Napi::Function::Callback
This is the type describing a callback returning a value that will be invoked
from JavaScript.
```cpp
typedef Value (*Callback)(const Napi::CallbackInfo& info);
```

## Methods

### Constructor
Expand Down Expand Up @@ -74,6 +95,86 @@ Returns a non-empty `Napi::Function` instance.
Creates an instance of a `Napi::Function` object.
```cpp
template <Napi::VoidCallback cb>
static Napi::Function New(napi_env env,
const char* utf8name = nullptr,
void* data = nullptr);
```

- `[template] cb`: The native function to invoke when the JavaScript function is
invoked.
- `[in] env`: The `napi_env` environment in which to construct the `Napi::Function` object.
- `[in] utf8name`: Null-terminated string to be used as the name of the function.
- `[in] data`: User-provided data context. This will be passed back into the
function when invoked later.

Returns an instance of a `Napi::Function` object.

### New

Creates an instance of a `Napi::Function` object.

```cpp
template <Napi::Callback cb>
static Napi::Function New(napi_env env,
const char* utf8name = nullptr,
void* data = nullptr);
```
- `[template] cb`: The native function to invoke when the JavaScript function is
invoked.
- `[in] env`: The `napi_env` environment in which to construct the `Napi::Function` object.
- `[in] utf8name`: Null-terminated string to be used as the name of the function.
- `[in] data`: User-provided data context. This will be passed back into the
function when invoked later.
Returns an instance of a `Napi::Function` object.
### New
Creates an instance of a `Napi::Function` object.
```cpp
template <Napi::VoidCallback cb>
static Napi::Function New(napi_env env,
const std::string& utf8name,
void* data = nullptr);
```

- `[template] cb`: The native function to invoke when the JavaScript function is
invoked.
- `[in] env`: The `napi_env` environment in which to construct the `Napi::Function` object.
- `[in] utf8name`: String to be used as the name of the function.
- `[in] data`: User-provided data context. This will be passed back into the
function when invoked later.

Returns an instance of a `Napi::Function` object.

### New

Creates an instance of a `Napi::Function` object.

```cpp
template <Napi::Callback cb>
static Napi::Function New(napi_env env,
const std::string& utf8name,
void* data = nullptr);
```
- `[template] cb`: The native function to invoke when the JavaScript function is
invoked.
- `[in] env`: The `napi_env` environment in which to construct the `Napi::Function` object.
- `[in] utf8name`: String to be used as the name of the function.
- `[in] data`: User-provided data context. This will be passed back into the
function when invoked later.
Returns an instance of a `Napi::Function` object.
### New
Creates an instance of a `Napi::Function` object.
```cpp
template <typename Callable>
static Napi::Function Napi::Function::New(napi_env env, Callable cb, const char* utf8name = nullptr, void* data = nullptr);
Expand Down
45 changes: 45 additions & 0 deletions napi-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -1766,6 +1766,51 @@ CreateFunction(napi_env env,
return status;
}

template <Function::VoidCallback cb>
inline Function Function::New(napi_env env, const char* utf8name, void* data) {
napi_value result = nullptr;
napi_status status = napi_create_function(
env, utf8name, NAPI_AUTO_LENGTH,
[](napi_env env, napi_callback_info info) {
CallbackInfo callbackInfo(env, info);
return details::WrapCallback([&] {
cb(callbackInfo);
return nullptr;
});
}, data, &result);
NAPI_THROW_IF_FAILED(env, status, Function());
return Function(env, result);
}

template <Function::Callback cb>
inline Function Function::New(napi_env env, const char* utf8name, void* data) {
napi_value result = nullptr;
napi_status status = napi_create_function(
env, utf8name, NAPI_AUTO_LENGTH,
[](napi_env env, napi_callback_info info) {
CallbackInfo callbackInfo(env, info);
return details::WrapCallback([&] {
return cb(callbackInfo);
});
}, data, &result);
NAPI_THROW_IF_FAILED(env, status, Function());
return Function(env, result);
}

template <Function::VoidCallback cb>
inline Function Function::New(napi_env env,
const std::string& utf8name,
void* data) {
return Function::New<cb>(env, utf8name.c_str(), data);
}

template <Function::Callback cb>
inline Function Function::New(napi_env env,
const std::string& utf8name,
void* data) {
return Function::New<cb>(env, utf8name.c_str(), data);
}

template <typename Callable>
inline Function Function::New(napi_env env,
Callable cb,
Expand Down
23 changes: 23 additions & 0 deletions napi.h
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,29 @@ namespace Napi {

class Function : public Object {
public:
typedef void (*VoidCallback)(const CallbackInfo& info);
typedef Value (*Callback)(const CallbackInfo& info);

template <VoidCallback cb>
static Function New(napi_env env,
const char* utf8name = nullptr,
void* data = nullptr);

template <Callback cb>
static Function New(napi_env env,
const char* utf8name = nullptr,
void* data = nullptr);

template <VoidCallback cb>
static Function New(napi_env env,
const std::string& utf8name,
void* data = nullptr);

template <Callback cb>
static Function New(napi_env env,
const std::string& utf8name,
void* data = nullptr);

/// Callable must implement operator() accepting a const CallbackInfo&
/// and return either void or Value.
template <typename Callable>
Expand Down
27 changes: 26 additions & 1 deletion test/function.cc
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ void IsConstructCall(const CallbackInfo& info) {
} // end anonymous namespace

Object InitFunction(Env env) {
Object result = Object::New(env);
Object exports = Object::New(env);
exports["voidCallback"] = Function::New(env, VoidCallback, "voidCallback");
exports["valueCallback"] = Function::New(env, ValueCallback, std::string("valueCallback"));
Expand All @@ -120,5 +121,29 @@ Object InitFunction(Env env) {
exports["callConstructorWithArgs"] = Function::New(env, CallConstructorWithArgs);
exports["callConstructorWithVector"] = Function::New(env, CallConstructorWithVector);
exports["isConstructCall"] = Function::New(env, IsConstructCall);
return exports;
result["plain"] = exports;

exports = Object::New(env);
exports["voidCallback"] = Function::New<VoidCallback>(env, "voidCallback");
exports["valueCallback"] =
Function::New<ValueCallback>(env, std::string("valueCallback"));
exports["voidCallbackWithData"] =
Function::New<VoidCallbackWithData>(env, nullptr, &testData);
exports["valueCallbackWithData"] =
Function::New<ValueCallbackWithData>(env, nullptr, &testData);
exports["callWithArgs"] = Function::New<CallWithArgs>(env);
exports["callWithVector"] = Function::New<CallWithVector>(env);
exports["callWithReceiverAndArgs"] =
Function::New<CallWithReceiverAndArgs>(env);
exports["callWithReceiverAndVector"] =
Function::New<CallWithReceiverAndVector>(env);
exports["callWithInvalidReceiver"] =
Function::New<CallWithInvalidReceiver>(env);
exports["callConstructorWithArgs"] =
Function::New<CallConstructorWithArgs>(env);
exports["callConstructorWithVector"] =
Function::New<CallConstructorWithVector>(env);
exports["isConstructCall"] = Function::New<IsConstructCall>(env);
result["templated"] = exports;
return result;
}
36 changes: 19 additions & 17 deletions test/function.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@
const buildType = process.config.target_defaults.default_configuration;
const assert = require('assert');

test(require(`./build/${buildType}/binding.node`));
test(require(`./build/${buildType}/binding_noexcept.node`));
test(require(`./build/${buildType}/binding.node`).function.plain);
test(require(`./build/${buildType}/binding_noexcept.node`).function.plain);
test(require(`./build/${buildType}/binding.node`).function.templated);
test(require(`./build/${buildType}/binding_noexcept.node`).function.templated);

function test(binding) {
let obj = {};
assert.deepStrictEqual(binding.function.voidCallback(obj), undefined);
assert.deepStrictEqual(binding.voidCallback(obj), undefined);
assert.deepStrictEqual(obj, { "foo": "bar" });

assert.deepStrictEqual(binding.function.valueCallback(), { "foo": "bar" });
assert.deepStrictEqual(binding.valueCallback(), { "foo": "bar" });

let args = null;
let ret = null;
Expand All @@ -25,50 +27,50 @@ function test(binding) {
}

ret = 4;
assert.equal(binding.function.callWithArgs(testFunction, 1, 2, 3), 4);
assert.equal(binding.callWithArgs(testFunction, 1, 2, 3), 4);
assert.strictEqual(receiver, undefined);
assert.deepStrictEqual(args, [ 1, 2, 3 ]);

ret = 5;
assert.equal(binding.function.callWithVector(testFunction, 2, 3, 4), 5);
assert.equal(binding.callWithVector(testFunction, 2, 3, 4), 5);
assert.strictEqual(receiver, undefined);
assert.deepStrictEqual(args, [ 2, 3, 4 ]);

ret = 6;
assert.equal(binding.function.callWithReceiverAndArgs(testFunction, obj, 3, 4, 5), 6);
assert.equal(binding.callWithReceiverAndArgs(testFunction, obj, 3, 4, 5), 6);
assert.deepStrictEqual(receiver, obj);
assert.deepStrictEqual(args, [ 3, 4, 5 ]);

ret = 7;
assert.equal(binding.function.callWithReceiverAndVector(testFunction, obj, 4, 5, 6), 7);
assert.equal(binding.callWithReceiverAndVector(testFunction, obj, 4, 5, 6), 7);
assert.deepStrictEqual(receiver, obj);
assert.deepStrictEqual(args, [ 4, 5, 6 ]);

assert.throws(() => {
binding.function.callWithInvalidReceiver();
binding.callWithInvalidReceiver();
}, /Invalid (pointer passed as )?argument/);

obj = binding.function.callConstructorWithArgs(testConstructor, 5, 6, 7);
obj = binding.callConstructorWithArgs(testConstructor, 5, 6, 7);
assert(obj instanceof testConstructor);
assert.deepStrictEqual(args, [ 5, 6, 7 ]);

obj = binding.function.callConstructorWithVector(testConstructor, 6, 7, 8);
obj = binding.callConstructorWithVector(testConstructor, 6, 7, 8);
assert(obj instanceof testConstructor);
assert.deepStrictEqual(args, [ 6, 7, 8 ]);

obj = {};
assert.deepStrictEqual(binding.function.voidCallbackWithData(obj), undefined);
assert.deepStrictEqual(binding.voidCallbackWithData(obj), undefined);
assert.deepStrictEqual(obj, { "foo": "bar", "data": 1 });

assert.deepStrictEqual(binding.function.valueCallbackWithData(), { "foo": "bar", "data": 1 });
assert.deepStrictEqual(binding.valueCallbackWithData(), { "foo": "bar", "data": 1 });

assert.equal(binding.function.voidCallback.name, 'voidCallback');
assert.equal(binding.function.valueCallback.name, 'valueCallback');
assert.equal(binding.voidCallback.name, 'voidCallback');
assert.equal(binding.valueCallback.name, 'valueCallback');

let testConstructCall = undefined;
binding.function.isConstructCall((result) => { testConstructCall = result; });
binding.isConstructCall((result) => { testConstructCall = result; });
assert.ok(!testConstructCall);
new binding.function.isConstructCall((result) => { testConstructCall = result; });
new binding.isConstructCall((result) => { testConstructCall = result; });
assert.ok(testConstructCall);

// TODO: Function::MakeCallback tests
Expand Down

0 comments on commit d7779b8

Please sign in to comment.