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

Use JS Proxy to simulate V8 ObjectTemplate behavior #202

Merged
merged 5 commits into from
Sep 10, 2022
Merged
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
3 changes: 3 additions & 0 deletions object-template-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The goal of this demo is to show how to implement V8 ObjectTemplate-like handlers.
NAN uses the ObjectTemplate directly: the code is adapted from NAN namedinterceptors unit test.
Node-API cannot use the ObjectTemplate: it uses the JavaScript Proxy object to get similar behavior.
11 changes: 11 additions & 0 deletions object-template-demo/nan/binding.gyp
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"targets": [
{
"target_name": "object-template-demo",
"sources": [ "object-template-demo.cc" ],
"include_dirs": [
"<!(node -e \"require('nan')\")"
]
}
]
}
10 changes: 10 additions & 0 deletions object-template-demo/nan/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const addon = require('bindings')('object-template-demo');

const interceptor = addon.create();
console.log(interceptor.prop); // 'foo'
interceptor.prop = 'setting a value';
console.log(interceptor.prop); // 'setting a value'
delete interceptor.something;
console.log(interceptor.prop); // 'goober';
console.log(Object.prototype.hasOwnProperty.call(interceptor, "thing")); // true
console.log(Object.keys(interceptor)[0]); // 'value'
122 changes: 122 additions & 0 deletions object-template-demo/nan/object-template-demo.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*********************************************************************
* NAN - Native Abstractions for Node.js
*
* Copyright (c) 2018 NAN contributors
*
* MIT License <https://github.com/nodejs/nan/blob/master/LICENSE.md>
********************************************************************/

#include <nan.h>
#include <cstring>

using namespace Nan; // NOLINT(build/namespaces)

class NamedInterceptor : public ObjectWrap {
char buf[256];

public:
NamedInterceptor() { std::strncpy(this->buf, "foo", sizeof (this->buf)); }
static NAN_MODULE_INIT(Init);
static v8::Local<v8::Value> NewInstance ();
static NAN_METHOD(New);

static NAN_PROPERTY_GETTER(PropertyGetter);
static NAN_PROPERTY_SETTER(PropertySetter);
static NAN_PROPERTY_ENUMERATOR(PropertyEnumerator);
static NAN_PROPERTY_DELETER(PropertyDeleter);
static NAN_PROPERTY_QUERY(PropertyQuery);
};

static Persistent<v8::FunctionTemplate> namedinterceptors_constructor;

NAN_METHOD(CreateNew) {
info.GetReturnValue().Set(NamedInterceptor::NewInstance());
}

NAN_MODULE_INIT(NamedInterceptor::Init) {
v8::Local<v8::FunctionTemplate> tpl =
Nan::New<v8::FunctionTemplate>(NamedInterceptor::New);
namedinterceptors_constructor.Reset(tpl);
tpl->SetClassName(Nan::New("NamedInterceptor").ToLocalChecked());
tpl->InstanceTemplate()->SetInternalFieldCount(1);
v8::Local<v8::ObjectTemplate> inst = tpl->InstanceTemplate();

SetNamedPropertyHandler(
inst
, NamedInterceptor::PropertyGetter
, NamedInterceptor::PropertySetter
, NamedInterceptor::PropertyQuery
, NamedInterceptor::PropertyDeleter
, NamedInterceptor::PropertyEnumerator);

v8::Local<v8::Function> createnew =
Nan::GetFunction(Nan::New<v8::FunctionTemplate>(CreateNew))
.ToLocalChecked();
Set(target, Nan::New("create").ToLocalChecked(), createnew);
}

v8::Local<v8::Value> NamedInterceptor::NewInstance () {
EscapableHandleScope scope;
v8::Local<v8::FunctionTemplate> constructorHandle =
Nan::New(namedinterceptors_constructor);
v8::Local<v8::Object> instance =
Nan::NewInstance(GetFunction(constructorHandle).ToLocalChecked())
.ToLocalChecked();
return scope.Escape(instance);
}

