diff --git a/doc/object_wrap.md b/doc/object_wrap.md index 0d3ef98..0b9dacd 100644 --- a/doc/object_wrap.md +++ b/doc/object_wrap.md @@ -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` +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 &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 diff --git a/napi-inl.h b/napi-inl.h index bd54feb..64d4f5a 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -4451,6 +4451,15 @@ inline ClassPropertyDescriptor ObjectWrap::StaticValue(Symbol name, return desc; } +template +inline Value ObjectWrap::OnCalledAsFunction( + const Napi::CallbackInfo& callbackInfo) { + NAPI_THROW( + TypeError::New(callbackInfo.Env(), + "Class constructors cannot be invoked without 'new'"), + Napi::Value()); +} + template inline void ObjectWrap::Finalize(Napi::Env /*env*/) {} @@ -4464,8 +4473,8 @@ inline napi_value ObjectWrap::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([&] { diff --git a/napi.h b/napi.h index 2864306..429e6a6 100644 --- a/napi.h +++ b/napi.h @@ -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: diff --git a/test/binding.cc b/test/binding.cc index 57be515..f7a96e9 100644 --- a/test/binding.cc +++ b/test/binding.cc @@ -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); @@ -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)); diff --git a/test/binding.gyp b/test/binding.gyp index 44f124b..a0470bd 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -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', diff --git a/test/objectwrap_function.cc b/test/objectwrap_function.cc new file mode 100644 index 0000000..be55ff3 --- /dev/null +++ b/test/objectwrap_function.cc @@ -0,0 +1,45 @@ +#include +#include +#include "test_helper.h" + +class FunctionTest : public Napi::ObjectWrap { + public: + FunctionTest(const Napi::CallbackInfo& info) + : Napi::ObjectWrap(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 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 + static std::unordered_map 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 + FunctionTest::constructors = {}; + +Napi::Object InitObjectWrapFunction(Napi::Env env) { + Napi::Object exports = Napi::Object::New(env); + FunctionTest::Initialize(env, exports); + return exports; +} diff --git a/test/objectwrap_function.js b/test/objectwrap_function.js new file mode 100644 index 0000000..7bcf6c0 --- /dev/null +++ b/test/objectwrap_function.js @@ -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);