Deep copy of a QScriptValue as Global Object

1.5k views Asked by At

I have a program using QtScript for some automation. I have added a bunch of C++ functions and classes to the global scope of the script engine so that scripts can access them, like so:

QScriptValue fun = engine->newFunction( systemFunc );
engine->globalObject().setProperty( "system", fun );

I would like to be able to run multiple scripts in succession, each with a fresh global state. So if one script sets a global variable, like

myGlobalVar = "stuff";

I want that variable to be erased before the next script runs. My method for doing this is to make a deep copy of the script engine's Global Object, and then restore it when a script finishes running. But the deep copies aren't working, since my system function suddenly breaks with the error:

TypeError: Result of expression 'system' [[object Object]] is not a function.

Here is my deep copy function, adapted from:
http://qt.gitorious.org/qt-labs/scxml/blobs/master/src/qscxml.cpp

QScriptValue copyObject( const QScriptValue& obj, QString level = "" )
{
    if( obj.isObject() || obj.isArray() ) {
        QScriptValue copy = obj.isArray() ? obj.engine()->newArray() : obj.engine()->newObject();
        copy.setData( obj.data() );
        QScriptValueIterator it(obj);
        while(it.hasNext()) {
            it.next();
            qDebug() << "copying" + level + "." + it.name();
            if( it.flags() & QScriptValue::SkipInEnumeration )
                 continue;
            copy.setProperty( it.name(), copyObject(it.value(), level + "." + it.name()) );
        }
        return copy;
    }

    return obj;
}

(the SkipInEnumeration was put in to avoid an infinite loop)

EDIT: Part of the problem, I think, is that in the debugger (QScriptEngineDebugger), the functions and constructors I've added are supposed to appear as type Function, but after copying, they appear as type Object. I haven't yet found a good way of creating a new Function that duplicates an existing one (QScriptEngine::newFunction takes an actual function pointer).

2

There are 2 answers

0
Dave Ceddia On BEST ANSWER

I got it working. Here's the solution in case it's useful for anyone else:

QScriptValue copyObject( const QScriptValue& obj)
{
    if( (obj.isObject() || obj.isArray()) && !obj.isFunction() ) {
        QScriptValue copy = obj.isArray() ? obj.engine()->newArray() : obj.engine()->newObject();
        copy.setData( obj.data() );
        QScriptValueIterator it(obj);
        while(it.hasNext()) {
            it.next();
            copy.setProperty( it.name(), copyObject(it.value()) );
        }
        return copy;
    }

    return obj;
}

The important part is the addition of the !obj.isFunction() check, which will just copy Functions as they are, and not do a deep copy. The subtlety here is that isObject() will return true if the item is a Function, which we don't want. This is documented in the Qt docs and I stumbled upon it a few moments ago.

Also, this check removed the need to avoid copying items marked SkipInEnumeration. The infinite loop is fixed by checking for functions and copying them as-is. Leaving in the SkipInEnumeration actually broke some other stuff, like the eval function and a bunch of other built-ins.

1
Lukas W On

For the purpose of making multi-threading available within QtScript, I needed a way to deep-copy QScriptValue objects to another QScriptEngine and stumbled upon this question. Unfortunately, Dave's code was not sufficient for this task, and has a few problems even when copying within only one QScriptEngine. So I needed a more sophisticated version. These are the problems I had to address in my solution:

  1. Dave's code results in a stack overflow when an object contains a reference to itself.
  2. I wanted my solution to respect references to objects so that multiple references to one object would not cause the referenced object to be copied more than once.
  3. As the deep-copied QScriptValue objects are used in a different QScriptEngine than their source objects, I needed a way to truly copy e.g. functions as well.

It might be useful for someone else, so here's the code I came up with:

class ScriptCopier
{
public:
    ScriptCopier(QScriptEngine& toEngine)
        : m_toEngine(toEngine) {}

    QScriptValue copy(const QScriptValue& obj);

    QScriptEngine& m_toEngine;
    QMap<quint64, QScriptValue> copiedObjs;
};


QScriptValue ScriptCopier::copy(const QScriptValue& obj)
{
    QScriptEngine& engine = m_toEngine;

    if (obj.isUndefined()) {
        return QScriptValue(QScriptValue::UndefinedValue);
    }
    if (obj.isNull()) {
        return QScriptValue(QScriptValue::NullValue);
    }

    // If we've already copied this object, don't copy it again.
    QScriptValue copy;
    if (obj.isObject())
    {
        if (copiedObjs.contains(obj.objectId()))
        {
            return copiedObjs.value(obj.objectId());
        }
        copiedObjs.insert(obj.objectId(), copy);
    }

    if (obj.isQObject())
    {
        copy = engine.newQObject(copy, obj.toQObject());
        copy.setPrototype(this->copy(obj.prototype()));
    }
    else if (obj.isQMetaObject())
    {
        copy = engine.newQMetaObject(obj.toQMetaObject());
    }
    else if (obj.isFunction())
    {
        // Calling .toString() on a pure JS function returns
        // the function's source code.
        // On a native function however toString() returns
        // something like "function() { [native code] }".
        // That's why we do a syntax check on the code.

        QString code = obj.toString();
        auto syntaxCheck = engine.checkSyntax(code);

        if (syntaxCheck.state() == syntaxCheck.Valid)
        {
            copy = engine.evaluate(QString() + "(" + code + ")");
        }
        else if (code.contains("[native code]"))
        {
            copy.setData(obj.data());
        }
        else
        {
            // Do error handling…
        }

    }
    else if (obj.isVariant())
    {
        QVariant var = obj.toVariant();
        copy = engine.newVariant(copy, obj.toVariant());
    }
    else if (obj.isObject() || obj.isArray())
    {
        if (obj.isObject()) {
            if (obj.scriptClass()) {
                copy = engine.newObject(obj.scriptClass(), this->copy(obj.data()));
            } else {
                copy = engine.newObject();
            }
        } else {
            copy = engine.newArray();
        }
        copy.setPrototype(this->copy(obj.prototype()));

        QScriptValueIterator it(obj);
        while ( it.hasNext())
        {
            it.next();

            const QString& name = it.name();
            const QScriptValue& property = it.value();

            copy.setProperty(name, this->copy(property));
        }
    }
    else
    {
        // Error handling…
    }

    return copy;
}

Note: This code uses the Qt-internal method QScriptValue::objectId().