ScriptEngine.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. #include "ScriptEngine.h"
  2. #include "WallpaperEngine/Logging/Log.h"
  3. #include <sstream>
  4. using namespace WallpaperEngine::Scripting;
  5. using namespace WallpaperEngine::Data::Model;
  6. static std::unique_ptr<ScriptEngine> sScriptEngine;
  7. ScriptEngine& ScriptEngine::instance () {
  8. if (!sScriptEngine) {
  9. sScriptEngine = std::unique_ptr<ScriptEngine> (new ScriptEngine ());
  10. }
  11. return *sScriptEngine;
  12. }
  13. ScriptEngine::ScriptEngine () {
  14. this->m_runtime = JS_NewRuntime ();
  15. if (!this->m_runtime) {
  16. sLog.error ("ScriptEngine: Failed to create JS runtime");
  17. return;
  18. }
  19. this->m_context = JS_NewContext (this->m_runtime);
  20. if (!this->m_context) {
  21. sLog.error ("ScriptEngine: Failed to create JS context");
  22. JS_FreeRuntime (this->m_runtime);
  23. this->m_runtime = nullptr;
  24. return;
  25. }
  26. }
  27. ScriptEngine::~ScriptEngine () {
  28. if (this->m_context) {
  29. JS_FreeContext (this->m_context);
  30. }
  31. if (this->m_runtime) {
  32. JS_FreeRuntime (this->m_runtime);
  33. }
  34. }
  35. JSValue ScriptEngine::dynamicValueToJS (const DynamicValue& value) const {
  36. JSContext* ctx = this->m_context;
  37. switch (value.getType ()) {
  38. case DynamicValue::Float:
  39. return JS_NewFloat64 (ctx, value.getFloat ());
  40. case DynamicValue::Int:
  41. return JS_NewInt32 (ctx, value.getInt ());
  42. case DynamicValue::Boolean:
  43. return JS_NewBool (ctx, value.getBool ());
  44. case DynamicValue::Vec2: {
  45. JSValue obj = JS_NewObject (ctx);
  46. JS_SetPropertyStr (ctx, obj, "x", JS_NewFloat64 (ctx, value.getVec2 ().x));
  47. JS_SetPropertyStr (ctx, obj, "y", JS_NewFloat64 (ctx, value.getVec2 ().y));
  48. return obj;
  49. }
  50. case DynamicValue::Vec3: {
  51. JSValue obj = JS_NewObject (ctx);
  52. JS_SetPropertyStr (ctx, obj, "x", JS_NewFloat64 (ctx, value.getVec3 ().x));
  53. JS_SetPropertyStr (ctx, obj, "y", JS_NewFloat64 (ctx, value.getVec3 ().y));
  54. JS_SetPropertyStr (ctx, obj, "z", JS_NewFloat64 (ctx, value.getVec3 ().z));
  55. return obj;
  56. }
  57. case DynamicValue::Vec4: {
  58. JSValue obj = JS_NewObject (ctx);
  59. JS_SetPropertyStr (ctx, obj, "x", JS_NewFloat64 (ctx, value.getVec4 ().x));
  60. JS_SetPropertyStr (ctx, obj, "y", JS_NewFloat64 (ctx, value.getVec4 ().y));
  61. JS_SetPropertyStr (ctx, obj, "z", JS_NewFloat64 (ctx, value.getVec4 ().z));
  62. JS_SetPropertyStr (ctx, obj, "w", JS_NewFloat64 (ctx, value.getVec4 ().w));
  63. return obj;
  64. }
  65. case DynamicValue::IVec2: {
  66. JSValue obj = JS_NewObject (ctx);
  67. JS_SetPropertyStr (ctx, obj, "x", JS_NewInt32 (ctx, value.getIVec2 ().x));
  68. JS_SetPropertyStr (ctx, obj, "y", JS_NewInt32 (ctx, value.getIVec2 ().y));
  69. return obj;
  70. }
  71. case DynamicValue::IVec3: {
  72. JSValue obj = JS_NewObject (ctx);
  73. JS_SetPropertyStr (ctx, obj, "x", JS_NewInt32 (ctx, value.getIVec3 ().x));
  74. JS_SetPropertyStr (ctx, obj, "y", JS_NewInt32 (ctx, value.getIVec3 ().y));
  75. JS_SetPropertyStr (ctx, obj, "z", JS_NewInt32 (ctx, value.getIVec3 ().z));
  76. return obj;
  77. }
  78. case DynamicValue::IVec4: {
  79. JSValue obj = JS_NewObject (ctx);
  80. JS_SetPropertyStr (ctx, obj, "x", JS_NewInt32 (ctx, value.getIVec4 ().x));
  81. JS_SetPropertyStr (ctx, obj, "y", JS_NewInt32 (ctx, value.getIVec4 ().y));
  82. JS_SetPropertyStr (ctx, obj, "z", JS_NewInt32 (ctx, value.getIVec4 ().z));
  83. JS_SetPropertyStr (ctx, obj, "w", JS_NewInt32 (ctx, value.getIVec4 ().w));
  84. return obj;
  85. }
  86. default:
  87. return JS_UNDEFINED;
  88. }
  89. }
  90. DynamicValueUniquePtr ScriptEngine::jsToDynamicValue (JSValue val, DynamicValue::UnderlyingType hint) const {
  91. JSContext* ctx = this->m_context;
  92. auto result = std::make_unique<DynamicValue> ();
  93. if (JS_IsException (val)) {
  94. return result;
  95. }
  96. // scalar types returned directly
  97. int tag = JS_VALUE_GET_TAG (val);
  98. if (tag == JS_TAG_INT) {
  99. int32_t i;
  100. JS_ToInt32 (ctx, &i, val);
  101. if (hint == DynamicValue::Float) {
  102. result->update (static_cast<float> (i));
  103. } else {
  104. result->update (static_cast<int> (i));
  105. }
  106. return result;
  107. }
  108. if (tag == JS_TAG_BOOL) {
  109. result->update (static_cast<bool> (JS_ToBool (ctx, val)));
  110. return result;
  111. }
  112. if (JS_TAG_IS_FLOAT64 (tag)) {
  113. double d;
  114. JS_ToFloat64 (ctx, &d, val);
  115. result->update (static_cast<float> (d));
  116. return result;
  117. }
  118. // object - extract x/y/z/w properties based on the hint type
  119. if (tag == JS_TAG_OBJECT) {
  120. auto readFloat = [&] (const char* prop) -> float {
  121. JSValue v = JS_GetPropertyStr (ctx, val, prop);
  122. double d = 0.0;
  123. if (!JS_IsException (v) && !JS_IsUndefined (v)) {
  124. JS_ToFloat64 (ctx, &d, v);
  125. }
  126. JS_FreeValue (ctx, v);
  127. return static_cast<float> (d);
  128. };
  129. auto readInt = [&] (const char* prop) -> int {
  130. JSValue v = JS_GetPropertyStr (ctx, val, prop);
  131. int32_t i = 0;
  132. if (!JS_IsException (v) && !JS_IsUndefined (v)) {
  133. JS_ToInt32 (ctx, &i, v);
  134. }
  135. JS_FreeValue (ctx, v);
  136. return static_cast<int> (i);
  137. };
  138. switch (hint) {
  139. case DynamicValue::Vec2:
  140. result->update (glm::vec2 (readFloat ("x"), readFloat ("y")));
  141. break;
  142. case DynamicValue::Vec3:
  143. result->update (glm::vec3 (readFloat ("x"), readFloat ("y"), readFloat ("z")));
  144. break;
  145. case DynamicValue::Vec4:
  146. result->update (
  147. glm::vec4 (readFloat ("x"), readFloat ("y"), readFloat ("z"), readFloat ("w")));
  148. break;
  149. case DynamicValue::IVec2:
  150. result->update (glm::ivec2 (readInt ("x"), readInt ("y")));
  151. break;
  152. case DynamicValue::IVec3:
  153. result->update (glm::ivec3 (readInt ("x"), readInt ("y"), readInt ("z")));
  154. break;
  155. case DynamicValue::IVec4:
  156. result->update (
  157. glm::ivec4 (readInt ("x"), readInt ("y"), readInt ("z"), readInt ("w")));
  158. break;
  159. default: {
  160. // try to read as vec3 by default (most common for origin/angles)
  161. float x = readFloat ("x");
  162. float y = readFloat ("y");
  163. float z = readFloat ("z");
  164. result->update (glm::vec3 (x, y, z));
  165. break;
  166. }
  167. }
  168. return result;
  169. }
  170. // fallback: try as float
  171. double d;
  172. if (JS_ToFloat64 (ctx, &d, val) == 0) {
  173. result->update (static_cast<float> (d));
  174. }
  175. return result;
  176. }
  177. /// Helper to check for and log JS exceptions
  178. static void logJSException (JSContext* ctx, const char* context) {
  179. JSValue exc = JS_GetException (ctx);
  180. if (!JS_IsNull (exc) && !JS_IsUndefined (exc)) {
  181. const char* str = JS_ToCString (ctx, exc);
  182. if (str) {
  183. sLog.error ("ScriptEngine [", context, "]: ", str);
  184. JS_FreeCString (ctx, str);
  185. }
  186. }
  187. JS_FreeValue (ctx, exc);
  188. }
  189. DynamicValueUniquePtr ScriptEngine::evaluate (
  190. const std::string& scriptSource,
  191. const std::map<std::string, DynamicValue*>& scriptProperties,
  192. const DynamicValue& currentValue
  193. ) {
  194. if (!this->m_context) {
  195. sLog.error ("ScriptEngine: No JS context available");
  196. auto fallback = std::make_unique<DynamicValue> ();
  197. fallback->update (currentValue);
  198. return fallback;
  199. }
  200. JSContext* ctx = this->m_context;
  201. // Build the scriptProperties object that createScriptProperties() will return
  202. JSValue propsObj = JS_NewObject (ctx);
  203. for (const auto& [name, dynVal] : scriptProperties) {
  204. if (dynVal) {
  205. JS_SetPropertyStr (ctx, propsObj, name.c_str (), this->dynamicValueToJS (*dynVal));
  206. }
  207. }
  208. // We need to rewrite the ES6 module into a regular script that we can evaluate
  209. // and extract the update() function from, because QuickJS module evaluation
  210. // via JS_EVAL_TYPE_MODULE doesn't easily let us get at exported functions
  211. // from C in a straightforward way.
  212. //
  213. // Strategy: Replace the ES6 module pattern with a plain script that:
  214. // 1. Has createScriptProperties() available as a global
  215. // 2. Defines update() in global scope
  216. // 3. We call update() with the value object
  217. //
  218. // The script pattern is always:
  219. // 'use strict';
  220. // export var scriptProperties = createScriptProperties()...finish();
  221. // export function update(value) { ... }
  222. //
  223. // We transform this to a self-contained IIFE that we evaluate directly.
  224. // Set createScriptProperties as a global that returns a builder
  225. // The builder supports .addSlider({...}).addCheckbox({...}).finish()
  226. // and returns an object with the property values
  227. // Create the builder as a JS object with fluent methods
  228. // that ultimately resolves to the propsObj
  229. std::ostringstream wrapper;
  230. wrapper << "(function() {\n"
  231. << " var __props = globalThis.__scriptProps;\n"
  232. << " function createScriptProperties() {\n"
  233. << " var builder = {\n"
  234. << " addSlider: function(opts) {\n"
  235. << " if (!(opts.name in __props)) __props[opts.name] = opts.value;\n"
  236. << " return builder;\n"
  237. << " },\n"
  238. << " addCheckbox: function(opts) {\n"
  239. << " if (!(opts.name in __props)) __props[opts.name] = opts.value;\n"
  240. << " return builder;\n"
  241. << " },\n"
  242. << " addCombo: function(opts) {\n"
  243. << " if (!(opts.name in __props)) __props[opts.name] = opts.value;\n"
  244. << " return builder;\n"
  245. << " },\n"
  246. << " addColor: function(opts) {\n"
  247. << " if (!(opts.name in __props)) __props[opts.name] = opts.value;\n"
  248. << " return builder;\n"
  249. << " },\n"
  250. << " addText: function(opts) {\n"
  251. << " if (!(opts.name in __props)) __props[opts.name] = opts.value;\n"
  252. << " return builder;\n"
  253. << " },\n"
  254. << " finish: function() { return __props; }\n"
  255. << " };\n"
  256. << " return builder;\n"
  257. << " }\n";
  258. // Strip 'use strict'; and export keywords, embed the script body
  259. std::string body = scriptSource;
  260. // Remove 'use strict'; declarations
  261. size_t pos;
  262. while ((pos = body.find ("'use strict';")) != std::string::npos) {
  263. body.erase (pos, 13);
  264. }
  265. while ((pos = body.find ("\"use strict\";")) != std::string::npos) {
  266. body.erase (pos, 13);
  267. }
  268. // Remove export keywords (export var ..., export function ...)
  269. while ((pos = body.find ("export ")) != std::string::npos) {
  270. body.erase (pos, 7);
  271. }
  272. wrapper << body << "\n"
  273. << " return update(globalThis.__currentValue);\n"
  274. << "})();\n";
  275. std::string evalScript = wrapper.str ();
  276. // Set globals: __scriptProps and __currentValue
  277. JSValue globalObj = JS_GetGlobalObject (ctx);
  278. JS_SetPropertyStr (ctx, globalObj, "__scriptProps", JS_DupValue (ctx, propsObj));
  279. JS_SetPropertyStr (ctx, globalObj, "__currentValue", this->dynamicValueToJS (currentValue));
  280. // Evaluate
  281. JSValue result = JS_Eval (ctx, evalScript.c_str (), evalScript.size (), "<script>", JS_EVAL_TYPE_GLOBAL);
  282. // Clean up globals
  283. JS_SetPropertyStr (ctx, globalObj, "__scriptProps", JS_UNDEFINED);
  284. JS_SetPropertyStr (ctx, globalObj, "__currentValue", JS_UNDEFINED);
  285. JS_FreeValue (ctx, globalObj);
  286. JS_FreeValue (ctx, propsObj);
  287. if (JS_IsException (result)) {
  288. logJSException (ctx, "evaluate");
  289. JS_FreeValue (ctx, result);
  290. auto fallback = std::make_unique<DynamicValue> ();
  291. fallback->update (currentValue);
  292. return fallback;
  293. }
  294. auto dynResult = this->jsToDynamicValue (result, currentValue.getType ());
  295. JS_FreeValue (ctx, result);
  296. return dynResult;
  297. }