NAN_METHOD(NamedInterceptor::New) {
NamedInterceptor* interceptor = new NamedInterceptor();
interceptor->Wrap(info.This());
info.GetReturnValue().Set(info.This());
}


NAN_PROPERTY_GETTER(NamedInterceptor::PropertyGetter) {
NamedInterceptor* interceptor =
ObjectWrap::Unwrap<NamedInterceptor>(info.Holder());
if (!std::strcmp(*Nan::Utf8String(property), "prop")) {
info.GetReturnValue().Set(Nan::New(interceptor->buf).ToLocalChecked());
} else {
info.GetReturnValue().Set(Nan::New("bar").ToLocalChecked());
}
}

NAN_PROPERTY_SETTER(NamedInterceptor::PropertySetter) {
NamedInterceptor* interceptor =
ObjectWrap::Unwrap<NamedInterceptor>(info.Holder());
if (!std::strcmp(*Nan::Utf8String(property), "prop")) {
std::strncpy(
interceptor->buf
, *Nan::Utf8String(value)
, sizeof (interceptor->buf));
info.GetReturnValue().Set(info.This());
} else {
info.GetReturnValue().Set(info.This());
}
}

NAN_PROPERTY_ENUMERATOR(NamedInterceptor::PropertyEnumerator) {
v8::Local<v8::Array> arr = Nan::New<v8::Array>();
Set(arr, 0, Nan::New("value").ToLocalChecked());
info.GetReturnValue().Set(arr);
}

NAN_PROPERTY_DELETER(NamedInterceptor::PropertyDeleter) {
NamedInterceptor* interceptor =
ObjectWrap::Unwrap<NamedInterceptor>(info.Holder());
std::strncpy(interceptor->buf, "goober", sizeof (interceptor->buf));
info.GetReturnValue().Set(True());
}

NAN_PROPERTY_QUERY(NamedInterceptor::PropertyQuery) {
Nan::Utf8String s(property);
if (!std::strcmp(*s, "thing")) {
return info.GetReturnValue().Set(Nan::New<v8::Integer>(v8::DontEnum));
}
if (!std::strcmp(*s, "value")) {
return info.GetReturnValue().Set(Nan::New(0));
}
}

NODE_MODULE(namedinterceptors, NamedInterceptor::Init)
15 changes: 15 additions & 0 deletions object-template-demo/nan/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "object-template-demo",
"version": "0.0.0",
"description": "Intercept named property access using V8 ObjectTemplate",
"main": "index.js",
"private": true,
"gypfile": true,
"scripts": {
"test": "node index.js"
},
"dependencies": {
"bindings": "~1.5.0",
"nan": "^2.14.0"
}
}
8 changes: 8 additions & 0 deletions object-template-demo/napi/binding.gyp
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"targets": [
{
"target_name": "object_template_demo",
"sources": [ "object-template-demo.cc", "proxy-template.cc" ]
}
]
}
10 changes: 10 additions & 0 deletions object-template-demo/napi/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const addon = require('bindings')('object_template_demo');

const interceptor = addon.create();
console.log(interceptor.prop); // 'foo'
interceptor.prop = 'setting a value';
console.log(interceptor.prop); // 'setting a value'
delete interceptor.something;
console.log(interceptor.prop); // 'goober';
console.log(Object.prototype.hasOwnProperty.call(interceptor, "thing")); // true
console.log(Object.keys(interceptor)[0]); // 'value'
141 changes: 141 additions & 0 deletions object-template-demo/napi/node-api-common.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#include <node_api.h>
#include <initializer_list>
#include <utility>

// Empty value so that macros here are able to return NULL or void
#define NODE_API_RETVAL_NOTHING // Intentionally blank #define

#define GET_AND_THROW_LAST_ERROR(env) \
do { \
const napi_extended_error_info* error_info; \
napi_get_last_error_info((env), &error_info); \
bool is_pending; \
const char* err_message = error_info->error_message; \
napi_is_exception_pending((env), &is_pending); \
/* If an exception is already pending, don't rethrow it */ \
if (!is_pending) { \
const char* error_message = \
err_message != NULL ? err_message : "empty error message"; \
napi_throw_error((env), NULL, error_message); \
} \
} while (0)

