0
0
Fork 0
mirror of https://github.com/matrix-construct/construct synced 2024-11-17 23:40:57 +01:00
construct/ircd/js.cc

2616 lines
61 KiB
C++

/*
* charybdis: standing on the shoulders of giant build times
*
* Copyright (C) 2016 Charybdis Development Team
* Copyright (C) 2016 Jason Volk <jason@zemos.net>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice is present in all copies.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
* IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <js/Initialization.h> // JS_Init() / JS_ShutDown()
#include <jsfriendapi.h>
#include <ircd/js/js.h>
namespace ircd {
namespace js {
// Logging facility for this submodule with SNOMASK.
struct log::log log
{
"js", 'J'
};
// Location of the thread_local runtext. externs exist in js/runtime.h and js/context.h.
// If these are null, js is not available on your thread.
__thread runtime *rt;
__thread context *cx;
__thread trap *tree;
// Internal prototypes
void handle_activity_ctypes(JSContext *, enum ::js::CTypesActivityType) noexcept;
const char *reflect(const ::js::CTypesActivityType &);
} // namespace js
} // namespace ircd
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js.h - Without 3rd party (JSAPI) symbols
//
ircd::js::init::init()
{
log.info("Initializing the JS engine [%s: %s]",
"SpiderMonkey",
version(ver::IMPLEMENTATION));
const scope exit([this]
{
// Ensure ~init() is always safe to call at any intermediate state
if(std::current_exception())
this->~init();
});
if(!JS_Init())
throw error("JS_Init(): failure");
struct runtime::opts runtime_opts;
struct context::opts context_opts;
log.info("Initializing the main JS Runtime (main_maxbytes: %zu)",
runtime_opts.max_bytes);
assert(!rt);
assert(!cx);
rt = new runtime(runtime_opts);
cx = new context(*rt, context_opts);
log.info("Initialized main JS Runtime and context (version: '%s')",
version(*cx));
{
// tree is registered by the kernel module's trap
const std::lock_guard<context> lock{*cx};
ircd::mods::load("kernel");
}
}
ircd::js::init::~init()
noexcept
{
if(cx && !!*cx) try
{
const std::lock_guard<context> lock{*cx};
ircd::mods::unload("kernel");
}
catch(const std::exception &e)
{
log.warning("Failed to unload the kernel: %s", e.what());
}
log.info("Terminating the JS Main Runtime");
delete cx; cx = nullptr;
delete rt; rt = nullptr;
log.info("Terminating the JS Engine");
JS_ShutDown();
}
const char *
ircd::js::version(const ver &type)
{
switch(type)
{
case ver::IMPLEMENTATION:
return JS_GetImplementationVersion();
default:
throw error("version(): Unknown version type requested");
}
}
void
__attribute__((noreturn))
js::ReportOutOfMemory(ExclusiveContext *const c)
{
ircd::js::log.critical("jsalloc(): Reported out of memory (ExclusiveContext: %p)", (const void *)c);
std::terminate();
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/js.h - With 3rd party (JSAPI) symbols
//
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/task.h
//
ircd::js::task::task(const std::string &source)
:global{[this]
{
// Global object is constructed using the root trap (JSClass) at *tree;
// This is a thread_local registered by the kernel.so module.
struct global global(*tree);
// The root trap is configured with HAS_PRIVATE and that slot is set so we can find this
// `struct task` from the global object using task::get(object). The global object
// can first be found from a context or active compartment. As a convenience `struct task`
// can be found contextually with task::get(void).
priv(global, this);
return global;
}()}
,main{[this, &source]
{
// A compartment for the global must be entered to compile in this scope
const compartment c(this->global);
// TODO: options
JS::CompileOptions opts(*cx);
JS::AutoObjectVector stack(*cx);
// The function must be compiled in this scope and returned as a heap_function
// before the compartment destructs.
return heap_function { stack, opts, "main", {}, source };
}()}
,generator{[this]
{
// A compartment for the global must be entered to run the generator wrapper.
const compartment c(this->global);
// Run the generator wrapper (main function) returning the generator object.
// The run() closure provides safety for entering the JS engine.
value state(run([this]
{
return this->main(this->global);
}));
// Construct the generator object here on the stack while in a compartment. The
// instance then contains returnable heap objects.
struct generator ret(state);
return ret;
}()}
{
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/global.h
//
ircd::js::global::global(trap &trap,
JSPrincipals *const &principals)
:heap_object
{
JS_NewGlobalObject(*cx, &trap.jsclass(), principals, JS::DontFireOnNewGlobalHook)
}
{
const compartment c(*this);
if(!JS_InitStandardClasses(*cx, *this))
throw error("Failed to init standard classes for global object");
JS_FireOnNewGlobalObject(*cx, *this);
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/generator.h
//
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/trap_function.h
//
ircd::js::trap_function::trap_function(std::string name,
const uint &arity,
const uint &flags)
:name{std::move(name)}
,arity{arity}
,flags{flags}
{
}
ircd::js::trap_function::~trap_function()
noexcept
{
}
ircd::js::function
ircd::js::trap_function::operator()(const object::handle &obj)
{
const auto jsf(::js::DefineFunctionWithReserved(*cx,
obj,
name.c_str(),
handle_call,
arity,
flags));
if(unlikely(!jsf))
throw internal_error("Failed to create trap_function");
function ret(jsf);
::js::SetFunctionNativeReserved(ret, 0, pointer_value(this));
return ret;
}
bool
ircd::js::trap_function::handle_call(JSContext *const c,
const unsigned argc,
JS::Value *const argv)
noexcept try
{
assert(&our(c) == cx);
const struct args args(argc, argv);
//const auto that(args.computeThis(c));
const object func(args.callee());
auto &trap(from(func));
log.debug("trap_function(%p) \"%s\": call",
(const void *)&trap,
trap.name.c_str());
args.rval().set(trap.on_call(func, args));
return true;
}
catch(const jserror &e)
{
e.set_pending();
return false;
}
catch(const std::exception &e)
{
const struct args args(argc, argv);
auto &func(args.callee());
auto &trap(from(&func));
log.error("trap_function(%p) \"%s\": %s",
reinterpret_cast<const void *>(&trap),
trap.name.c_str(),
e.what());
JS_ReportError(*cx, "BUG: trap_function(%p) \"%s\": %s",
reinterpret_cast<const void *>(&trap),
trap.name.c_str(),
e.what());
return false;
}
ircd::js::trap_function &
ircd::js::trap_function::from(JSObject *const &func)
{
const auto tval(::js::GetFunctionNativeReserved(func, 0));
return *pointer_value<trap_function>(tval);
}
ircd::js::value
ircd::js::trap_function::on_call(object::handle,
const args &)
{
return {};
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/trap.h
//
ircd::js::trap::trap(const std::string &path,
const uint32_t &flags)
:parent{[&path]
{
const auto ret(rsplit(path, ".").first);
return ret == path? std::string{} : ret;
}()}
,_name
{
path.empty()? std::string{""} : token_last(path, ".")
}
,ps
{
{0},
{0}
}
,fs
{
{0},
JS_FS_END
}
,_class{std::make_unique<JSClass>(JSClass
{
this->_name.c_str(),
flags,
handle_add, // flags & JSCLASS_GLOBAL_FLAGS? nullptr : handle_add,
handle_del,
handle_get,
handle_set,
handle_enu,
handle_has,
nullptr, // JSConvertOp - Obsolete since SpiderMonkey 44 // 45 = mayResolve?
handle_dtor,
handle_call,
handle_inst,
handle_ctor,
flags & JSCLASS_GLOBAL_FLAGS? JS_GlobalObjectTraceHook : handle_trace,
{ this } // reserved[0] TODO: ?????????
})}
{
add_this();
}
ircd::js::trap::~trap()
noexcept
{
del_this();
assert(_class->reserved[0] == this);
//_class->reserved[0] = nullptr;
//_class->trace = nullptr;
memset(_class.get(), 0x0, sizeof(JSClass));
// Must run GC here to force reclamation of objects before
// the JSClass hosted by this trap destructs.
//run_gc(*rt);
}
void
ircd::js::trap::del_this()
try
{
if(name().empty())
{
tree = nullptr;
return;
}
auto &parent(find(this->parent));
if(!parent.children.erase(name()))
throw std::out_of_range("child not in parent's map");
log.debug("Unregistered trap '%s' in `%s'",
name().c_str(),
this->parent.c_str());
}
catch(const std::exception &e)
{
log.error("Failed to unregister object trap '%s' in `%s': %s",
name().c_str(),
parent.c_str(),
e.what());
return;
}
void
ircd::js::trap::add_this()
try
{
if(name().empty())
{
if(tree)
throw error("ircd::js::tree is already active. Won't overwrite.");
tree = this;
return;
}
auto &parent(find(this->parent));
const auto iit(parent.children.emplace(name(), this));
if(!iit.second)
throw error("Failed to overwrite existing");
log.debug("Registered trap '%s' in `%s'",
name().c_str(),
this->parent.c_str());
}
catch(const std::out_of_range &e)
{
log.error("Failed to register object trap '%s' in `%s': missing parent.",
name().c_str(),
parent.c_str());
throw;
}
catch(const std::exception &e)
{
log.error("Failed to register object trap '%s' in `%s': %s",
name().c_str(),
parent.c_str(),
e.what());
throw;
}
ircd::js::object
ircd::js::trap::operator()(const object &parent,
const object &parent_proto)
{
return JS_InitClass(*cx,
parent,
parent_proto,
_class.get(),
nullptr,
0,
ps,
fs,
nullptr,
nullptr);
}
ircd::js::trap &
ircd::js::trap::find(const std::string &path)
{
if(unlikely(!tree))
throw error("Failed to find trap tree root");
trap *ret(tree);
const auto parts(tokens(path, "."));
for(const auto &part : parts)
ret = &ret->child(part);
return *ret;
}
ircd::js::trap &
ircd::js::trap::child(const string &name)
try
{
if(name.empty())
return *this;
return *children.at(name);
}
catch(const std::out_of_range &e)
{
throw reference_error("%s", name.c_str());
}
const ircd::js::trap &
ircd::js::trap::child(const string &name)
const
try
{
if(name.empty())
return *this;
return *children.at(name);
}
catch(const std::out_of_range &e)
{
throw reference_error("%s", name.c_str());
}
void
ircd::js::trap::handle_dtor(JSFreeOp *const op,
JSObject *const obj)
noexcept try
{
assert(op);
assert(obj);
assert(&our_runtime(*op) == rt);
auto &trap(from(*obj));
trap.debug("dtor %p", (const void *)obj);
trap.on_dtor(*obj);
}
catch(const jserror &e)
{
e.set_pending();
return;
}
catch(const std::exception &e)
{
auto &trap(from(*obj));
trap.host_exception("dtor: %s", e.what());
return;
}
bool
ircd::js::trap::handle_ctor(JSContext *const c,
unsigned argc,
JS::Value *const argv)
noexcept try
{
assert(&our(c) == cx);
const struct args args(argc, argv);
object that(args.callee());
auto &trap(from(that));
trap.debug("ctor: '%s'", trap.name().c_str());
object ret(JS_NewObjectForConstructor(*cx, &trap.jsclass(), args));
trap.on_ctor(ret, args);
args.rval().set(ret);
return true;
}
catch(const jserror &e)
{
e.set_pending();
return false;
}
catch(const std::exception &e)
{
auto ca(JS::CallArgsFromVp(argc, argv));
auto &trap(from(object(ca.callee())));
trap.host_exception("ctor: %s", e.what());
return false;
}
bool
ircd::js::trap::handle_call(JSContext *const c,
unsigned argc,
JS::Value *const argv)
noexcept try
{
assert(&our(c) == cx);
const struct args args(argc, argv);
object that(args.computeThis(c));
object func(args.callee());
auto &trap_that(from(that));
auto &trap_func(from(func));
trap_that.debug("call: '%s'", trap_func.name().c_str());
trap_func.debug("call");
args.rval().set(trap_func.on_call(func, args));
return true;
}
catch(const jserror &e)
{
e.set_pending();
return false;
}
catch(const std::exception &e)
{
auto ca(JS::CallArgsFromVp(argc, argv));
auto &trap(from(object(ca.computeThis(c))));
trap.host_exception("call: %s", e.what());
return false;
}
bool
ircd::js::trap::handle_enu(JSContext *const c,
JS::HandleObject obj)
noexcept try
{
assert(&our(c) == cx);
auto &trap(from(obj));
trap.debug("enu");
return trap.on_enu(obj);
}
catch(const jserror &e)
{
e.set_pending();
return false;
}
catch(const std::exception &e)
{
auto &trap(from(obj));
trap.host_exception("enu: %s", e.what());
return false;
}
bool
ircd::js::trap::handle_has(JSContext *const c,
JS::HandleObject obj,
JS::HandleId id,
bool *const resolved)
noexcept try
{
assert(&our(c) == cx);
auto &trap(from(obj));
trap.debug("has: '%s'", string(id).c_str());
*resolved = trap.on_has(obj, id);
return true;
}
catch(const jserror &e)
{
e.set_pending();
return false;
}
catch(const std::exception &e)
{
auto &trap(from(obj));
trap.host_exception("has: '%s': %s",
string(id).c_str(),
e.what());
return false;
}
bool
ircd::js::trap::handle_del(JSContext *const c,
JS::HandleObject obj,
JS::HandleId id,
JS::ObjectOpResult &res)
noexcept try
{
assert(&our(c) == cx);
auto &trap(from(obj));
trap.debug("del: '%s'", string(id).c_str());
if(trap.on_del(obj, id))
res.succeed();
return true;
}
catch(const jserror &e)
{
e.set_pending();
return false;
}
catch(const std::exception &e)
{
auto &trap(from(obj));
trap.host_exception("del '%s': %s",
string(id).c_str(),
e.what());
return false;
}
bool
ircd::js::trap::handle_get(JSContext *const c,
JS::HandleObject obj,
JS::HandleId id,
JS::MutableHandleValue val)
noexcept try
{
assert(&our(c) == cx);
auto &trap(from(obj));
trap.debug("get: '%s'", string(id).c_str());
const value ret(trap.on_get(obj, id, val));
val.set(ret.get());
return true;
}
catch(const jserror &e)
{
e.set_pending();
return false;
}
catch(const std::exception &e)
{
auto &trap(from(obj));
trap.host_exception("get: '%s': %s",
string(id).c_str(),
e.what());
return false;
}
bool
ircd::js::trap::handle_set(JSContext *const c,
JS::HandleObject obj,
JS::HandleId id,
JS::MutableHandleValue val,
JS::ObjectOpResult &res)
noexcept try
{
assert(&our(c) == cx);
auto &trap(from(obj));
trap.debug("set: '%s'", string(id).c_str());
const value ret(trap.on_set(obj, id, val));
val.set(ret.get());
if(!val.isUndefined())
res.succeed();
return true;
}
catch(const jserror &e)
{
e.set_pending();
return false;
}
catch(const std::exception &e)
{
auto &trap(from(obj));
trap.host_exception("set: '%s': %s",
string(id).c_str(),
e.what());
return false;
}
bool
ircd::js::trap::handle_add(JSContext *const c,
JS::HandleObject obj,
JS::HandleId id,
JS::HandleValue val)
noexcept try
{
assert(&our(c) == cx);
auto &trap(from(obj));
trap.debug("add: '%s'", string(id).c_str());
trap.on_add(obj, id, val);
return true;
}
catch(const jserror &e)
{
e.set_pending();
return false;
}
catch(const std::exception &e)
{
auto &trap(from(obj));
trap.host_exception("add: '%s': %s",
string(id).c_str(),
e.what());
return false;
}
bool
ircd::js::trap::handle_inst(JSContext *const c,
JS::HandleObject obj,
JS::MutableHandleValue val,
bool *const has_instance)
noexcept try
{
assert(&our(c) == cx);
auto &trap(from(obj));
trap.debug("inst");
return false;
}
catch(const jserror &e)
{
e.set_pending();
return false;
}
catch(const std::exception &e)
{
auto &trap(from(obj));
trap.host_exception("inst: %s", e.what());
return false;
}
void
ircd::js::trap::handle_trace(JSTracer *const tracer,
JSObject *const obj)
noexcept try
{
auto &trap(from(*obj));
trap.debug("trace");
}
catch(const jserror &e)
{
e.set_pending();
return;
}
catch(const std::exception &e)
{
auto &trap(from(*obj));
trap.host_exception("trace: %s", e.what());
return;
}
ircd::js::trap &
ircd::js::trap::from(const JS::HandleObject &o)
{
return from(*o.get());
}
ircd::js::trap &
ircd::js::trap::from(const JSObject &o)
{
auto *const c(JS_GetClass(const_cast<JSObject *>(&o)));
if(!c)
{
log.critical("trap::from(): Trapped on an object without a JSClass!");
std::terminate(); //TODO: exception
}
if(!c->reserved[0])
{
log.critical("trap::from(): Trap called on a trap instance that has gone out of scope!");
std::terminate(); //TODO: exception
}
return *static_cast<trap *>(c->reserved[0]); //TODO: ???
}
void
ircd::js::trap::debug(const char *const fmt,
...)
const
{
va_list ap;
va_start(ap, fmt);
char buf[1024];
vsnprintf(buf, sizeof(buf), fmt, ap);
log.debug("trap(%p) \"%s\": %s",
reinterpret_cast<const void *>(this),
name().c_str(),
buf);
va_end(ap);
}
void
ircd::js::trap::host_exception(const char *const fmt,
...)
const
{
va_list ap;
va_start(ap, fmt);
char buf[1024];
vsnprintf(buf, sizeof(buf), fmt, ap);
log.error("trap(%p) \"%s\": %s",
reinterpret_cast<const void *>(this),
name().c_str(),
buf);
JS_ReportError(*cx, "BUG: trap(%p) \"%s\" %s",
reinterpret_cast<const void *>(this),
name().c_str(),
buf);
va_end(ap);
}
void
ircd::js::trap::on_dtor(JSObject &)
{
}
void
ircd::js::trap::on_ctor(object &obj,
const args &)
{
}
bool
ircd::js::trap::on_enu(object::handle)
{
return true;
}
bool
ircd::js::trap::on_has(object::handle,
id::handle)
{
return true;
}
bool
ircd::js::trap::on_del(object::handle,
id::handle)
{
return true;
}
void
ircd::js::trap::on_add(object::handle,
id::handle,
value::handle)
{
}
ircd::js::value
ircd::js::trap::on_get(object::handle obj,
id::handle id,
value::handle val)
{
const string name(id);
const auto it(children.find(name));
if(it == end(children))
return val;
auto &child(*it->second);
return child(obj);
}
ircd::js::value
ircd::js::trap::on_set(object::handle,
id::handle,
value::handle val)
{
return val;
}
ircd::js::value
ircd::js::trap::on_call(object::handle,
const args &)
{
return {};
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/script.h
//
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/function_literal.h
//
ircd::js::function_literal::function_literal(const char *const &name,
const std::initializer_list<const char *> &prototype,
const char *const &text)
:root<JSFunction *, lifetime::persist>{}
,name{name}
,text{text}
,prototype{prototype}
{
JS::CompileOptions opts{*cx};
JS::AutoObjectVector stack{*cx};
if(!JS::CompileFunction(*cx,
stack,
opts,
name,
this->prototype.size(),
&this->prototype.front(),
text,
strlen(text),
&(*this)))
{
throw syntax_error("Failed to compile function literal");
}
}
ircd::js::function_literal::function_literal(function_literal &&other)
noexcept
:root<JSFunction *, lifetime::persist>
{
std::move(other)
}
,name{std::move(other.name)}
,text{std::move(other.text)}
,prototype{std::move(other.prototype)}
{
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/function.h
//
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/call.h
//
ircd::js::value
ircd::js::call(const object &obj,
const function::handle &func,
const vector<value>::handle &args)
{
value ret;
if(!JS_CallFunction(*cx, obj, func, args, &ret))
throw jserror(jserror::pending);
return ret;
}
ircd::js::value
ircd::js::call(const object &obj,
const value::handle &val,
const vector<value>::handle &args)
{
value ret;
if(!JS_CallFunctionValue(*cx, obj, val, args, &ret))
throw jserror(jserror::pending);
return ret;
}
ircd::js::value
ircd::js::call(const object &obj,
const char *const &name,
const vector<value>::handle &args)
{
value ret;
if(!JS_CallFunctionName(*cx, obj, name, args, &ret))
throw jserror(jserror::pending);
return ret;
}
ircd::js::value
ircd::js::call(const object &obj,
const std::string &name,
const vector<value>::handle &args)
{
return call(obj, name.c_str(), args);
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/del.h
//
void
ircd::js::del(const object::handle &src,
const char *const path)
{
value val;
object obj(src);
const char *fail(nullptr);
tokens(path, ".", [&path, &val, &obj, &fail]
(char *const &part)
{
if(fail)
throw type_error("cannot recurse through non-object '%s' in `%s'", fail, path);
if(!JS_GetProperty(*cx, obj, part, &val) || undefined(val))
throw reference_error("%s", part);
object tmp(obj.get());
if(!JS_ValueToObject(*cx, val, &obj) || !obj.get())
{
fail = part;
obj = std::move(tmp);
}
});
del(obj, id(val));
}
void
ircd::js::del(const object::handle &obj,
const id::handle &id)
{
JS::ObjectOpResult res;
if(!JS_DeletePropertyById(*cx, obj, id, res))
throw jserror(jserror::pending);
if(!res.checkStrict(*cx, obj, id))
throw jserror(jserror::pending);
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/set.h
//
void
ircd::js::set(const object::handle &src,
const char *const path,
const value &val)
{
value tmp;
object obj(src);
char buffer[strlen(path) + 1];
const char *fail(nullptr), *key(nullptr);
tokens(path, ".", buffer, sizeof(buffer), [&path, &tmp, &obj, &fail, &key]
(const char *const &part)
{
if(fail)
throw type_error("cannot recurse through non-object '%s' in `%s'", fail, path);
if(key)
throw reference_error("%s", part);
key = part;
if(!JS_GetProperty(*cx, obj, part, &tmp) || undefined(tmp))
return;
if(!JS_ValueToObject(*cx, tmp, &obj) || !obj.get())
fail = part;
});
if(!key)
return;
if(!JS_SetProperty(*cx, obj, key, val))
throw jserror(jserror::pending);
}
void
ircd::js::set(const object::handle &obj,
const id::handle &id,
const value &val)
{
if(!JS_SetPropertyById(*cx, obj, id, val))
throw jserror(jserror::pending);
}
void
ircd::js::set(const object::handle &obj,
const reserved &slot,
const value &val)
{
JS_SetReservedSlot(obj, slot, val);
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/get.h
//
ircd::js::value
ircd::js::get(const object::handle &src,
const char *const path)
{
value ret;
object obj(src);
const char *fail(nullptr);
tokens(path, ".", [&obj, &path, &ret, &fail]
(const char *const &part)
{
if(fail)
throw type_error("cannot recurse through non-object '%s' in `%s'", fail, path);
if(!JS_GetProperty(*cx, obj, part, &ret) || undefined(ret))
throw reference_error("%s", part);
if(!JS_ValueToObject(*cx, ret, &obj) || !obj.get())
fail = part;
});
return ret;
}
ircd::js::value
ircd::js::get(const object::handle &obj,
const id::handle &id)
{
value ret;
if(!JS_GetPropertyById(*cx, obj, id, &ret) || undefined(ret))
throw reference_error("%s", string(id).c_str());
return ret;
}
ircd::js::value
ircd::js::get(const object::handle &obj,
const reserved &slot)
{
return JS_GetReservedSlot(obj, slot);
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/has.h
//
bool
ircd::js::has(const object::handle &src,
const char *const path)
{
bool ret(true);
object obj(src);
const char *fail(nullptr);
tokens(path, ".", [&obj, &path, &ret, &fail]
(const char *const &part)
{
if(fail)
throw type_error("cannot recurse through non-object '%s' in `%s'", fail, path);
if(!JS_HasProperty(*cx, obj, part, &ret))
throw jserror(jserror::pending);
if(!ret)
return;
value tmp;
if(!JS_GetProperty(*cx, obj, part, &tmp) || undefined(tmp))
{
ret = false;
return;
}
if(!JS_ValueToObject(*cx, tmp, &obj) || !obj.get())
fail = part;
});
return ret;
}
bool
ircd::js::has(const object::handle &obj,
const id::handle &id)
{
bool ret;
if(!JS_HasPropertyById(*cx, obj, id, &ret))
throw jserror(jserror::pending);
return ret;
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/string.h
//
char *
ircd::js::c_str(const JSString *const &str)
{
static thread_local char cbuf[CSTR_BUFS][CSTR_BUFSIZE];
static thread_local size_t ctr;
char *const buf(cbuf[ctr]);
native(str, buf, CSTR_BUFSIZE);
ctr = (ctr + 1) % CSTR_BUFS;
return buf;
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/value.h
//
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/native.h
//
namespace ircd {
namespace js {
void native_external_noop(const JSStringFinalizer *const fin, char16_t *const buf);
void native_external_deleter(const JSStringFinalizer *const fin, char16_t *const buf);
JSStringFinalizer native_external_delete
{
native_external_deleter
};
JSStringFinalizer native_external_static
{
native_external_noop
};
} // namespace js
} // namespace ircd
std::string
ircd::js::native(const JSString *const &s)
{
std::string ret(native_size(s) + 1, char());
native(s, &ret.front(), ret.size());
ret.resize(ret.size() - 1);
return ret;
}
size_t
ircd::js::native_size(const JSString *const &s)
{
return JS_GetStringEncodingLength(*cx, const_cast<JSString *>(s));
}
size_t
ircd::js::native(const JSString *const &s,
char *const &buf,
const size_t &max)
{
if(unlikely(!max))
return 0;
ssize_t ret(s? JS_EncodeStringToBuffer(*cx, const_cast<JSString *>(s), buf, max) : 0);
ret = std::max(ret, ssize_t(0));
ret = std::min(ret, ssize_t(max - 1));
buf[ret] = '\0';
return ret;
}
void
ircd::js::native_external_deleter(const JSStringFinalizer *const fin,
char16_t *const buf)
{
log.debug("string delete (fin: %p buf: %p)",
(const void *)fin,
(const void *)buf);
delete[] buf;
}
void
ircd::js::native_external_noop(const JSStringFinalizer *const fin,
char16_t *const buf)
{
log.debug("string literal release (fin: %p buf: %p)",
(const void *)fin,
(const void *)buf);
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/error.h
//
ircd::js::jserror::jserror(const JS::Value &val)
:ircd::js::error{generate_skip}
,val{val}
{
}
ircd::js::jserror::jserror(generate_skip_t)
:ircd::js::error(generate_skip)
,val{}
{
}
ircd::js::jserror::jserror(const char *const fmt,
...)
:ircd::js::error{generate_skip}
,val{}
{
va_list ap;
va_start(ap, fmt);
generate(JSEXN_ERR, fmt, ap);
va_end(ap);
}
ircd::js::jserror::jserror(const JSErrorReport &report)
:ircd::js::error{generate_skip}
,val{}
{
create(report);
}
ircd::js::jserror::jserror(pending_t)
:ircd::js::error{generate_skip}
,val{}
{
if(unlikely(!restore_exception(*cx)))
{
snprintf(ircd::exception::buf, sizeof(ircd::exception::buf),
"(internal error): Failed to restore exception.");
return;
}
auto &report(cx->report);
if(JS_GetPendingException(*cx, &val))
{
JS::RootedObject obj(*cx, &val.toObject());
if(likely(JS_ErrorFromException(*cx, obj)))
report = *JS_ErrorFromException(*cx, obj);
generate_what_our(report);
JS_ClearPendingException(*cx);
return;
}
switch(report.errorNumber)
{
case 61: // JSAPI's code for interruption
snprintf(ircd::exception::buf, sizeof(ircd::exception::buf),
"interrupted @ line: %u col: %u",
report.lineno,
report.column);
break;
case 105: // JSAPI's code for user reported error
snprintf(ircd::exception::buf, sizeof(ircd::exception::buf),
"(BUG) Host exception");
break;
default:
snprintf(ircd::exception::buf, sizeof(ircd::exception::buf),
"Unknown non-exception #%u flags[%02x]",
report.errorNumber,
report.flags);
break;
}
}
void
ircd::js::jserror::set_pending()
const
{
JS_SetPendingException(*cx, value(val));
}
void
ircd::js::jserror::generate(const JSExnType &type,
const char *const &fmt,
va_list ap)
{
ircd::exception::generate(fmt, ap);
const auto msg(locale::char16::conv(what()));
JSErrorReport report;
report.ucmessage = msg.c_str();
report.exnType = type;
create(report);
}
void
ircd::js::jserror::create(const JSErrorReport &report)
{
JSErrorReport cpy(report);
create(cpy);
}
void
ircd::js::jserror::create(JSErrorReport &report)
{
JS::AutoFilename fn;
const auto col(report.column? nullptr : &report.column);
const auto line(report.lineno? nullptr : &report.lineno);
DescribeScriptedCaller(*cx, &fn, line, col);
JS::RootedString msg
{
*cx,
JS_NewUCStringCopyZ(*cx, report.ucmessage)
};
JS::RootedString file
{
*cx,
JS_NewStringCopyZ(*cx, fn.get()?: "<unknown>")
};
JS::RootedObject stack(*cx, nullptr);
const auto type((JSExnType)report.exnType);
if(!JS::CreateError(*cx,
type,
stack,
file,
report.lineno,
report.column,
&report,
msg,
&val))
{
throw error("Failed to construct jserror exception!");
}
generate_what_our(report);
}
void
ircd::js::jserror::generate_what_our(const JSErrorReport &report)
{
char linebuf[64];
snprintf(linebuf, sizeof(linebuf), "@%u+%u: ",
report.lineno,
report.column);
const auto msg(report.ucmessage? locale::char16::conv(report.ucmessage) : std::string{});
snprintf(ircd::exception::buf, sizeof(ircd::exception::buf), "%s%s%s%s",
reflect((JSExnType)report.exnType),
msg.empty()? "." : ": ",
msg.empty()? "" : !report.lineno && !report.column? "" : linebuf,
msg.c_str());
}
void
ircd::js::jserror::generate_what_js(const JSErrorReport &report)
{
const auto str(native(::js::ErrorReportToString(*cx, const_cast<JSErrorReport *>(&report))));
snprintf(ircd::exception::buf, sizeof(ircd::exception::buf), "%s", str.c_str());
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/debug.h
//
void
ircd::js::log_gcparams()
{
for(int i(0); i < 50; ++i)
{
const auto key(static_cast<JSGCParamKey>(i));
const char *const name(reflect(key));
if(!strlen(name))
continue;
// These trigger assertion failures
switch(key)
{
case JSGC_NUMBER:
case JSGC_MAX_CODE_CACHE_BYTES:
case JSGC_DECOMMIT_THRESHOLD:
continue;
default:
break;
}
log.debug("context(%p) %s => %u",
(const void *)cx,
name,
get(*cx, key));
}
}
void
ircd::js::backtrace()
{
#ifdef JS_DEBUG
::js::DumpBacktrace(*cx);
#endif
}
void
ircd::js::dump(const JSString *const &v)
{
#ifdef JS_DEBUG
::js::DumpString(const_cast<JSString *>(v));
#endif
}
void
ircd::js::dump(const JSAtom *const &v)
{
#ifdef JS_DEBUG
::js::DumpAtom(const_cast<JSAtom *>(v));
#endif
}
void
ircd::js::dump(const JSObject *const &v)
{
#ifdef JS_DEBUG
::js::DumpObject(const_cast<JSObject *>(v));
#endif
}
void
ircd::js::dump(const JS::Value &v)
{
#ifdef JS_DEBUG
::js::DumpValue(v);
#endif
}
void
ircd::js::dump(const jsid &v)
{
#ifdef JS_DEBUG
::js::DumpId(v);
#endif
}
void
ircd::js::dump(const JSContext *v)
{
#ifdef JS_DEBUG
::js::DumpPC(const_cast<JSContext *>(v));
#endif
}
void
ircd::js::dump(const JSScript *const &v)
{
#ifdef JS_DEBUG
::js::DumpScript(*cx, const_cast<JSScript *>(v));
#endif
}
void
ircd::js::dump(const char16_t *const &v, const size_t &len)
{
#ifdef JS_DEBUG
::js::DumpChars(v, len);
#endif
}
void
ircd::js::dump(const ::js::InterpreterFrame *v)
{
#ifdef JS_DEBUG
::js::DumpInterpreterFrame(*cx, const_cast<::js::InterpreterFrame *>(v));
#endif
}
std::string
ircd::js::debug(const JSErrorReport &r)
{
std::stringstream ss;
if(JSREPORT_IS_WARNING(r.flags))
ss << "WARNING ";
if(JSREPORT_IS_EXCEPTION(r.flags))
ss << "EXCEPTION ";
if(JSREPORT_IS_STRICT(r.flags))
ss << "STRICT ";
if(JSREPORT_IS_STRICT_MODE_ERROR(r.flags))
ss << "STRICT_MODE_ERROR ";
if(r.isMuted)
ss << "MUTED ";
if(r.filename)
ss << "file[" << r.filename << "] ";
if(r.lineno)
ss << "line[" << r.lineno << "] ";
if(r.column)
ss << "col[" << r.column << "] ";
if(r.linebuf())
ss << "code[" << r.linebuf() << "] ";
//if(r.tokenptr)
// ss << "toke[" << r.tokenptr << "] ";
if(r.errorNumber)
ss << "errnum[" << r.errorNumber << "] ";
if(r.exnType)
ss << reflect(JSExnType(r.exnType)) << " ";
if(r.ucmessage)
ss << "\"" << locale::char16::conv(r.ucmessage) << "\" ";
for(auto it(r.messageArgs); it && *it; ++it)
ss << "\"" << locale::char16::conv(*it) << "\" ";
return ss.str();
}
std::string
ircd::js::debug(const JS::HandleObject &o)
{
std::stringstream ss;
if(JS_IsGlobalObject(o)) ss << "Global ";
if(JS_IsNative(o)) ss << "Native ";
if(JS::IsCallable(o)) ss << "Callable ";
if(JS::IsConstructor(o)) ss << "Constructor ";
bool ret;
if(JS_IsExtensible(*cx, o, &ret) && ret)
ss << "Extensible ";
if(JS_IsArrayObject(*cx, o, &ret) && ret)
ss << "Array ";
return ss.str();
}
std::string
ircd::js::debug(const JS::Value &v)
{
std::stringstream ss;
if(v.isNull()) ss << "Null ";
if(v.isUndefined()) ss << "Undefined ";
if(v.isBoolean()) ss << "Boolean ";
if(v.isTrue()) ss << "TrueValue ";
if(v.isFalse()) ss << "FalseValue ";
if(v.isNumber()) ss << "Number ";
if(v.isDouble()) ss << "Double ";
if(v.isInt32()) ss << "Int32 ";
if(v.isString()) ss << "String ";
if(v.isObject()) ss << "Object ";
if(v.isSymbol()) ss << "Symbol ";
return ss.str();
}
const char *
ircd::js::reflect_telemetry(const int &id)
{
switch(id)
{
case JS_TELEMETRY_GC_REASON: return "GC_REASON";
case JS_TELEMETRY_GC_IS_COMPARTMENTAL: return "GC_IS_COMPARTMENTAL";
case JS_TELEMETRY_GC_MS: return "GC_MS";
case JS_TELEMETRY_GC_BUDGET_MS: return "GC_BUDGET_MS";
case JS_TELEMETRY_GC_ANIMATION_MS: return "GC_ANIMATION_MS";
case JS_TELEMETRY_GC_MAX_PAUSE_MS: return "GC_MAX_PAUSE_MS";
case JS_TELEMETRY_GC_MARK_MS: return "GC_MARK_MS";
case JS_TELEMETRY_GC_SWEEP_MS: return "GC_SWEEP_MS";
case JS_TELEMETRY_GC_MARK_ROOTS_MS: return "GC_MARK_ROOTS_MS";
case JS_TELEMETRY_GC_MARK_GRAY_MS: return "GC_MARK_GRAY_MS";
case JS_TELEMETRY_GC_SLICE_MS: return "GC_SLICE_MS";
case JS_TELEMETRY_GC_SLOW_PHASE: return "GC_SLOW_PHASE";
case JS_TELEMETRY_GC_MMU_50: return "GC_MMU_50";
case JS_TELEMETRY_GC_RESET: return "GC_RESET";
case JS_TELEMETRY_GC_INCREMENTAL_DISABLED: return "GC_INCREMENTAL_DISABLED";
case JS_TELEMETRY_GC_NON_INCREMENTAL: return "GC_NON_INCREMENTAL";
case JS_TELEMETRY_GC_SCC_SWEEP_TOTAL_MS: return "GC_SCC_SWEEP_TOTAL_MS";
case JS_TELEMETRY_GC_SCC_SWEEP_MAX_PAUSE_MS: return "GC_SCC_SWEEP_MAX_PAUSE_MS";
case JS_TELEMETRY_GC_MINOR_REASON: return "GC_MINOR_REASON";
case JS_TELEMETRY_GC_MINOR_REASON_LONG: return "GC_MINOR_REASON_LONG";
case JS_TELEMETRY_GC_MINOR_US: return "GC_MINOR_US";
case JS_TELEMETRY_DEPRECATED_LANGUAGE_EXTENSIONS_IN_CONTENT: return "DEPRECATED_LANGUAGE_EXTENSIONS_IN_CONTENT";
case JS_TELEMETRY_DEPRECATED_LANGUAGE_EXTENSIONS_IN_ADDONS: return "DEPRECATED_LANGUAGE_EXTENSIONS_IN_ADDONS";
case JS_TELEMETRY_ADDON_EXCEPTIONS: return "ADDON_EXCEPTIONS";
}
return "";
}
const char *
ircd::js::reflect(const ::js::CTypesActivityType &t)
{
using namespace ::js;
switch(t)
{
case CTYPES_CALL_BEGIN: return "CTYPES_CALL_BEGIN";
case CTYPES_CALL_END: return "CTYPES_CALL_END";
case CTYPES_CALLBACK_BEGIN: return "CTYPES_CALLBACK_BEGIN";
case CTYPES_CALLBACK_END: return "CTYPES_CALLBACK_END";
}
return "";
}
const char *
ircd::js::reflect(const JSContextOp &op)
{
switch(op)
{
case JSCONTEXT_NEW: return "JSCONTEXT_NEW";
case JSCONTEXT_DESTROY: return "JSCONTEXT_DESTROY";
}
return "";
}
const char *
ircd::js::reflect(const JSFinalizeStatus &s)
{
switch(s)
{
case JSFINALIZE_GROUP_START: return "GROUP_START";
case JSFINALIZE_GROUP_END: return "GROUP_END";
case JSFINALIZE_COLLECTION_END: return "COLLECTION_END";
}
return "";
}
const char *
ircd::js::reflect(const JSGCParamKey &s)
{
switch(s)
{
case JSGC_MAX_BYTES: return "JSGC_MAX_BYTES";
case JSGC_MAX_MALLOC_BYTES: return "JSGC_MAX_MALLOC_BYTES";
case JSGC_BYTES: return "JSGC_BYTES";
case JSGC_NUMBER: return "JSGC_NUMBER";
case JSGC_MAX_CODE_CACHE_BYTES: return "JSGC_MAX_CODE_CACHE_BYTES";
case JSGC_MODE: return "JSGC_MODE";
case JSGC_UNUSED_CHUNKS: return "JSGC_UNUSED_CHUNKS";
case JSGC_TOTAL_CHUNKS: return "JSGC_TOTAL_CHUNKS";
case JSGC_SLICE_TIME_BUDGET: return "JSGC_SLICE_TIME_BUDGET";
case JSGC_MARK_STACK_LIMIT: return "JSGC_MARK_STACK_LIMIT";
case JSGC_HIGH_FREQUENCY_TIME_LIMIT: return "JSGC_HIGH_FREQUENCY_TIME_LIMIT";
case JSGC_HIGH_FREQUENCY_LOW_LIMIT: return "JSGC_HIGH_FREQUENCY_LOW_LIMIT";
case JSGC_HIGH_FREQUENCY_HIGH_LIMIT: return "JSGC_HIGH_FREQUENCY_HIGH_LIMIT";
case JSGC_HIGH_FREQUENCY_HEAP_GROWTH_MAX: return "JSGC_HIGH_FREQUENCY_HEAP_GROWTH_MAX";
case JSGC_HIGH_FREQUENCY_HEAP_GROWTH_MIN: return "JSGC_HIGH_FREQUENCY_HEAP_GROWTH_MIN";
case JSGC_LOW_FREQUENCY_HEAP_GROWTH: return "JSGC_LOW_FREQUENCY_HEAP_GROWTH";
case JSGC_DYNAMIC_HEAP_GROWTH: return "JSGC_DYNAMIC_HEAP_GROWTH";
case JSGC_DYNAMIC_MARK_SLICE: return "JSGC_DYNAMIC_MARK_SLICE";
case JSGC_ALLOCATION_THRESHOLD: return "JSGC_ALLOCATION_THRESHOLD";
case JSGC_DECOMMIT_THRESHOLD: return "JSGC_DECOMMIT_THRESHOLD";
case JSGC_MIN_EMPTY_CHUNK_COUNT: return "JSGC_MIN_EMPTY_CHUNK_COUNT";
case JSGC_MAX_EMPTY_CHUNK_COUNT: return "JSGC_MAX_EMPTY_CHUNK_COUNT";
case JSGC_COMPACTING_ENABLED: return "JSGC_COMPACTING_ENABLED";
}
return "";
}
const char *
ircd::js::reflect(const JSGCStatus &s)
{
switch(s)
{
case JSGC_BEGIN: return "BEGIN";
case JSGC_END: return "END";
}
return "";
}
const char *
ircd::js::reflect(const JSExnType &e)
{
switch(e)
{
case JSEXN_NONE: return "?NONE?";
case JSEXN_ERR: return "Error";
case JSEXN_INTERNALERR: return "InternalError";
case JSEXN_EVALERR: return "EvalError";
case JSEXN_RANGEERR: return "RangeError";
case JSEXN_REFERENCEERR: return "ReferenceError";
case JSEXN_SYNTAXERR: return "SyntaxError";
case JSEXN_TYPEERR: return "TypeError";
case JSEXN_URIERR: return "URIError";
case JSEXN_LIMIT: return "?LIMIT?";
}
return "";
}
const char *
ircd::js::reflect(const JSType &t)
{
switch(t)
{
case JSTYPE_VOID: return "VOID";
case JSTYPE_OBJECT: return "OBJECT";
case JSTYPE_FUNCTION: return "FUNCTION";
case JSTYPE_STRING: return "STRING";
case JSTYPE_NUMBER: return "NUMBER";
case JSTYPE_BOOLEAN: return "BOOLEAN";
case JSTYPE_NULL: return "NULL";
case JSTYPE_SYMBOL: return "SYMBOL";
case JSTYPE_LIMIT: return "LIMIT";
}
return "";
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/compartment.h
//
ircd::js::compartment::compartment(const JSVersion &ver)
:compartment{*cx, ver}
{
}
ircd::js::compartment::compartment(context &c,
const JSVersion &ver)
:compartment
{
current_global(c)?: throw error("Cannot enter compartment without global"),
c,
ver
}
{
}
ircd::js::compartment::compartment(JSObject *const &obj,
const JSVersion &ver)
:compartment{obj, *cx, ver}
{
}
ircd::js::compartment::compartment(JSObject *const &obj,
context &c,
const JSVersion &ver)
:c{&c}
,prev{JS_EnterCompartment(c, obj)}
,ours{current_compartment(c)}
,cprev{static_cast<compartment *>(JS_GetCompartmentPrivate(ours))}
{
JS_SetCompartmentPrivate(ours, this);
JS_SetVersionForCompartment(ours, ver);
}
ircd::js::compartment::compartment(compartment &&other)
noexcept
:c{std::move(other.c)}
,prev{std::move(other.prev)}
,ours{std::move(other.ours)}
,cprev{std::move(other.cprev)}
{
JS_SetCompartmentPrivate(ours, this);
other.ours = nullptr;
}
ircd::js::compartment::~compartment()
noexcept
{
// branch not taken on std::move()
if(ours)
{
JS_SetCompartmentPrivate(ours, cprev);
JS_LeaveCompartment(*c, prev);
}
}
void
ircd::js::for_each_compartment_our(const compartment::closure_our &closure)
{
for_each_compartment([&closure]
(JSCompartment *const &c)
{
if(our(c))
closure(*our(c));
});
}
void
ircd::js::for_each_compartment(const compartment::closure &closure)
{
JS_IterateCompartments(*rt,
const_cast<compartment::closure *>(&closure),
compartment::handle_iterate);
}
void
ircd::js::compartment::handle_iterate(JSRuntime *const rt,
void *const priv,
JSCompartment *const c)
noexcept
{
const auto &closure(*static_cast<compartment::closure *>(priv));
closure(c);
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/context.h
//
ircd::js::context::context(const struct opts &opts)
:context{*rt, opts}
{
}
ircd::js::context::context(struct runtime &runtime,
const struct opts &opts)
:custom_ptr<JSContext>
{
// Construct the context
[this, &runtime, &opts]
{
const auto ret(JS_NewContext(runtime, opts.stack_chunk_size));
// Use their privdata pointer to point to our instance. We can then use our(JSContext*)
// to get back to `this` instance.
JS_SetContextPrivate(ret, this);
return ret;
}(),
// Plant the destruction
[](JSContext *const ctx)
noexcept
{
if(!ctx)
return;
// Free the user's privdata managed object
delete static_cast<const privdata *>(JS_GetSecondContextPrivate(ctx));
JS_DestroyContext(ctx);
}
}
,opts(opts)
,except{nullptr}
,state
{{
0, // Semaphore value starting at 0.
phase::ACCEPT, // ACCEPT phase indicates nothing is running.
irq::JS, // irq::JS is otherwise here in case JS triggers an interrupt.
}}
,timer
{
std::bind(&context::handle_timeout, this)
}
{
assert(&runtime == rt); // Trying to construct on thread without runtime thread_local
timer.set(opts.timer_limit);
}
ircd::js::context::~context()
noexcept
{
}
void
ircd::js::context::handle_timeout()
noexcept
{
// At this time there is no yield logic so if the timer calls the script is terminated.
interrupt(*this, irq::TERMINATE);
}
bool
ircd::js::context::handle_interrupt()
{
auto state(this->state.load(std::memory_order_acquire));
// Spurious interrupt; ignore.
if(unlikely(state.phase != phase::INTR && state.phase != phase::ENTER))
{
log.warning("context(%p): Spurious interrupt (irq: %02x)",
(const void *)this,
uint(state.irq));
return true;
}
// After the interrupt is handled the phase indicates entry back to JS,
// IRQ is left indicating JS in case we don't trigger the next interrupt.
const scope interrupt_return([this, &state]
{
state.phase = phase::ENTER;
state.irq = irq::JS;
this->state.store(state, std::memory_order_release);
});
// Call the user hook if available
if(on_intr)
{
// The user's handler returns -1 for non-overriding behavior
const auto ret(on_intr(state.irq));
if(ret != -1)
return ret;
}
switch(state.irq)
{
default:
case irq::NONE:
assert(0);
case irq::JS:
case irq::USER:
return true;
case irq::YIELD:
ctx::yield();
return true;
case irq::TERMINATE:
return false;
}
}
void
ircd::js::leave(context &c)
{
c.timer.cancel();
// Load the state to keep the current sem value up to date.
// This thread is the only writer to that value.
auto state(c.state.load(std::memory_order_relaxed));
// The ACCEPT phase locks out the interruptor
state.phase = phase::ACCEPT;
// The ACCEPT is released and the current phase seen by interruptor is acquired.
state = c.state.exchange(state, std::memory_order_acq_rel);
// The executor (us) must check if the interruptor (them) has committed to an interrupt
// targeting the JS run we are now leaving. JS may have exited after the commitment
// and before the interrupt arrival.
if(state.phase == phase::INTR)
assert(interrupt_poll(c));
}
void
ircd::js::enter(context &c)
{
// State was already acquired by the last leave();
auto state(c.state.load(std::memory_order_relaxed));
// Increment the semaphore for the next execution;
++state.sem;
// Set the IRQ to JS in case we don't trigger an interrupt, the handler
// will see a correct value.
state.irq = irq::JS;
state.phase = phase::ENTER;
// Commit to next execution.
c.state.store(state, std::memory_order_release);
c.timer.start();
}
bool
ircd::js::interrupt(context &c,
const irq &req)
{
if(req == irq::NONE)
return false;
// Acquire the execution state. Proceed if something was running.
const auto state(c.state.load(std::memory_order_acquire));
if(state.phase != phase::ENTER)
return false;
// The expected value of the state to transact.
struct context::state in
{
state.sem, // Expect the sem value to match, else the execution has changed.
phase::ENTER, // Expect the execution to be occurring.
irq::JS, // JS is always expected here instead of NONE.
};
// The restatement after the transaction.
const struct context::state out
{
state.sem, // Keep the same sem value.
phase::INTR, // Indicate our commitment to interrupting.
req // Include the irq type.
};
// Attempt commitment to interrupt here.
static const auto order_fail(std::memory_order_relaxed);
static const auto order_success(std::memory_order_acq_rel);
if(!c.state.compare_exchange_strong(in, out, order_success, order_fail))
{
// To reliably read from `in` here, order_fail should not be relaxed.
return false;
}
// Commitment now puts the burden on the executor to not allow this interrupt to bleed into
// the next execution, even if JS has already exited before its arrival.
interrupt(c.runtime());
return true;
}
bool
ircd::js::interrupt_poll(const context &c)
{
return JS_CheckForInterrupt(c);
}
bool
ircd::js::restore_exception(context &c)
{
if(unlikely(!c.except))
return false;
JS_RestoreExceptionState(c, c.except);
c.except = nullptr;
return true;
}
void
ircd::js::save_exception(context &c,
const JSErrorReport &report)
{
if(c.except)
throw error("(internal error): Won't overwrite saved exception");
c.except = JS_SaveExceptionState(c),
c.report = report;
}
void
ircd::js::run_gc(context &c)
{
JS_MaybeGC(c);
}
void
ircd::js::out_of_memory(context &c)
{
JS_ReportOutOfMemory(c);
}
void
ircd::js::allocation_overflow(context &c)
{
JS_ReportAllocationOverflow(c);
}
uint32_t
ircd::js::get(context &c,
const JSGCParamKey &key)
{
//return JS_GetGCParameterForThread(c, key); // broken
return JS_GetGCParameter(c.runtime(), key);
}
void
ircd::js::set(context &c,
const JSGCParamKey &key,
const uint32_t &val)
{
//JS_SetGCParameterForThread(c, key, val); // broken
JS_SetGCParameter(c.runtime(), key, val);
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/timer.h
//
ircd::js::timer::timer(const callback &timeout)
:finished{false}
,timeout{timeout}
,started{time_point::min()}
,limit{0us}
,state
{{
0, false
}}
,thread{std::bind(&timer::worker, this)}
{
}
ircd::js::timer::~timer()
noexcept
{
// This is all on the main IRCd thread, and the only point when it waits on the timer
// thread. That's okay as long as this is on IRCd shutdown etc.
std::unique_lock<decltype(mutex)> lock(mutex);
finished = true;
// Wait for timer thread to exit. Notify before the unlock() could double tap,
// but should always make for a reliable way to do this shutdown.
cond.notify_all();
lock.unlock();
thread.join();
}
ircd::js::timer::time_point
ircd::js::timer::start()
{
// The counter is incremented indicating a new timing request, invalidating
// anything the timer was previously doing.
auto state(this->state.load(std::memory_order_relaxed));
++state.sem;
state.running = true;
// Commit to starting a new timer operation, unconditionally overwrite previous.
started = steady_clock::now();
this->state.store(state, std::memory_order_release);
// The timing thread is notified here. It may have already started on our new
// request. However, this notifcation must not be delayed and the timing thread
// must wake up soon/now.
cond.notify_one();
return started;
}
bool
ircd::js::timer::cancel()
{
// Relaxed acquire of the state to get the sem value.
auto state(this->state.load(std::memory_order_relaxed));
// Expect the timer to be running and order the timer to idle with an invalidation.
struct state in { state.sem, true };
const struct state out { state.sem + 1, false };
// Commit to cancellation. On a successful state transition, wake up the thread.
static const auto order_fail(std::memory_order_relaxed);
static const auto order_success(std::memory_order_acq_rel);
if(this->state.compare_exchange_strong(in, out, order_success, order_fail))
{
cond.notify_one();
return true;
}
else return false;
}
void
ircd::js::timer::set(const callback &timeout)
{
this->timeout = timeout;
std::atomic_thread_fence(std::memory_order_release);
}
void
ircd::js::timer::set(const microseconds &limit)
{
this->limit.store(limit, std::memory_order_release);
}
void
ircd::js::timer::worker()
{
// This lock is only ever held by this thread except during a finish condition.
// Notifications to the condition are only broadcast by the main thread.
std::unique_lock<decltype(mutex)> lock(mutex);
while(likely(!finished))
handle(lock);
}
void
ircd::js::timer::handle(std::unique_lock<std::mutex> &lock)
{
struct state ours, theirs;
const auto idle_condition([this, &ours]
{
if(unlikely(finished))
return true;
// Acquire the request operation
ours = this->state.load(std::memory_order_acquire);
// Require a start time
if(started == time_point::min())
return false;
return ours.running;
});
const auto cancel_condition([this, &ours, &theirs]
{
if(unlikely(finished))
return true;
// Acquire the request state and compare it to our saved state to see
// if the counter has changed or if there is cancellation.
theirs = this->state.load(std::memory_order_consume);
return ours.sem != theirs.sem || !theirs.running;
});
// Wait for a running condition into `in`
cond.wait(lock, idle_condition);
if(unlikely(finished))
return;
// Wait for timeout or cancellation
const auto limit(this->limit.load(std::memory_order_consume));
if(cond.wait_until(lock, started + limit, cancel_condition))
{
// A cancel or increment of the counter into the next request has occurred.
// This thread will go back to sleep in the idle_condition unless or until
// the next start() is triggered.
return;
}
// A timeout has occurred. This is the last chance for a belated cancellation to be
// observed. If a transition from running to !running can take place on this counter
// value, the user has not invalidated this request and desires the timeout.
assert(ours.running);
const struct state out { ours.sem, false };
static const auto order_fail(std::memory_order_relaxed);
static const auto order_success(std::memory_order_acq_rel);
if(state.compare_exchange_strong(ours, out, order_success, order_fail))
if(likely(timeout))
timeout();
}
///////////////////////////////////////////////////////////////////////////////
//
// ircd/js/runtime.h
//
ircd::js::runtime::runtime(const struct opts &opts,
runtime *const &parent)
:custom_ptr<JSRuntime>
{
JS_NewRuntime(opts.max_bytes,
opts.max_nursery_bytes,
parent? static_cast<JSRuntime *>(*parent) : nullptr),
JS_DestroyRuntime
}
,opts(opts)
,tid{std::this_thread::get_id()}
{
// We use their privdata to find `this` via our(JSRuntime*) function.
// Any additional user privdata will have to ride a member in this class itself.
JS_SetRuntimePrivate(get(), this);
JS_SetErrorReporter(get(), handle_error);
JS::SetOutOfMemoryCallback(get(), handle_out_of_memory, nullptr);
JS::SetLargeAllocationFailureCallback(get(), handle_large_allocation_failure, nullptr);
JS_SetGCCallback(get(), handle_gc, nullptr);
JS_AddFinalizeCallback(get(), handle_finalize, nullptr);
JS_SetAccumulateTelemetryCallback(get(), handle_telemetry);
JS_SetSweepZoneCallback(get(), handle_zone_sweep);
JS_SetDestroyZoneCallback(get(), handle_zone_destroy);
JS_SetCompartmentNameCallback(get(), handle_compartment_name);
JS_SetDestroyCompartmentCallback(get(), handle_compartment_destroy);
JS_SetContextCallback(get(), handle_context, nullptr);
::js::SetActivityCallback(get(), handle_activity, this);
::js::SetCTypesActivityCallback(get(), handle_activity_ctypes);
JS_SetInterruptCallback(get(), handle_interrupt);
JS_SetNativeStackQuota(get(), opts.code_stack_max, opts.trusted_stack_max, opts.untrusted_stack_max);
}
ircd::js::runtime::runtime(runtime &&other)
noexcept
:custom_ptr<JSRuntime>{std::move(other)}
,opts(std::move(other.opts))
,tid{std::move(other.tid)}
{
// Branch not taken for null/defaulted instance of JSRuntime smart ptr
if(!!*this)
JS_SetRuntimePrivate(get(), this);
}
ircd::js::runtime &
ircd::js::runtime::operator=(runtime &&other)
noexcept
{
static_cast<custom_ptr<JSRuntime> &>(*this) = std::move(other);
opts = std::move(other.opts);
tid = std::move(other.tid);
// Branch not taken for null/defaulted instance of JSRuntime smart ptr
if(!!*this)
JS_SetRuntimePrivate(get(), this);
return *this;
}
ircd::js::runtime::~runtime()
noexcept
{
}
bool
ircd::js::runtime::handle_interrupt(JSContext *const ctx)
noexcept
{
log.debug("JSContext(%p): Interrupt", (const void *)ctx);
auto &c(our(ctx));
return c.handle_interrupt();
}
void
ircd::js::runtime::handle_activity(void *const priv,
const bool active)
noexcept
{
assert(priv);
const auto tid(std::this_thread::get_id());
auto &runtime(*static_cast<struct runtime *>(priv));
log.debug("[thread %s] runtime(%p): %s",
ircd::string(tid).c_str(),
(const void *)&runtime,
active? "ACTIVE" : "IDLE");
}
void
ircd::js::handle_activity_ctypes(JSContext *const c,
const ::js::CTypesActivityType t)
noexcept
{
log.debug("context(%p): %s",
(const void *)c,
reflect(t));
}
bool
ircd::js::runtime::handle_context(JSContext *const c,
const uint op,
void *const priv)
noexcept
{
log.debug("context(%p): %s (priv: %p)",
(const void *)c,
reflect(static_cast<JSContextOp>(op)),
priv);
return true;
}
void
ircd::js::runtime::handle_compartment_destroy(JSFreeOp *const fop,
JSCompartment *const compartment)
noexcept
{
log.debug("runtime(%p): compartment: %p %s%sdestroy: fop(%p)",
(const void *)(our_runtime(*fop).ptr()),
(const void *)compartment,
::js::IsSystemCompartment(compartment)? "[system] " : "",
::js::IsAtomsCompartment(compartment)? "[atoms] " : "",
(const void *)fop);
}
void
ircd::js::runtime::handle_compartment_name(JSRuntime *const rt,
JSCompartment *const compartment,
char *const buf,
const size_t max)
noexcept
{
log.debug("runtime(%p): comaprtment: %p (buf@%p: max: %zu)",
(const void *)rt,
(const void *)compartment,
(const void *)buf,
max);
}
void
ircd::js::runtime::handle_zone_destroy(JS::Zone *const zone)
noexcept
{
log.debug("runtime(%p): zone: %p %s%sdestroy",
(const void *)rt,
(const void *)zone,
::js::IsSystemZone(zone)? "[system] " : "",
::js::IsAtomsZone(zone)? "[atoms] " : "");
}
void
ircd::js::runtime::handle_zone_sweep(JS::Zone *const zone)
noexcept
{
log.debug("runtime(%p): zone: %p %s%ssweep",
(const void *)rt,
(const void *)zone,
::js::IsSystemZone(zone)? "[system] " : "",
::js::IsAtomsZone(zone)? "[atoms] " : "");
}
void
ircd::js::runtime::handle_telemetry(const int id,
const uint32_t sample,
const char *const key)
noexcept
{
const auto tid(std::this_thread::get_id());
log.debug("[thread %s] runtime(%p) telemetry(%02d) %s: %u %s",
ircd::string(tid).c_str(),
(const void *)rt,
id,
reflect_telemetry(id),
sample,
key?: "");
}
void
ircd::js::runtime::handle_finalize(JSFreeOp *const fop,
const JSFinalizeStatus status,
const bool is_compartment,
void *const priv)
noexcept
{
log.debug("fop(%p): %s %s",
(const void *)fop,
reflect(status),
is_compartment? "COMPARTMENT" : "");
}
void
ircd::js::runtime::handle_gc(JSRuntime *const rt,
const JSGCStatus status,
void *const priv)
noexcept
{
log.debug("runtime(%p): GC %s",
(const void *)rt,
reflect(status));
}
void
ircd::js::runtime::handle_large_allocation_failure(void *const priv)
noexcept
{
log.error("Large allocation failure");
}
void
ircd::js::runtime::handle_out_of_memory(JSContext *const ctx,
void *const priv)
noexcept
{
log.error("JSContext(%p): out of memory", (const void *)ctx);
}
void
ircd::js::runtime::handle_error(JSContext *const ctx,
const char *const msg,
JSErrorReport *const report)
noexcept
{
assert(report);
/*
log.debug("JSContext(%p) Error report: %s [%s]",
(const void *)ctx,
msg,
debug(*report).c_str());
*/
/*
log.critical("Unhandled: JSContext(%p): %s [%s]",
(const void *)ctx,
msg,
debug(*report).c_str());
*/
save_exception(our(ctx), *report);
}