/*
Ousía
Copyright (C) 2014, 2015 Benjamin Paaßen, Andreas Stöckel
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
#include
#include "MozJsScriptEngine.hpp"
namespace ousia {
namespace script {
/*
* Some important links to the SpiderMonkey (mozjs) documentation:
*
* Documentation overview:
* https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/
*
* User Guide:
* https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/JSAPI_User_Guide
*
* API Reference:
* https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/JSAPI_reference
*/
/* Constants */
static const uint32_t MOZJS_RT_MEMSIZE = 64L * 1024L * 1024L;
static const uint32_t MOZJS_CTX_STACK_CHUNK_SIZE = 8192;
/* Class MozJsScriptEngineFunction */
MozJsScriptEngineFunction::MozJsScriptEngineFunction(
MozJsScriptEngineScope &scope, JS::Value &fun, JSObject *parent)
: scope(scope)
{
this->fun = new JS::RootedValue(scope.cx, fun);
this->parent = new JS::RootedObject(scope.cx, parent);
}
MozJsScriptEngineFunction::~MozJsScriptEngineFunction()
{
delete parent;
delete fun;
}
MozJsScriptEngineFunction *MozJsScriptEngineFunction::clone() const
{
return new MozJsScriptEngineFunction(scope, fun->get(), parent->get());
}
Variant MozJsScriptEngineFunction::call(const std::vector &args) const
{
// TODO: Input parameter
JS::Value val;
scope.handleErr(JS_CallFunctionValue(scope.cx, parent->get(), fun->get(), 0,
nullptr, &val));
return scope.valueToVariant(val);
}
/* Class MozJsScriptEngineScope */
/**
* The class of the global object.
*/
static JSClass globalClass = {
"global", JSCLASS_GLOBAL_FLAGS, JS_PropertyStub,
JS_DeletePropertyStub, JS_PropertyStub, JS_StrictPropertyStub,
JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub,
nullptr, nullptr, nullptr,
nullptr, nullptr};
MozJsScriptEngineScope::MozJsScriptEngineScope(JSRuntime *rt) : rt(rt)
{
// Create the execution context
cx = JS_NewContext(rt, MOZJS_CTX_STACK_CHUNK_SIZE);
if (!cx) {
throw ScriptEngineException{"MozJs JS_NewContext failed"};
}
// Start a context request
JS_BeginRequest(cx);
// Set some context options
JS_SetOptions(cx, JS_GetOptions(cx) | JSOPTION_EXTRA_WARNINGS |
JSOPTION_VAROBJFIX | JSOPTION_DONT_REPORT_UNCAUGHT);
// Create the rooted global object
global =
new JS::RootedObject(cx, JS_NewGlobalObject(cx, &globalClass, nullptr));
// Enter a compartment (heap memory region) surrounding the global object
oldCompartment = JS_EnterCompartment(cx, *global);
// Populate the global object with the standard classes
if (!JS_InitStandardClasses(cx, *global)) {
throw ScriptEngineException{"MozJS JS_InitStandardClasses failed"};
}
}
MozJsScriptEngineScope::~MozJsScriptEngineScope()
{
// Leave the compartment
JS_LeaveCompartment(cx, oldCompartment);
// Free the reference to the local object
delete global;
// End the request
JS_EndRequest(cx);
// Destroy the execution context
JS_DestroyContext(cx);
}
Variant MozJsScriptEngineScope::arrayToVariant(JSObject *obj)
{
// Retrieve the array length
uint32_t len = 0;
handleErr(JS_GetArrayLength(cx, obj, &len));
// Create the result vector and reserve as much memory as needed
std::vector array;
array.reserve(len);
// Fill the result vector
JS::Value arrayVal;
for (uint32_t i = 0; i < len; i++) {
handleErr(JS_GetElement(cx, obj, i, &arrayVal));
array.push_back(valueToVariant(arrayVal, obj));
}
return Variant{array};
}
Variant MozJsScriptEngineScope::objectToVariant(JSObject *obj)
{
// Enumerate all object properties, perform error handling
JS::AutoIdArray ids(cx, JS_Enumerate(cx, obj));
if (!ids) {
handleErr();
}
// Iterate over all ids, add them to a map
std::map map;
JS::Value key;
JS::Value val;
for (size_t i = 0; i < ids.length(); i++) {
handleErr(JS_IdToValue(cx, ids[i], &key));
handleErr(JS_GetPropertyById(cx, obj, ids[i], &val));
map.insert(std::make_pair(
toString(key), valueToVariant(val, obj)));
}
return Variant{map};
}
Variant MozJsScriptEngineScope::valueToVariant(JS::Value &val, JSObject *parent)
{
if (val.isNull()) {
return Variant::Null;
}
if (val.isBoolean()) {
return Variant{val.toBoolean()};
}
if (val.isInt32()) {
return Variant{(int64_t)val.toInt32()};
}
if (val.isDouble()) {
return Variant{val.toDouble()};
}
if (val.isString()) {
// TODO: Remove the need for using "c_str"!
return Variant{toString(val.toString()).c_str()};
}
if (val.isObject()) {
JSObject &obj = val.toObject();
if (JS_IsArrayObject(cx, &obj)) {
return arrayToVariant(&obj);
}
if (JS_ObjectIsFunction(cx, &obj)) {
// TODO: Variant of the Variant function constructor which grants
// ownership of the pointer
MozJsScriptEngineFunction fun(*this, val, parent);
return Variant{&fun};
}
return objectToVariant(&obj);
}
return Variant::Null;
}
void MozJsScriptEngineScope::handleErr(bool ok)
{
if (!ok && JS_IsExceptionPending(cx)) {
JS::Value exception;
if (JS_GetPendingException(cx, &exception)) {
// Fetch messgage string, line and column
JS::Value msg, line, col;
JS_GetPendingException(cx, &exception);
JS_GetProperty(cx, JSVAL_TO_OBJECT(exception), "message", &msg);
JS_GetProperty(cx, JSVAL_TO_OBJECT(exception), "lineNumber", &line);
JS_GetProperty(cx, JSVAL_TO_OBJECT(exception), "columnNumber",
&col);
// Clear the exception
JS_ClearPendingException(cx);
// Produce a nice error message in case the caught exception is of
// the "Error" class
if (msg.isString() && line.isInt32() && col.isInt32()) {
// Throw a script engine exception with the corresponding line,
// column and string
throw ScriptEngineException{line.toInt32(), col.toInt32(),
toString(msg)};
}
// Otherwise simply convert the exception to a string
throw ScriptEngineException{toString(exception)};
}
}
}
std::string MozJsScriptEngineScope::toString(JS::Value &val)
{
// If the given value already is a Javascript string, return it directly.
if (val.isString()) {
return toString(val.toString());
}
// The given value is not really a string, so convert it to one first
JSString *str = JS_ValueToString(cx, val);
if (!str) {
throw ScriptEngineException{"Cannot convert value to string"};
}
return toString(str);
}
std::string MozJsScriptEngineScope::toString(JSString *str)
{
// Encode the string
char *buf = JS_EncodeStringToUTF8(cx, str);
if (!buf) {
throw ScriptEngineException{"JS_EncodeStringToUTF8 failed"};
}
// Copy the string into a std::string, free the original buffer and return
std::string res{buf};
JS_free(cx, buf);
return res;
}
Variant MozJsScriptEngineScope::doRun(const std::string &code)
{
JS::Value rval;
handleErr(JS_EvaluateScript(cx, *global, code.c_str(), code.length(), "", 0,
&rval));
return valueToVariant(rval);
}
void MozJsScriptEngineScope::doSetVariable(const std::string &name,
const Variant &val, bool constant)
{
// TODO
}
Variant MozJsScriptEngineScope::doGetVariable(const std::string &name)
{
// TODO
return Variant::Null;
}
/* Class MozJsScriptEngine */
MozJsScriptEngine::MozJsScriptEngine()
{
rt = JS_NewRuntime(MOZJS_RT_MEMSIZE, JS_NO_HELPER_THREADS);
if (!rt) {
throw ScriptEngineException{"MozJs JS_NewRuntime failed"};
}
}
MozJsScriptEngine::~MozJsScriptEngine()
{
JS_DestroyRuntime(rt);
JS_ShutDown();
}
MozJsScriptEngineScope *MozJsScriptEngine::createScope()
{
return new MozJsScriptEngineScope(rt);
}
}
}