Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NonConstructor to allow customization of ObjectWrap behavior #1125

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);