Skip to content

Commit 0f57746

Browse files
authored
Use JS Proxy to simulate V8 ObjectTemplate behavior (#202)
* Use Proxy to simulate V8 ObjectTemplate behavior * Fix build issues for std::strncpy * Attempt to fix build issue * Attempt to fix build issues * Fixed spelling
1 parent 0896a44 commit 0f57746

12 files changed

+1615
-0
lines changed

object-template-demo/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The goal of this demo is to show how to implement V8 ObjectTemplate-like handlers.
2+
NAN uses the ObjectTemplate directly: the code is adapted from NAN namedinterceptors unit test.
3+
Node-API cannot use the ObjectTemplate: it uses the JavaScript Proxy object to get similar behavior.

object-template-demo/nan/binding.gyp

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"targets": [
3+
{
4+
"target_name": "object-template-demo",
5+
"sources": [ "object-template-demo.cc" ],
6+
"include_dirs": [
7+
"<!(node -e \"require('nan')\")"
8+
]
9+
}
10+
]
11+
}

object-template-demo/nan/index.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const addon = require('bindings')('object-template-demo');
2+
3+
const interceptor = addon.create();
4+
console.log(interceptor.prop); // 'foo'
5+
interceptor.prop = 'setting a value';
6+
console.log(interceptor.prop); // 'setting a value'
7+
delete interceptor.something;
8+
console.log(interceptor.prop); // 'goober';
9+
console.log(Object.prototype.hasOwnProperty.call(interceptor, "thing")); // true
10+
console.log(Object.keys(interceptor)[0]); // 'value'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*********************************************************************
2+
* NAN - Native Abstractions for Node.js
3+
*
4+
* Copyright (c) 2018 NAN contributors
5+
*
6+
* MIT License <https://github.com/nodejs/nan/blob/master/LICENSE.md>
7+
********************************************************************/
8+
9+
#include <nan.h>
10+
#include <cstring>
11+
12+
using namespace Nan; // NOLINT(build/namespaces)
13+
14+
class NamedInterceptor : public ObjectWrap {
15+
char buf[256];
16+
17+
public:
18+
NamedInterceptor() { std::strncpy(this->buf, "foo", sizeof (this->buf)); }
19+
static NAN_MODULE_INIT(Init);
20+
static v8::Local<v8::Value> NewInstance ();
21+
static NAN_METHOD(New);
22+
23+
static NAN_PROPERTY_GETTER(PropertyGetter);
24+
static NAN_PROPERTY_SETTER(PropertySetter);
25+
static NAN_PROPERTY_ENUMERATOR(PropertyEnumerator);
26+
static NAN_PROPERTY_DELETER(PropertyDeleter);
27+
static NAN_PROPERTY_QUERY(PropertyQuery);
28+
};
29+
30+
static Persistent<v8::FunctionTemplate> namedinterceptors_constructor;
31+
32+
NAN_METHOD(CreateNew) {
33+
info.GetReturnValue().Set(NamedInterceptor::NewInstance());
34+
}
35+
36+
NAN_MODULE_INIT(NamedInterceptor::Init) {
37+
v8::Local<v8::FunctionTemplate> tpl =
38+
Nan::New<v8::FunctionTemplate>(NamedInterceptor::New);
39+
namedinterceptors_constructor.Reset(tpl);
40+
tpl->SetClassName(Nan::New("NamedInterceptor").ToLocalChecked());
41+
tpl->InstanceTemplate()->SetInternalFieldCount(1);
42+
v8::Local<v8::ObjectTemplate> inst = tpl->InstanceTemplate();
43+
44+
SetNamedPropertyHandler(
45+
inst
46+
, NamedInterceptor::PropertyGetter
47+
, NamedInterceptor::PropertySetter
48+
, NamedInterceptor::PropertyQuery
49+
, NamedInterceptor::PropertyDeleter
50+
, NamedInterceptor::PropertyEnumerator);
51+
52+
v8::Local<v8::Function> createnew =
53+
Nan::GetFunction(Nan::New<v8::FunctionTemplate>(CreateNew))
54+
.ToLocalChecked();
55+
Set(target, Nan::New("create").ToLocalChecked(), createnew);
56+
}
57+
58+
v8::Local<v8::Value> NamedInterceptor::NewInstance () {
59+
EscapableHandleScope scope;
60+
v8::Local<v8::FunctionTemplate> constructorHandle =
61+
Nan::New(namedinterceptors_constructor);
62+
v8::Local<v8::Object> instance =
63+
Nan::NewInstance(GetFunction(constructorHandle).ToLocalChecked())
64+
.ToLocalChecked();
65+
return scope.Escape(instance);
66+
}
67+
68+
NAN_METHOD(NamedInterceptor::New) {
69+
NamedInterceptor* interceptor = new NamedInterceptor();
70+
interceptor->Wrap(info.This());
71+
info.GetReturnValue().Set(info.This());
72+
}
73+
74+
75+
NAN_PROPERTY_GETTER(NamedInterceptor::PropertyGetter) {
76+
NamedInterceptor* interceptor =
77+
ObjectWrap::Unwrap<NamedInterceptor>(info.Holder());
78+
if (!std::strcmp(*Nan::Utf8String(property), "prop")) {
79+
info.GetReturnValue().Set(Nan::New(interceptor->buf).ToLocalChecked());
80+
} else {
81+
info.GetReturnValue().Set(Nan::New("bar").ToLocalChecked());
82+
}
83+
}
84+
85+
NAN_PROPERTY_SETTER(NamedInterceptor::PropertySetter) {
86+
NamedInterceptor* interceptor =
87+
ObjectWrap::Unwrap<NamedInterceptor>(info.Holder());
88+
if (!std::strcmp(*Nan::Utf8String(property), "prop")) {
89+
std::strncpy(
90+
interceptor->buf
91+
, *Nan::Utf8String(value)
92+
, sizeof (interceptor->buf));
93+
info.GetReturnValue().Set(info.This());
94+
} else {
95+
info.GetReturnValue().Set(info.This());
96+
}
97+
}
98+
99+
NAN_PROPERTY_ENUMERATOR(NamedInterceptor::PropertyEnumerator) {
100+
v8::Local<v8::Array> arr = Nan::New<v8::Array>();
101+
Set(arr, 0, Nan::New("value").ToLocalChecked());
102+
info.GetReturnValue().Set(arr);
103+
}
104+
105+
NAN_PROPERTY_DELETER(NamedInterceptor::PropertyDeleter) {
106+
NamedInterceptor* interceptor =
107+
ObjectWrap::Unwrap<NamedInterceptor>(info.Holder());
108+
std::strncpy(interceptor->buf, "goober", sizeof (interceptor->buf));
109+
info.GetReturnValue().Set(True());
110+
}
111+
112+
NAN_PROPERTY_QUERY(NamedInterceptor::PropertyQuery) {
113+
Nan::Utf8String s(property);
114+
if (!std::strcmp(*s, "thing")) {
115+
return info.GetReturnValue().Set(Nan::New<v8::Integer>(v8::DontEnum));
116+
}
117+
if (!std::strcmp(*s, "value")) {
118+
return info.GetReturnValue().Set(Nan::New(0));
119+
}
120+
}
121+
122+
NODE_MODULE(namedinterceptors, NamedInterceptor::Init)

