1710 lines · 74.5 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 "../ScriptEngineLogging.h"#include "../ScriptProgram.h"#include "../ScriptValue.h" #include "ScriptContextV8Wrapper.h"#include "ScriptObjectV8Proxy.h"#include "ScriptProgramV8Wrapper.h"#include "ScriptValueV8Wrapper.h"#include "V8Lambda.h"#include "ScriptEngineLoggingV8.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;} ScriptValue ScriptEngineV8::makeError(const ScriptValue& _other, const QString& type) {    if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {        return nullValue();    }    v8::Locker locker(_v8Isolate);    v8::Isolate::Scope isolateScope(_v8Isolate);    v8::HandleScope handleScope(_v8Isolate);    v8::Context::Scope contextScope(getContext());    return nullValue();}     //V8TODO    /*    auto other = _other;    if (_other.constGet()->IsString()) {        other = QScriptEngine::newObject();        other.setProperty("message", _other.toString());    }    auto proto = QScriptEngine::globalObject().property(type);    if (!proto.isFunction()) {        proto = QScriptEngine::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 = QScriptEngine::globalObject().property("Error");    }    if (other.engine() != this) {        // JS Objects are parented to a specific script engine instance        // -- this effectively ~clones it locally by routing through a QVariant and back        other = QScriptEngine::toScriptValue(other.toVariant());    }    // ~ var err = new Error(other.message)    auto err = proto.construct(V8ScriptValueList({ other.property("message") }));     // transfer over any existing properties    V8ScriptValueIterator it(other);    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();    }    v8::Locker locker(_v8Isolate);    v8::Isolate::Scope isolateScope(_v8Isolate);    v8::HandleScope handleScope(_v8Isolate);    v8::Context::Scope contextScope(getContext());    ScriptSyntaxCheckResultPointer syntaxCheck = program->checkSyntax();    //V8TODO    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());        //err.setProperty("stack", currentContext()->backtrace().join(ScriptManager::SCRIPT_BACKTRACE_SEP));        {            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();}/*ScriptValue ScriptEngineV8::lintScript(const QString& sourceCode, const QString& fileName, const int lineNumber) {    if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {        return nullValue();    }    //V8TODO    const auto syntaxCheck = checkSyntax(sourceCode);    if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) {        auto err = QScriptEngine::globalObject().property("SyntaxError").construct(V8ScriptValueList({ syntaxCheck.errorMessage() }));        err.setProperty("fileName", fileName);        err.setProperty("lineNumber", syntaxCheck.errorLineNumber());        err.setProperty("expressionBeginOffset", syntaxCheck.errorColumnNumber());        err.setProperty("stack", currentContext()->backtrace().join(ScriptManager::SCRIPT_BACKTRACE_SEP));        {            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, fileName, line, column);            err.setProperty("formatted", message);        }        return ScriptValue(new ScriptValueV8Wrapper(this, std::move(err)));    }    return undefinedValue();}*/   // Lambda/*ScriptValue ScriptEngineV8::newLambdaFunction(std::function<V8ScriptValue(V8ScriptContext*, ScriptEngineV8*)> operation,                                                 const V8ScriptValue& data,                                                 const ValueOwnership& ownership) {    v8::HandleScope handleScope(_v8Isolate);    v8::Context::Scope contextScope(getContext());    auto lambda = new Lambda(this, operation, data);    auto object = newQObject(lambda, ownership);    //V8TODO - I'm not sure if this works    auto call = object.property("call");    call.setPrototype(object);  // context->callee().prototype() === Lambda QObject    call.setData(ScriptValue(new ScriptValueV8Wrapper(this, data)));         // context->callee().data() will === data param    return call;}*/QString Lambda::toString() const {    v8::Locker locker(_engine->getIsolate());    v8::Isolate::Scope isolateScope(_engine->getIsolate());    v8::HandleScope handleScope(_engine->getIsolate());    v8::Context::Scope contextScope(_engine->getContext());    v8::Local<v8::String> string;    QString qString("");    if (_data.constGet()->ToString(_engine->getContext()).ToLocal(&string)) {        v8::String::Utf8Value utf8Value(_engine->getIsolate(), string);        qString = QString(*utf8Value);    }    //V8TODO it was data.isValid() originally    //I have no idea what happens here    return QString("[Lambda%1]").arg((!_data.constGet()->IsNullOrUndefined()) ? " " + qString : qString);} Lambda::~Lambda() {#ifdef DEBUG_JS_LAMBDA_FUNCS    qCDebug(scriptengine_v8) << "~Lambda"             << "this" << this;#endif} Lambda::Lambda(ScriptEngineV8* engine,               std::function<V8ScriptValue(ScriptEngineV8*)> operation,               V8ScriptValue data) :    _engine(engine),    _operation(operation), _data(data) {#ifdef DEBUG_JS_LAMBDA_FUNCS    qCDebug(scriptengine_v8) << "Lambda" << data.toString();#endif}V8ScriptValue Lambda::call() {    if (!_engine->IS_THREADSAFE_INVOCATION(__FUNCTION__)) {        return V8ScriptValue(_engine, v8::Null(_engine->getIsolate()));    }    // V8TODO: it needs to be done in entirely different way for V8    Q_ASSERT(false);    //return _operation(_engine->getContext(), _engine);    //return V8ScriptValue(_engine->getIsolate(), v8::Null(_engine->getIsolate()));    //return operation(static_cast<QScriptEngine*>(engine)->currentContext(), engine);} #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:        // V8TODO --single-threaded is to check if it fixes random crashes        // --jitless - might improve debugging performance due to no JIT?        // --assert-types         v8::V8::InitializeICU();        v8::V8::SetFlagsFromString("--stack-size=256 --verify-heap --assert-types");        //v8::V8::SetFlagsFromString("--stack-size=256 --single-threaded");        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        //v8::UniquePersistent<v8::Value> null = v8::UniquePersistent<v8::Value>(_v8Isolate, v8::Null(_v8Isolate));        //_nullValue = ScriptValue(new ScriptValueV8Wrapper(this, std::move(null)));         //V8ScriptValue undefined = v8::UniquePersistent<v8::Value>(_v8Isolate,v8::Undefined(_v8Isolate));        //_undefinedValue = ScriptValue(new ScriptValueV8Wrapper(this, std::move(undefined)));         // V8TODO:        //QScriptEngine::setProcessEventsInterval(MSECS_PER_SECOND);    }     //_currentThread = QThread::currentThread();     //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);*/        //moveToThread(scriptManager->thread());        //setThread(scriptManager->thread());    //}} void ScriptEngineV8::registerEnum(const QString& enumName, QMetaEnum newEnum) {    if (!newEnum.isValid()) {        qCCritical(scriptengine_v8) << "registerEnum called on invalid enum with name " << enumName;        return;    }    v8::Locker locker(_v8Isolate);    v8::Isolate::Scope isolateScope(_v8Isolate);    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(fullName, V8ScriptValue(this, v8::Integer::New(_v8Isolate, newEnum.keyToValue(keyName))));    }} void ScriptEngineV8::registerValue(const QString& valueName, V8ScriptValue value) {    if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING        qCDebug(scriptengine_v8) << "*** WARNING *** ScriptEngineV8::registerValue() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]";#endif        QMetaObject::invokeMethod(this, "registerValue",                                  Q_ARG(const QString&, valueName),                                  Q_ARG(V8ScriptValue, value));        return;    }    v8::Locker locker(_v8Isolate);    v8::Isolate::Scope isolateScope(_v8Isolate);    v8::HandleScope handleScope(_v8Isolate);    v8::Local<v8::Context> context = getContext();    v8::Context::Scope contextScope(getContext());    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) {                //This was commented out                //QObject *object = new QObject;                v8::Local<v8::Object> partValue = v8::Object::New(_v8Isolate);  //newQObject(object, QScriptEngine::ScriptOwnership);                //V8ScriptValue partValue = QScriptEngine::newArray();  //newQObject(object, QScriptEngine::ScriptOwnership);                if (!partObject->Set(context, pathPartV8, partValue).FromMaybe(false)) {                    Q_ASSERT(false);                }            } else {                //partObject = currentPath->ToObject();                //V8TODO: do these still happen if asserts are disabled?                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(const QString& name, QObject* object) {    if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING        qCDebug(scriptengine_v8) << "*** WARNING *** ScriptEngineV8::registerGlobalObject() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "]  name:" << name;#endif        QMetaObject::invokeMethod(this, "registerGlobalObject",                                  Q_ARG(const QString&, name),                                  Q_ARG(QObject*, object));        return;    }#ifdef THREAD_DEBUGGING    qCDebug(scriptengine_v8) << "ScriptEngineV8::registerGlobalObject() called on thread [" << QThread::currentThread() << "] name:" << name;#endif    /*bool is_isolate_exit_needed = false;    if(!_v8Isolate->IsCurrent() && !_v8Locker) {        // V8TODO: Theoretically only script thread should access this, so it should be safe        _v8Locker.reset(new v8::Locker(_v8Isolate));        _v8Isolate->Enter();        is_isolate_exit_needed = true;    }*/    //{    v8::Locker locker(_v8Isolate);    v8::Isolate::Scope isolateScope(_v8Isolate);    v8::HandleScope handleScope(_v8Isolate);    Q_ASSERT(_v8Isolate->IsCurrent());    v8::Local<v8::Context> context = getContext();    v8::Context::Scope contextScope(context);    v8::Local<v8::Object> v8GlobalObject = getContext()->Global();    v8::Local<v8::String> v8Name = v8::String::NewFromUtf8(_v8Isolate, name.toStdString().c_str()).ToLocalChecked();     // V8TODO: Is IsEmpty check enough or IsValid is needed too?    if (!v8GlobalObject->Get(getContext(), v8Name).IsEmpty()) {        if (object) {            V8ScriptValue value = ScriptObjectV8Proxy::newQObject(this, object, ScriptEngine::QtOwnership);            if(!v8GlobalObject->Set(getContext(), v8Name, value.get()).FromMaybe(false)) {                Q_ASSERT(false);            }        } else {            if(!v8GlobalObject->Set(getContext(), v8Name, v8::Null(_v8Isolate)).FromMaybe(false)) {                Q_ASSERT(false);            }        }    }    //}    /*if (is_isolate_exit_needed) {        _v8Isolate->Exit();        _v8Locker.reset(nullptr);    }*/} void ScriptEngineV8::registerFunction(const QString& name, ScriptEngine::FunctionSignature functionSignature, int numArguments) {    //if (QThread::currentThread() != ) {    //}    if (QThread::currentThread() != thread()) {#ifdef THREAD_DEBUGGING        qCDebug(scriptengine_v8) << "*** WARNING *** ScriptEngineV8::registerFunction() called on wrong thread [" << QThread::currentThread() << "], invoking on correct thread [" << thread() << "] name:" << name;#endif        QMetaObject::invokeMethod(this, "registerFunction",                                  Q_ARG(const QString&, name),                                  Q_ARG(ScriptEngine::FunctionSignature, functionSignature),                                  Q_ARG(int, numArguments));        return;    }#ifdef THREAD_DEBUGGING    qCDebug(scriptengine_v8) << "ScriptEngineV8::registerFunction() called on thread [" << QThread::currentThread() << "] name:" << name;#endif     /*bool is_isolate_exit_needed = false;    if(!_v8Isolate->IsCurrent() && !_v8Locker) {        // V8TODO: Theoretically only script thread should access this, so it should be safe        _v8Locker.reset(new v8::Locker(_v8Isolate));        _v8Isolate->Enter();        is_isolate_exit_needed = true;    }    {*/        //auto scriptFun = static_cast<ScriptValueV8Wrapper*>(newFunction(functionSignature, numArguments).ptr())->toV8Value().constGet();    v8::Locker locker(_v8Isolate);

Lines 1–500 of 1710.