1645 lines · 70.1 KB · C++
RawDownload
////  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(&currentPath)) {            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.