diff options
author | 2023-05-19 00:45:18 +0100 | |
---|---|---|
committer | 2023-05-18 16:45:18 -0700 | |
commit | ac64eb420d6ce29ecca97100b7c64afd63afb26d (patch) | |
tree | 96b0d7cd328c037a4d6a9314aafca7bdd64b1466 /src/bun.js/bindings/NodeVMScript.cpp | |
parent | b76974a2a8a794db41b72e174b86d8536793f8e6 (diff) | |
download | bun-ac64eb420d6ce29ecca97100b7c64afd63afb26d.tar.gz bun-ac64eb420d6ce29ecca97100b7c64afd63afb26d.tar.zst bun-ac64eb420d6ce29ecca97100b7c64afd63afb26d.zip |
Implement `node:vm` (#2785)
* feat: begin implementing node:vm Script object
* refactor: clean up and address review comments
* refactor: rename Script to VMModuleScript
* fix: expose VMModuleScript.prototype
also oops I forgot to commit the new files last time
* feat(vm): Implement contexts and scripts
* feat(vm): implement globalThis
* feat(vm): expose node:vm module with global helper functions
* refactor(vm): rename VMModuleScript to NodeVMScript
* feat: implement script options
* doc: add TODOs for runIn*Context options
Diffstat (limited to 'src/bun.js/bindings/NodeVMScript.cpp')
-rw-r--r-- | src/bun.js/bindings/NodeVMScript.cpp | 332 |
1 files changed, 332 insertions, 0 deletions
diff --git a/src/bun.js/bindings/NodeVMScript.cpp b/src/bun.js/bindings/NodeVMScript.cpp new file mode 100644 index 000000000..0b0b7bcd4 --- /dev/null +++ b/src/bun.js/bindings/NodeVMScript.cpp @@ -0,0 +1,332 @@ +#include "root.h" + +#include "NodeVMScript.h" +#include "JavaScriptCore/JSObjectInlines.h" +#include "wtf/text/ExternalStringImpl.h" + +#include "JavaScriptCore/FunctionPrototype.h" +#include "JavaScriptCore/HeapAnalyzer.h" + +#include "JavaScriptCore/JSDestructibleObjectHeapCellType.h" +#include "JavaScriptCore/SlotVisitorMacros.h" +#include "JavaScriptCore/ObjectConstructor.h" +#include "JavaScriptCore/SubspaceInlines.h" +#include "wtf/GetPtr.h" +#include "wtf/PointerPreparations.h" +#include "wtf/URL.h" +#include "JavaScriptCore/TypedArrayInlines.h" +#include "JavaScriptCore/PropertyNameArray.h" +#include "JavaScriptCore/JSWeakMap.h" +#include "JavaScriptCore/JSWeakMapInlines.h" +#include "JavaScriptCore/JSWithScope.h" +#include "Buffer.h" +#include "GCDefferalContext.h" +#include "Buffer.h" + +#include <JavaScriptCore/DOMJITAbstractHeap.h> +#include "DOMJITIDLConvert.h" +#include "DOMJITIDLType.h" +#include "DOMJITIDLTypeFilter.h" +#include "DOMJITHelpers.h" +#include <JavaScriptCore/DFGAbstractHeap.h> + +namespace WebCore { +using namespace JSC; + +static EncodedJSValue constructScript(JSGlobalObject* globalObject, CallFrame* callFrame, JSValue newTarget = JSValue()) +{ + VM& vm = globalObject->vm(); + JSValue callee = callFrame->jsCallee(); + ArgList args(callFrame); + JSValue sourceArg = args.at(0); + String sourceString = sourceArg.isUndefined() ? emptyString() : sourceArg.toWTFString(globalObject); + + JSValue optionsArg = args.at(1); + String filename = ""_s; + OrdinalNumber lineOffset, columnOffset; + if (!optionsArg.isUndefined()) { + if (!optionsArg.isObject()) { + auto scope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(globalObject, scope, "options must be an object"_s); + } + JSObject* options = asObject(optionsArg); + + JSValue filenameOpt = options->get(globalObject, Identifier::fromString(vm, "filename"_s)); + if (filenameOpt.isString()) { + filename = filenameOpt.toWTFString(globalObject); + } + + JSValue lineOffsetOpt = options->get(globalObject, Identifier::fromString(vm, "lineOffset"_s)); + if (lineOffsetOpt.isAnyInt()) { + lineOffset = OrdinalNumber::fromZeroBasedInt(lineOffsetOpt.asAnyInt()); + } + JSValue columnOffsetOpt = options->get(globalObject, Identifier::fromString(vm, "columnOffset"_s)); + if (columnOffsetOpt.isAnyInt()) { + columnOffset = OrdinalNumber::fromZeroBasedInt(columnOffsetOpt.asAnyInt()); + } + + // TODO: cachedData + // TODO: importModuleDynamically + } + + auto* zigGlobalObject = reinterpret_cast<Zig::GlobalObject*>(globalObject); + Structure* structure = zigGlobalObject->NodeVMScriptStructure(); + if (zigGlobalObject->NodeVMScript() != newTarget) { + auto scope = DECLARE_THROW_SCOPE(vm); + JSObject* targetObj = asObject(newTarget); + auto* functionGlobalObject = reinterpret_cast<Zig::GlobalObject*>(getFunctionRealm(globalObject, targetObj)); + RETURN_IF_EXCEPTION(scope, {}); + structure = InternalFunction::createSubclassStructure( + globalObject, targetObj, functionGlobalObject->NodeVMScriptStructure()); + scope.release(); + } + + auto scope = DECLARE_THROW_SCOPE(vm); + SourceCode source( + JSC::StringSourceProvider::create(sourceString, JSC::SourceOrigin(), filename, TextPosition(lineOffset, columnOffset)), + lineOffset.zeroBasedInt(), columnOffset.zeroBasedInt()); + RETURN_IF_EXCEPTION(scope, {}); + NodeVMScript* script = NodeVMScript::create(vm, globalObject, structure, source); + return JSValue::encode(JSValue(script)); +} + +static EncodedJSValue runInContext(JSGlobalObject* globalObject, NodeVMScript* script, JSObject* globalThis, JSScope* scope, JSValue optionsArg) +{ + auto& vm = globalObject->vm(); + + if (!optionsArg.isUndefined()) { + if (!optionsArg.isObject()) { + auto scope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(globalObject, scope, "options must be an object"_s); + } + JSObject* options = asObject(optionsArg); + + // TODO: displayErrors - Not really sure what this option even does or why it's useful + // TODO: timeout - I can't figure out how to make Watchdog work so leaving this for now + // TODO: breakOnSigint - Bun doesn't support signal handlers at all yet I believe + } + + auto err_scope = DECLARE_THROW_SCOPE(vm); + auto* eval = DirectEvalExecutable::create( + globalObject, script->source(), DerivedContextType::None, NeedsClassFieldInitializer::No, PrivateBrandRequirement::None, + false, false, EvalContextType::None, nullptr, nullptr, ECMAMode::sloppy()); + RETURN_IF_EXCEPTION(err_scope, {}); + + return JSValue::encode(vm.interpreter.executeEval(eval, globalThis, scope)); +} + +JSC_DEFINE_HOST_FUNCTION(scriptConstructorCall, (JSGlobalObject* globalObject, CallFrame* callFrame)) +{ + return constructScript(globalObject, callFrame); +} + +JSC_DEFINE_HOST_FUNCTION(scriptConstructorConstruct, (JSGlobalObject* globalObject, CallFrame* callFrame)) +{ + return constructScript(globalObject, callFrame, callFrame->newTarget()); +} + +JSC_DEFINE_CUSTOM_GETTER(scriptGetCachedDataRejected, (JSGlobalObject* globalObject, EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = globalObject->vm(); + return JSValue::encode(jsBoolean(true)); // TODO +} +JSC_DEFINE_HOST_FUNCTION(scriptCreateCachedData, (JSGlobalObject* globalObject, CallFrame* callFrame)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + return throwVMError(globalObject, scope, "TODO: Script.createCachedData"_s); +} + +JSC_DEFINE_HOST_FUNCTION(scriptRunInContext, (JSGlobalObject* globalObject, CallFrame* callFrame)) +{ + auto& vm = globalObject->vm(); + + JSValue thisValue = callFrame->thisValue(); + auto* script = jsDynamicCast<NodeVMScript*>(thisValue); + if (UNLIKELY(!script)) { + auto scope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(globalObject, scope, "Script.prototype.runInContext can only be called on a Script object"_s); + } + + ArgList args(callFrame); + + JSValue contextArg = args.at(0); + if (!UNLIKELY(contextArg.isObject())) { + auto scope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(globalObject, scope, "context parameter must be a contextified object"_s); + } + JSObject* context = asObject(contextArg); + + auto* zigGlobalObject = reinterpret_cast<Zig::GlobalObject*>(globalObject); + JSValue scopeVal = zigGlobalObject->vmModuleContextMap()->get(context); + if (UNLIKELY(scopeVal.isUndefined())) { + auto scope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(globalObject, scope, "context parameter must be a contextified object"_s); + } + JSScope* scope = jsDynamicCast<JSScope*>(scopeVal); + ASSERT(scope); + + return runInContext(globalObject, script, context, scope, args.at(1)); +} +JSC_DEFINE_HOST_FUNCTION(scriptRunInThisContext, (JSGlobalObject* globalObject, CallFrame* callFrame)) +{ + auto& vm = globalObject->vm(); + JSValue thisValue = callFrame->thisValue(); + auto* script = jsDynamicCast<NodeVMScript*>(thisValue); + if (UNLIKELY(!script)) { + auto scope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(globalObject, scope, "Script.prototype.runInThisContext can only be called on a Script object"_s); + } + + ArgList args(callFrame); + return runInContext(globalObject, script, globalObject->globalThis(), globalObject->globalScope(), args.at(0)); +} + +JSC_DEFINE_CUSTOM_GETTER(scriptGetSourceMapURL, (JSGlobalObject* globalObject, EncodedJSValue thisValueEncoded, PropertyName)) +{ + auto& vm = globalObject->vm(); + JSValue thisValue = JSValue::decode(thisValueEncoded); + auto* script = jsDynamicCast<NodeVMScript*>(thisValue); + if (UNLIKELY(!script)) { + auto scope = DECLARE_THROW_SCOPE(vm); + return throwVMTypeError(globalObject, scope, "Script.prototype.sourceMapURL getter can only be called on a Script object"_s); + } + + // FIXME: doesn't seem to work? Just returns undefined + const auto& url = script->source().provider()->sourceMappingURLDirective(); + return JSValue::encode(jsString(vm, url)); +} + +JSC_DEFINE_HOST_FUNCTION(vmModule_createContext, (JSGlobalObject* globalObject, CallFrame* callFrame)) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + ArgList args(callFrame); + JSValue contextArg = args.at(0); + if (!contextArg.isObject()) { + return throwVMTypeError(globalObject, scope, "parameter to createContext must be an object"_s); + } + JSObject* context = asObject(contextArg); + + PropertyDescriptor descriptor; + descriptor.setWritable(false); + descriptor.setEnumerable(false); + descriptor.setValue(context); + JSObject::defineOwnProperty(context, globalObject, Identifier::fromString(vm, "globalThis"_s), descriptor, true); + JSScope* contextScope = JSWithScope::create(vm, globalObject, globalObject->globalScope(), context); + + auto* zigGlobalObject = reinterpret_cast<Zig::GlobalObject*>(globalObject); + zigGlobalObject->vmModuleContextMap()->set(vm, context, contextScope); + + return JSValue::encode(context); +} + +JSC_DEFINE_HOST_FUNCTION(vmModule_isContext, (JSGlobalObject* globalObject, CallFrame* callFrame)) +{ + ArgList args(callFrame); + JSValue contextArg = args.at(0); + bool isContext; + if (!contextArg.isObject()) { + isContext = false; + } else { + auto* zigGlobalObject = reinterpret_cast<Zig::GlobalObject*>(globalObject); + isContext = zigGlobalObject->vmModuleContextMap()->has(asObject(contextArg)); + } + return JSValue::encode(jsBoolean(isContext)); +} + +class NodeVMScriptPrototype final : public JSNonFinalObject { +public: + using Base = JSNonFinalObject; + + static NodeVMScriptPrototype* create(VM& vm, JSGlobalObject* globalObject, Structure* structure) + { + NodeVMScriptPrototype* ptr = new (NotNull, allocateCell<NodeVMScriptPrototype>(vm)) NodeVMScriptPrototype(vm, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template<typename CellType, SubspaceAccess> + static GCClient::IsoSubspace* subspaceFor(VM& vm) + { + return &vm.plainObjectSpace(); + } + static Structure* createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) + { + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); + } + +private: + NodeVMScriptPrototype(VM& vm, Structure* structure) + : Base(vm, structure) + { + } + + void finishCreation(VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(NodeVMScriptPrototype, NodeVMScriptPrototype::Base); + +static const struct HashTableValue scriptPrototypeTableValues[] = { + { "cachedDataRejected"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly|PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, scriptGetCachedDataRejected, nullptr } }, + { "createCachedData"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly|PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, scriptCreateCachedData, 0 } }, + { "runInContext"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly|PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, scriptRunInContext, 0 } }, + { "runInThisContext"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly|PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, scriptRunInThisContext, 0 } }, + { "sourceMapURL"_s, static_cast<unsigned>(PropertyAttribute::ReadOnly|PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, scriptGetSourceMapURL, nullptr } }, +}; + +const ClassInfo NodeVMScriptPrototype::s_info = { "Script"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeVMScriptPrototype) }; +const ClassInfo NodeVMScript::s_info = { "Script"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeVMScript) }; +const ClassInfo NodeVMScriptConstructor::s_info = { "Script"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NodeVMScriptConstructor) }; + +NodeVMScriptConstructor::NodeVMScriptConstructor(VM& vm, Structure* structure) + : NodeVMScriptConstructor::Base(vm, structure, scriptConstructorCall, scriptConstructorConstruct) +{ +} + +NodeVMScriptConstructor* NodeVMScriptConstructor::create(VM& vm, JSGlobalObject* globalObject, Structure* structure, JSObject* prototype) +{ + NodeVMScriptConstructor* ptr = new (NotNull, allocateCell<NodeVMScriptConstructor>(vm)) NodeVMScriptConstructor(vm, structure); + ptr->finishCreation(vm, prototype); + return ptr; +} + +void NodeVMScriptConstructor::finishCreation(VM& vm, JSObject* prototype) +{ + Base::finishCreation(vm, 1, "Script"_s, PropertyAdditionMode::WithStructureTransition); + putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); + ASSERT(inherits(info())); +} + +void NodeVMScriptPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, NodeVMScript::info(), scriptPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +JSObject* NodeVMScript::createPrototype(VM& vm, JSGlobalObject* globalObject) +{ + return NodeVMScriptPrototype::create(vm, globalObject, NodeVMScriptPrototype::createStructure(vm, globalObject, globalObject->objectPrototype())); +} + +NodeVMScript* NodeVMScript::create(VM& vm, JSGlobalObject* globalObject, Structure* structure, SourceCode source) +{ + NodeVMScript* ptr = new (NotNull, allocateCell<NodeVMScript>(vm)) NodeVMScript(vm, structure, source); + ptr->finishCreation(vm); + return ptr; +} + +void NodeVMScript::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +void NodeVMScript::destroy(JSCell* cell) +{ + static_cast<NodeVMScript*>(cell)->NodeVMScript::~NodeVMScript(); +} + +} |