toon-boom-extension-framework/docs/HarmonyPremium_QtScript.md

211 lines
10 KiB
Markdown

# 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 `QObject` wrapper around the scripting runtime named **`SCR_ScriptEngineImpl`**.
- A higher-level manager **`SCR_ScriptManager`** constructs `SCR_ScriptEngineImpl`, registers a large set of interfaces (via `SCR_Scripting_registerInterfaces` at `0x140914850`), 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/`this` objects).
- **`require(filename)`** → resolves/remaps the path, loads the file, wraps it in a JavaScript closure that returns `exports`, evaluates it, and returns the `exports` object (or returns the uncaught exception).
- There are small helper routines used throughout Harmony to bind C++ into the scripting global object:
- bind `QObject*` via `QScriptEngine::newQObject(...)` then `QScriptValue::setProperty(...)`
- bind native callbacks via `QScriptEngine::newFunction(...)` then `QScriptValue::setProperty(...)`
## Main components
### `SCR_ScriptEngineImpl`
Observed in `SCR_ScriptEngineImpl_ctor` (`0x14082AEC0`).
What it does:
- Is a `QObject` subclass (has `qt_metacast`, metaobject data, etc).
- Allocates an internal `QScriptEngine` and stores it in the object (other methods access it via a field at **byte offset `+40`** from `this`).
- 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 a `QScriptValue` callable (`QScriptValue::call`), restores the old global, and maps uncaught exceptions into the return value.
- `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 via `setGlobalObject`, evaluates a `QString` script body, restores the previous global.
- On unwind: triggers `collectGarbage()` when leaving the outermost evaluation.
- `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_evalInNewGlobal` when 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`)
- Installs native global functions:
- `"include"` bound to `QS_include` (`0x1408246C0`)
- `"require"` bound to `QS_require` (`0x140827D60`)
- Done via `SCR_ScriptRuntime_defineGlobalFunction` (`0x14082CAC0`)
## Builtins: `include()` / `require()`
### `include(filename)` (`QS_include`, `0x1408246C0`)
Behavior (from decompilation):
- Validates `argumentCount == 1` and that the argument is a string.
- Retrieves the global `"___scriptManager___"` object, converts it to a `QObject*`, and `QMetaObject::cast`s it to the expected interface type (error message says “scene manager interface”).
- Calls a virtual function at vtable offset `+88` on 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 == 1` and that the argument is a string.
- Retrieves/casts global `"___scriptManager___"` as above.
- Calls a virtual function at vtable offset `+96` to 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:
```javascript
(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*` using `QScriptEngine::newQObject(...)`
- Writes it into the global object with `QScriptValue::setProperty(...)`
- The property flags argument is passed as `2048` in the decompiler output.
### Define global native function (`SCR_ScriptRuntime_defineGlobalFunction`, `0x14082CAC0`)
- Gets `engine->globalObject()`
- Creates a `QScriptValue` function using `QScriptEngine::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 as `2048`)
### 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 a `QScriptEngine*` stored at **byte offset `+40`** from the underlying script-engine wrapper object).
- `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`).
- `SCR_ScriptEngineImpl_setSceneInterface` (`0x14082CDA0`)
- Writes to `(*a1)+32`. Called from `SCR_ScriptManager_ctor` after interface registration; in the observed init flow it stores the singleton-like `"scene"` interface pointer (also injected as global `Scene`/`scene`).
## 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_ctor` injects `"___scriptManager___"` and registers `include/require`, or
- Anywhere we can get a pointer to the live `QScriptEngine` used for scripting (e.g. from a `SCR_ScriptEngineImpl` instance; 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"`)
- `specialFolders`
- `exports` (set to a fresh script object created via `QScriptEngine::newObject`)
- `about`
- `Action` (registered at least once; later may be rebound if an action manager exists)
- `Drawing`
- `element`
- `fileMapper`
- `KeyModifiers`
- `MessageLog`
- `preferences`
- `scene` (lowercase; appears to alias the same underlying object as `Scene`)
- `System`
- `DrawingTools`
- `TimelineMarker`
- `Settings` (an instance of `SCR_SettingsInterface`)
- `column`
- `node`
- `selection`
- `PaletteObjectManager`
- `frame` (only when `WHO_Identity::family() != 4`)
- `compositionOrder` (backed by an object constructed with name `"composition"`)
- `copyPaste`
- `exporter`
- `func`
- `MovieImport`
- `render`
- `waypoint`
- `Backdrop`
- `sound`
- `stateUtil` (backed by an object constructed with name `"TB_StateUtil"`)
Also observed inside this function:
- Multiple `QMetaType` custom conversions registered via `QScriptEngine::registerCustomType(...)` (exact C++ types currently appear as `unk_140F7....` in the decompiler output).
- Additional interface initializers called (e.g. `SCR_DrawingKey::registerInterface(engine)`), plus many helper routines like `sub_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:
- `Dialog`
- `Label`
- `Button`
- `LineEdit`
- `NumberEdit`
- `DateEdit`
- `TimeEdit`
- `TextEdit`
- `SpinBox`
- `CheckBox`
- `RadioButton`
- `ComboBox`
- `GroupBox`
- `Slider`
- `ImportDrawingDlg` (conditional: `WHO_Identity::family() != 4`)
- `ExportVideoDlg` (conditional: `WHO_Identity::family() != 4`)
It also creates a `MessageBox` object with methods:
- `MessageBox.warning`
- `MessageBox.information`
- `MessageBox.critical`
And it registers several additional script-visible helpers:
- `DateEditEnum`
- `FileAccess`
- `DirSpec`
- `FileDialog` (conditional on an `a2` parameter in the decompiler output)
- `Input` (conditional on an `a2` parameter in the decompiler output)