object-template-demo/nan/package.json

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "object-template-demo",
3+
"version": "0.0.0",
4+
"description": "Intercept named property access using V8 ObjectTemplate",
5+
"main": "index.js",
6+
"private": true,
7+
"gypfile": true,
8+
"scripts": {
9+
"test": "node index.js"
10+
},
11+
"dependencies": {
12+
"bindings": "~1.5.0",
13+
"nan": "^2.14.0"
14+
}
15+
}

object-template-demo/napi/binding.gyp

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"targets": [
3+
{
4+
"target_name": "object_template_demo",
5+
"sources": [ "object-template-demo.cc", "proxy-template.cc" ]
6+
}
7+
]
8+
}

object-template-demo/napi/index.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const addon = require('bindings')('object_template_demo');
2+
3+
const interceptor = addon.create();
4+
console.log(interceptor.prop); // 'foo'
5+
interceptor.prop = 'setting a value';
6+
console.log(interceptor.prop); // 'setting a value'
7+
delete interceptor.something;
8+
console.log(interceptor.prop); // 'goober';
9+
console.log(Object.prototype.hasOwnProperty.call(interceptor, "thing")); // true
10+
console.log(Object.keys(interceptor)[0]); // 'value'
+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
#include <node_api.h>
2+
#include <initializer_list>
3+
#include <utility>
4+
5+
// Empty value so that macros here are able to return NULL or void
6+
#define NODE_API_RETVAL_NOTHING // Intentionally blank #define
7+
8+
#define GET_AND_THROW_LAST_ERROR(env) \
9+
do { \
10+
const napi_extended_error_info* error_info; \
11+
napi_get_last_error_info((env), &error_info); \
12+
bool is_pending; \
13+
const char* err_message = error_info->error_message; \
14+
napi_is_exception_pending((env), &is_pending); \
15+
/* If an exception is already pending, don't rethrow it */ \
16+
if (!is_pending) { \
17+
const char* error_message = \
18+
err_message != NULL ? err_message : "empty error message"; \
19+
napi_throw_error((env), NULL, error_message); \
20+
} \
21+
} while (0)
22+
23+
#define NODE_API_ASSERT_BASE(env, assertion, message, ret_val) \
24+
do { \
25+
if (!(assertion)) { \
26+
napi_throw_error( \
27+
(env), NULL, "assertion (" #assertion ") failed: " message); \
28+
return ret_val; \
29+
} \
30+
} while (0)
31+
32+
// Returns NULL on failed assertion.
33+
// This is meant to be used inside napi_callback methods.
34+
#define NODE_API_ASSERT(env, assertion, message) \
35+
NODE_API_ASSERT_BASE(env, assertion, message, NULL)
36+
37+
#define NODE_API_CALL_BASE(env, the_call, ret_val) \
38+
do { \
39+
if ((the_call) != napi_ok) { \
40+
GET_AND_THROW_LAST_ERROR((env)); \
41+
return ret_val; \
42+
} \
43+
} while (0)
44+
45+
// Returns NULL if the_call doesn't return napi_ok.
46+
#define NODE_API_CALL(env, the_call) NODE_API_CALL_BASE(env, the_call, NULL)
47+
48+
// Returns empty if the_call doesn't return napi_ok.
49+
#define NODE_API_CALL_RETURN_VOID(env, the_call) \
50+
NODE_API_CALL_BASE(env, the_call, NODE_API_RETVAL_NOTHING)
51+
52+
#define CHECK_NAPI(...) \
53+
do { \
54+
napi_status res__ = (__VA_ARGS__); \
55+
if (res__ != napi_ok) { \
56+
return res__; \
57+
} \
58+
} while (0)
59+
60+
#define NAPI_CALL(expr) NODE_API_CALL(env, expr);
61+
62+
#ifdef __cpp_lib_span
63+
#include <span>
64+
using std::span;
65+
#else
66+
/**
67+
* @brief A span of values that can be used to pass arguments to function.
68+
*
69+
* For C++20 we should consider to replace it with std::span.
70+
*/
71+
template <typename T>
72+
struct span {
73+
constexpr span(std::initializer_list<T> il) noexcept
74+
: data_{const_cast<T*>(il.begin())}, size_{il.size()} {}
75+
constexpr span(T* data, size_t size) noexcept : data_{data}, size_{size} {}
76+
[[nodiscard]] constexpr T* data() const noexcept { return data_; }
77+
[[nodiscard]] constexpr size_t size() const noexcept { return size_; }
78+
[[nodiscard]] constexpr T* begin() const noexcept { return data_; }
79+
[[nodiscard]] constexpr T* end() const noexcept { return data_ + size_; }
80+
const T& operator[](size_t index) const noexcept { return *(data_ + index); }
81+
82+
private:
83+
T* data_;
84+
size_t size_;
85+
};
86+
#endif // __cpp_lib_span
87+
88+
struct RefHolder {
89+
RefHolder(std::nullptr_t = nullptr) noexcept {}
90+
explicit RefHolder(napi_env env, napi_value value) : env_(env) {
91+
// Start with 2 to avoid ever going to 0 that creates a weak ref.
92+
napi_create_reference(env, value, 2, &ref_);
93+
}
94+
95+
// The class is movable.
96+
RefHolder(RefHolder&& other) noexcept
97+
: env_(std::exchange(other.env_, nullptr)),
98+
ref_(std::exchange(other.ref_, nullptr)) {}
99+
100+
RefHolder& operator=(RefHolder&& other) noexcept {
101+
if (this != &other) {
102+
swap(*this, other);
103+
RefHolder temp(std::move(other));
104+
}
105+
return *this;
106+
}
107+
108+
// The class is not copyable.
109+
RefHolder(const RefHolder& other) = delete;
110+
RefHolder& operator=(const RefHolder& other) = delete;
111+
112+
~RefHolder() noexcept {
113+
if (env_ != nullptr && ref_ != nullptr) {
114+
uint32_t refCount{};
115+
napi_reference_unref(env_, ref_, &refCount);
116+
if (refCount == 1) {
117+
napi_delete_reference(env_, ref_);
118+
}
119+
}
120+
}
121+
122+
operator napi_value() const {
123+
napi_value result{};
124+
if (ref_ != nullptr) {
125+
napi_get_reference_value(env_, ref_, &result);
126+
}
127+
return result;
128+
}
129+
130+
explicit operator bool() const noexcept { return ref_ != nullptr; }
131+
132+
friend void swap(RefHolder& left, RefHolder& right) noexcept {
133+
using std::swap;
134+
swap(left.env_, right.env_);
135+
swap(left.ref_, right.ref_);
136+
}
137+
138+
private:
139+
napi_env env_{};
140+
napi_ref ref_{};
141+
};

0 commit comments

Comments
 (0)