//// ScriptEngineV8.cpp// libraries/script-engine/src/v8//// Created by Brad Hefta-Gaub on 12/14/13.// Modified for V8 by dr Karol Suprynowicz on 2022/10/08// Copyright 2013 High Fidelity, Inc.// Copyright 2022-2023 Overte e.V.//// Distributed under the Apache License, Version 2.0.// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html// SPDX-License-Identifier: Apache-2.0// #include "ScriptEngineV8.h" #include <chrono>#include <mutex>#include <thread> #include <QtCore/QCoreApplication>#include <QtCore/QEventLoop>#include <QtCore/QFileInfo>#include <QtCore/QTimer>#include <QtCore/QThread>#include <QtCore/QRegularExpression> #include <QtCore/QFuture>#include <QtConcurrent/QtConcurrentRun> #include <QtWidgets/QMainWindow>#include <QtWidgets/QApplication>#include <QtWidgets/QMenuBar>#include <QtWidgets/QMenu> #include <QtNetwork/QNetworkRequest>#include <QtNetwork/QNetworkReply> #include <shared/LocalFileAccessGate.h>#include <shared/QtHelpers.h>#include <shared/AbstractLoggerInterface.h> #include <Profile.h> #include <v8-profiler.h> #include "../ScriptEngineLogging.h"#include "../ScriptProgram.h"#include "../ScriptEngineCast.h"#include "../ScriptValue.h"#include "../ScriptManagerScriptingInterface.h" #include "ScriptContextV8Wrapper.h"#include "ScriptObjectV8Proxy.h"#include "ScriptProgramV8Wrapper.h"#include "ScriptValueV8Wrapper.h"#include "ScriptEngineLoggingV8.h"#include "ScriptValueIteratorV8Wrapper.h"#include "shared/FileUtils.h" static const int MAX_DEBUG_VALUE_LENGTH { 80 }; std::once_flag ScriptEngineV8::_v8InitOnceFlag;QMutex ScriptEngineV8::_v8InitMutex; bool ScriptEngineV8::IS_THREADSAFE_INVOCATION(const QThread* thread, const QString& method) { const QThread* currentThread = QThread::currentThread(); if (currentThread == thread) { return true; } qCCritical(scriptengine_v8) << QString("Scripting::%1 @ %2 -- ignoring thread-unsafe call from %3") .arg(method) .arg(thread ? thread->objectName() : "(!thread)") .arg(QThread::currentThread()->objectName()); qCDebug(scriptengine_v8) << "(please resolve on the calling side by using invokeMethod, executeOnScriptThread, etc.)"; Q_ASSERT(false); return false;} std::unique_ptr<ScriptEngine::ScriptEngineScopeGuard> ScriptEngineV8::getScopeGuard() { return std::make_unique<ScriptEngineScopeGuardV8>(_v8Isolate);} QString getFileNameFromTryCatch(v8::TryCatch &tryCatch, v8::Isolate *isolate, v8::Local<v8::Context> &context ) { v8::Local<v8::Message> exceptionMessage = tryCatch.Message(); QString errorFileName; auto resource = exceptionMessage->GetScriptResourceName(); v8::Local<v8::String> v8resourceString; if (resource->ToString(context).ToLocal(&v8resourceString)) { errorFileName = QString(*v8::String::Utf8Value(isolate, v8resourceString)); } return errorFileName;} ScriptValue ScriptEngineV8::makeError(const ScriptValue& _other, const QString& type) { if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return nullValue(); } auto other = _other; if (_other.isString()) { other = newObject(); other.setProperty("message", _other.toString()); } auto proto = globalObject().property(type); if (!proto.isFunction()) { proto = globalObject().property(other.prototype().property("constructor").property("name").toString()); } if (!proto.isFunction()) {#ifdef DEBUG_JS_EXCEPTIONS qCDebug(shared) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead";#endif proto = globalObject().property("Error"); } if (other.engine().get() != this) { // JS Objects are parented to a specific script engine instance // -- this effectively ~clones it locally by routing through a QVariant and back other = toScriptValue(other.toVariant()); } // ~ var err = new Error(other.message) auto err = proto.construct(ScriptValueList({ other.property("message") })); // transfer over any existing properties auto it = other.newIterator(); while (it->hasNext()) { it->next(); err.setProperty(it->name(), it->value()); } return err;} // check syntax and when there are issues returns an actual "SyntaxError" with the detailsScriptValue ScriptEngineV8::checkScriptSyntax(ScriptProgramPointer program) { if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return nullValue(); } Q_ASSERT(_v8Isolate->IsCurrent()); v8::HandleScope handleScope(_v8Isolate); v8::Context::Scope contextScope(getContext()); ScriptSyntaxCheckResultPointer syntaxCheck = program->checkSyntax(); if (syntaxCheck->state() != ScriptSyntaxCheckResult::Valid) { auto err = globalObject().property("SyntaxError").construct(ScriptValueList({ newValue(syntaxCheck->errorMessage()) })); err.setProperty("fileName", program->fileName()); err.setProperty("lineNumber", syntaxCheck->errorLineNumber()); err.setProperty("expressionBeginOffset", syntaxCheck->errorColumnNumber()); err.setProperty("stack", syntaxCheck->errorBacktrace()); { const auto error = syntaxCheck->errorMessage(); const auto line = QString::number(syntaxCheck->errorLineNumber()); const auto column = QString::number(syntaxCheck->errorColumnNumber()); // for compatibility with legacy reporting const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, program->fileName(), line, column); err.setProperty("formatted", message); } return err; } return undefinedValue();} #ifdef DEBUG_JSvoid ScriptEngineV8::_debugDump(const QString& header, const V8ScriptValue& object, const QString& footer) { // V8TODO /*if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return; } if (!header.isEmpty()) { qCDebug(shared) << header; } if (!object.isObject()) { qCDebug(shared) << "(!isObject)" << object.toVariant().toString() << object.toString(); return; } V8ScriptValueIterator it(object); while (it.hasNext()) { it.next(); qCDebug(shared) << it.name() << ":" << it.value().toString(); } if (!footer.isEmpty()) { qCDebug(shared) << footer; }*/}#endif v8::Platform* ScriptEngineV8::getV8Platform() { static std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform(); return platform.get();} ScriptEngineV8::ScriptEngineV8(ScriptManager *manager) : ScriptEngine(manager), _evaluatingCounter(0) //V8TODO _arrayBufferClass(new ArrayBufferClass(this)){ _v8InitMutex.lock(); std::call_once ( _v8InitOnceFlag, [ ]{ v8::V8::InitializeExternalStartupData(""); // Experimentally determined that the maximum size that works on Linux with a stack size of 8192K is 8182. // That would seem to be the overhead of our code and V8. // // Windows stacks are 1MB. // // Based on that, going with 256K for stacks for now. That seems like a reasonable value. // We'll probably need a more complex system on the longer term, with configurable limits. // Flags to try: // --single-threaded is to check if it fixes random crashes // --jitless - might improve debugging performance due to no JIT? // --assert-types v8::V8::InitializeICU();#ifdef OVERTE_V8_MEMORY_DEBUG v8::V8::SetFlagsFromString("--stack-size=256 --track_gc_object_stats --assert-types");#else v8::V8::SetFlagsFromString("--stack-size=256");#endif v8::Platform* platform = getV8Platform(); v8::V8::InitializePlatform(platform); v8::V8::Initialize(); qCDebug(scriptengine_v8) << "V8 platform initialized"; } ); _v8InitMutex.unlock(); qCDebug(scriptengine_v8) << "Creating new script engine"; { v8::Isolate::CreateParams isolateParams; isolateParams.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator(); _v8Isolate = v8::Isolate::New(isolateParams); v8::Locker locker(_v8Isolate); v8::Isolate::Scope isolateScope(_v8Isolate); v8::HandleScope handleScope(_v8Isolate); v8::Local<v8::Context> context = v8::Context::New(_v8Isolate); Q_ASSERT(!context.IsEmpty()); v8::Context::Scope contextScope(context); _contexts.append(std::make_shared<ScriptContextV8Wrapper>(this,context, ScriptContextPointer())); V8ScriptValue nullScriptValue(this, v8::Null(_v8Isolate)); _nullValue = ScriptValue(new ScriptValueV8Wrapper(this, nullScriptValue)); V8ScriptValue undefined(this, v8::Undefined(_v8Isolate)); _undefinedValue = ScriptValue(new ScriptValueV8Wrapper(this, undefined)); registerSystemTypes(); // V8TODO: dispose of isolate on ScriptEngineV8 destruction // V8TODO: //QScriptEngine::setProcessEventsInterval(MSECS_PER_SECOND); } //if (_scriptManager) { // V8TODO: port to V8 /*connect(this, &QScriptEngine::signalHandlerException, this, [this](const V8ScriptValue& exception) { if (hasUncaughtException()) { // the engine's uncaughtException() seems to produce much better stack traces here emit _scriptManager->unhandledException(cloneUncaughtException("signalHandlerException")); clearExceptions(); } else { // ... but may not always be available -- so if needed we fallback to the passed exception V8ScriptValue thrown = makeError(exception); emit _scriptManager->unhandledException(ScriptValue(new ScriptValueV8Wrapper(this, std::move(thrown)))); } }, Qt::DirectConnection);*/ //}} ScriptEngineV8::~ScriptEngineV8() { auto scopeGuard = getScopeGuard(); // Process remaining events to avoid problems with `deleteLater` calling destructor of script proxies after script engine has been deleted: { QEventLoop loop; loop.processEvents(); } // This is necessary for script engines that don't run in ScriptManager::run(), for example entity scripts: disconnectSignalProxies(); deleteUnusedValueWrappers(); _contexts.clear(); _nullValue = ScriptValue(); _undefinedValue = ScriptValue(); _customPrototypes.clear(); { QMutexLocker guard(&_qobjectWrapperMapProtect); _qobjectWrapperMap.clear(); _qobjectWrapperMapV8.clear(); } // Events need to be processed one more time for processing any remaining deleteLater calls: { QEventLoop loop; loop.processEvents(); }#ifdef OVERTE_SCRIPT_USE_AFTER_DELETE_GUARD _wasDestroyed = true;#endif qDebug() << "ScriptEngineV8::~ScriptEngineV8: script engine destroyed";} void ScriptEngineV8::perManagerLoopIterationCleanup() { deleteUnusedValueWrappers();} void ScriptEngineV8::disconnectSignalProxies() { _signalProxySetLock.lockForRead(); while (!_signalProxySet.empty()) { auto proxy = *_signalProxySet.begin(); _signalProxySetLock.unlock(); delete proxy; _signalProxySetLock.lockForRead(); } _signalProxySetLock.unlock();} void ScriptEngineV8::deleteUnusedValueWrappers() { while (!_scriptValueWrappersToDelete.empty()) { auto wrapper = _scriptValueWrappersToDelete.dequeue(); wrapper->release(); }} void ScriptEngineV8::registerEnum(ScriptEngineScopeGuard* scopeGuard, const QString& enumName, QMetaEnum newEnum) { Q_ASSERT(scopeGuard); auto* scopeGuardV8 = dynamic_cast<ScriptEngineScopeGuardV8*>(scopeGuard); Q_ASSERT(scopeGuardV8); if (!newEnum.isValid()) { qCCritical(scriptengine_v8) << "registerEnum called on invalid enum with name " << enumName; return; } Q_ASSERT(_v8Isolate->IsCurrent()); v8::HandleScope handleScope(_v8Isolate); v8::Context::Scope contextScope(getContext()); for (int i = 0; i < newEnum.keyCount(); i++) { const char* keyName = newEnum.key(i); QString fullName = enumName + "." + keyName; registerValue(scopeGuardV8, fullName, V8ScriptValue(this, v8::Integer::New(_v8Isolate, newEnum.keyToValue(keyName)))); }} void ScriptEngineV8::registerValue(ScriptEngineScopeGuardV8* scopeGuard, const QString& valueName, V8ScriptValue value) { Q_ASSERT(scopeGuard); Q_ASSERT(QThread::currentThread() == thread()); Q_ASSERT(_v8Isolate->IsCurrent()); v8::HandleScope handleScope(_v8Isolate); v8::Local<v8::Context> context = getContext(); v8::Context::Scope contextScope(context); QStringList pathToValue = valueName.split("."); int partsToGo = pathToValue.length(); v8::Local<v8::Object> partObject = context->Global(); for (const auto& pathPart : pathToValue) { partsToGo--; v8::Local<v8::String> pathPartV8 = v8::String::NewFromUtf8(_v8Isolate, pathPart.toStdString().c_str(),v8::NewStringType::kNormal).ToLocalChecked(); v8::Local<v8::Value> currentPath; bool createProperty = false; if (!partObject->Get(context, pathPartV8).ToLocal(¤tPath)) { createProperty = true; } if (currentPath->IsUndefined()) { createProperty = true; } if (createProperty) { if (partsToGo > 0) { v8::Local<v8::Object> partValue = v8::Object::New(_v8Isolate); if (!partObject->Set(context, pathPartV8, partValue).FromMaybe(false)) { Q_ASSERT(false); } } else { if (!partObject->Set(context, pathPartV8, value.constGet()).FromMaybe(false)) { Q_ASSERT(false); } } } v8::Local<v8::Value> child; if (!partObject->Get(context, pathPartV8).ToLocal(&child)) { Q_ASSERT(false); } if (partsToGo > 0) { if (!child->IsObject()) { QString details = *v8::String::Utf8Value(_v8Isolate, child->ToDetailString(context).ToLocalChecked()); qCDebug(scriptengine_v8) << "ScriptEngineV8::registerValue: Part of path is not an object: " << pathPart << " details: " << details; Q_ASSERT(false); } partObject = v8::Local<v8::Object>::Cast(child); } }} void ScriptEngineV8::registerGlobalObject(ScriptEngineScopeGuard* scopeGuard, const QString& name, QObject* object, ScriptEngine::ValueOwnership) { Q_ASSERT(scopeGuard && dynamic_cast<ScriptEngineScopeGuardV8*>(scopeGuard)); Q_ASSERT(QThread::currentThread() == thread()); Q_ASSERT(_v8Isolate->IsCurrent()); v8::HandleScope handleScope(_v8Isolate); Q_ASSERT(_v8Isolate->IsCurrent()); v8::Local<v8::Context> context = getContext(); v8::Context::Scope contextScope(context); v8::Local<v8::Object> v8GlobalObject = context->Global(); v8::Local<v8::String> v8Name = v8::String::NewFromUtf8(_v8Isolate, name.toStdString().c_str()).ToLocalChecked(); if (!v8GlobalObject->Get(context, v8Name).IsEmpty()) { if (object) { V8ScriptValue value = ScriptObjectV8Proxy::newQObject(this, object, ScriptEngine::QtOwnership); if(!v8GlobalObject->Set(context, v8Name, value.get()).FromMaybe(false)) { Q_ASSERT(false); } } else { if(!v8GlobalObject->Set(context, v8Name, v8::Null(_v8Isolate)).FromMaybe(false)) { Q_ASSERT(false); } } }} void ScriptEngineV8::registerFunction(ScriptEngineScopeGuard* scopeGuard, const QString& name, ScriptEngine::FunctionSignature functionSignature, int numArguments) { Q_ASSERT(scopeGuard && dynamic_cast<ScriptEngineScopeGuardV8*>(scopeGuard)); Q_ASSERT(QThread::currentThread() == thread()); Q_ASSERT(_v8Isolate->IsCurrent()); v8::HandleScope handleScope(_v8Isolate); v8::Context::Scope contextScope(getContext()); auto scriptFun = newFunction(functionSignature, numArguments); globalObject().setProperty(name, scriptFun);} void ScriptEngineV8::registerFunction(ScriptEngineScopeGuard* scopeGuard, const QString& parent, const QString& name, ScriptEngine::FunctionSignature functionSignature, int numArguments) { Q_ASSERT(scopeGuard && dynamic_cast<ScriptEngineScopeGuardV8*>(scopeGuard)); Q_ASSERT(QThread::currentThread() == thread()); Q_ASSERT(_v8Isolate->IsCurrent()); v8::HandleScope handleScope(_v8Isolate); v8::Context::Scope contextScope(getContext()); ScriptValue object = globalObject().property(parent); if (object.isValid()) { ScriptValue scriptFun = newFunction(functionSignature, numArguments); object.setProperty(name, scriptFun); }} void ScriptEngineV8::registerGetterSetter(ScriptEngineScopeGuard* scopeGuard, const QString& name, ScriptEngine::FunctionSignature getter, ScriptEngine::FunctionSignature setter, const QString& parent) { Q_ASSERT(scopeGuard && dynamic_cast<ScriptEngineScopeGuardV8*>(scopeGuard)); Q_ASSERT(QThread::currentThread() == thread()); Q_ASSERT(_v8Isolate->IsCurrent()); v8::HandleScope handleScope(_v8Isolate); auto context = getContext(); v8::Context::Scope contextScope(context); ScriptValue setterFunction = newFunction(setter, 1); ScriptValue getterFunction = newFunction(getter); V8ScriptValue unwrappedGetter = ScriptValueV8Wrapper::fullUnwrap(this, getterFunction); V8ScriptValue unwrappedSetter = ScriptValueV8Wrapper::fullUnwrap(this, setterFunction); v8::PropertyDescriptor propertyDescriptor(unwrappedGetter.get(), unwrappedSetter.get()); if (!parent.isNull() && !parent.isEmpty()) { ScriptValue object = globalObject().property(parent); if (object.isValid()) { V8ScriptValue v8parent = ScriptValueV8Wrapper::fullUnwrap(this, object); Q_ASSERT(v8parent.get()->IsObject()); v8::Local<v8::Object> v8ParentObject = v8::Local<v8::Object>::Cast(v8parent.get()); v8::Local<v8::String> v8propertyName = v8::String::NewFromUtf8(_v8Isolate, name.toStdString().c_str()).ToLocalChecked(); v8::Local<v8::Object> v8ObjectToSetProperty; ScriptObjectV8Proxy *proxy = ScriptObjectV8Proxy::unwrapProxy(V8ScriptValue(this, v8ParentObject)); // If object is ScriptObjectV8Proxy, then setting property needs to be handled differently if (proxy) { v8ObjectToSetProperty = v8ParentObject->GetInternalField(2).As<v8::Object>(); } else { v8ObjectToSetProperty = v8ParentObject; } if (!v8ObjectToSetProperty->DefineProperty(context, v8propertyName, propertyDescriptor).FromMaybe(false)) { qCDebug(scriptengine_v8) << "DefineProperty failed for registerGetterSetter \"" << name << "\" for parent: \"" << parent << "\""; } } else { qCDebug(scriptengine_v8) << "Parent object \"" << parent << "\" for registerGetterSetter \"" << name << "\" is not valid: "; } } else { v8::Local<v8::String> v8propertyName = v8::String::NewFromUtf8(_v8Isolate, name.toStdString().c_str()).ToLocalChecked(); if (!context->Global()->DefineProperty(context, v8propertyName, propertyDescriptor).FromMaybe(false)) { qCDebug(scriptengine_v8) << "DefineProperty failed for registerGetterSetter \"" << name << "\" for global object"; } }} v8::Local<v8::Context> ScriptEngineV8::getContext() {#ifdef OVERTE_SCRIPT_USE_AFTER_DELETE_GUARD Q_ASSERT(!_wasDestroyed);#endif v8::EscapableHandleScope handleScope(_v8Isolate); Q_ASSERT(!_contexts.isEmpty()); return handleScope.Escape(_contexts.last().get()->toV8Value());} const v8::Local<v8::Context> ScriptEngineV8::getConstContext() const { v8::EscapableHandleScope handleScope(_v8Isolate); Q_ASSERT(!_contexts.isEmpty()); return handleScope.Escape(_contexts.last().get()->toV8Value());} // Stored objects are used to create global objects for evaluateInClosurevoid ScriptEngineV8::storeGlobalObjectContents() { if (areGlobalObjectContentsStored) {Lines 1–500 of 1645. Load more
// // ScriptEngineV8.cpp // libraries/script-engine/src/v8 // // Created by Brad Hefta-Gaub on 12/14/13. // Modified for V8 by dr Karol Suprynowicz on 2022/10/08 // Copyright 2013 High Fidelity, Inc. // Copyright 2022-2023 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // SPDX-License-Identifier: Apache-2.0 // #include "ScriptEngineV8.h" #include <chrono> #include <mutex> #include <thread> #include <QtCore/QCoreApplication> #include <QtCore/QEventLoop> #include <QtCore/QFileInfo> #include <QtCore/QTimer> #include <QtCore/QThread> #include <QtCore/QRegularExpression> #include <QtCore/QFuture> #include <QtConcurrent/QtConcurrentRun> #include <QtWidgets/QMainWindow> #include <QtWidgets/QApplication> #include <QtWidgets/QMenuBar> #include <QtWidgets/QMenu> #include <QtNetwork/QNetworkRequest> #include <QtNetwork/QNetworkReply> #include <shared/LocalFileAccessGate.h> #include <shared/QtHelpers.h> #include <shared/AbstractLoggerInterface.h> #include <Profile.h> #include <v8-profiler.h> #include "../ScriptEngineLogging.h" #include "../ScriptProgram.h" #include "../ScriptEngineCast.h" #include "../ScriptValue.h" #include "../ScriptManagerScriptingInterface.h" #include "ScriptContextV8Wrapper.h" #include "ScriptObjectV8Proxy.h" #include "ScriptProgramV8Wrapper.h" #include "ScriptValueV8Wrapper.h" #include "ScriptEngineLoggingV8.h" #include "ScriptValueIteratorV8Wrapper.h" #include "shared/FileUtils.h" static const int MAX_DEBUG_VALUE_LENGTH { 80 }; std::once_flag ScriptEngineV8::_v8InitOnceFlag; QMutex ScriptEngineV8::_v8InitMutex; bool ScriptEngineV8::IS_THREADSAFE_INVOCATION(const QThread* thread, const QString& method) { const QThread* currentThread = QThread::currentThread(); if (currentThread == thread) { return true; } qCCritical(scriptengine_v8) << QString("Scripting::%1 @ %2 -- ignoring thread-unsafe call from %3") .arg(method) .arg(thread ? thread->objectName() : "(!thread)") .arg(QThread::currentThread()->objectName()); qCDebug(scriptengine_v8) << "(please resolve on the calling side by using invokeMethod, executeOnScriptThread, etc.)"; Q_ASSERT(false); return false; } std::unique_ptr<ScriptEngine::ScriptEngineScopeGuard> ScriptEngineV8::getScopeGuard() { return std::make_unique<ScriptEngineScopeGuardV8>(_v8Isolate); } QString getFileNameFromTryCatch(v8::TryCatch &tryCatch, v8::Isolate *isolate, v8::Local<v8::Context> &context ) { v8::Local<v8::Message> exceptionMessage = tryCatch.Message(); QString errorFileName; auto resource = exceptionMessage->GetScriptResourceName(); v8::Local<v8::String> v8resourceString; if (resource->ToString(context).ToLocal(&v8resourceString)) { errorFileName = QString(*v8::String::Utf8Value(isolate, v8resourceString)); } return errorFileName; } ScriptValue ScriptEngineV8::makeError(const ScriptValue& _other, const QString& type) { if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return nullValue(); } auto other = _other; if (_other.isString()) { other = newObject(); other.setProperty("message", _other.toString()); } auto proto = globalObject().property(type); if (!proto.isFunction()) { proto = globalObject().property(other.prototype().property("constructor").property("name").toString()); } if (!proto.isFunction()) { #ifdef DEBUG_JS_EXCEPTIONS qCDebug(shared) << "BaseScriptEngine::makeError -- couldn't find constructor for" << type << " -- using Error instead"; #endif proto = globalObject().property("Error"); } if (other.engine().get() != this) { // JS Objects are parented to a specific script engine instance // -- this effectively ~clones it locally by routing through a QVariant and back other = toScriptValue(other.toVariant()); } // ~ var err = new Error(other.message) auto err = proto.construct(ScriptValueList({ other.property("message") })); // transfer over any existing properties auto it = other.newIterator(); while (it->hasNext()) { it->next(); err.setProperty(it->name(), it->value()); } return err; } // check syntax and when there are issues returns an actual "SyntaxError" with the details ScriptValue ScriptEngineV8::checkScriptSyntax(ScriptProgramPointer program) { if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return nullValue(); } Q_ASSERT(_v8Isolate->IsCurrent()); v8::HandleScope handleScope(_v8Isolate); v8::Context::Scope contextScope(getContext()); ScriptSyntaxCheckResultPointer syntaxCheck = program->checkSyntax(); if (syntaxCheck->state() != ScriptSyntaxCheckResult::Valid) { auto err = globalObject().property("SyntaxError").construct(ScriptValueList({ newValue(syntaxCheck->errorMessage()) })); err.setProperty("fileName", program->fileName()); err.setProperty("lineNumber", syntaxCheck->errorLineNumber()); err.setProperty("expressionBeginOffset", syntaxCheck->errorColumnNumber()); err.setProperty("stack", syntaxCheck->errorBacktrace()); { const auto error = syntaxCheck->errorMessage(); const auto line = QString::number(syntaxCheck->errorLineNumber()); const auto column = QString::number(syntaxCheck->errorColumnNumber()); // for compatibility with legacy reporting const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, program->fileName(), line, column); err.setProperty("formatted", message); } return err; } return undefinedValue(); } #ifdef DEBUG_JS void ScriptEngineV8::_debugDump(const QString& header, const V8ScriptValue& object, const QString& footer) { // V8TODO /*if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) { return; } if (!header.isEmpty()) { qCDebug(shared) << header; } if (!object.isObject()) { qCDebug(shared) << "(!isObject)" << object.toVariant().toString() << object.toString(); return; } V8ScriptValueIterator it(object); while (it.hasNext()) { it.next(); qCDebug(shared) << it.name() << ":" << it.value().toString(); } if (!footer.isEmpty()) { qCDebug(shared) << footer; }*/ } #endif v8::Platform* ScriptEngineV8::getV8Platform() { static std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform(); return platform.get(); } ScriptEngineV8::ScriptEngineV8(ScriptManager *manager) : ScriptEngine(manager), _evaluatingCounter(0) //V8TODO _arrayBufferClass(new ArrayBufferClass(this)) { _v8InitMutex.lock(); std::call_once ( _v8InitOnceFlag, [ ]{ v8::V8::InitializeExternalStartupData(""); // Experimentally determined that the maximum size that works on Linux with a stack size of 8192K is 8182. // That would seem to be the overhead of our code and V8. // // Windows stacks are 1MB. // // Based on that, going with 256K for stacks for now. That seems like a reasonable value. // We'll probably need a more complex system on the longer term, with configurable limits. // Flags to try: // --single-threaded is to check if it fixes random crashes // --jitless - might improve debugging performance due to no JIT? // --assert-types v8::V8::InitializeICU(); #ifdef OVERTE_V8_MEMORY_DEBUG v8::V8::SetFlagsFromString("--stack-size=256 --track_gc_object_stats --assert-types"); #else v8::V8::SetFlagsFromString("--stack-size=256"); #endif v8::Platform* platform = getV8Platform(); v8::V8::InitializePlatform(platform); v8::V8::Initialize(); qCDebug(scriptengine_v8) << "V8 platform initialized"; } ); _v8InitMutex.unlock(); qCDebug(scriptengine_v8) << "Creating new script engine"; { v8::Isolate::CreateParams isolateParams; isolateParams.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator(); _v8Isolate = v8::Isolate::New(isolateParams); v8::Locker locker(_v8Isolate); v8::Isolate::Scope isolateScope(_v8Isolate); v8::HandleScope handleScope(_v8Isolate); v8::Local<v8::Context> context = v8::Context::New(_v8Isolate); Q_ASSERT(!context.IsEmpty()); v8::Context::Scope contextScope(context); _contexts.append(std::make_shared<ScriptContextV8Wrapper>(this,context, ScriptContextPointer())); V8ScriptValue nullScriptValue(this, v8::Null(_v8Isolate)); _nullValue = ScriptValue(new ScriptValueV8Wrapper(this, nullScriptValue)); V8ScriptValue undefined(this, v8::Undefined(_v8Isolate)); _undefinedValue = ScriptValue(new ScriptValueV8Wrapper(this, undefined)); registerSystemTypes(); // V8TODO: dispose of isolate on ScriptEngineV8 destruction // V8TODO: //QScriptEngine::setProcessEventsInterval(MSECS_PER_SECOND); } //if (_scriptManager) { // V8TODO: port to V8 /*connect(this, &QScriptEngine::signalHandlerException, this, [this](const V8ScriptValue& exception) { if (hasUncaughtException()) { // the engine's uncaughtException() seems to produce much better stack traces here emit _scriptManager->unhandledException(cloneUncaughtException("signalHandlerException")); clearExceptions(); } else { // ... but may not always be available -- so if needed we fallback to the passed exception V8ScriptValue thrown = makeError(exception); emit _scriptManager->unhandledException(ScriptValue(new ScriptValueV8Wrapper(this, std::move(thrown)))); } }, Qt::DirectConnection);*/ //} } ScriptEngineV8::~ScriptEngineV8() { auto scopeGuard = getScopeGuard(); // Process remaining events to avoid problems with `deleteLater` calling destructor of script proxies after script engine has been deleted: { QEventLoop loop; loop.processEvents(); } // This is necessary for script engines that don't run in ScriptManager::run(), for example entity scripts: disconnectSignalProxies(); deleteUnusedValueWrappers(); _contexts.clear(); _nullValue = ScriptValue(); _undefinedValue = ScriptValue(); _customPrototypes.clear(); { QMutexLocker guard(&_qobjectWrapperMapProtect); _qobjectWrapperMap.clear(); _qobjectWrapperMapV8.clear(); } // Events need to be processed one more time for processing any remaining deleteLater calls: { QEventLoop loop; loop.processEvents(); } #ifdef OVERTE_SCRIPT_USE_AFTER_DELETE_GUARD _wasDestroyed = true; #endif qDebug() << "ScriptEngineV8::~ScriptEngineV8: script engine destroyed"; } void ScriptEngineV8::perManagerLoopIterationCleanup() { deleteUnusedValueWrappers(); } void ScriptEngineV8::disconnectSignalProxies() { _signalProxySetLock.lockForRead(); while (!_signalProxySet.empty()) { auto proxy = *_signalProxySet.begin(); _signalProxySetLock.unlock(); delete proxy; _signalProxySetLock.lockForRead(); } _signalProxySetLock.unlock(); } void ScriptEngineV8::deleteUnusedValueWrappers() { while (!_scriptValueWrappersToDelete.empty()) { auto wrapper = _scriptValueWrappersToDelete.dequeue(); wrapper->release(); } } void ScriptEngineV8::registerEnum(ScriptEngineScopeGuard* scopeGuard, const QString& enumName, QMetaEnum newEnum) { Q_ASSERT(scopeGuard); auto* scopeGuardV8 = dynamic_cast<ScriptEngineScopeGuardV8*>(scopeGuard); Q_ASSERT(scopeGuardV8); if (!newEnum.isValid()) { qCCritical(scriptengine_v8) << "registerEnum called on invalid enum with name " << enumName; return; } Q_ASSERT(_v8Isolate->IsCurrent()); v8::HandleScope handleScope(_v8Isolate); v8::Context::Scope contextScope(getContext()); for (int i = 0; i < newEnum.keyCount(); i++) { const char* keyName = newEnum.key(i); QString fullName = enumName + "." + keyName; registerValue(scopeGuardV8, fullName, V8ScriptValue(this, v8::Integer::New(_v8Isolate, newEnum.keyToValue(keyName)))); } } void ScriptEngineV8::registerValue(ScriptEngineScopeGuardV8* scopeGuard, const QString& valueName, V8ScriptValue value) { Q_ASSERT(scopeGuard); Q_ASSERT(QThread::currentThread() == thread()); Q_ASSERT(_v8Isolate->IsCurrent()); v8::HandleScope handleScope(_v8Isolate); v8::Local<v8::Context> context = getContext(); v8::Context::Scope contextScope(context); QStringList pathToValue = valueName.split("."); int partsToGo = pathToValue.length(); v8::Local<v8::Object> partObject = context->Global(); for (const auto& pathPart : pathToValue) { partsToGo--; v8::Local<v8::String> pathPartV8 = v8::String::NewFromUtf8(_v8Isolate, pathPart.toStdString().c_str(),v8::NewStringType::kNormal).ToLocalChecked(); v8::Local<v8::Value> currentPath; bool createProperty = false; if (!partObject->Get(context, pathPartV8).ToLocal(¤tPath)) { createProperty = true; } if (currentPath->IsUndefined()) { createProperty = true; } if (createProperty) { if (partsToGo > 0) { v8::Local<v8::Object> partValue = v8::Object::New(_v8Isolate); if (!partObject->Set(context, pathPartV8, partValue).FromMaybe(false)) { Q_ASSERT(false); } } else { if (!partObject->Set(context, pathPartV8, value.constGet()).FromMaybe(false)) { Q_ASSERT(false); } } } v8::Local<v8::Value> child; if (!partObject->Get(context, pathPartV8).ToLocal(&child)) { Q_ASSERT(false); } if (partsToGo > 0) { if (!child->IsObject()) { QString details = *v8::String::Utf8Value(_v8Isolate, child->ToDetailString(context).ToLocalChecked()); qCDebug(scriptengine_v8) << "ScriptEngineV8::registerValue: Part of path is not an object: " << pathPart << " details: " << details; Q_ASSERT(false); } partObject = v8::Local<v8::Object>::Cast(child); } } } void ScriptEngineV8::registerGlobalObject(ScriptEngineScopeGuard* scopeGuard, const QString& name, QObject* object, ScriptEngine::ValueOwnership) { Q_ASSERT(scopeGuard && dynamic_cast<ScriptEngineScopeGuardV8*>(scopeGuard)); Q_ASSERT(QThread::currentThread() == thread()); Q_ASSERT(_v8Isolate->IsCurrent()); v8::HandleScope handleScope(_v8Isolate); Q_ASSERT(_v8Isolate->IsCurrent()); v8::Local<v8::Context> context = getContext(); v8::Context::Scope contextScope(context); v8::Local<v8::Object> v8GlobalObject = context->Global(); v8::Local<v8::String> v8Name = v8::String::NewFromUtf8(_v8Isolate, name.toStdString().c_str()).ToLocalChecked(); if (!v8GlobalObject->Get(context, v8Name).IsEmpty()) { if (object) { V8ScriptValue value = ScriptObjectV8Proxy::newQObject(this, object, ScriptEngine::QtOwnership); if(!v8GlobalObject->Set(context, v8Name, value.get()).FromMaybe(false)) { Q_ASSERT(false); } } else { if(!v8GlobalObject->Set(context, v8Name, v8::Null(_v8Isolate)).FromMaybe(false)) { Q_ASSERT(false); } } } } void ScriptEngineV8::registerFunction(ScriptEngineScopeGuard* scopeGuard, const QString& name, ScriptEngine::FunctionSignature functionSignature, int numArguments) { Q_ASSERT(scopeGuard && dynamic_cast<ScriptEngineScopeGuardV8*>(scopeGuard)); Q_ASSERT(QThread::currentThread() == thread()); Q_ASSERT(_v8Isolate->IsCurrent()); v8::HandleScope handleScope(_v8Isolate); v8::Context::Scope contextScope(getContext()); auto scriptFun = newFunction(functionSignature, numArguments); globalObject().setProperty(name, scriptFun); } void ScriptEngineV8::registerFunction(ScriptEngineScopeGuard* scopeGuard, const QString& parent, const QString& name, ScriptEngine::FunctionSignature functionSignature, int numArguments) { Q_ASSERT(scopeGuard && dynamic_cast<ScriptEngineScopeGuardV8*>(scopeGuard)); Q_ASSERT(QThread::currentThread() == thread()); Q_ASSERT(_v8Isolate->IsCurrent()); v8::HandleScope handleScope(_v8Isolate); v8::Context::Scope contextScope(getContext()); ScriptValue object = globalObject().property(parent); if (object.isValid()) { ScriptValue scriptFun = newFunction(functionSignature, numArguments); object.setProperty(name, scriptFun); } } void ScriptEngineV8::registerGetterSetter(ScriptEngineScopeGuard* scopeGuard, const QString& name, ScriptEngine::FunctionSignature getter, ScriptEngine::FunctionSignature setter, const QString& parent) { Q_ASSERT(scopeGuard && dynamic_cast<ScriptEngineScopeGuardV8*>(scopeGuard)); Q_ASSERT(QThread::currentThread() == thread()); Q_ASSERT(_v8Isolate->IsCurrent()); v8::HandleScope handleScope(_v8Isolate); auto context = getContext(); v8::Context::Scope contextScope(context); ScriptValue setterFunction = newFunction(setter, 1); ScriptValue getterFunction = newFunction(getter); V8ScriptValue unwrappedGetter = ScriptValueV8Wrapper::fullUnwrap(this, getterFunction); V8ScriptValue unwrappedSetter = ScriptValueV8Wrapper::fullUnwrap(this, setterFunction); v8::PropertyDescriptor propertyDescriptor(unwrappedGetter.get(), unwrappedSetter.get()); if (!parent.isNull() && !parent.isEmpty()) { ScriptValue object = globalObject().property(parent); if (object.isValid()) { V8ScriptValue v8parent = ScriptValueV8Wrapper::fullUnwrap(this, object); Q_ASSERT(v8parent.get()->IsObject()); v8::Local<v8::Object> v8ParentObject = v8::Local<v8::Object>::Cast(v8parent.get()); v8::Local<v8::String> v8propertyName = v8::String::NewFromUtf8(_v8Isolate, name.toStdString().c_str()).ToLocalChecked(); v8::Local<v8::Object> v8ObjectToSetProperty; ScriptObjectV8Proxy *proxy = ScriptObjectV8Proxy::unwrapProxy(V8ScriptValue(this, v8ParentObject)); // If object is ScriptObjectV8Proxy, then setting property needs to be handled differently if (proxy) { v8ObjectToSetProperty = v8ParentObject->GetInternalField(2).As<v8::Object>(); } else { v8ObjectToSetProperty = v8ParentObject; } if (!v8ObjectToSetProperty->DefineProperty(context, v8propertyName, propertyDescriptor).FromMaybe(false)) { qCDebug(scriptengine_v8) << "DefineProperty failed for registerGetterSetter \"" << name << "\" for parent: \"" << parent << "\""; } } else { qCDebug(scriptengine_v8) << "Parent object \"" << parent << "\" for registerGetterSetter \"" << name << "\" is not valid: "; } } else { v8::Local<v8::String> v8propertyName = v8::String::NewFromUtf8(_v8Isolate, name.toStdString().c_str()).ToLocalChecked(); if (!context->Global()->DefineProperty(context, v8propertyName, propertyDescriptor).FromMaybe(false)) { qCDebug(scriptengine_v8) << "DefineProperty failed for registerGetterSetter \"" << name << "\" for global object"; } } } v8::Local<v8::Context> ScriptEngineV8::getContext() { #ifdef OVERTE_SCRIPT_USE_AFTER_DELETE_GUARD Q_ASSERT(!_wasDestroyed); #endif v8::EscapableHandleScope handleScope(_v8Isolate); Q_ASSERT(!_contexts.isEmpty()); return handleScope.Escape(_contexts.last().get()->toV8Value()); } const v8::Local<v8::Context> ScriptEngineV8::getConstContext() const { v8::EscapableHandleScope handleScope(_v8Isolate); Q_ASSERT(!_contexts.isEmpty()); return handleScope.Escape(_contexts.last().get()->toV8Value()); } // Stored objects are used to create global objects for evaluateInClosure void ScriptEngineV8::storeGlobalObjectContents() { if (areGlobalObjectContentsStored) {