#define NODE_API_ASSERT_BASE(env, assertion, message, ret_val) \
do { \
if (!(assertion)) { \
napi_throw_error( \
(env), NULL, "assertion (" #assertion ") failed: " message); \
return ret_val; \
} \
} while (0)

// Returns NULL on failed assertion.
// This is meant to be used inside napi_callback methods.
#define NODE_API_ASSERT(env, assertion, message) \
NODE_API_ASSERT_BASE(env, assertion, message, NULL)

#define NODE_API_CALL_BASE(env, the_call, ret_val) \
do { \
if ((the_call) != napi_ok) { \
GET_AND_THROW_LAST_ERROR((env)); \
return ret_val; \
} \
} while (0)

// Returns NULL if the_call doesn't return napi_ok.
#define NODE_API_CALL(env, the_call) NODE_API_CALL_BASE(env, the_call, NULL)

// Returns empty if the_call doesn't return napi_ok.
#define NODE_API_CALL_RETURN_VOID(env, the_call) \
NODE_API_CALL_BASE(env, the_call, NODE_API_RETVAL_NOTHING)

#define CHECK_NAPI(...) \
do { \
napi_status res__ = (__VA_ARGS__); \
if (res__ != napi_ok) { \
return res__; \
} \
} while (0)

#define NAPI_CALL(expr) NODE_API_CALL(env, expr);

#ifdef __cpp_lib_span
#include <span>
using std::span;
#else
/**
* @brief A span of values that can be used to pass arguments to function.
*
* For C++20 we should consider to replace it with std::span.
*/
template <typename T>
struct span {
constexpr span(std::initializer_list<T> il) noexcept
: data_{const_cast<T*>(il.begin())}, size_{il.size()} {}
constexpr span(T* data, size_t size) noexcept : data_{data}, size_{size} {}
[[nodiscard]] constexpr T* data() const noexcept { return data_; }
[[nodiscard]] constexpr size_t size() const noexcept { return size_; }
[[nodiscard]] constexpr T* begin() const noexcept { return data_; }
[[nodiscard]] constexpr T* end() const noexcept { return data_ + size_; }
const T& operator[](size_t index) const noexcept { return *(data_ + index); }

private:
T* data_;
size_t size_;
};
#endif // __cpp_lib_span

struct RefHolder {
RefHolder(std::nullptr_t = nullptr) noexcept {}
explicit RefHolder(napi_env env, napi_value value) : env_(env) {
// Start with 2 to avoid ever going to 0 that creates a weak ref.
napi_create_reference(env, value, 2, &ref_);
}

// The class is movable.
RefHolder(RefHolder&& other) noexcept
: env_(std::exchange(other.env_, nullptr)),
ref_(std::exchange(other.ref_, nullptr)) {}

RefHolder& operator=(RefHolder&& other) noexcept {
if (this != &other) {
swap(*this, other);
RefHolder temp(std::move(other));
}
return *this;
}

// The class is not copyable.
RefHolder(const RefHolder& other) = delete;
RefHolder& operator=(const RefHolder& other) = delete;

~RefHolder() noexcept {
if (env_ != nullptr && ref_ != nullptr) {
uint32_t refCount{};
napi_reference_unref(env_, ref_, &refCount);
if (refCount == 1) {
napi_delete_reference(env_, ref_);
}
}
}

operator napi_value() const {
napi_value result{};
if (ref_ != nullptr) {
napi_get_reference_value(env_, ref_, &result);
}
return result;
}

explicit operator bool() const noexcept { return ref_ != nullptr; }

friend void swap(RefHolder& left, RefHolder& right) noexcept {
using std::swap;
swap(left.env_, right.env_);
swap(left.ref_, right.ref_);
}

private:
napi_env env_{};
napi_ref ref_{};
};
Loading