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