1 // Aseprite Scripting Library
2 // Copyright (c) 2015-2017 David Capello
3 //
4 // This file is released under the terms of the MIT license.
5 // Read LICENSE.txt for more information.
6 
7 #ifdef HAVE_CONFIG_H
8 #include "config.h"
9 #endif
10 
11 #include "script/engine.h"
12 
13 #include "base/convert_to.h"
14 #include "base/exception.h"
15 #include "base/file_handle.h"
16 #include "base/fstream_path.h"
17 #include "base/memory.h"
18 #include "script/engine_delegate.h"
19 
20 #include <fstream>
21 #include <iostream>
22 #include <map>
23 #include <sstream>
24 
25 extern "C" {
26 #include <mujs/mujs.h>
27 }
28 
29 namespace script {
30 
31 static const char* stacktrace_js =
32   "Error.prototype.toString = function() {\n"
33   "  if (this.stackTrace)\n"
34   "    return this.name + ': ' + this.message + this.stackTrace;\n"
35   "  return this.name + ': ' + this.message;\n"
36   "}";
37 
setContextUserData(void * userData)38 void Context::setContextUserData(void* userData)
39 {
40   js_setcontext(m_handle, userData);
41 }
42 
getContextUserData()43 void* Context::getContextUserData()
44 {
45   return js_getcontext(m_handle);
46 }
47 
error(const char * err)48 void Context::error(const char* err)
49 {
50   js_error(m_handle, "%s", err);
51 }
52 
pop()53 void Context::pop()
54 {
55   js_pop(m_handle, 1);
56 }
57 
pop(index_t count)58 void Context::pop(index_t count)
59 {
60   js_pop(m_handle, count);
61 }
62 
remove(index_t idx)63 void Context::remove(index_t idx)
64 {
65   js_remove(m_handle, idx);
66 }
67 
duplicateTop()68 void Context::duplicateTop()
69 {
70   js_dup(m_handle);
71 }
72 
top()73 index_t Context::top()
74 {
75   return js_gettop(m_handle);
76 }
77 
isUndefined(index_t i)78 bool Context::isUndefined(index_t i)
79 {
80   return (js_isundefined(m_handle, i) ? true: false);
81 }
82 
isNull(index_t i)83 bool Context::isNull(index_t i)
84 {
85   return (js_isnull(m_handle, i) ? true: false);
86 }
87 
isNullOrUndefined(index_t i)88 bool Context::isNullOrUndefined(index_t i)
89 {
90   return (js_isnull(m_handle, i) ||
91           js_isundefined(m_handle, i)? true: false);
92 }
93 
isBool(index_t i)94 bool Context::isBool(index_t i)
95 {
96   return (js_isboolean(m_handle, i) ? true: false);
97 }
98 
isNumber(index_t i)99 bool Context::isNumber(index_t i)
100 {
101   return (js_isnumber(m_handle, i) ? true: false);
102 }
103 
isString(index_t i)104 bool Context::isString(index_t i)
105 {
106   return (js_isstring(m_handle, i) ? true: false);
107 }
108 
isObject(index_t i)109 bool Context::isObject(index_t i)
110 {
111   return (js_isobject(m_handle, i) ? true: false);
112 }
113 
isArray(index_t i)114 bool Context::isArray(index_t i)
115 {
116   return (js_isarray(m_handle, i) ? true: false);
117 }
118 
isUserData(index_t i,const char * tag)119 bool Context::isUserData(index_t i, const char* tag)
120 {
121   return (js_isuserdata(m_handle, i, tag) ? true: false);
122 }
123 
toBool(index_t i)124 bool Context::toBool(index_t i)
125 {
126   return (js_toboolean(m_handle, i) ? true: false);
127 }
128 
toNumber(index_t i)129 double Context::toNumber(index_t i)
130 {
131   return js_tonumber(m_handle, i);
132 }
133 
toInt(index_t i)134 int Context::toInt(index_t i)
135 {
136   return js_toint32(m_handle, i);
137 }
138 
toUInt(index_t i)139 unsigned int Context::toUInt(index_t i)
140 {
141   return js_touint32(m_handle, i);
142 }
143 
toString(index_t i)144 const char* Context::toString(index_t i)
145 {
146   return js_tostring(m_handle, i);
147 }
148 
toUserData(index_t i,const char * tag)149 void* Context::toUserData(index_t i, const char* tag)
150 {
151   return js_touserdata(m_handle, i, tag);
152 }
153 
requireBool(index_t i)154 bool Context::requireBool(index_t i)
155 {
156   if (js_isboolean(m_handle, i))
157     return true;
158   else {
159     js_typeerror(m_handle, "not a boolean (index %d)\n", i);
160     return false;
161   }
162 }
163 
requireNumber(index_t i)164 double Context::requireNumber(index_t i)
165 {
166   if (js_isnumber(m_handle, i))
167     return js_tonumber(m_handle, i);
168   else {
169     js_typeerror(m_handle, "not a number (index %d)\n", i);
170     return 0.0;
171   }
172 }
173 
requireInt(index_t i)174 int Context::requireInt(index_t i)
175 {
176   return (int)requireNumber(i);
177 }
178 
requireUInt(index_t i)179 unsigned int Context::requireUInt(index_t i)
180 {
181   return (unsigned int)requireNumber(i);
182 }
183 
requireString(index_t i)184 const char* Context::requireString(index_t i)
185 {
186   if (js_isstring(m_handle, i))
187     return js_tostring(m_handle, i);
188   else {
189     js_typeerror(m_handle, "not a string (index %d)\n", i);
190     return nullptr;
191   }
192 }
193 
requireUserData(index_t i,const char * tag)194 void* Context::requireUserData(index_t i, const char* tag)
195 {
196   if (js_isuserdata(m_handle, i, tag))
197     return js_touserdata(m_handle, i, tag);
198   else {
199     js_typeerror(m_handle, "not a user data (index %d, tag %s)\n", i, tag);
200     return nullptr;
201   }
202 }
203 
pushUndefined()204 void Context::pushUndefined()
205 {
206   js_pushundefined(m_handle);
207 }
208 
pushNull()209 void Context::pushNull()
210 {
211   js_pushnull(m_handle);
212 }
213 
pushBool(bool val)214 void Context::pushBool(bool val)
215 {
216   js_pushboolean(m_handle, val);
217 }
218 
pushNumber(double val)219 void Context::pushNumber(double val)
220 {
221   js_pushnumber(m_handle, val);
222 }
223 
pushInt(int val)224 void Context::pushInt(int val)
225 {
226   js_pushnumber(m_handle, double(val));
227 }
228 
pushUInt(unsigned int val)229 void Context::pushUInt(unsigned int val)
230 {
231   js_pushnumber(m_handle, double(val));
232 }
233 
pushString(const char * str)234 void Context::pushString(const char* str)
235 {
236   js_pushstring(m_handle, str);
237 }
238 
pushGlobalObject()239 void Context::pushGlobalObject()
240 {
241   js_pushglobal(m_handle);
242 }
243 
newObject()244 void Context::newObject()
245 {
246   js_newobject(m_handle);
247 }
248 
newObject(const char * className,void * userData,FinalizeFunction finalize)249 void Context::newObject(const char* className,
250                         void* userData,
251                         FinalizeFunction finalize)
252 {
253   js_getglobal(m_handle, className);         // class
254   js_getproperty(m_handle, -1, "prototype"); // class prototype
255   js_newuserdata(m_handle, className, userData, finalize); // class userdata
256   js_rot2(m_handle);   // userdata class
257   js_pop(m_handle, 1); // userdata
258 }
259 
newUserData(const char * tag,void * userData,FinalizeFunction finalize)260 void Context::newUserData(const char* tag,
261                           void* userData,
262                           FinalizeFunction finalize)
263 {
264   js_newuserdata(m_handle, tag, userData, finalize);
265 }
266 
registerConstants(index_t idx,const ConstantEntry * consts)267 void Context::registerConstants(index_t idx,
268                                 const ConstantEntry* consts)
269 {
270   if (idx < 0)
271     --idx;
272   for (; consts->id; ++consts) {
273     js_pushnumber(m_handle, consts->value);
274     js_defproperty(m_handle, idx, consts->id, JS_DONTENUM);
275   }
276 }
277 
registerProp(index_t idx,const char * id,Function getter,Function setter)278 void Context::registerProp(index_t idx,
279                            const char* id,
280                            Function getter,
281                            Function setter)
282 {
283   if (idx < 0)
284     idx -= 2;
285 
286   if (getter)
287     js_newcfunction(m_handle, getter,
288                     (std::string(id) + ".getter").c_str(), 0);
289   else
290     js_pushnull(m_handle);
291 
292   if (setter)
293     js_newcfunction(m_handle, setter,
294                     (std::string(id) + ".setter").c_str(), 1);
295   else
296     js_pushnull(m_handle);
297 
298   js_defaccessor(m_handle, idx, id, JS_DONTENUM);
299 }
300 
registerProps(index_t idx,const PropertyEntry * props)301 void Context::registerProps(index_t idx, const PropertyEntry* props)
302 {
303   for (int i=0; props[i].id; ++i) {
304     registerProp(idx,
305                  props[i].id,
306                  props[i].getter,
307                  props[i].setter);
308   }
309 }
310 
registerFunc(index_t idx,const char * id,const Function func,index_t nargs)311 void Context::registerFunc(index_t idx,
312                            const char* id,
313                            const Function func,
314                            index_t nargs)
315 {
316   js_newcfunction(m_handle, func, id, nargs);
317   js_defproperty(m_handle, idx, id, JS_DONTENUM);
318 }
319 
registerFuncs(index_t idx,const FunctionEntry * methods)320 void Context::registerFuncs(index_t idx, const FunctionEntry* methods)
321 {
322   if (idx < 0)
323     --idx;
324   for (; methods->id; ++methods) {
325     registerFunc(idx,
326                  methods->id,
327                  methods->value,
328                  methods->nargs);
329   }
330 }
331 
registerObject(index_t idx,const char * id,const FunctionEntry * methods,const PropertyEntry * props)332 void Context::registerObject(index_t idx,
333                              const char* id,
334                              const FunctionEntry* methods,
335                              const PropertyEntry* props)
336 {
337   if (idx < 0)
338     --idx;
339 
340   newObject();
341   if (methods) registerFuncs(-1, methods);
342   if (props) registerProps(-1, props);
343   js_defproperty(m_handle, idx, id, JS_DONTENUM);
344 }
345 
registerClass(index_t idx,const char * id,Function ctorFunc,int ctorNargs,const FunctionEntry * methods,const PropertyEntry * props)346 void Context::registerClass(index_t idx,
347                             const char* id,
348                             Function ctorFunc, int ctorNargs,
349                             const FunctionEntry* methods,
350                             const PropertyEntry* props)
351 {
352   if (idx < 0)
353     idx -= 2;
354 
355   js_getglobal(m_handle, "Object");
356   js_getproperty(m_handle, -1, "prototype");
357   js_newuserdata(m_handle, id, nullptr, nullptr);
358   if (methods) registerFuncs(-1, methods);
359   if (props) registerProps(-1, props);
360   js_newcconstructor(m_handle, ctorFunc, ctorFunc, id, ctorNargs);
361   js_defproperty(m_handle, idx, id, JS_DONTENUM);
362   js_pop(m_handle, 1);          // pop Object
363 }
364 
hasProp(index_t i,const char * propName)365 bool Context::hasProp(index_t i, const char* propName)
366 {
367   return (js_hasproperty(m_handle, i, propName) ? true: false);
368 }
369 
getProp(index_t i,const char * propName)370 void Context::getProp(index_t i, const char* propName)
371 {
372   js_getproperty(m_handle, i, propName);
373 }
374 
setProp(index_t i,const char * propName)375 void Context::setProp(index_t i, const char* propName)
376 {
377   js_defproperty(m_handle, i, propName, JS_DONTENUM);
378 }
379 
Engine(EngineDelegate * delegate)380 Engine::Engine(EngineDelegate* delegate)
381   : m_ctx(js_newstate(NULL, NULL, JS_STRICT))
382   , m_delegate(delegate)
383   , m_printLastResult(false)
384 {
385   // Pre-scripts
386   js_dostring(m_ctx.handle(), stacktrace_js);
387 }
388 
~Engine()389 Engine::~Engine()
390 {
391   js_freestate(m_ctx.handle());
392 }
393 
printLastResult()394 void Engine::printLastResult()
395 {
396   m_printLastResult = true;
397 }
398 
eval(const std::string & jsCode,const std::string & filename)399 void Engine::eval(const std::string& jsCode,
400                   const std::string& filename)
401 {
402   bool errFlag = true;
403   onBeforeEval();
404 
405   ContextHandle handle = m_ctx.handle();
406 
407   if (js_ploadstring(handle, filename.c_str(), jsCode.c_str()) == 0) {
408     js_pushundefined(handle);
409     if (js_pcall(handle, 0) == 0) {
410       if (m_printLastResult) {
411         if (!js_isundefined(handle, -1)) {
412           const char* result = js_tostring(handle, -1);
413           if (result)
414             m_delegate->onConsolePrint(result);
415         }
416       }
417       errFlag = false;
418     }
419   }
420 
421   // Print error message
422   if (errFlag) {
423     std::string err;
424     const char* s = js_trystring(handle, -1, "Error");
425     if (s)
426       m_delegate->onConsolePrint(s);
427   }
428 
429   js_pop(handle, 1);
430 
431   onAfterEval(errFlag);
432 }
433 
evalFile(const std::string & filename)434 void Engine::evalFile(const std::string& filename)
435 {
436   std::stringstream buf;
437   {
438     std::ifstream s(FSTREAM_PATH(filename));
439     buf << s.rdbuf();
440   }
441   eval(buf.str(), filename);
442 }
443 
444 } // namespace script
445