Skip to content

Commit

Permalink
src: allow customization of ObjectWrap behavior
Browse files Browse the repository at this point in the history
Add new method to allow customization of ObjectWrap behavior

PR-URL: nodejs/node-addon-api#1125
Reviewed-By: Michael Dawson <midawson@redhat.com
  • Loading branch information
John French committed Feb 17, 2022
1 parent f1597f5 commit 8e9b5ad
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 2 deletions.
23 changes: 23 additions & 0 deletions doc/object_wrap.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,29 @@ property of the `Napi::CallbackInfo`.

Returns a `Napi::Function` representing the constructor function for the class.

### OnCalledAsFunction

Provides an opportunity to customize the behavior when a `Napi::ObjectWrap<T>`
class is called from JavaScript as a function (without the **new** operator).

The default behavior in this scenario is to throw a `Napi::TypeError` with the
message `Class constructors cannot be invoked without 'new'`. Define this
public method on your derived class to override that behavior.

For example, you could internally re-call the JavaScript contstructor _with_
the **new** operator (via
`Napi::Function::New(const std::vector<napi_value> &args)`), and return the
resulting object. Or you might do something else entirely, such as the way
[`Date()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#constructor)
produces a string when called as a function.

```cpp
static Napi::Value OnCalledAsFunction(const Napi::CallbackInfo& callbackInfo);
```
- `[in] callbackInfo`: The object representing the components of the JavaScript
request being made.
### Finalize
Provides an opportunity to run cleanup code that requires access to the
Expand Down
13 changes: 11 additions & 2 deletions napi-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -4451,6 +4451,15 @@ inline ClassPropertyDescriptor<T> ObjectWrap<T>::StaticValue(Symbol name,
return desc;
}

template <typename T>
inline Value ObjectWrap<T>::OnCalledAsFunction(
const Napi::CallbackInfo& callbackInfo) {
NAPI_THROW(
TypeError::New(callbackInfo.Env(),
"Class constructors cannot be invoked without 'new'"),
Napi::Value());
}

template <typename T>
inline void ObjectWrap<T>::Finalize(Napi::Env /*env*/) {}

Expand All @@ -4464,8 +4473,8 @@ inline napi_value ObjectWrap<T>::ConstructorCallbackWrapper(

bool isConstructCall = (new_target != nullptr);
if (!isConstructCall) {
napi_throw_type_error(env, nullptr, "Class constructors cannot be invoked without 'new'");
return nullptr;
return details::WrapCallback(
[&] { return T::OnCalledAsFunction(CallbackInfo(env, info)); });
}

napi_value wrapper = details::WrapCallback([&] {
Expand Down
2 changes: 2 additions & 0 deletions napi.h
Original file line number Diff line number Diff line change
Expand Up @@ -2199,6 +2199,8 @@ namespace Napi {
static PropertyDescriptor StaticValue(Symbol name,
Napi::Value value,
napi_property_attributes attributes = napi_default);
static Napi::Value OnCalledAsFunction(
const Napi::CallbackInfo& callbackInfo);
virtual void Finalize(Napi::Env env);

private:
Expand Down
2 changes: 2 additions & 0 deletions test/binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Object InitTypedArray(Env env);
Object InitGlobalObject(Env env);
Object InitObjectWrap(Env env);
Object InitObjectWrapConstructorException(Env env);
Object InitObjectWrapFunction(Env env);
Object InitObjectWrapRemoveWrap(Env env);
Object InitObjectWrapMultipleInheritance(Env env);
Object InitObjectReference(Env env);
Expand Down Expand Up @@ -152,6 +153,7 @@ Object Init(Env env, Object exports) {
exports.Set("objectwrap", InitObjectWrap(env));
exports.Set("objectwrapConstructorException",
InitObjectWrapConstructorException(env));
exports.Set("objectwrap_function", InitObjectWrapFunction(env));
exports.Set("objectwrap_removewrap", InitObjectWrapRemoveWrap(env));
exports.Set("objectwrap_multiple_inheritance", InitObjectWrapMultipleInheritance(env));
exports.Set("objectreference", InitObjectReference(env));
Expand Down
1 change: 1 addition & 0 deletions test/binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
'typedarray.cc',
'objectwrap.cc',
'objectwrap_constructor_exception.cc',
'objectwrap_function.cc',
'objectwrap_removewrap.cc',
'objectwrap_multiple_inheritance.cc',
'object_reference.cc',
Expand Down
45 changes: 45 additions & 0 deletions test/objectwrap_function.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#include <napi.h>
#include <unordered_map>
#include "test_helper.h"

class FunctionTest : public Napi::ObjectWrap<FunctionTest> {
public:
FunctionTest(const Napi::CallbackInfo& info)
: Napi::ObjectWrap<FunctionTest>(info) {}

static Napi::Value OnCalledAsFunction(const Napi::CallbackInfo& info) {
// If called with a "true" argument, throw an exeption to test the handling.
if (!info[0].IsUndefined() && MaybeUnwrap(info[0].ToBoolean())) {
NAPI_THROW(Napi::Error::New(info.Env(), "an exception"), Napi::Value());
}
// Otherwise, act as a factory.
std::vector<napi_value> args;
for (size_t i = 0; i < info.Length(); i++) args.push_back(info[i]);
return MaybeUnwrap(GetConstructor(info.Env()).New(args));
}

// Constructor-per-env map in a static member because env.SetInstanceData()
// would interfere with Napi::Addon<T>
static std::unordered_map<napi_env, Napi::FunctionReference> constructors;

static void Initialize(Napi::Env env, Napi::Object exports) {
const char* name = "FunctionTest";
Napi::Function func = DefineClass(env, name, {});
constructors[env] = Napi::Persistent(func);
env.AddCleanupHook([env] { constructors.erase(env); });
exports.Set(name, func);
}

static Napi::Function GetConstructor(Napi::Env env) {
return constructors[env].Value();
}
};

std::unordered_map<napi_env, Napi::FunctionReference>
FunctionTest::constructors = {};

Napi::Object InitObjectWrapFunction(Napi::Env env) {
Napi::Object exports = Napi::Object::New(env);
FunctionTest::Initialize(env, exports);
return exports;
}
22 changes: 22 additions & 0 deletions test/objectwrap_function.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

const assert = require('assert');
const testUtil = require('./testUtil');

function test (binding) {
return testUtil.runGCTests([
'objectwrap function',
() => {
const { FunctionTest } = binding.objectwrap_function;
const newConstructed = new FunctionTest();
const functionConstructed = FunctionTest();
assert(newConstructed instanceof FunctionTest);
assert(functionConstructed instanceof FunctionTest);
assert.throws(() => (FunctionTest(true)), /an exception/);
},
// Do on gc before returning.
() => {}
]);
}

module.exports = require('./common').runTest(test);

0 comments on commit 8e9b5ad

Please sign in to comment.