/*
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
#include
#include
#include
#include "MozJsScriptEngine.hpp"
namespace ousia {
namespace script {
namespace mozjs {
/*
* 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 */
static const uint32_t MOZJS_FUNCTION_DATA_MAGIC = 0x87aac4ca;
struct MozJsFunctionData {
/**
* Magic number used to make sure a pointer points to an instance of this
* struct.
*/
uint32_t magic;
/**
* Reference to the script engine scope.
*/
MozJsScriptEngineScope &scope;
/**
* Actual function associated with the object.
*/
std::unique_ptr function;
/**
* Constructor of the MozJsPrivateFunctionData instance.
*/
MozJsFunctionData(MozJsScriptEngineScope &scope, Function *function)
: magic(MOZJS_FUNCTION_DATA_MAGIC), scope(scope), function(function)
{
}
/**
* Destructor, resets the magic to zero, marking this instance as invalid.
*/
~MozJsFunctionData() { magic = 0; }
/**
* Returns true if the magic is set to the correct value, indicating that
* this actually is an instance of MozPrivateFunctionData.
*/
bool valid() { return magic == MOZJS_FUNCTION_DATA_MAGIC; }
};
/**
* Function used for deleting the private data that may be associated to a
* JSObject.
*/
void finalizeFunction(JSFreeOp *fop, JSObject *obj)
{
MozJsFunctionData *data =
static_cast(JS_GetPrivate(obj));
if (data) {
assert(data->valid());
delete data;
}
}
/**
* Function used for calling back into the host.
*/
JSBool callFunction(JSContext *cx, unsigned argc, JS::Value *vp)
{
// Fetch the arguments (including the callee and the parent/this object)
JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
// Fetch the underlying function object
JSObject &callee = args.callee();
MozJsFunctionData *data =
static_cast(JS_GetPrivate(&callee));
if (!data || !data->valid()) {
JS_ReportError(cx, "No valid function data attached to callable!");
return JS_FALSE;
}
// Assemble the function arguments
std::vector arguments;
arguments.reserve(args.length());
for (unsigned i = 0; i < args.length(); i++) {
JS::Value val = args.get(i);
arguments.push_back(data->scope.valueToVariant(val));
}
try {
// Call the host function
Variant res = data->function->call(arguments);
// Convert the result to a JS::RootedValue
JS::RootedValue rval(cx);
data->scope.variantToValue(res, rval);
// Return the result to the script code
args.rval().set(rval);
return JS_TRUE;
}
catch (ArgumentValidatorError ex) {
JS_ReportError(cx, ex.what());
return JS_FALSE;
}
}
/**
* 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};
static JSClass functionClass = {
"function", JSCLASS_HAS_PRIVATE, JS_PropertyStub,
JS_DeletePropertyStub, JS_PropertyStub, JS_StrictPropertyStub,
JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub,
finalizeFunction, nullptr, callFunction,
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;
}
void MozJsScriptEngineScope::variantToValue(const Variant &var,
JS::RootedValue &val)
{
switch (var.getType()) {
case VariantType::null: {
val.setNull();
return;
}
case VariantType::boolean: {
val.setBoolean(var.getBooleanValue());
return;
}
case VariantType::integer: {
val.setInt32(var.getIntegerValue());
return;
}
case VariantType::number: {
val.setDouble(var.getNumberValue());
return;
}
case VariantType::string: {
// Allocate enough memory for the string stored in the variant
const size_t size = var.getStringValue().size();
const char *src = var.getStringValue().c_str();
JS::RootedString s(cx, JS_NewStringCopyN(cx, src, size));
if (!s) {
throw ScriptEngineException{"Out of JavaScript heap memory"};
}
val.setString(s);
return;
}
case VariantType::array: {
const std::vector &src = var.getArrayValue();
JS::RootedObject a(cx, JS_NewArrayObject(cx, src.size(), nullptr));
for (size_t i = 0; i < src.size(); i++) {
JS::RootedValue aval(cx);
variantToValue(src[i], aval);
JS_DefineElement(cx, a, i, aval, JS_PropertyStub,
JS_StrictPropertyStub,
JSPROP_ENUMERATE | JSPROP_INDEX);
}
val.setObjectOrNull(a.get());
return;
}
case VariantType::map: {
const std::map &src = var.getMapValue();
JS::RootedObject m(cx, JS_NewObject(cx, nullptr, nullptr, nullptr));
for (auto &e : src) {
setObjectProperty(m, e.first, e.second, false);
}
val.setObjectOrNull(m.get());
return;
}
case VariantType::function: {
JS::RootedObject f(
cx, JS_NewObject(cx, &functionClass, nullptr, nullptr));
JS_SetPrivate(f, new MozJsFunctionData(
*this, var.getFunctionValue()->clone()));
JS_FreezeObject(cx, f);
val.setObjectOrNull(f.get());
return;
}
default: {
val.setNull();
return;
}
}
}
void MozJsScriptEngineScope::setObjectProperty(JS::RootedObject &obj,
const std::string &name,
const Variant &var,
bool constant)
{
// Construct the property flags for the given variant type -- objects and
// functions are treated as readonly properties no matter what "constant"
// is set to.
int flags = JSPROP_PERMANENT | JSPROP_ENUMERATE;
if (constant || var.getType() == VariantType::object ||
var.getType() == VariantType::function) {
flags |= JSPROP_READONLY;
}
// Handle errors occuring while setting the property
JS::RootedValue val(cx);
variantToValue(var, val);
handleErr(JS_DefineProperty(cx, obj, name.c_str(), val, JS_PropertyStub,
JS_StrictPropertyStub, flags));
}
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 &var, bool constant)
{
setObjectProperty(*global, name, var, constant);
}
Variant MozJsScriptEngineScope::doGetVariable(const std::string &name)
{
JS::Value rval;
handleErr(JS_GetProperty(cx, *global, name.c_str(), &rval));
return valueToVariant(rval);
}
/* 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);
}
}
}
}