Skip to content

Commit

Permalink
Expose Controller and ControllerEngine scripting apis through a proxy
Browse files Browse the repository at this point in the history
Since QJSEngine takes ownership of exposed QObjects it tried to delete
controller and controllerengine when QJSEngine was destroyed (i.e script reload
and mixxx shutdown). See https://bugreports.qt.io/browse/QTBUG-41171

This also has the benefit that we have a tighter control on what's exposed.
Since, Signals and slots, properties and children of object are available
as properties of the created QJSValue, lots of undesired things were leaked
into the JS engine.
  • Loading branch information
ferranpujolcamins committed Aug 18, 2018
1 parent 9be45f3 commit a0339e3
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 28 deletions.
1 change: 1 addition & 0 deletions build/depends.py
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,7 @@ def sources(self, build):
"controllers/delegates/midibytedelegate.cpp",
"controllers/delegates/midioptionsdelegate.cpp",
"controllers/engine/controllerengine.cpp",
"controllers/engine/controllerenginejsproxy.cpp",
"controllers/learningutils.cpp",
"controllers/midi/midimessage.cpp",
"controllers/midi/midiutils.cpp",
Expand Down
22 changes: 21 additions & 1 deletion src/controllers/controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class Controller : public QObject, ConstControllerPresetVisitor {
protected:
// The length parameter is here for backwards compatibility for when scripts
// were required to specify it.
Q_INVOKABLE void send(QList<int> data, unsigned int length = 0);
void send(QList<int> data, unsigned int length = 0);

// To be called in sub-class' open() functions after opening the device but
// before starting any input polling/processing.
Expand Down Expand Up @@ -162,10 +162,30 @@ class Controller : public QObject, ConstControllerPresetVisitor {
bool m_bLearning;
QTime m_userActivityInhibitTimer;

friend class ControllerJSProxy;
// accesses lots of our stuff, but in the same thread
friend class ControllerManager;
// For testing
friend class ControllerPresetValidationTest;
};

// An object of this class gets exposed to the JS engine, so the methods of this class
// constitute the api that is provided to scripts under "controller" object.
// See comments on ControllerEngineJSProxy.
class ControllerJSProxy: public QObject {
public:
ControllerJSProxy(Controller* m_pController)
: m_pController(m_pController) {
}

// The length parameter is here for backwards compatibility for when scripts
// were required to specify it.
Q_INVOKABLE void send(QList<int> data, unsigned int length = 0) {
m_pController->send(data, length);
}

private:
Controller* m_pController;
};

#endif
10 changes: 7 additions & 3 deletions src/controllers/engine/controllerengine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include "controllers/engine/controllerengine.h"

#include "controllers/engine/controllerenginejsproxy.h"
#include "controllers/controller.h"
#include "controllers/controllerdebug.h"
#include "control/controlobject.h"
Expand Down Expand Up @@ -192,16 +193,19 @@ void ControllerEngine::initializeScriptEngine() {

// Make this ControllerEngine instance available to scripts as 'engine'.
QJSValue engineGlobalObject = m_pEngine->globalObject();
engineGlobalObject.setProperty("engine", m_pEngine->newQObject(this));
ControllerEngineJSProxy* proxy = new ControllerEngineJSProxy(this);
engineGlobalObject.setProperty("engine", m_pEngine->newQObject(proxy));

if (m_pController) {
qDebug() << "Controller in script engine is:" << m_pController->getName();

ControllerJSProxy* controllerProxy = new ControllerJSProxy(m_pController);

// Make the Controller instance available to scripts
engineGlobalObject.setProperty("controller", m_pEngine->newQObject(m_pController));
engineGlobalObject.setProperty("controller", m_pEngine->newQObject(controllerProxy));

// ...under the legacy name as well
engineGlobalObject.setProperty("midi", m_pEngine->newQObject(m_pController));
engineGlobalObject.setProperty("midi", m_pEngine->newQObject(controllerProxy));
}

// m_pBaClass = new ByteArrayClass(m_pEngine);
Expand Down
50 changes: 26 additions & 24 deletions src/controllers/engine/controllerengine.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
class Controller;
class ControlObjectScript;
class ControllerEngine;
class ControllerEngineJSProxy;

// ScriptConnection represents a connection between
// a ControlObject and a script callback function that gets executed when
Expand Down Expand Up @@ -101,35 +102,35 @@ class ControllerEngine : public QObject {
void triggerScriptConnection(const ScriptConnection conn);

protected:
Q_INVOKABLE double getValue(QString group, QString name);
Q_INVOKABLE void setValue(QString group, QString name, double newValue);
Q_INVOKABLE double getParameter(QString group, QString name);
Q_INVOKABLE void setParameter(QString group, QString name, double newValue);
Q_INVOKABLE double getParameterForValue(QString group, QString name, double value);
Q_INVOKABLE void reset(QString group, QString name);
Q_INVOKABLE double getDefaultValue(QString group, QString name);
Q_INVOKABLE double getDefaultParameter(QString group, QString name);
Q_INVOKABLE QJSValue makeConnection(QString group, QString name,
double getValue(QString group, QString name);
void setValue(QString group, QString name, double newValue);
double getParameter(QString group, QString name);
void setParameter(QString group, QString name, double newValue);
double getParameterForValue(QString group, QString name, double value);
void reset(QString group, QString name);
double getDefaultValue(QString group, QString name);
double getDefaultParameter(QString group, QString name);
QJSValue makeConnection(QString group, QString name,
const QJSValue callback);
// DEPRECATED: Use makeConnection instead.
Q_INVOKABLE QJSValue connectControl(QString group, QString name,
QJSValue connectControl(QString group, QString name,
const QJSValue passedCallback,
bool disconnect = false);
// Called indirectly by the objects returned by connectControl
Q_INVOKABLE void trigger(QString group, QString name);
Q_INVOKABLE void log(QString message);
Q_INVOKABLE int beginTimer(int interval, QJSValue scriptCode, bool oneShot = false);
Q_INVOKABLE void stopTimer(int timerId);
Q_INVOKABLE void scratchEnable(int deck, int intervalsPerRev, double rpm,
double alpha, double beta, bool ramp = true);
Q_INVOKABLE void scratchTick(int deck, int interval);
Q_INVOKABLE void scratchDisable(int deck, bool ramp = true);
Q_INVOKABLE bool isScratching(int deck);
Q_INVOKABLE void softTakeover(QString group, QString name, bool set);
Q_INVOKABLE void softTakeoverIgnoreNextValue(QString group, QString name);
Q_INVOKABLE void brake(int deck, bool activate, double factor=1.0, double rate=1.0);
Q_INVOKABLE void spinback(int deck, bool activate, double factor=1.8, double rate=-10.0);
Q_INVOKABLE void softStart(int deck, bool activate, double factor=1.0);
void trigger(QString group, QString name);
void log(QString message);
int beginTimer(int interval, QJSValue scriptCode, bool oneShot = false);
void stopTimer(int timerId);
void scratchEnable(int deck, int intervalsPerRev, double rpm,
double alpha, double beta, bool ramp = true);
void scratchTick(int deck, int interval);
void scratchDisable(int deck, bool ramp = true);
bool isScratching(int deck);
void softTakeover(QString group, QString name, bool set);
void softTakeoverIgnoreNextValue(QString group, QString name);
void brake(int deck, bool activate, double factor=1.0, double rate=1.0);
void spinback(int deck, bool activate, double factor=1.8, double rate=-10.0);
void softStart(int deck, bool activate, double factor=1.0);

// Handler for timers that scripts set.
virtual void timerEvent(QTimerEvent *event);
Expand Down Expand Up @@ -219,6 +220,7 @@ class ControllerEngine : public QObject {
QFileSystemWatcher m_scriptWatcher;
QList<QString> m_lastScriptPaths;

friend class ControllerEngineJSProxy;
friend class ControllerEngineTest;
};

Expand Down
116 changes: 116 additions & 0 deletions src/controllers/engine/controllerenginejsproxy.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#include "controllerenginejsproxy.h"
#include "controllers/engine/controllerengine.h"

ControllerEngineJSProxy::ControllerEngineJSProxy(ControllerEngine* m_pEngine)
: m_pEngine(m_pEngine) {

}

ControllerEngineJSProxy::~ControllerEngineJSProxy() {

}

double ControllerEngineJSProxy::getValue(QString group, QString name) {
return m_pEngine->getValue(group, name);
}

void ControllerEngineJSProxy::setValue(QString group, QString name,
double newValue) {
m_pEngine->setValue(group, name, newValue);
}

double ControllerEngineJSProxy::getParameter(QString group, QString name) {
return m_pEngine->getParameter(group, name);
}

void ControllerEngineJSProxy::setParameter(QString group, QString name,
double newValue) {
m_pEngine->setParameter(group, name, newValue);
}

double ControllerEngineJSProxy::getParameterForValue(QString group,
QString name, double value) {
return m_pEngine->getParameterForValue(group, name, value);
}

void ControllerEngineJSProxy::reset(QString group, QString name) {
m_pEngine->reset(group, name);
}

double ControllerEngineJSProxy::getDefaultValue(QString group, QString name) {
return m_pEngine->getDefaultValue(group, name);
}

double ControllerEngineJSProxy::getDefaultParameter(QString group,
QString name) {
return m_pEngine->getDefaultParameter(group, name);
}

QJSValue ControllerEngineJSProxy::makeConnection(QString group, QString name,
const QJSValue callback) {
return m_pEngine->makeConnection(group, name, callback);
}

QJSValue ControllerEngineJSProxy::connectControl(QString group, QString name,
const QJSValue passedCallback, bool disconnect) {
return m_pEngine->connectControl(group, name, passedCallback, disconnect);
}

void ControllerEngineJSProxy::trigger(QString group, QString name) {
m_pEngine->trigger(group, name);
}

void ControllerEngineJSProxy::log(QString message) {
m_pEngine->log(message);
}

int ControllerEngineJSProxy::beginTimer(int interval, QJSValue scriptCode,
bool oneShot) {
return m_pEngine->beginTimer(interval, scriptCode, oneShot);
}

void ControllerEngineJSProxy::stopTimer(int timerId) {
m_pEngine->stopTimer(timerId);
}

void ControllerEngineJSProxy::scratchEnable(int deck, int intervalsPerRev,
double rpm, double alpha, double beta, bool ramp) {
m_pEngine->scratchEnable(deck, intervalsPerRev, rpm, alpha, beta, ramp);
}

void ControllerEngineJSProxy::scratchTick(int deck, int interval) {
m_pEngine->scratchTick(deck, interval);
}

void ControllerEngineJSProxy::scratchDisable(int deck, bool ramp) {
m_pEngine->scratchDisable(deck, ramp);
}

bool ControllerEngineJSProxy::isScratching(int deck) {
return m_pEngine->isScratching(deck);
}

void ControllerEngineJSProxy::softTakeover(QString group, QString name,
bool set) {
m_pEngine->softTakeover(group, name, set);
}

void ControllerEngineJSProxy::softTakeoverIgnoreNextValue(QString group,
QString name) {
m_pEngine->softTakeoverIgnoreNextValue(group, name);
}

void ControllerEngineJSProxy::brake(int deck, bool activate, double factor,
double rate) {
m_pEngine->brake(deck, activate, factor, rate);
}

void ControllerEngineJSProxy::spinback(int deck, bool activate, double factor,
double rate) {
m_pEngine->spinback(deck, activate, factor, rate);
}

void ControllerEngineJSProxy::softStart(int deck, bool activate,
double factor) {
m_pEngine->softStart(deck, activate, factor);
}
59 changes: 59 additions & 0 deletions src/controllers/engine/controllerenginejsproxy.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#ifndef CONTROLLERENGINEJSPROXY_H
#define CONTROLLERENGINEJSPROXY_H

#include <QObject>
#include <QJSValue>

class ControllerEngine;

// An object of this class gets exposed to the JS engine, so the methods of this class
// constitute the api that is provided to scripts under "engine" object.
//
// The implementation simply forwards its method calls to the ControllerEngine.
// We cannot expose ControllerEngine directly to the JS engine because the JS engine would take
// ownership of ControllerEngine. This is problematic when we reload a script file, because we
// destroy the existing JS engine to create a new one. Then, since the JS engine owns ControllerEngine
// it will try to delete it. See this Qt bug: https://bugreports.qt.io/browse/QTBUG-41171
class ControllerEngineJSProxy: public QObject {
Q_OBJECT
public:

ControllerEngineJSProxy(ControllerEngine* m_pEngine);

virtual ~ControllerEngineJSProxy();

Q_INVOKABLE double getValue(QString group, QString name);
Q_INVOKABLE void setValue(QString group, QString name, double newValue);
Q_INVOKABLE double getParameter(QString group, QString name);
Q_INVOKABLE void setParameter(QString group, QString name, double newValue);
Q_INVOKABLE double getParameterForValue(QString group, QString name, double value);
Q_INVOKABLE void reset(QString group, QString name);
Q_INVOKABLE double getDefaultValue(QString group, QString name);
Q_INVOKABLE double getDefaultParameter(QString group, QString name);
Q_INVOKABLE QJSValue makeConnection(QString group, QString name,
const QJSValue callback);
// DEPRECATED: Use makeConnection instead.
Q_INVOKABLE QJSValue connectControl(QString group, QString name,
const QJSValue passedCallback,
bool disconnect = false);
// Called indirectly by the objects returned by connectControl
Q_INVOKABLE void trigger(QString group, QString name);
Q_INVOKABLE void log(QString message);
Q_INVOKABLE int beginTimer(int interval, QJSValue scriptCode, bool oneShot = false);
Q_INVOKABLE void stopTimer(int timerId);
Q_INVOKABLE void scratchEnable(int deck, int intervalsPerRev, double rpm,
double alpha, double beta, bool ramp = true);
Q_INVOKABLE void scratchTick(int deck, int interval);
Q_INVOKABLE void scratchDisable(int deck, bool ramp = true);
Q_INVOKABLE bool isScratching(int deck);
Q_INVOKABLE void softTakeover(QString group, QString name, bool set);
Q_INVOKABLE void softTakeoverIgnoreNextValue(QString group, QString name);
Q_INVOKABLE void brake(int deck, bool activate, double factor=1.0, double rate=1.0);
Q_INVOKABLE void spinback(int deck, bool activate, double factor=1.8, double rate=-10.0);
Q_INVOKABLE void softStart(int deck, bool activate, double factor=1.0);

private:
ControllerEngine* m_pEngine;
};

#endif // CONTROLLERENGINEJSPROXY_H

0 comments on commit a0339e3

Please sign in to comment.