Deep copy of a QScriptValue as Global Object
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 sc开发者_如何学编程ripts 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.cppQScriptValue 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).
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:
- Dave's code results in a stack overflow when an object contains a reference to itself.
- 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.
- As the deep-copied
QScriptValue
objects are used in a differentQScriptEngine
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()
.
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.
精彩评论