10 KiB
HarmonyPremium: QtScript / QScriptEngine integration
This document captures the current understanding of Harmony Premium's QtScript integration as observed in RE/HarmonyPremium.exe.i64.
Key takeaways (high signal)
- Harmony has an internal
QObjectwrapper around the scripting runtime namedSCR_ScriptEngineImpl. - A higher-level manager
SCR_ScriptManagerconstructsSCR_ScriptEngineImpl, registers a large set of interfaces (viaSCR_Scripting_registerInterfacesat0x140914850), then injects itself into script as a global object named"___scriptManager___". - Two global native functions are installed into the script global object:
include(filename)→ forwards to a virtual method on the script/scene manager interface (and tries to execute the included file in the caller's scope by temporarily adopting the parent context's activation/thisobjects).require(filename)→ resolves/remaps the path, loads the file, wraps it in a JavaScript closure that returnsexports, evaluates it, and returns theexportsobject (or returns the uncaught exception).
- There are small helper routines used throughout Harmony to bind C++ into the scripting global object:
- bind
QObject*viaQScriptEngine::newQObject(...)thenQScriptValue::setProperty(...) - bind native callbacks via
QScriptEngine::newFunction(...)thenQScriptValue::setProperty(...)
- bind
Main components
SCR_ScriptEngineImpl
Observed in SCR_ScriptEngineImpl_ctor (0x14082AEC0).
What it does:
- Is a
QObjectsubclass (hasqt_metacast, metaobject data, etc). - Allocates an internal
QScriptEngineand stores it in the object (other methods access it via a field at byte offset+40fromthis). - Installs a custom
QScriptEngineAgent(SCR_ScriptEngineImpl::ScriptAgent) and sets the engine's process-events interval from preference key"SCR_EVENT_PROCESSING_INTERVAL". - Maintains a recursion depth counter (checked against
12) used to prevent runaway recursion / re-entrancy during evaluation and calls.
Evaluation-related methods (renamed in the IDA DB during this session):
SCR_ScriptEngineImpl_callWithTempGlobal(0x14082B330)- Temporarily swaps the engine global object (
setGlobalObject), calls aQScriptValuecallable (QScriptValue::call), restores the old global, and maps uncaught exceptions into the return value.
- Temporarily swaps the engine global object (
SCR_ScriptEngineImpl_evalInNewGlobal(0x14082BEB0)- Creates a fresh global object (
newObject), sets its prototype to a provided prototype or to the previous global, swaps it in viasetGlobalObject, evaluates aQStringscript body, restores the previous global. - On unwind: triggers
collectGarbage()when leaving the outermost evaluation.
- Creates a fresh global object (
SCR_ScriptEngineImpl_evalFile(0x14082C590)- Reads a script file as UTF-8 and evaluates it.
- Writes
__file__into the current scope chain before evaluation (see helper below). - Uses
SCR_ScriptEngineImpl_evalInNewGlobalwhen not already nested; otherwise evaluates directly in the existing global object.
SCR_ScriptManager
Observed in SCR_ScriptManager_ctor (0x14081FD60).
What it does:
- Constructs
SCR_ScriptEngineImpl. - Calls a large interface registration routine (
SCR_Scripting_registerInterfaces,0x140914850) which populates the scripting environment with many bindings. - Injects itself into script as global property
"___scriptManager___":- Done via
SCR_ScriptRuntime_defineGlobalQObject(0x14082CB50)
- Done via
- Installs native global functions:
"include"bound toQS_include(0x1408246C0)"require"bound toQS_require(0x140827D60)- Done via
SCR_ScriptRuntime_defineGlobalFunction(0x14082CAC0)
Builtins: include() / require()
include(filename) (QS_include, 0x1408246C0)
Behavior (from decompilation):
- Validates
argumentCount == 1and that the argument is a string. - Retrieves the global
"___scriptManager___"object, converts it to aQObject*, andQMetaObject::casts it to the expected interface type (error message says “scene manager interface”). - Calls a virtual function at vtable offset
+88on that interface, passing the filename string. - If a parent script context exists, it temporarily sets the current context's activation object and this-object to the parent context's ones while doing the include, then restores them.
require(filename) (QS_require, 0x140827D60)
Behavior (from decompilation):
- Validates
argumentCount == 1and that the argument is a string. - Retrieves/casts global
"___scriptManager___"as above. - Calls a virtual function at vtable offset
+96to resolve the requested module path, then applies an OS remap (oswRemapPath::RemapPath2). - If the resolved path is a directory, appends
"/index.js". - Reads the file and wraps it into a JS closure of the form:
(function()
{
var exports = {};
var __file__ = "<resolved path>";
// file contents
return exports;
})
- Evaluates the wrapper; if it yields a function, calls it and returns the resulting
exports. - If the engine has an uncaught exception, returns the uncaught exception value.
Helper routines used for global bindings
Define global QObject (SCR_ScriptRuntime_defineGlobalQObject, 0x14082CB50)
- Gets
engine->globalObject() - Wraps a
QObject*usingQScriptEngine::newQObject(...) - Writes it into the global object with
QScriptValue::setProperty(...)- The property flags argument is passed as
2048in the decompiler output.
- The property flags argument is passed as
Define global native function (SCR_ScriptRuntime_defineGlobalFunction, 0x14082CAC0)
- Gets
engine->globalObject() - Creates a
QScriptValuefunction usingQScriptEngine::newFunction(callback) - Writes it into the global object via
setProperty(..., flags=2048)
Set __file__ in scope chain (SCR_ScriptRuntime_setScopeFileVar, 0x14082CCB0)
- Fetches
QScriptEngine::currentContext()->scopeChain() - Sets
scopeChain()[0].__file__ = <path>(property flags passed as2048)
Accessors used throughout registration
Several small helpers reveal how Harmony threads the live QScriptEngine* and other host pointers through its scripting subsystem. In decompiler output these are simple pointer-chasing accessors:
SCR_ScriptRuntime_getEngine(0x14082BCD0)- Returns
*(*a1 + 40)(i.e., reads aQScriptEngine*stored at byte offset+40from the underlying script-engine wrapper object).
- Returns
SCR_ScriptRuntime_getHostContext(0x14082CCA0)- Returns
*(*a1 + 24)(a host/session/context pointer used to initialize many interface objects via a virtual method at vtable offset+88).
- Returns
SCR_ScriptEngineImpl_setSceneInterface(0x14082CDA0)- Writes to
(*a1)+32. Called fromSCR_ScriptManager_ctorafter interface registration; in the observed init flow it stores the singleton-like"scene"interface pointer (also injected as globalScene/scene).
- Writes to
Why this matters for our injection goal
The above shows Harmony already has an established pattern for binding objects into the script global object (via newQObject + setProperty). The cleanest "natural" hook points to attach our own module object are:
- Right after
SCR_ScriptManager_ctorinjects"___scriptManager___"and registersinclude/require, or - Anywhere we can get a pointer to the live
QScriptEngineused for scripting (e.g. from aSCR_ScriptEngineImplinstance; other methods access it via the field at byte offset+40).
Next step: dig deeper into SCR_Scripting_registerInterfaces to map each global binding back to its concrete C++ type and owning subsystem (useful for choosing robust runtime hook points).
Global namespace installed by Harmony
This section summarizes what SCR_Scripting_registerInterfaces (0x140914850) and SCR_Scripting_registerUiTypes (0x140913920) install into the engine.
SCR_Scripting_registerInterfaces (0x140914850): core global objects
Direct globalObject().setProperty(<name>, newQObject(...), flags=2048) calls are visible in the decompilation for (non-exhaustive, but high-confidence) global names:
Scene(capitalized; created from a singleton-like object named"scene")specialFoldersexports(set to a fresh script object created viaQScriptEngine::newObject)aboutAction(registered at least once; later may be rebound if an action manager exists)DrawingelementfileMapperKeyModifiersMessageLogpreferencesscene(lowercase; appears to alias the same underlying object asScene)SystemDrawingToolsTimelineMarkerSettings(an instance ofSCR_SettingsInterface)columnnodeselectionPaletteObjectManagerframe(only whenWHO_Identity::family() != 4)compositionOrder(backed by an object constructed with name"composition")copyPasteexporterfuncMovieImportrenderwaypointBackdropsoundstateUtil(backed by an object constructed with name"TB_StateUtil")
Also observed inside this function:
- Multiple
QMetaTypecustom conversions registered viaQScriptEngine::registerCustomType(...)(exact C++ types currently appear asunk_140F7....in the decompiler output). - Additional interface initializers called (e.g.
SCR_DrawingKey::registerInterface(engine)), plus many helper routines likesub_14096.../sub_14097...that likely register more types/enums.
SCR_Scripting_registerUiTypes (0x140913920): UI/widget constructors and helpers
This function installs a number of QMetaObject-backed constructors into the global object using newFunction(...) + newQMetaObject(...) + setProperty(..., flags=2048). Observed names include:
DialogLabelButtonLineEditNumberEditDateEditTimeEditTextEditSpinBoxCheckBoxRadioButtonComboBoxGroupBoxSliderImportDrawingDlg(conditional:WHO_Identity::family() != 4)ExportVideoDlg(conditional:WHO_Identity::family() != 4)
It also creates a MessageBox object with methods:
MessageBox.warningMessageBox.informationMessageBox.critical
And it registers several additional script-visible helpers:
DateEditEnumFileAccessDirSpecFileDialog(conditional on ana2parameter in the decompiler output)Input(conditional on ana2parameter in the decompiler output)