diff options
Diffstat (limited to 'src/javascript/jsc/bindings/napi.cpp')
-rw-r--r-- | src/javascript/jsc/bindings/napi.cpp | 533 |
1 files changed, 520 insertions, 13 deletions
diff --git a/src/javascript/jsc/bindings/napi.cpp b/src/javascript/jsc/bindings/napi.cpp index 027609ec7..c8a8bb383 100644 --- a/src/javascript/jsc/bindings/napi.cpp +++ b/src/javascript/jsc/bindings/napi.cpp @@ -40,15 +40,184 @@ #include "JavaScriptCore/ArrayBuffer.h" #include "JavaScriptCore/JSArrayBuffer.h" #include "JSFFIFunction.h" +#include "JavaScriptCore/JavaScript.h" +#include "JavaScriptCore/JSWeakValue.h" +#include "napi.h" +#include "JavaScriptCore/GetterSetter.h" #include <iostream> using namespace JSC; +using namespace Zig; + +class NapiRefWeakHandleOwner final : public JSC::WeakHandleOwner { +public: + void finalize(JSC::Handle<JSC::Unknown>, void* context) final + { + auto* weakValue = reinterpret_cast<NapiRef*>(context); + weakValue->clear(); + } +}; + +static NapiRefWeakHandleOwner& weakValueHandleOwner() +{ + static NeverDestroyed<NapiRefWeakHandleOwner> jscWeakValueHandleOwner; + return jscWeakValueHandleOwner; +} + +void NapiFinalizer::call(JSC::JSGlobalObject* globalObject, void* data) +{ + if (finalize_cb) { + finalize_cb(reinterpret_cast<napi_env>(globalObject), finalize_hint, data); + } +} + +void NapiRef::ref() +{ + ++refCount; + if (refCount == 1 && weakValueRef.isSet()) { + auto& vm = globalObject.get()->vm(); + if (weakValueRef.isString()) { + strongRef.set(vm, JSC::JSValue(weakValueRef.string())); + } else if (weakValueRef.isObject()) { + strongRef.set(vm, JSC::JSValue(weakValueRef.object())); + } else { + strongRef.set(vm, weakValueRef.primitive()); + } + + weakValueRef.clear(); + } +} + +void NapiRef::unref() +{ + bool clear = refCount == 1; + refCount = refCount > 0 ? refCount - 1 : 0; + if (clear) { + JSC::JSValue val = strongRef.get(); + if (val.isString()) { + weakValueRef.setString(val.toString(globalObject.get()), weakValueHandleOwner(), this); + } else if (val.isObject()) { + weakValueRef.setObject(val.getObject(), weakValueHandleOwner(), this); + } else { + weakValueRef.setPrimitive(val); + } + strongRef.clear(); + } +} + +void NapiRef::clear() +{ + this->finalizer.call(this->globalObject.get(), nullptr); + this->globalObject.clear(); + this->weakValueRef.clear(); + this->strongRef.clear(); +} // namespace Napi { // class Reference // } +#define StackAllocatedCallFramePointerTag 62 +typedef struct StackAllocatedCallFrame { + void* dataPtr; + JSC::EncodedJSValue thisValue; + // this is "bar" in: + // set foo(bar) + JSC::EncodedJSValue argument1; +} StackAllocatedCallFrame; + +extern "C" Zig::GlobalObject* +Bun__getDefaultGlobal(); + +static uint32_t getPropertyAttributes(napi_property_attributes attributes) +{ + uint32_t result = 0; + if (!(attributes & napi_key_configurable)) { + result |= JSC::PropertyAttribute::DontDelete; + } + + if (!(attributes & napi_key_enumerable)) { + result |= JSC::PropertyAttribute::DontEnum; + } + + if (!(attributes & napi_key_writable)) { + result |= JSC::PropertyAttribute::ReadOnly; + } + + return result; +} + +static uint32_t getPropertyAttributes(napi_property_descriptor prop) +{ + uint32_t result = getPropertyAttributes(prop.attributes); + + if (!(prop.getter && !prop.setter)) { + result |= JSC::PropertyAttribute::ReadOnly; + } + + if (prop.method) { + result |= JSC::PropertyAttribute::Function; + } + + return result; +} + +const ClassInfo NapiClass::s_info = { "Function"_s, &NapiClass::Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NapiClass) }; + +static void defineNapiProperty(Zig::GlobalObject* globalObject, JSC::JSObject* to, napi_property_descriptor property) +{ + JSC::VM& vm = globalObject->vm(); + void* dataPtr = property.data; + auto is_static = !!(property.attributes & napi_static); + WTF::String nameStr; + if (property.utf8name != nullptr) { + nameStr = WTF::String::fromUTF8(property.utf8name); + } else if (property.name) { + nameStr = toJS(property.name).toWTFString(globalObject); + } + + auto propertyName = JSC::PropertyName(JSC::Identifier::fromString(vm, nameStr)); + + if (property.method) { + auto function = Zig::JSFFIFunction::create(vm, globalObject, 1, nameStr, reinterpret_cast<Zig::FFIFunction>(property.method)); + function->dataPtr = dataPtr; + JSC::JSValue value = JSC::JSValue(function); + + to->putDirect(vm, Identifier::fromString(vm, nameStr), value, getPropertyAttributes(property)); + return; + } + + if (property.getter != nullptr || property.setter != nullptr) { + JSC::JSValue getter = {}; + JSC::JSValue setter = {}; + + if (property.getter) { + auto callGetter = property.getter; + auto function = Zig::JSFFIFunction::create(vm, globalObject, 0, nameStr, reinterpret_cast<Zig::FFIFunction>(property.getter)); + function->dataPtr = dataPtr; + getter = JSC::JSValue(function); + } + + if (property.setter) { + auto callGetter = property.setter; + auto function = Zig::JSFFIFunction::create(vm, globalObject, 1, nameStr, reinterpret_cast<Zig::FFIFunction>(property.setter)); + function->dataPtr = dataPtr; + getter = JSC::JSValue(function); + } -extern "C" Zig::GlobalObject* Bun__getDefaultGlobal(); + auto getterSetter = JSC::GetterSetter::create(vm, globalObject, getter, setter); + to->putDirect(vm, propertyName, getterSetter, getPropertyAttributes(property) | JSC::PropertyAttribute::Accessor); + + } else { + // TODO: is dataPtr allowed when given a value? + JSC::JSValue value = JSC::jsUndefined(); + + if (property.value) { + value = toJS(property.value); + } + + to->putDirect(vm, propertyName, value, getPropertyAttributes(property)); + } +} extern "C" void napi_module_register(napi_module* mod) { @@ -91,6 +260,55 @@ extern "C" void napi_module_register(napi_module* mod) promise->result(vm); } +extern "C" napi_status napi_wrap(napi_env env, + napi_value js_object, + void* native_object, + napi_finalize finalize_cb, + void* finalize_hint, + napi_ref* result) +{ + auto* globalObject = toJS(env); + auto* object = toJS(js_object).getObject(); + auto clientData = WebCore::clientData(vm); + + if (native_object) { + uintptr_t ref_ptr = reinterpret_cast<uintptr_t>(native_object); + double ref_double = bitwise_cast<double>(ref_ptr); + val->putDirect(vm, clientData->builtinNames().passwordPrivateName(), JSC::jsNumber(ref_double), JSC::PropertyAttribute::DontEnum | 0); + } + + if (result) { + auto* ref = new NapiRef(globalObject, object); + auto clientData = WebCore::clientData(vm); + if (finalize_cb) { + ref->finalizer = { finalize_cb, finalize_hint }; + } + *result = reinterpret_cast<napi_ref>(ref); + } +} + +extern "C" napi_status napi_unwrap(napi_env env, napi_value js_object, + void** result) +{ + auto* globalObject = toJS(env); + auto* object = toJS(js_object).getObject(); + auto clientData = WebCore::clientData(vm); + + if (native_object) { + uintptr_t ref_ptr = reinterpret_cast<uintptr_t>(native_object); + double ref_double = bitwise_cast<double>(ref_ptr); + val->putDirect(vm, clientData->builtinNames().passwordPrivateName(), JSC::jsNumber(ref_double), JSC::PropertyAttribute::DontEnum | 0); + } + + if (result) { + auto* ref = new NapiRef(globalObject, object); + if (finalize_cb) { + ref->finalizer = { finalize_cb, finalize_hint }; + } + *result = reinterpret_cast<napi_ref>(ref); + } +} + extern "C" napi_status napi_create_function(napi_env env, const char* utf8name, size_t length, napi_callback cb, void* data, napi_value* result) @@ -117,16 +335,19 @@ extern "C" napi_status napi_get_cb_info( { Zig::GlobalObject* globalObject = reinterpret_cast<Zig::GlobalObject*>(env); JSC::VM& vm = globalObject->vm(); - JSC::CallFrame* callFrame = reinterpret_cast<JSC::CallFrame*>(cbinfo); - auto inputArgsCount = argc == nullptr ? 0 : *argc; + JSC::CallFrame* callFrame = reinterpret_cast<JSC::CallFrame*>(cbinfo); + // napi expects arguments to be copied into the argv array. if (inputArgsCount > 0) { auto outputArgsCount = callFrame->argumentCount(); auto argsToCopy = inputArgsCount < outputArgsCount ? inputArgsCount : outputArgsCount; *argc = argsToCopy; memcpy(argv, callFrame->addressOfArgumentsStart(), argsToCopy * sizeof(JSC::JSValue)); + + // If the user didn't provide expected number of args, we need to fill the rest with undefined. + // TODO: can we use memset() here? auto argv_ptr = argv[outputArgsCount]; for (size_t i = outputArgsCount; i < inputArgsCount; i++) { argv[i] = reinterpret_cast<napi_value>(JSC::JSValue::encode(JSC::jsUndefined())); @@ -139,8 +360,32 @@ extern "C" napi_status napi_get_cb_info( } if (data != nullptr) { - Zig::JSFFIFunction* ffiFunction = JSC::jsDynamicCast<Zig::JSFFIFunction*>(vm, JSC::JSValue(callFrame->jsCallee())); - *data = reinterpret_cast<void*>(ffiFunction->dataPtr); + JSC::JSValue callee = JSC::JSValue(callFrame->jsCallee()); + if (Zig::JSFFIFunction* ffiFunction = JSC::jsDynamicCast<Zig::JSFFIFunction*>(callee)) { + *data = reinterpret_cast<void*>(ffiFunction->dataPtr); + } else { + *data = nullptr; + } + } + + return napi_ok; +} + +extern "C" napi_status +napi_define_properties(napi_env env, napi_value object, size_t property_count, + const napi_property_descriptor* properties) +{ + Zig::GlobalObject* globalObject = reinterpret_cast<Zig::GlobalObject*>(env); + JSC::VM& vm = globalObject->vm(); + + JSC::JSValue objectValue = JSC::JSValue(reinterpret_cast<JSC::EncodedJSValue>(object)); + JSC::JSObject* objectObject = objectValue.getObject(); + if (!objectObject) { + return napi_object_expected; + } + + for (size_t i = 0; i < property_count; i++) { + defineNapiProperty(globalObject, objectObject, properties[i]); } return napi_ok; @@ -167,8 +412,73 @@ extern "C" napi_status napi_create_reference(napi_env env, napi_value value, { Zig::GlobalObject* globalObject = reinterpret_cast<Zig::GlobalObject*>(env); - JSC::Strong<JSC::Unknown> data = { globalObject->vm(), JSC::JSValue::decode(reinterpret_cast<JSC::EncodedJSValue>(value)) }; - *reinterpret_cast<JSC::Strong<JSC::Unknown>*>(result) = data; + auto* ref = new NapiRef(toJS(env), initial_refcount); + JSC::JSValue val = toJS(value); + auto clientData = WebCore::clientData(vm); + + if (initial_refcount > 0) { + ref->strongRef.set(globalObject->vm(), val); + } else { + if (val.isString()) { + ref->weakValueRef.setString(val.toString(globalObject), weakValueHandleOwner(), ref); + } else if (val.isObject()) { + ref->weakValueRef.setObject(val.getObject(), weakValueHandleOwner(), ref); + } else { + ref->weakValueRef.setPrimitive(val); + } + } + + uintptr_t ref_ptr = reinterpret_cast<uintptr_t>(ref); + double ref_double = bitwise_cast<double>(ref_ptr); + val->putDirect(vm, clientData->builtinNames().passwordPrivateName(), JSC::jsNumber(ref_double), JSC::PropertyAttribute::DontEnum | 0); + + *result = toNapi(ref); + + return napi_ok; +} + +extern "C" napi_status napi_reference_unref(napi_env env, napi_ref ref, + uint32_t* result) +{ + NapiRef* napiRef = toJS(ref); + napiRef->unref(); + *result = napiRef->refCount; + return napi_ok; +} + +// Attempts to get a referenced value. If the reference is weak, +// the value might no longer be available, in that case the call +// is still successful but the result is NULL. +extern "C" napi_status napi_get_reference_value(napi_env env, napi_ref ref, + napi_value* result) +{ + NapiRef* napiRef = toJS(ref); + *result = toNapi(napiRef->value()); + return napi_ok; +} + +extern "C" napi_status napi_reference_ref(napi_env env, napi_ref ref, + uint32_t* result) +{ + NapiRef* napiRef = toJS(ref); + napiRef->ref(); + *result = napiRef->refCount; + return napi_ok; +} + +extern "C" napi_status napi_reference_delete(napi_env env, napi_ref ref, + uint32_t* result) +{ + NapiRef* napiRef = toJS(ref); + napiRef->ref(); + *result = napiRef->refCount(); + return napi_ok; +} + +extern "C" napi_status napi_delete_reference(napi_env env, napi_ref ref) +{ + NapiRef* napiRef = toJS(ref); + ~napiRef(); return napi_ok; } @@ -185,7 +495,7 @@ extern "C" napi_status napi_is_detached_arraybuffer(napi_env env, return napi_arraybuffer_expected; } - JSC::JSArrayBuffer* jsArrayBuffer = JSC::jsDynamicCast<JSC::JSArrayBuffer*>(vm, value); + JSC::JSArrayBuffer* jsArrayBuffer = JSC::jsDynamicCast<JSC::JSArrayBuffer*>(value); if (!jsArrayBuffer) { return napi_arraybuffer_expected; } @@ -208,7 +518,7 @@ extern "C" napi_status napi_detach_arraybuffer(napi_env env, return napi_arraybuffer_expected; } - JSC::JSArrayBuffer* jsArrayBuffer = JSC::jsDynamicCast<JSC::JSArrayBuffer*>(vm, value); + JSC::JSArrayBuffer* jsArrayBuffer = JSC::jsDynamicCast<JSC::JSArrayBuffer*>(value); if (!jsArrayBuffer) { return napi_arraybuffer_expected; } @@ -248,6 +558,25 @@ extern "C" napi_status napi_throw_type_error(napi_env env, const char* code, JSC::throwException(globalObject, throwScope, error); return napi_ok; } + +extern "C" napi_status napi_create_type_error(napi_env env, napi_value code, + napi_value msg, + napi_value* result) +{ + Zig::GlobalObject* globalObject = reinterpret_cast<Zig::GlobalObject*>(env); + JSC::VM& vm = globalObject->vm(); + + JSC::JSValue codeValue = JSC::JSValue::decode(reinterpret_cast<JSC::EncodedJSValue>(code)); + JSC::JSValue messageValue = JSC::JSValue::decode(reinterpret_cast<JSC::EncodedJSValue>(msg)); + + auto error = JSC::createTypeError(globalObject, messageValue.toWTFString(globalObject)); + if (codeValue) { + error->putDirect(vm, Identifier::fromString(vm, "code"_s), codeValue, 0); + } + + *result = reinterpret_cast<napi_value>(JSC::JSValue::encode(error)); + return napi_ok; +} extern "C" napi_status napi_throw_range_error(napi_env env, const char* code, const char* msg) { @@ -290,7 +619,8 @@ extern "C" napi_status napi_object_seal(napi_env env, napi_value object_value) JSC::EncodedJSValue encodedValue = reinterpret_cast<JSC::EncodedJSValue>(object_value); JSC::JSValue value = JSC::JSValue::decode(encodedValue); - if (!value.isObject()) { + + if (UNLIKELY(!value.isObject())) { return napi_object_expected; } @@ -334,6 +664,20 @@ extern "C" napi_status napi_get_new_target(napi_env env, { Zig::GlobalObject* globalObject = reinterpret_cast<Zig::GlobalObject*>(env); JSC::VM& vm = globalObject->vm(); + // handle: + // - if they call this function when it was originally a getter/setter call + // - if they call this function without a result + if (UNLIKELY(result == nullptr || cbinfo == nullptr)) { + return napi_invalid_arg; + } + + if (reinterpret_cast<size_t>(cbinfo) & (static_cast<size_t>(1) << StackAllocatedCallFramePointerTag)) { + // This is a stack allocated call frame, so we can't get the new target. + // We'll just return undefined. + // TODO: verify this is what napi does too + *result = reinterpret_cast<napi_value>(JSC::JSValue::encode(JSC::jsUndefined())); + return napi_ok; + } CallFrame* callFrame = reinterpret_cast<JSC::CallFrame*>(cbinfo); JSC::JSValue newTarget = callFrame->newTarget(); @@ -351,11 +695,174 @@ extern "C" napi_status napi_create_dataview(napi_env env, size_t length, auto throwScope = DECLARE_THROW_SCOPE(vm); JSC::EncodedJSValue encodedArraybuffer = reinterpret_cast<JSC::EncodedJSValue>(arraybuffer); - auto arraybufferValue = JSC::jsDynamicCast<JSC::JSArrayBuffer*>(vm, JSC::JSValue::decode(encodedArraybuffer)); + auto arraybufferValue = JSC::jsDynamicCast<JSC::JSArrayBuffer*>(JSC::JSValue::decode(encodedArraybuffer)); if (!arraybufferValue) { - return napi_invalid_arg; + return napi_arraybuffer_expected; } auto dataView = JSC::DataView::create(arraybufferValue->impl(), byte_offset, length); - *result = reinterpret_cast<napi_value>(dataView->wrap(globalObject, globalObject)); + + if (result != nullptr) { + *result = reinterpret_cast<napi_value>(dataView->wrap(globalObject, globalObject)); + } + + return napi_ok; +} + +namespace Zig { + +template<typename Visitor> +void NapiClass::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + NapiClass* thisObject = jsCast<NapiClass*>(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); +} + +DEFINE_VISIT_CHILDREN(NapiClass); + +NapiClass* NapiClass::create(VM& vm, Zig::GlobalObject* globalObject, const char* utf8name, + size_t length, + napi_callback constructor, + void* data, + size_t property_count, + const napi_property_descriptor* properties) +{ + WTF::String name = WTF::String::fromUTF8(utf8name, length); + NativeExecutable* executable = vm.getHostFunction(reinterpret_cast<FFIFunction>(constructor), JSC::NoIntrinsic, callHostFunctionAsConstructor, nullptr, name); + + Structure* structure = globalObject->NapiClassStructure(); + NapiClass* napiClass = new (NotNull, allocateCell<NapiClass>(vm)) NapiClass(vm, executable, globalObject, structure); + napiClass->finishCreation(vm, executable, length, name, constructor, data, property_count, properties); + return napiClass; +} + +CallData NapiClass::getConstructData(JSCell* cell) +{ + auto construct = JSC::jsCast<NapiClass*>(cell)->constructor(); + if (!construct) { + return NapiClass::Base::getConstructData(cell); + } + + CallData constructData; + constructData.type = CallData::Type::Native; + constructData.native.function = construct; + return constructData; +} + +void NapiClass::finishCreation(VM& vm, NativeExecutable* executable, unsigned length, const String& name, napi_callback constructor, + void* data, + size_t property_count, + const napi_property_descriptor* properties) +{ + Base::finishCreation(vm, executable, length, name); + ASSERT(inherits(info())); + this->m_constructor = reinterpret_cast<FFIFunction>(constructor); + auto globalObject = reinterpret_cast<Zig::GlobalObject*>(this->globalObject()); + + // toStringTag + "prototype" + // size_t staticPropertyCount = 2; + // prototype always has "constructor", + size_t prototypePropertyCount = 2; + + this->putDirect(vm, vm.propertyNames->name, jsString(vm, name), JSC::PropertyAttribute::DontEnum | 0); + + auto clientData = WebCore::clientData(vm); + + for (size_t i = 0; i < property_count; i++) { + const napi_property_descriptor& property = properties[i]; + // staticPropertyCount += property.attributes & napi_static ? 1 : 0; + prototypePropertyCount += property.attributes & napi_static ? 0 : 1; + } + + JSC::JSObject* prototype = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), prototypePropertyCount); + + for (size_t i = 0; i < property_count; i++) { + const napi_property_descriptor& property = properties[i]; + + if (property.attributes & napi_static) { + defineNapiProperty(globalObject, this, property); + } else { + defineNapiProperty(globalObject, prototype, property); + } + } + + this->putDirect(vm, vm.propertyNames->prototype, prototype, JSC::PropertyAttribute::DontEnum | 0); + prototype->putDirect(vm, vm.propertyNames->constructor, this, JSC::PropertyAttribute::DontEnum | 0); + prototype->putDirect(vm, clientData->builtinNames().passwordPrivateName(), JSC::jsNumber(0), JSC::PropertyAttribute::DontEnum | 0); +} +} + +extern "C" napi_status napi_get_all_property_names( + napi_env env, napi_value objectNapi, napi_key_collection_mode key_mode, + napi_key_filter key_filter, napi_key_conversion key_conversion, + napi_value* result) +{ + DontEnumPropertiesMode jsc_key_mode = key_mode == napi_key_include_prototypes ? DontEnumPropertiesMode::Include : DontEnumPropertiesMode::Exclude; + PropertyNameMode jsc_property_mode = PropertyNameMode::StringsAndSymbols; + if (key_filter == napi_key_skip_symbols) { + jsc_property_mode = PropertyNameMode::Strings; + } else if (key_filter == napi_key_skip_strings) { + jsc_property_mode = PropertyNameMode::Symbols; + } + + auto globalObject = reinterpret_cast<Zig::GlobalObject*>(env); + JSC::VM& vm = globalObject->vm(); + + auto objectValue = JSC::JSValue::decode(reinterpret_cast<EncodedJSValue>(objectNapi)); + auto* object = objectValue.getObject(); + if (!object) { + return napi_object_expected; + } + + JSC::JSArray* exportKeys = ownPropertyKeys(globalObject, object, jsc_property_mode, jsc_key_mode, std::nullopt); + // TODO: filter + *result = toNapi(JSC::JSValue::encode(exportKeys)); + return napi_ok; +} + +extern "C" napi_status napi_define_class(napi_env env, + const char* utf8name, + size_t length, + napi_callback constructor, + void* data, + size_t property_count, + const napi_property_descriptor* properties, + napi_value* result) +{ + Zig::GlobalObject* globalObject = reinterpret_cast<Zig::GlobalObject*>(env); + JSC::VM& vm = globalObject->vm(); + + NapiClass* napiClass = NapiClass::create(vm, globalObject, utf8name, length, constructor, data, property_count, properties); + JSC::JSValue value = JSC::JSValue(napiClass); + if (data != nullptr) { + napiClass->dataPtr = data; + } + + *result = reinterpret_cast<napi_value>(JSC::JSValue::encode(value)); + return napi_ok; +} + +extern "C" napi_status napi_coerce_to_string(napi_env env, napi_value value, + napi_value* result) +{ + if (UNLIKELY(result == nullptr)) { + return napi_invalid_arg; + } + + Zig::GlobalObject* globalObject = reinterpret_cast<Zig::GlobalObject*>(env); + JSC::VM& vm = globalObject->vm(); + + auto scope = DECLARE_CATCH_SCOPE(vm); + JSC::JSValue jsValue = JSC::JSValue::decode(reinterpret_cast<JSC::EncodedJSValue>(value)); + + // .toString() can throw + JSC::JSValue resultValue = JSC::JSValue(jsValue.toString(globalObject)); + *result = reinterpret_cast<napi_value>(JSC::JSValue::encode(resultValue)); + + if (UNLIKELY(scope.exception())) { + *result = reinterpret_cast<napi_value>(JSC::JSValue::encode(JSC::jsUndefined())); + return napi_generic_failure; + } + scope.clearException(); return napi_ok; }
\ No newline at end of file |