// Matrix Construct // // Copyright (C) Matrix Construct Developers, Authors & Contributors // Copyright (C) 2016-2018 Jason Volk // // 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. The // full license for this software is available in the LICENSE file. #include #include // JS_Init() / JS_ShutDown() #include // For GetThreadType() linkage hack (see: down) // Must be defined until we address issues with SpiderMonkey fixed inside any // IRCD_JS_FIX blocks. #define IRCD_JS_FIX // This was only ever defined for the SpiderMonkey headers and some of our hacks, // but we need to undef it to not step on the log level log::DEBUG. // Use JS_DEBUG as an analog instead. #undef DEBUG // Logging facility for this submodule with SNOMASK. decltype(ircd::js::log) ircd::js::log { "js", 'J' }; namespace ircd { namespace js { // Location of the thread_local runtext (js/context.h) // If this is null, js is not available on your thread. __thread context *cx; __thread trap *tree; // Whenever a JSClass is seen by the runtime it has to remain reachable for the lifetime // of the runtimet. They don't give any further access or callbacks to free the object, // expecting it to be static or something (yea right). What we have here is a place for // traps to dump their JSClass on destruction and then this can be reaped later. std::forward_list> class_drain; // Internal prototypes const char *reflect(const ::js::CTypesActivityType &); } // namespace js } // namespace ircd /////////////////////////////////////////////////////////////////////////////// // // ircd/js.h - Without 3rd party (JSAPI) symbols // ircd::js::init::init() { if(ircd::nojs) { log::warning { log, "Not initializing the JS engine due to -nojs set by user." }; return; } log::info { log, "Initializing the JS engine [%s: %s]", "SpiderMonkey", version(ver::IMPLEMENTATION) }; const unwind exit([this] { // Ensure ~init() is always safe to call at any intermediate state if(std::current_exception()) this->~init(); }); { const char *errmsg; if((errmsg = JS_InitWithFailureDiagnostic())) throw error("JS_Init(): %s", errmsg); } struct context::opts context_opts; log::info { log, "Initializing the main JS context (main_maxbytes: %zu)", context_opts.max_bytes }; assert(!cx); cx = new context(context_opts); // Additional options //set(*cx, JSGC_MODE, JSGC_MODE_INCREMENTAL); log::info { log, "Initialized main JS context (version: '%s')", version(*cx) }; } ircd::js::init::~init() noexcept { if(!cx) return; log::info { log, "Terminating the main JS context" }; delete cx; cx = nullptr; log::info { log, "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"); } } /////////////////////////////////////////////////////////////////////////////// // // ircd/js/js.h - With 3rd party (JSAPI) symbols // /////////////////////////////////////////////////////////////////////////////// // // ircd/js/task.h // ircd::js::task::task(const std::string &source) :task{locale::char16::conv(source)} { } ircd::js::task::task(const std::u16string &source) try :pid { 0 } ,global{[this] { JS::CompartmentCreationOptions opts; // 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, nullptr, opts); // 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). JS_SetPrivate(global, this); return global; }()} ,main{[this, &source] () -> struct module { // A compartment for the global must be entered to compile in this scope const compartment c(this->global); // TODO: options JS::CompileOptions opts(*cx); //opts.setCanLazilyParse(true); //opts.setSourceIsLazy(true); //opts.setIntroductionType("GeneratorFunction"); //opts.forceAsync = true; // The compilation is also conducted asynchronously: it will yield the current // ircd::ctx until it is complete. const auto instantiate{true}; return { module::yielding, opts, source, nullptr, instantiate }; }()} { //const compartment c(this->global); } catch(const std::exception &e) { throw; } ircd::js::task::~task() noexcept { run_gc(*cx); } bool ircd::js::task::enter(const std::weak_ptr &ptr, const std::function &closure) try { const life_guard task(ptr); return enter(*task, closure); } catch(const std::bad_weak_ptr &e) { ircd::js::log.warning("task::enter(%p, closure: %p): expired task", (const void *)&ptr, (const void *)&closure); return false; } bool ircd::js::task::enter(task &t, const std::function &closure) { const std::lock_guard lock{*cx}; const compartment compartment(t.global); run([&closure, &t] { closure(t); }); return true; } ircd::js::object ircd::js::reflect(const task &task) { const object global(task.global); const object reflect(get(global, "Reflect")); const function parse(get(reflect, "parse")); return parse(global, decompile(task)); } ircd::js::string ircd::js::decompile(const task &task, const bool &pretty) { return decompile(task.main, "main", pretty); } /////////////////////////////////////////////////////////////////////////////// // // ircd/js/global.h // ircd::js::global::global(trap &trap, JSPrincipals *const &principals, JS::CompartmentCreationOptions copts, JS::CompartmentBehaviors bopts) :object{[&trap, &principals, &copts, &bopts] { //copts.setMergeable(true); //copts.setInvisibleToDebugger(true); copts.setTrace(handle_trace); const JS::CompartmentOptions opts { copts, bopts }; return JS_NewGlobalObject(*cx, &trap.jsclass(), principals, JS::DontFireOnNewGlobalHook, opts); }()} ,module_resolve_hook{[this] { const compartment c(*this); return std::make_unique("ModuleResolveHook", 0, 0, [this] (object::handle func, value::handle that, const args &args) { assert(args.size() == 2); const object requestor(args.at(0)); const string requesting(args.at(1)); module &importer(module::our(requestor)); return import(importer, requesting, object(that)); }); }()} { const compartment c(*this); if(!JS_InitStandardClasses(*cx, *this)) throw error("Failed to init standard classes for global object"); JS_InitReflectParse(*cx, *this); JS::SetModuleResolveHook(*cx, *module_resolve_hook); JS_FireOnNewGlobalObject(*cx, *this); } ircd::js::global::~global() noexcept { } void ircd::js::global::handle_trace(JSTracer *const tracer, JSObject *const obj) noexcept { //assert(tracer->runtime() == cx->get()); log.debug("context(%p): global tracer(%p) object(%p)", nullptr,//(const void *)&our(tracer->context()), (const void *)tracer, (const void *)obj); } ircd::js::object ircd::js::global::import(module &importer, const string &requesting, const object &that) { log.debug("context(%p): module '%s' import '%s' (that: %p existing: %d)", (const void *)cx, std::string(""), std::string(requesting), (const void *)that.address(), imports.find(requesting) != end(imports)); const auto it(imports.find(requesting)); if(it != end(imports)) { const auto &exporter(*it->second); const string name(exporter.trap->name); js::set(that, name, exporter.trap->construct()); return static_cast(exporter); } static const std::string prefix { std::string(fs::MODPATH) + "/" }; string fname(prefix); const string delim("_"); tokens(requesting, '.', [&fname, &delim] (const string &token) { fname += token; fname += delim; }); fname = substr(fname, 0, fname.size() - 1); fname += ".so"; ircd::module *const m(new ircd::module(std::string(fname))); const auto exporter(m->get("IRCD_JS_MODULE")); const auto iit(imports.emplace(requesting, exporter)); const string name(exporter->trap->name); js::set(that, name, exporter->trap->construct()); return static_cast(*exporter); } /////////////////////////////////////////////////////////////////////////////// // // ircd/js/trap_property.h // ircd::js::trap::property::property(struct trap &trap, const std::string &name, const uint &flags) try :trap{&trap} ,name{name} ,spec{0} { spec.name = this->name.data(), spec.flags = flags, spec.accessors.setter.native = { handle_set }; spec.accessors.getter.native = { handle_get }; const auto iit(trap.memprop.emplace(this->name, this)); if(!iit.second) throw error("trap property '%s' already exists on '%s'", this->name, trap.name); log.debug("Registered property '%s' on trap '%s'", this->name, trap.name); } catch(const error &e) { throw error("Failed to register property '%s': out slots on trap '%s': %s", this->name.c_str(), trap.name.c_str(), e.what()); } ircd::js::trap::property::~property() noexcept { const auto erased(trap->memprop.erase(name)); assert(erased); } bool ircd::js::trap::property::handle_get(JSContext *const c, const unsigned argc, JS::Value *const argv) noexcept try { using js::function; const struct args args(argc, argv); object that(args.computeThis(c)); function func(args.callee()); const string name(js::name(func)); auto &trap(from(that)); trap.debug(that.get(), "get '%s' (property)", name.c_str()); property &prop(*trap.memprop.at(name)); args.rval().set(prop.on_get(func, that)); return true; } catch(const jserror &e) { e.set_pending(); return false; } catch(const std::exception &e) { auto ca(JS::CallArgsFromVp(argc, argv)); object that(ca.computeThis(c)); auto &trap(from(that)); trap.host_exception(that.get(), "property get: %s", e.what()); return false; } bool ircd::js::trap::property::handle_set(JSContext *const c, const unsigned argc, JS::Value *const argv) noexcept try { using js::function; const struct args args(argc, argv); object that(args.computeThis(c)); function func(args.callee()); const string name(js::name(func)); auto &trap(from(that)); trap.debug(that.get(), "set '%s' (property)", name.c_str()); property &prop(*trap.memprop.at(name)); args.rval().set(prop.on_get(func, that)); return true; } catch(const jserror &e) { e.set_pending(); return false; } catch(const std::exception &e) { auto ca(JS::CallArgsFromVp(argc, argv)); object that(ca.computeThis(c)); auto &trap(from(that)); trap.host_exception(that.get(), "property set: %s", e.what()); return false; } ircd::js::value ircd::js::trap::property::on_get(function::handle, object::handle that) { return {}; } ircd::js::value ircd::js::trap::property::on_set(function::handle, object::handle that, value::handle val) { return val; } /////////////////////////////////////////////////////////////////////////////// // // ircd/js/trap_function.h // ircd::js::trap::function::function(struct trap &trap, const std::string &name, const uint16_t &flags, const uint16_t &arity, const closure &lambda) :trap{&trap} ,name{name} ,lambda{lambda} ,spec { this->name.data(), { handle_call }, arity, flags } { const auto iit(trap.memfunc.emplace(this->name, this)); if(!iit.second) throw error("trap function '%s' already exists on '%s'", this->name, trap.name); log.debug("Registered function '%s' on trap '%s'", this->name, trap.name); } ircd::js::trap::function::~function() noexcept { const auto erased(trap->memfunc.erase(this->name)); assert(erased); } ircd::js::function ircd::js::trap::function::operator()(const object::handle &obj) const { const auto jsf(::js::DefineFunctionWithReserved(*cx, obj, spec.name, spec.call.op, spec.nargs, spec.flags)); if(unlikely(!jsf)) throw internal_error("Failed to create trap::function"); js::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 object func(args.callee()); auto &trap(from(func)); if(args.isConstructing()) { log.debug("trap(%p) %s() ctor argv[%u]", (const void *)&trap, trap.name, argc); const value that(trap.on_new(func, args)); args.rval().set(that); log.debug("trap(%p) this(%p) %s() leave", (const void *)&trap, (const void *)that.address(), trap.name); } else { const value that(args.computeThis(c)); log.debug("trap(%p) this(%p) %s() call argv[%u]", (const void *)&trap, (const void *)that.address(), trap.name, argc); args.rval().set(trap.on_call(func, that, args)); log.debug("trap(%p) this(%p) %s() leave", (const void *)&trap, (const void *)that.address(), trap.name); } 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(%p) \"%s()\": %s", reinterpret_cast(&trap), trap.name, e.what()); JS_ReportErrorUTF8(*cx, "BUG: trap(%p) \"%s()\": %s", reinterpret_cast(&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(tval); } ircd::js::value ircd::js::trap::function::on_call(object::handle obj, value::handle val, const args &args) { return lambda(obj, val, args); } ircd::js::value ircd::js::trap::function::on_new(object::handle obj, const args &args) { value ud; return on_call(obj, ud, args); } /////////////////////////////////////////////////////////////////////////////// // // ircd/js/trap.h // namespace ircd { namespace js { } // namespace js } // namespace ircd ircd::js::trap::trap(const std::string &name, const uint &flags, const uint &prop_flags) :name{name} ,prototrap{nullptr} ,classp { this->name.data(), flags | JSCLASS_FOREGROUND_FINALIZE, (flags & JSCLASS_IS_GLOBAL)? &gcops : &cops, { this } // reserved[0] TODO: ????????? } { if(this->name == "[global]") ircd::js::tree = this; log.debug("Registered trap '%s' @ %p", jsclass().name, (const void *)this); } ircd::js::trap::~trap() noexcept { log.debug("Unregistered trap '%s' @ %p", jsclass().name, (const void *)this); } const JSClassOps ircd::js::trap::cops { 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, handle_trace, }; const JSClassOps ircd::js::trap::gcops { 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, JS_GlobalObjectTraceHook, }; ircd::js::object ircd::js::trap::construct(const vector::handle &argv) { const object globals; return construct(globals, argv); } ircd::js::object ircd::js::trap::construct(const object::handle &globals, const vector::handle &argv) { const object prototype(this->prototype(globals)); return JS_New(*cx, prototype, argv); } ircd::js::object ircd::js::trap::prototype(const object::handle &globals) { // Create parent object of this object in the prototype chain const object super { prototrap? prototrap->construct() : object{object::uninitialized} }; // Build static property descriptor array std::vector sprop { this->sprop.size() + 1, // JS requires classical null terminator { 0 } }; // Build static function descriptor array std::vector sfunc { this->sfunc.size() + 1, // JS requires classical null terminator { 0 } }; // Build member property descriptor array std::vector memprop { this->memprop.size() + 1, // JS requires classical null terminator { 0 } }; std::transform(begin(this->memprop), end(this->memprop), begin(memprop), [] (const auto &it) { const auto &prop(*it.second); return prop.spec; }); // Mozilla really screwed the pooch here. There is no way to specify a function // with a reserved slot in order to find our trap::function instance from the // JSNative callback. Their API surface is too broad and there are too many different // and mutually exclusive ways to define functions. So we have to be inconsistent // here and not use InitClass()'s facility for defining member functions. // Build member function descriptor array std::vector memfunc { this->memfunc.size() + 1, // JS requires classical vector terminator { 0 } }; // std::transform(begin(this->memfunc), end(this->memfunc), begin(memfunc), [] // (const auto &it) // { // const auto &function(*it.second); // return function.spec; // }); // Create object const object proto { JS_InitClass(*cx, globals, super, &jsclass(), nullptr, 0, memprop.data(), memfunc.data(), sprop.data(), sfunc.data()) }; // Member functions defined after the fact here. std::for_each(begin(this->memfunc), end(this->memfunc), [&proto] (const auto &it) { const auto &deffun(*it.second); const auto func(deffun(proto)); }); //JS_DefineConstIntegers(*cx, proto, cis.data()); //JS_DefineConstDoubles(*cx, proto, cds.data()); return proto; } 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(obj, "dtor"); trap.on_gc(obj); } catch(const std::exception &e) { auto &trap(from(*obj)); log.critical("Unhandled on GC (fop: %p obj: %p): %s", (const void *)op, (const void *)obj, e.what()); assert(0); } bool ircd::js::trap::handle_ctor(JSContext *const c, unsigned argc, JS::Value *const argv) noexcept try { assert(&our(c) == cx); assert(!pending_exception(*cx)); const struct args args(argc, argv); object that(args.callee()); auto &trap(from(that)); trap.debug(that.get(), "ctor '%s' argv[%zu]", trap.name.c_str(), args.size()); object ret(JS_NewObjectWithGivenProto(*cx, &trap.jsclass(), that)); trap.on_new(that, ret, args); args.rval().set(JS::Value(ret)); return true; } catch(const jserror &e) { e.set_pending(); return false; } catch(const std::exception &e) { auto ca(JS::CallArgsFromVp(argc, argv)); object that(ca.callee()); auto &trap(from(that)); trap.host_exception(that.get(), "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); assert(!pending_exception(*cx)); const struct args args(argc, argv); value that(args.computeThis(c)); object func(args.callee()); //auto &trap_that(from(object(that))); auto &trap_func(from(func)); //trap_that.debug(that.get(), "call: '%s'", trap_func.name().c_str()); trap_func.debug(func.get(), "call argv[%zu]", args.size()); args.rval().set(trap_func.on_call(func, that, args)); return true; } catch(const jserror &e) { e.set_pending(); return false; } catch(const std::exception &e) { auto ca(JS::CallArgsFromVp(argc, argv)); object func(ca.callee()); auto &trap(from(func)); trap.host_exception(func.get(), "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(obj.get(), "enumerate"); trap.on_enu(obj); return true; } catch(const jserror &e) { e.set_pending(); return false; } catch(const std::exception &e) { auto &trap(from(obj)); trap.host_exception(obj.get(), "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); assert(!pending_exception(*cx)); auto &trap(trap::from(obj)); trap.debug(obj.get(), "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(obj.get(), "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); assert(!pending_exception(*cx)); auto &trap(from(obj)); trap.debug(obj.get(), "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(obj.get(), "del '%s': %s", string(id).c_str(), e.what()); return false; } bool ircd::js::trap::handle_getter(JSContext *const c, unsigned argc, JS::Value *const argv) noexcept try { using js::function; const struct args args(argc, argv); const object that(args.computeThis(c)); const function func(args.callee()); const string name(js::name(func)); auto &trap(from(that)); trap.debug(that.get(), "get '%s' (getter)", name.c_str()); //value &val(it->second); //args.rval().set(val); return true; } catch(const jserror &e) { e.set_pending(); return false; } catch(const std::exception &e) { const auto ca(JS::CallArgsFromVp(argc, argv)); const object that(ca.computeThis(c)); auto &trap(from(that)); trap.host_exception(that.get(), "getter: %s", e.what()); return false; } bool ircd::js::trap::handle_setter(JSContext *const c, unsigned argc, JS::Value *const argv) noexcept try { using js::function; const struct args args(argc, argv); const object that(args.computeThis(c)); const function func(args.callee()); const value val(args[0]); const auto type(js::type(val)); const string name(js::name(func)); auto &trap(from(that)); trap.debug(that.get(), "set '%s' (%s) (setter)", name.c_str(), reflect(type)); switch(js::type(type)) { case jstype::OBJECT: { //const auto flags(JSPROP_SHARED); //object ret(JS_DefineObject(*cx, object(val), "", &trap.jsclass(), flags)); //args.rval().set(ret); //return true; } default: args.rval().set(val); return true; } } catch(const jserror &e) { e.set_pending(); return false; } catch(const std::exception &e) { const auto ca(JS::CallArgsFromVp(argc, argv)); const object that(ca.computeThis(c)); auto &trap(from(that)); trap.host_exception(that.get(), "setter: %s", 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); assert(!pending_exception(*cx)); auto &trap(from(obj)); trap.debug(obj.get(), "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(obj.get(), "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); assert(!pending_exception(*cx)); auto &trap(from(obj)); trap.debug(obj.get(), "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(obj.get(), "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); assert(!pending_exception(*cx)); auto &trap(from(obj)); const string name(id); trap.debug(obj.get(), "add '%s' %s @%p", name.c_str(), reflect(type(val)), (const void *)val.address()); 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(obj.get(), "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(obj.get(), "inst"); return false; } catch(const jserror &e) { e.set_pending(); return false; } catch(const std::exception &e) { auto &trap(from(obj)); trap.host_exception(obj.get(), "inst: %s", e.what()); return false; } void ircd::js::trap::handle_trace(JSTracer *const tracer, JSObject *const obj) noexcept try { assert(cx); assert(tracer); assert(obj); auto &trap(from(*obj)); trap.debug(obj, "trace"); trap.on_trace(obj); } catch(const jserror &e) { e.set_pending(); return; } catch(const std::exception &e) { auto &trap(from(*obj)); trap.host_exception(obj, "trace: %s", e.what()); return; } ircd::js::trap & ircd::js::trap::from(const JSObject &o) { return from(&o); } ircd::js::trap & ircd::js::trap::from(const JSObject *const &o) { auto *const c(JS_GetClass(const_cast(o))); if(!c) { log.critical("trap::from(): Trapped on an object without a JSClass!"); ircd::terminate(); //TODO: exception } if(!c->reserved[0]) { log.critical("trap::from(): Trap called on a trap instance that has gone out of scope!"); ircd::terminate(); //TODO: exception } return *static_cast(c->reserved[0]); //TODO: ??? } void ircd::js::trap::debug(const void *const &that, 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) this(%p) '%s' %s", reinterpret_cast(this), that, jsclass().name, buf); va_end(ap); } void ircd::js::trap::host_exception(const void *const &that, 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) this(%p) '%s' %s", reinterpret_cast(this), that, jsclass().name, buf); JS_ReportErrorUTF8(*cx, "BUG: trap(%p) this(%p) '%s' %s", reinterpret_cast(this), that, jsclass().name, buf); va_end(ap); } void ircd::js::trap::on_gc(JSObject *const &that) { if(jsclass().flags & JSCLASS_IS_GLOBAL) //TODO: leak?? return; if(jsclass().flags & JSCLASS_HAS_PRIVATE) del(that, priv); } void ircd::js::trap::on_trace(const JSObject *const &) { } void ircd::js::trap::on_new(object::handle, object &, const args &) { } void ircd::js::trap::on_enu(object::handle) { } bool ircd::js::trap::on_has(object::handle, id::handle id) { // const string sid(id); // if(children.count(sid)) // return false; // // if(std::any_of(begin(memfun), end(memfun), [&sid] // (const auto &it) // { // const auto &memfun(*it.second); // return sid == memfun.name; // })) // return false; // // value val; // const auto flags(JSPROP_SHARED | JSPROP_ENUMERATE); // if(!JS_DefinePropertyById(*cx, obj, id, val, flags, handle_getter, handle_setter)) // throw jserror("Failed to define property '%s'", sid.c_str()); // // const auto flags(0); // if(!JS_DefineObject(*cx, obj, sid.c_str(), &jsclass(), flags)) // throw jserror("Failed to define property '%s'", sid.c_str()); return false; } 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) { return val; } 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, value::handle, const args &) { return {}; } /////////////////////////////////////////////////////////////////////////////// // // ircd/js/module.h // /////////////////////////////////////////////////////////////////////////////// // // ircd/js/script.h // namespace ircd { namespace js { void handle_compile_async(void *, void *) noexcept; } // namespace js } // namespace ircd bool ircd::js::compilable(const std::string &str, const object &stack) { return compilable(str.c_str(), str.size(), stack); } bool ircd::js::compilable(const char *const &str, const size_t &len, const object &stack) { return JS_BufferIsCompilableUnit(*cx, stack, str, len); } size_t ircd::js::bytecodes(const JS::Handle &s, uint8_t *const &buf, const size_t &max) { JS::TranscodeBuffer tcbuf; const auto code { JS::EncodeScript(*cx, tcbuf, s) }; switch(code) { case JS::TranscodeResult_Ok: { if(unlikely(tcbuf.length() > max)) throw internal_error { "Insufficient buffer size %zu where %zu is required.", max, tcbuf.length() }; memcpy(buf, tcbuf.begin(), tcbuf.length()); return tcbuf.length(); } case JS::TranscodeResult_Throw: throw jserror(jserror::pending); default: throw internal_error { "Failed to encode script to XDR (error %d)", code }; }; } ircd::js::string ircd::js::decompile(const JS::Handle &s, const char *const &name, const bool &pretty) { uint flags(0); flags |= pretty; return JS_DecompileScript(*cx, s, name, flags); } ircd::ctx::future ircd::js::compile_async(const JS::ReadOnlyCompileOptions &opts, const std::u16string &src, const bool &module) { auto promise(std::make_unique>()); if(!JS::CanCompileOffThread(*cx, opts, src.size())) { log.debug("context(%p): Rejected asynchronous script compile (script size: %zu)", (const void *)cx, src.size()); ctx::future ret(*promise); promise->set_value(nullptr); return ret; } const auto &compile { module? JS::CompileOffThreadModule : JS::CompileOffThread }; if(!compile(*cx, opts, src.data(), src.size(), handle_compile_async, promise.get())) throw internal_error("Failed to compile %s concurrently", module? "module" : "script"); return *promise.release(); } void ircd::js::handle_compile_async(void *const token, void *const priv) noexcept { // This frame is entered on a thread owned by SpiderMonkey, not IRCd. Do not call // ircd::log from here, it is not thread-safe. // printf("[thread %s]: context(%p): compile(%p) READY (priv: %p)\n", // ircd::string(std::this_thread::get_id()).c_str(), // (const void *)cx, // token, // priv); // Setting the value of the promise and then deleting the promise is thread-safe. // Note that JS::FinishOffThreadScript(); will need to be called on the main thread. auto *const _promise(reinterpret_cast *>(priv)); const std::unique_ptr> promise(_promise); promise->set_value(token); } /////////////////////////////////////////////////////////////////////////////// // // ircd/js/function.h // ircd::js::value ircd::js::function::operator()(const object::handle &that, const vector::handle &argv) const { return call(*this, that, argv); } /////////////////////////////////////////////////////////////////////////////// // // ircd/js/function_literal.h // ircd::js::function::literal::literal(const char *const &name, const std::initializer_list &prototype, const char *const &text) :root{} ,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::literal(literal &&other) noexcept :root { std::move(other) } ,name{std::move(other.name)} ,text{std::move(other.text)} ,prototype{std::move(other.prototype)} { } ircd::js::function::literal operator ""_function(const char *const text, const size_t len) { return { "", {}, text }; } /////////////////////////////////////////////////////////////////////////////// // // ircd/js/function_native.h // ircd::js::function::native::native(const char *const &name, const uint &flags, const uint &arity, const closure &lambda) :native::root::type{[this, &name, &flags, &arity] { const auto jsf(::js::DefineFunctionWithReserved(*cx, object(), name, handle_call, arity, flags)); if(unlikely(!jsf)) throw internal_error("Failed to create function::native '%s'", name); js::function ret(jsf); ::js::SetFunctionNativeReserved(ret, 0, pointer_value(this)); return ret; }()} ,_name{name} ,lambda{lambda} { } ircd::js::function::native::~native() noexcept { } ircd::js::value ircd::js::function::native::operator()(const object::handle &that, const vector::handle &argv) const { return call(*this, that, argv); } ircd::js::string ircd::js::function::native::name() const { return JS_GetFunctionId(*this); } ircd::js::string ircd::js::function::native::display_name() const { return JS_GetFunctionDisplayId(*this); } uint16_t ircd::js::function::native::arity() const { return JS_GetFunctionArity(*this); } bool ircd::js::function::native::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 object func(args.callee()); auto &instance(from(func)); if(args.isConstructing()) { log.debug("native(%p) %s() ctor argv[%u]", (const void *)&instance, instance._name, argc); const value that(instance.on_new(func, args)); args.rval().set(that); log.debug("native(%p) this(%p) %s() leave", (const void *)&instance, (const void *)that.address(), instance._name); } else { const value that(args.computeThis(c)); log.debug("native(%p) this(%p) %s() call argv[%u]", (const void *)&instance, (const void *)that.address(), instance._name, argc); args.rval().set(instance.on_call(func, that, args)); log.debug("native(%p) this(%p) %s() leave", (const void *)&instance, (const void *)that.address(), instance._name); } return true; } catch(const jserror &e) { e.set_pending(); return false; } catch(const std::exception &e) { const struct args args(argc, argv); const object func(args.callee()); const function::native &instance(from(func)); log.error("native(%p) \"%s()\": %s", std::addressof(instance), instance._name, e.what()); JS_ReportErrorUTF8(*cx, "BUG: native(%p) \"%s()\": %s", std::addressof(instance), instance._name, e.what()); return false; } ircd::js::function::native & ircd::js::function::native::from(JSObject *const &func) { const auto tval(::js::GetFunctionNativeReserved(func, 0)); return *pointer_value(tval); } ircd::js::value ircd::js::function::native::on_call(object::handle obj, value::handle val, const args &args) { return lambda(obj, val, args); } ircd::js::value ircd::js::function::native::on_new(object::handle obj, const args &args) { value ud; return on_call(obj, ud, args); } /////////////////////////////////////////////////////////////////////////////// // // ircd/js/for_each.h // void ircd::js::for_each(object::handle obj, const each_key_val &closure) { for_each(obj, iter::none, closure); } void ircd::js::for_each(object::handle obj, const iter &flags, const each_key_val &closure) { for_each(obj, flags, each_id([&obj, &closure] (const id &key) { const value val(get(obj, key)); closure(key, val); })); } void ircd::js::for_each(object::handle obj, const each_key &closure) { for_each(obj, iter::none, closure); } void ircd::js::for_each(object::handle obj, const iter &flags, const each_key &closure) { for_each(obj, flags, each_id([&obj, &closure] (const id &id) { closure(id); })); } void ircd::js::for_each(object::handle obj, const each_id &closure) { for_each(obj, iter::none, closure); } void ircd::js::for_each(object::handle obj, const iter &flags, const each_id &closure) { JS::AutoIdVector props(*cx); if(::js::GetPropertyKeys(*cx, obj, uint(flags), &props)) for(size_t i(0); i < props.length(); ++i) closure(props[i]); } /////////////////////////////////////////////////////////////////////////////// // // ircd/js/call.h // ircd::js::value ircd::js::call(const std::string &name, const object::handle &that, const vector::handle &args) { return call(name.c_str(), that, args); } ircd::js::value ircd::js::call(const char *const &name, const object::handle &that, const vector::handle &args) { value ret; if(!JS_CallFunctionName(*cx, that, name, args, &ret)) throw jserror(jserror::pending); return ret; } ircd::js::value ircd::js::call(const value::handle &val, const object::handle &that, const vector::handle &args) { value ret; if(!JS_CallFunctionValue(*cx, that, val, args, &ret)) throw jserror(jserror::pending); return ret; } ircd::js::value ircd::js::call(const function::handle &func, const object::handle &that, const vector::handle &args) { value ret; if(!JS_CallFunction(*cx, that, func, args, &ret)) throw jserror(jserror::pending); return ret; } /////////////////////////////////////////////////////////////////////////////// // // 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); char buffer[strlen(path) + 1]; ircd::tokens(path, ".", mutable_buffer{buffer, sizeof(buffer)}, [&path, &val, &obj, &fail] (const string_view &part) { if(fail) throw type_error("cannot recurse through non-object '%s' in `%s'", fail, path); if(!JS_GetProperty(*cx, obj, part.data(), &val) || undefined(val)) throw reference_error("%s", part.data()); object tmp(obj.get()); if(!JS_ValueToObject(*cx, val, &obj) || !obj.get()) { fail = part.data(); obj = std::move(tmp); } }); del(obj, id(val)); } void ircd::js::del(const object::handle &obj, const uint32_t &idx) { JS::ObjectOpResult res; if(!JS_DeleteElement(*cx, obj, idx, res)) throw jserror(jserror::pending); if(!res.checkStrict(*cx, obj)) throw jserror(jserror::pending); } void ircd::js::del(const object::handle &obj, const id &id) { return del(obj, id::handle(id)); } 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); } void ircd::js::del(JSObject *const &obj, priv_t) { if(unlikely(~flags(obj) & JSCLASS_HAS_PRIVATE)) throw error("del(priv): Object has no private slot"); void *const existing(JS_GetPrivate(obj)); delete reinterpret_cast(existing); JS_SetPrivate(obj, nullptr); } /////////////////////////////////////////////////////////////////////////////// // // ircd/js/set.h // void ircd::js::set(const object::handle &src, const char *const path, const value &val) { object obj(src); char buffer[strlen(path) + 1]; const char *fail(nullptr), *key(nullptr); ircd::tokens(path, ".", mutable_buffer{buffer, sizeof(buffer)}, [&path, &obj, &fail, &key] (const string_view &part) { if(fail) throw type_error("cannot recurse through non-object '%s' in `%s'", fail, path); if(key) throw reference_error("%s", part.data()); key = part.data(); if(strcmp(key, path) == 0) return; value tmp; if(!JS_GetProperty(*cx, obj, part.data(), &tmp) || undefined(tmp)) return; if(!JS_ValueToObject(*cx, tmp, &obj) || !obj.get()) fail = part.data(); }); if(!key) return; if(!JS_SetProperty(*cx, obj, key, val)) throw jserror(jserror::pending); } void ircd::js::set(const object::handle &obj, const id &id, const value &val) { set(obj, id::handle(id), val); } void ircd::js::set(const object::handle &obj, const id::handle &id, const value &val) { set(obj, id, value::handle(val)); } void ircd::js::set(const object::handle &obj, const id::handle &id, const value::handle &val) { if(!JS_SetPropertyById(*cx, obj, id, val)) throw jserror(jserror::pending); } void ircd::js::set(JSObject *const &obj, priv_data &data) { set(obj, shared_from(data)); } void ircd::js::set(JSObject *const &obj, const std::shared_ptr &data) { if(unlikely(~flags(obj) & JSCLASS_HAS_PRIVATE)) throw error("set(priv): Object has no private slot"); void *const existing(JS_GetPrivate(obj)); delete reinterpret_cast(existing); JS_SetPrivate(obj, new priv_ptr(data)); } void ircd::js::set(JSObject *const &obj, const reserved &slot, const JS::Value &val) { JS_SetReservedSlot(obj, uint(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); char buffer[strlen(path) + 1]; ircd::tokens(path, ".", mutable_buffer{buffer, sizeof(buffer)}, [&obj, &path, &ret, &fail] (const string_view &part) { if(fail) throw type_error("cannot recurse through non-object '%s' in `%s'", fail, path); if(!JS_GetProperty(*cx, obj, part.data(), &ret) || undefined(ret)) throw reference_error("%s", part.data()); if(!JS_ValueToObject(*cx, ret, &obj) || !obj.get()) fail = part.data(); }); return ret; } ircd::js::value ircd::js::get(const object::handle &obj, const uint32_t &idx) { value ret; if(!JS_GetElement(*cx, obj, idx, &ret) || undefined(ret)) throw reference_error("[%u]", idx); return ret; } ircd::js::value ircd::js::get(const object::handle &obj, const id &id) { return get(obj, id::handle(id)); } 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; } JS::Value ircd::js::get(JSObject *const &obj, const reserved &slot) { return JS_GetReservedSlot(obj, uint(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); char buffer[strlen(path) + 1]; ircd::tokens(path, ".", mutable_buffer{buffer, sizeof(buffer)}, [&obj, &path, &ret, &fail] (const string_view &part) { if(fail) throw type_error("cannot recurse through non-object '%s' in `%s'", fail, path); if(!JS_HasProperty(*cx, obj, part.data(), &ret)) throw jserror(jserror::pending); if(!ret) return; value tmp; if(!JS_GetProperty(*cx, obj, part.data(), &tmp) || undefined(tmp)) { ret = false; return; } if(!JS_ValueToObject(*cx, tmp, &obj) || !obj.get()) fail = part.data(); }); return ret; } bool ircd::js::has(const object::handle &obj, const uint32_t &idx) { bool ret; if(!JS_HasElement(*cx, obj, idx, &ret)) throw jserror(jserror::pending); return ret; } bool ircd::js::has(const object::handle &obj, const id &id) { return has(obj, id::handle(id)); } 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; } bool ircd::js::has(const JSObject *const &obj, priv_t) { if(~flags(obj) & JSCLASS_HAS_PRIVATE) return false; const auto vp(JS_GetPrivate(const_cast(obj))); const auto sp(reinterpret_cast(vp)); return sp && !!*sp; } bool ircd::js::has(const JSObject *const &obj, const reserved &slot) { return flags(obj) & JSCLASS_HAS_RESERVED_SLOTS(uint(slot)); } /////////////////////////////////////////////////////////////////////////////// // // ircd/js/object.h // /////////////////////////////////////////////////////////////////////////////// // // ircd/js/priv.h // // Anchor the struct priv_data vtable here. ircd::js::priv_data::~priv_data() noexcept { } /////////////////////////////////////////////////////////////////////////////// // // 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/json.h // namespace ircd { namespace js { namespace json { bool write_callback(const char16_t *, uint32_t, void *) noexcept; } // namespace json } // namespace js } // namespace ircd ircd::js::value ircd::js::json::parse(const string &string) { value ret; if(!JS_ParseJSON(*cx, string, &ret)) throw jserror(jserror::pending); return ret; } ircd::js::value ircd::js::json::parse(const char *const &str) { return parse(locale::char16::conv(str)); } ircd::js::value ircd::js::json::parse(const std::string &str) { return parse(locale::char16::conv(str)); } ircd::js::value ircd::js::json::parse(const std::u16string &str) { return parse(str.c_str(), str.size()); } ircd::js::value ircd::js::json::parse(const char16_t *const &str, const size_t &size) { value ret; if(!JS_ParseJSON(*cx, str, size, &ret)) throw jserror(jserror::pending); return ret; } std::u16string ircd::js::json::stringify(const value &val, const bool &pretty) { value v(val); return stringify(value::handle_mutable(&v), pretty); } std::u16string ircd::js::json::stringify(value &v, const bool &pretty) { return stringify(value::handle_mutable(&v), pretty); } std::u16string ircd::js::json::stringify(const value::handle_mutable &val, const bool &pretty) { const object fmtr; const string sp(string::literal, pretty? u"\t" : u""); return stringify(val, fmtr, sp); } std::u16string ircd::js::json::stringify(const value::handle_mutable &val, const JS::HandleObject &fmtr, const value &sp) { std::u16string ret; stringify(val, fmtr, sp, [&ret] (const char16_t *const &ptr, const uint &len) { ret.assign(ptr, len); return true; }); return ret; } void ircd::js::json::stringify(const value::handle_mutable &val, const closure &cont) { value sp; object fmtr; return stringify(val, fmtr, sp, cont); } void ircd::js::json::stringify(const value::handle_mutable &val, const JS::HandleObject &fmtr, const value &sp, const closure &cont) { if(!JS_Stringify(*cx, val, fmtr, sp, write_callback, const_cast(&cont))) throw jserror(jserror::pending); } bool ircd::js::json::write_callback(const char16_t *const str, const uint32_t len, void *const priv) noexcept { const auto &func(*reinterpret_cast(priv)); return func(str, len); } /////////////////////////////////////////////////////////////////////////////// // // ircd/js/native.h // namespace ircd { namespace js { void native_external_noop(JS::Zone *const zone, const JSStringFinalizer *const fin, char16_t *const buf); void native_external_deleter(JS::Zone *const zone, 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(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(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(JS::Zone *const zone, const JSStringFinalizer *const fin, char16_t *const buf) { log.debug("context(%p) zone(%p): string(%p) delete (dtor @%p) \"%s\"", (const void *)cx, (const void *)zone, (const void *)buf, (const void *)fin, locale::char16::conv(buf).c_str()); delete[] buf; } void ircd::js::native_external_noop(JS::Zone *const zone, const JSStringFinalizer *const fin, char16_t *const buf) { log.debug("string literal release (zone: %p fin: %p buf: %p)", (const void *)zone, (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(JSObject *const &obj) :ircd::js::error{generate_skip} ,val{obj? JS::ObjectValue(*obj) : JS::UndefinedValue() } { } ircd::js::jserror::jserror(JSObject &obj) :ircd::js::error{generate_skip} ,val{JS::ObjectValue(obj)} { } ircd::js::jserror::jserror(generate_skip_t) :ircd::js::error(generate_skip) ,val{} { } 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))) // throw error("No exception pending"); value val; if(unlikely(!JS_GetPendingException(*cx, &val))) throw error("Failed to recover pending exception"); object obj(val); const auto rp(JS_ErrorFromException(*cx, obj)); if(unlikely(!rp)) throw error("Error report from exception failed"); auto &report(*rp); generate_what_our(report); log.debug("jserror(%p): from pending [%s]: %s", (const void *)this, debug(report), what()); this->val = val; clear_exception(*cx); } void ircd::js::jserror::set_uncatchable() const { set_pending(); clear_exception(*cx); } void ircd::js::jserror::set_pending() const { JS_SetPendingException(*cx, &val); save_exception(*cx); } void ircd::js::jserror::generate(const JSExnType &type, const char *const &fmt, const va_rtti &ap) { ircd::exception::generate(fmt, ap); const auto msg(locale::char16::conv(what())); JSErrorReport report; //report.message = 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_NewStringCopyUTF8N(*cx, report.message()) }; JS::RootedString file { *cx, JS_NewStringCopyZ(*cx, fn.get()?: "") }; 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!"); } JS::Rooted obj(*cx); if(unlikely(!JS_ValueToObject(*cx, &val, &obj))) throw error("Failed to convert value to object on exception!"); JS::Rooted msgv(*cx, JS::StringValue(msg)); if(unlikely(!JS_SetProperty(*cx, obj, "message", msgv))) throw error("Failed to set jserror.message property on 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.message()); const auto empty(false); snprintf(ircd::exception::buf, sizeof(ircd::exception::buf), "%s%s%s%s", reflect((JSExnType)report.exnType), empty? "." : ": ", 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(&report)))); //snprintf(ircd::exception::buf, sizeof(ircd::exception::buf), "%s", str.c_str()); } void ircd::js::replace_message(JSErrorReport &report, const char *fmt, ...) { va_list ap; va_start(ap, fmt); thread_local char buf[1024]; vsnprintf(buf, sizeof(buf), fmt, ap); const size_t ucsz(sizeof(buf) * 2); custom_ptr ucbuf(reinterpret_cast(js_malloc(ucsz)), js_free); locale::char16::conv(buf, ucbuf.get(), ucsz); //custom_ptr existing(const_cast(report.message()), js_free); //report.ucmessage = ucbuf.release(); va_end(ap); } #if defined(IRCD_JS_FIX) void ::JSErrorReport::freeLinebuf() { js_free(const_cast(linebuf())); } #endif // IRCD_JS_FIX #if defined(IRCD_JS_FIX) void ::JSErrorReport::freeMessage() { } #endif // IRCD_JS_FIX /////////////////////////////////////////////////////////////////////////////// // // 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(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(*cx, const_cast(&closure), compartment::handle_iterate); } void ircd::js::compartment::handle_iterate(JSContext *const cx, void *const priv, JSCompartment *const c) noexcept { const auto &closure(*static_cast(priv)); closure(c); } /////////////////////////////////////////////////////////////////////////////// // // ircd/js/context.h // namespace ircd { namespace js { void handle_activity_ctypes(JSContext *, enum ::js::CTypesActivityType) noexcept; } // namespace js } // namespace ircd ircd::js::context::context(const struct opts &opts, JSContext *const &parent) :context { std::make_unique(opts), parent } { } ircd::js::context::context(std::unique_ptr opts, JSContext *const &parent) :custom_ptr { // Construct the context [this, &opts, &parent] { const auto ret(JS_NewContext(opts->max_bytes, opts->max_nursery_bytes, parent)); // 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(JS_GetSecondContextPrivate(ctx)); JS_DestroyContext(ctx); } } ,opts{std::move(opts)} ,tid{std::this_thread::get_id()} ,except{nullptr} ,state {{ 0, // Semaphore value starting at 0. phase::LEAVE, // LEAVE phase indicates nothing is running. irq::JS, // irq::JS is otherwise here in case JS triggers an interrupt. }} ,timer { std::bind(&context::handle_timeout, get()) } ,star{nullptr} { JS::SetWarningReporter(get(), handle_error); JS_AddInterruptCallback(get(), handle_interrupt); //JS_SetAccumulateTelemetryCallback(get(), handle_telemetry); JS::SetOutOfMemoryCallback(get(), handle_out_of_memory, nullptr); JS::SetLargeAllocationFailureCallback(get(), handle_large_allocation_failure, nullptr); JS_SetGCCallback(get(), handle_gc, nullptr); JS::SetGCSliceCallback(get(), handle_slice); JS_SetObjectsTenuredCallback(get(), handle_objects_tenured, nullptr); JS_AddFinalizeCallback(get(), handle_finalize, nullptr); JS_SetCompartmentNameCallback(get(), handle_compartment_name); JS_SetDestroyCompartmentCallback(get(), handle_compartment_destroy); JS_SetSweepZoneCallback(get(), handle_zone_sweep); JS_SetDestroyZoneCallback(get(), handle_zone_destroy); ::js::SetPreserveWrapperCallback(get(), handle_preserve_wrapper); JS_SetGrayGCRootsTracer(get(), handle_trace_gray, nullptr); JS_AddExtraGCRootsTracer(get(), handle_trace_extra, nullptr); ::js::SetActivityCallback(get(), handle_activity, this); ::js::SetCTypesActivityCallback(get(), handle_activity_ctypes); JS::SetEnqueuePromiseJobCallback(get(), handle_promise_enqueue_job, this); JS::SetPromiseRejectionTrackerCallback(get(), handle_promise_rejection_tracker, this); JS::SetAsyncTaskCallbacks(get(), handle_start_async_task, handle_finish_async_task); JS::SetGetIncumbentGlobalCallback(get(), handle_get_incumbent_global); ::js::SetStopwatchStartCallback(get(), handle_stopwatch_start, this); ::js::SetStopwatchCommitCallback(get(), handle_stopwatch_commit, this); ::js::SetGetPerformanceGroupsCallback(get(), handle_get_performance_groups, nullptr); JS::SetBuildIdOp(get(), handle_set_build_id_op); timer.set(this->opts->timer_limit); JS_SetNativeStackQuota(get(), this->opts->code_stack_max, this->opts->trusted_stack_max, this->opts->untrusted_stack_max); JS_SetParallelParsingEnabled(get(), this->opts->concurrent_parsing); JS_SetOffthreadIonCompilationEnabled(get(), this->opts->concurrent_jit); JS_SetGCZeal(get(), this->opts->gc_zeal_mode, this->opts->gc_zeal_freq); if(!JS::InitSelfHostedCode(get())) throw error("JS::InitSelfHostedCode: failure."); } ircd::js::context::~context() noexcept { // If items still exist on the tracing lists at this point (runtime shutdown): // that is bad. The objects are still reachable but should have removed themselves. if(unlikely(!tracing.heap.empty())) { // When the context terminates with an active exception the exception itself // can be ignored and leak. This case should eventually lead to IRCd termination... if(std::uncaught_exception() && tracing.heap.size() == 1) return; log.critical("context(%p): !!! LEAK !!! %zu traceable items still reachable on the heap", (const void *)this, tracing.heap.size()); assert(0); } } 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 LEAVE phase locks out the interruptor state.phase = phase::LEAVE; // The LEAVE 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. JS_RequestInterruptCallback(c); 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) { if(c.except) { //log.warning("save_exception(): Dropping unrestored exception @ %p", (const void *)c.except); //JS_DropExceptionState(*cx, c.except); throw error("(internal error): Won't overwrite saved exception @ %p", (const void *)c.except); } c.except = JS_SaveExceptionState(c); } bool ircd::js::maybe_gc(context &c) noexcept { // JS_MaybeGC dereferences the context's current zone without checking if the context // is even in a compartment/has a current zone; we must check here. if(!current_zone(c)) return false; JS_MaybeGC(c); return true; } 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, key); } void ircd::js::set(context &c, const JSGCParamKey &key, const uint32_t &val) { //JS_SetGCParameterForThread(c, key, val); // broken JS_SetGCParameter(c, key, val); } bool ircd::js::run_gc(context &c) noexcept { JS_GC(c); return true; } // // Callback surface // bool ircd::js::context::handle_set_build_id_op(JS::BuildIdCharVector *const vector) noexcept { static const uint32_t build_id { 0 }; assert(vector); const bool resized(vector->resize(sizeof(build_id))); assert(resized); memcpy(vector->begin(), &build_id, sizeof(build_id)); return true; } JSObject * ircd::js::context::handle_get_incumbent_global(JSContext *const cx) noexcept { auto &c(our(cx)); log.debug("context(%p): get incumbent global (current_global: %p)", (const void *)cx, current_global(c)); return current_global(c); } bool ircd::js::context::handle_start_async_task(JSContext *const cx, JS::AsyncTask *const task) noexcept { log.debug("context(%p): async task(%p) START", (const void *)cx, task); return true; } bool ircd::js::context::handle_finish_async_task(JS::AsyncTask *const task) noexcept { log.debug("context(%p): async task(%p) FINISH", nullptr, task); return true; } bool ircd::js::context::handle_promise_enqueue_job(JSContext *const c, JS::HandleObject job, JS::HandleObject allocation_site, JS::HandleObject incumbent_global, void *const priv) noexcept { log.debug("context(%p): promise enqueue job (priv: %p)", (const void *)c, priv); // dump(job); // printf("--\n"); // dump(allocation_site); return true; } void ircd::js::context::handle_promise_rejection_tracker(JSContext *const c, JS::HandleObject promise, PromiseRejectionHandlingState state, void *const priv) noexcept { log.debug("context(%p): promise rejection track (state: %s priv: %p)", (const void *)c, reflect(state), priv); } void ircd::js::context::handle_activity(void *const priv, const bool active) noexcept { assert(priv); auto &c(*static_cast(priv)); //const auto tid(std::this_thread::get_id()); const auto &msg(active? "ENTER" : "LEAVE"); log.debug("context(%p): %s", //ircd::string(tid).c_str(), (const void *)c.ptr(), msg); } void ircd::js::handle_activity_ctypes(JSContext *const cx, const ::js::CTypesActivityType t) noexcept { log.debug("context(%p): %s", (const void *)cx, reflect(t)); } bool ircd::js::context::handle_preserve_wrapper(JSContext *const cx, JSObject *const obj) noexcept { log.debug("context(%p): (object: %p) preserve wrapper", (const void *)cx, (const void *)obj); return true; } void ircd::js::context::handle_gc(JSContext *const cx, const JSGCStatus status, void *const priv) noexcept { log.debug("context(%p): GC %s (priv: %p)", (const void *)cx, reflect(status), (const void *)priv); } void ircd::js::context::handle_slice(JSContext *const cx, JS::GCProgress progress, const JS::GCDescription &d) noexcept { log.debug("context(%p): SLICE: %s (description: %p)", (const void *)cx, reflect(progress), (const void *)&d); } void ircd::js::context::handle_objects_tenured(JSContext *const cx, void *const priv) noexcept { log.debug("context(%p): objects tenured (priv: %p)", (const void *)cx, (const void *)priv); } void ircd::js::context::handle_finalize(JSFreeOp *const fop, const JSFinalizeStatus status, const bool is_compartment, void *const priv) noexcept { log.debug("context(%p): fop(%p): %s %s (priv: %p)", (const void *)cx, (const void *)fop, reflect(status), is_compartment? "COMPARTMENT" : "", priv); } void ircd::js::context::handle_compartment_destroy(JSFreeOp *const fop, JSCompartment *const compartment) noexcept { auto *const c(our(compartment)); auto *const cx(c? &static_cast(*c) : (context *)nullptr); log.debug("context(%p): compartment: %p %s%sdestroy: fop(%p)", cx? (const void *)cx->ptr() : (const void *)nullptr, (const void *)compartment, ::js::IsSystemCompartment(compartment)? "[system] " : "", ::js::IsAtomsCompartment(compartment)? "[atoms] " : "", (const void *)fop); } void ircd::js::context::handle_compartment_name(JSContext *const cx, JSCompartment *const compartment, char *const buf, const size_t max) noexcept { log.debug("context(%p): comaprtment: %p (buf@%p: max: %zu)", (const void *)cx, (const void *)compartment, (const void *)buf, max); } void ircd::js::context::handle_zone_destroy(JS::Zone *const zone) noexcept { log.debug("context(%p): zone: %p %s%sdestroy", (const void *)cx, (const void *)zone, ::js::IsSystemZone(zone)? "[system] " : "", ::js::IsAtomsZone(zone)? "[atoms] " : ""); } void ircd::js::context::handle_zone_sweep(JS::Zone *const zone) noexcept { log.debug("context(%p): zone: %p %s%ssweep", (const void *)cx, (const void *)zone, ::js::IsSystemZone(zone)? "[system] " : "", ::js::IsAtomsZone(zone)? "[atoms] " : ""); } void ircd::js::context::handle_weak_pointer_compartment(JSContext *const cx, JSCompartment *const comp, void *const data) noexcept { log.debug("context(%p): weak pointer compartment(%p) %p", (const void *)cx, (const void *)comp, data); } void ircd::js::context::handle_weak_pointer_zone(JSContext *const cx, void *const data) noexcept { log.debug("context(%p): weak pointer zone %p", (const void *)cx, data); } void ircd::js::context::handle_trace_extra(JSTracer *const tracer, void *const priv) noexcept { log.debug("context(%p): tracer(%p) %s: extra (priv: %p) count: %zu", (const void *)cx, (const void *)tracer, debug(*tracer).c_str(), priv, cx->tracing.heap.size()); if(unlikely(std::uncaught_exception() && cx->tracing.heap.size() == 1)) { log.warning("context(%p): tracer(%p) %s: extra skipped due to uncaught exception", (const void *)cx, (const void *)tracer, debug(*tracer).c_str()); return; } cx->tracing(tracer); } void ircd::js::context::handle_trace_gray(JSTracer *const tracer, void *const priv) noexcept { log.debug("context(%p): tracer(%p): gray (priv: %p)", (const void *)cx, (const void *)tracer, priv); } void ircd::js::context::handle_large_allocation_failure(void *const priv) noexcept { log.error("context(%p): Large allocation failure (priv: %p)", (const void *)cx, priv); assert(0); //TODO: XXX } void ircd::js::context::handle_out_of_memory(JSContext *const cx, void *const priv) noexcept { log.error("context(%p): out of memory", (const void *)cx); assert(0); //TODO: XXX } bool ircd::js::context::handle_stopwatch_start(const uint64_t us, void *const priv) noexcept { assert(priv); auto &c(*static_cast(priv)); log.debug("context(%p): stopwatch start (microseconds: %lu priv: %p)", (const void *)c.get(), us, priv); return true; } bool ircd::js::context::handle_stopwatch_commit(const uint64_t us, ::js::PerformanceGroupVector &vec, void *const priv) noexcept { assert(priv); auto &c(*static_cast(priv)); log.debug("context(%p): stopwatch commit (microseconds: %lu priv: %p)", (const void *)c.get(), us, priv); return true; } bool ircd::js::context::handle_get_performance_groups(JSContext *const cx, ::js::PerformanceGroupVector &vec, void *const priv) noexcept { log.debug("context(%p): get performance groups (priv: %p)", (const void *)cx, priv); return true; } void ircd::js::context::handle_telemetry(const int id, const uint32_t sample, const char *const key) noexcept { //const auto tid(std::this_thread::get_id()); log.debug("context(%p): telemetry(%02d) %s: %u %s", //ircd::string(tid).c_str(), (const void *)cx, id, reflect_telemetry(id), sample, key?: ""); } void ircd::js::context::handle_timeout(JSContext *const cx) noexcept { // At this time there is no yield logic so if the timer calls the script is terminated. auto &c(our(cx)); interrupt(c, irq::TERMINATE); } bool ircd::js::context::handle_interrupt(JSContext *const cx) noexcept { auto &c(our(cx)); auto state(c.state.load(std::memory_order_acquire)); log.debug("context(%p): Interrupt: IRQ[%u] phase[%u]", (const void *)cx, uint(state.irq), uint(state.phase)); // Spurious interrupt; ignore. if(unlikely(state.phase != phase::INTR && state.phase != phase::ENTER)) { log.warning("context(%p): Spurious interrupt (irq: %02x)", (const void *)cx, 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 unwind interrupt_return([&c, &state] { state.phase = phase::ENTER; state.irq = irq::JS; c.state.store(state, std::memory_order_release); }); // Call the user hook if available if(c.on_intr) { // The user's handler returns -1 for non-overriding behavior const auto ret(c.on_intr(state.irq)); if(ret != -1) return ret; } switch(state.irq) { case irq::JS: case irq::USER: return true; case irq::YIELD: ctx::yield(); return true; case irq::TERMINATE: return false; default: case irq::NONE: assert(0); return false; } } void ircd::js::context::handle_error(JSContext *const cx, JSErrorReport *const report) noexcept try { assert(report); const log::level level { JSREPORT_IS_WARNING(report->flags)? log::WARNING: log::DEBUG }; log(level, "context(%p): %s", (const void *)cx, debug(*report).c_str()); auto &c(our(cx)); if(JSREPORT_IS_EXCEPTION(report->flags)) { // If except state is saved this is a redundant report from an exception as our // intertwined c++ -> js -> c++ stack blows up. if(c.except) return; // This is likely an uncaught exception from a throw in JS. We create a new exception // object because we lost the one from the user and make that pending now in case some // other opportunity for the user to catch this is presented. jserror e(*report); e.set_pending(); return; } if(report->exnType == JSEXN_INTERNALERR) { static const std::string msg("god save jsapi"); internal_error ie("%s", msg.c_str()); ie.set_pending(); return; } switch(report->errorNumber) { case 61: // JSAPI's code for interruption { report->exnType = JSEXN_ERR; report->flags |= JSREPORT_EXCEPTION; replace_message(*report, "interrupted @ line[%u] col[%u]", report->lineno, report->column); jserror e(*report); e.set_uncatchable(); return; } case 105: // JSAPI's code for user reported error { report->exnType = JSEXN_INTERNALERR; report->flags |= JSREPORT_EXCEPTION; replace_message(*report, "(BUG) Host exception"); jserror e(*report); e.set_uncatchable(); return; } } } catch(const std::exception &e) { log.critical("triple fault: %s\n", e.what()); ircd::terminate(); } /////////////////////////////////////////////////////////////////////////////// // // 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 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 lock(mutex); while(likely(!finished)) handle(lock); } void ircd::js::timer::handle(std::unique_lock &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/tracing.h // namespace ircd { namespace js { void trace_heap(JSTracer *const &tracer, tracing::thing &thing); } // namespace js } // namespace ircd ircd::js::tracing::tracing() { } ircd::js::tracing::~tracing() noexcept { if(std::uncaught_exception() && heap.size() == 1) return; assert(heap.empty()); } void ircd::js::tracing::operator()(JSTracer *const &tracer) { for(auto &thing : cx->tracing.heap) trace_heap(tracer, thing); } void ircd::js::trace_heap(JSTracer *const &tracer, tracing::thing &thing) { if(thing.type != jstype::OBJECT) log.debug("context(%p): tracer(%p): heap<%s> @ %p", nullptr, //(const void *)tracer->runtime(), (const void *)tracer, reflect(thing.type), (const void *)thing.ptr); if(likely(thing.ptr)) switch(thing.type) { case jstype::VALUE: { const auto ptr(reinterpret_cast *>(thing.ptr)); if(!ptr->address()) break; //JS_CallValueTracer(tracer, ptr, "heap"); break; } case jstype::OBJECT: { const auto ptr(reinterpret_cast *>(thing.ptr)); //if(!ptr->get()) break; log.debug("context(%p): tracer(%p): heap<%s> @ %p object(%p trap: %p '%s')", nullptr, //(const void *)tracer->runtime(), (const void *)tracer, reflect(thing.type), (const void *)thing.ptr, (const void *)ptr->get(), (const void *)(has_jsclass(*ptr)? &jsclass(*ptr) : nullptr), has_jsclass(*ptr)? jsclass(*ptr).name : ""); //JS_CallObjectTracer(tracer, ptr, "heap"); break; } case jstype::FUNCTION: { const auto ptr(reinterpret_cast *>(thing.ptr)); //if(!ptr->get()) break; //JS_CallFunctionTracer(tracer, ptr, "heap"); break; } case jstype::SCRIPT: { const auto ptr(reinterpret_cast *>(thing.ptr)); //if(!ptr->get()) break; //JS_CallScriptTracer(tracer, ptr, "heap"); break; } case jstype::STRING: { const auto ptr(reinterpret_cast *>(thing.ptr)); //if(!ptr->get()) break; //JS_CallStringTracer(tracer, ptr, "heap"); break; } case jstype::ID: { const auto ptr(reinterpret_cast *>(thing.ptr)); //if(!ptr->address()) break; //JS_CallIdTracer(tracer, ptr, "heap"); break; } case jstype::SYMBOL: { const auto ptr(reinterpret_cast *>(thing.ptr)); //if(!ptr->get()) break; break; } default: { log.warning("context(%p): tracer(%p): heap<%s> @ %p", nullptr, //(const void *)tracer->runtime(), (const void *)tracer, reflect(thing.type), (const void *)thing.ptr); break; } } } /////////////////////////////////////////////////////////////////////////////// // // ircd/js/debug.h // void ircd::js::log_gcparams() { for(int i(0); i < 50; ++i) { const auto key(static_cast(i)); const char *const name(reflect(key)); if(!strlen(name)) continue; // These trigger assertion failures switch(key) { case JSGC_NUMBER: case JSGC_MAX_MALLOC_BYTES: case JSGC_ALLOCATION_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_promise(const JS::HandleObject &promise) { #ifdef JS_DEBUG JS::DumpPromiseAllocationSite(*cx, promise); JS::DumpPromiseResolutionSite(*cx, promise); #endif } void ircd::js::dump(const JSString *const &v) { #ifdef JS_DEBUG ::js::DumpString(const_cast(v)); #endif } void ircd::js::dump(const JSAtom *const &v) { #ifdef JS_DEBUG ::js::DumpAtom(const_cast(v)); #endif } void ircd::js::dump(const JSObject *const &v) { #ifdef JS_DEBUG ::js::DumpObject(const_cast(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(v)); #endif } void ircd::js::dump(const JSScript *const &v) { #ifdef JS_DEBUG ::js::DumpScript(*cx, const_cast(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 JSTracer &t) { return t.isMarkingTracer()? "MARKING": t.isWeakMarkingTracer()? "WEAKMARKING": t.isTenuringTracer()? "TENURING": t.isCallbackTracer()? "CALLBACK": "UNKNOWN"; } 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(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[" << std::u16string(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.message()) // ss << "\"" << locale::char16::conv(r.message()) << "\" "; // 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 "; if(JS_ObjectIsFunction(*cx, o)) ss << "Function "; bool ret; if(JS_IsExtensible(*cx, o, &ret)) if(ret) ss << "Extensible "; if(JS_IsArrayObject(*cx, o, &ret)) if(ret) ss << "Array "; if(JS::IsArray(*cx, o, &ret)) if(ret) ss << "Array.isArray "; if(JS_ObjectIsRegExp(*cx, o, &ret)) if(ret) ss << "RegExp "; if(JS_ObjectIsDate(*cx, o, &ret)) if(ret) ss << "Date "; if(JS::IsPromiseObject(o)) { ss << "Promise[#" << JS::GetPromiseID(o); ss << " " << reflect(JS::GetPromiseState(o)); ss << "] "; } if(JS_IsArrayBufferObject(o)) { ss << "ArrayBuffer[" << JS_GetArrayBufferByteLength(o); ss << " " << JS_IsDetachedArrayBufferObject(o)? "DETACHED" : "ATTACHED"; ss << " " << JS_IsMappedArrayBufferObject(o)? "MAPPED" : "UNMAPPED"; ss << "] "; } if(JS_IsSharedArrayBufferObject(o)) { ss << "SharedArrayBuffer[" << JS_GetSharedArrayBufferByteLength(o); ss << " " << JS_IsDetachedArrayBufferObject(o)? "DETACHED" : "ATTACHED"; ss << " " << JS_IsMappedArrayBufferObject(o)? "MAPPED" : "UNMAPPED"; ss << "] "; } if(JS_IsArrayBufferViewObject(o)) { ss << "ArrayBufferView[" << JS_GetArrayBufferViewByteLength(o); ss << "] "; } if(JS_IsDataViewObject(o)) { ss << "DataView[" << JS_GetDataViewByteLength(o); ss << " @" << JS_GetDataViewByteOffset(o); ss << "] "; } 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.isSymbol()) ss << "Symbol "; if(v.isObject()) ss << "Object "; if(v.isObject()) { JS::RootedObject obj(*cx); JS::RootedValue rv(*cx, v); if(JS_ValueToObject(*cx, rv, &obj)) ss << "(" << debug(obj) << ") "; } 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_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_prop(const uint &flag) { switch(flag) { case JSPROP_ENUMERATE: return "JSPROP_ENUMERATE"; case JSPROP_READONLY: return "JSPROP_READONLY"; case JSPROP_PERMANENT: return "JSPROP_PERMANENT"; case JSPROP_PROPOP_ACCESSORS: return "JSPROP_PROPOP_ACCESSORS"; case JSPROP_GETTER: return "JSPROP_GETTER"; case JSPROP_SETTER: return "JSPROP_SETTER"; case JSPROP_SHARED: return "JSPROP_SHARED"; case JSPROP_INTERNAL_USE_BIT: return "JSPROP_INTERNAL_USE_BIT"; case JSFUN_STUB_GSOPS: return "JSFUN_STUB_GSOPS"; case JSFUN_CONSTRUCTOR: return "JSFUN_CONSTRUCTOR"; case JSPROP_REDEFINE_NONCONFIGURABLE: return "JSPROP_REDEFINE_NONCONFIGURABLE"; case JSPROP_RESOLVING: return "JSPROP_RESOLVING"; case JSPROP_IGNORE_ENUMERATE: return "JSPROP_IGNORE_ENUMERATE"; case JSPROP_IGNORE_READONLY: return "JSPROP_IGNORE_READONLY"; case JSPROP_IGNORE_PERMANENT: return "JSPROP_IGNORE_PERMANENT"; case JSPROP_IGNORE_VALUE: return "JSPROP_IGNORE_VALUE"; } return ""; } const char * ircd::js::reflect(const JS::PromiseState &state) { switch(state) { case JS::PromiseState::Pending: return "Pending"; case JS::PromiseState::Fulfilled: return "Fulfilled"; case JS::PromiseState::Rejected: return "Rejected"; } return ""; } const char * ircd::js::reflect(const PromiseRejectionHandlingState &state) { switch(state) { case PromiseRejectionHandlingState::Unhandled: return "Unhandled"; case PromiseRejectionHandlingState::Handled: return "Handled"; } 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 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_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_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"; case JSGC_REFRESH_FRAME_SLICES_ENABLED: return "JSGC_REFRESH_FRAME_SLICES_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 JSGCMode &s) { switch(s) { case JSGC_MODE_GLOBAL: return "GLOBAL"; case JSGC_MODE_INCREMENTAL: return "INCREMENTAL"; case JSGC_MODE_ZONE: return "ZONE"; } return ""; } const char * ircd::js::reflect(const JS::GCProgress &s) { switch(s) { case JS::GC_CYCLE_BEGIN: return "CYCLE_BEGIN"; case JS::GC_SLICE_BEGIN: return "SLICE_BEGIN"; case JS::GC_SLICE_END: return "SLICE_END"; case JS::GC_CYCLE_END: return "CYCLE_END"; } return ""; } const char * ircd::js::reflect(const JSExnType &e) { switch(e) { case JSEXN_WARN: return "Warning"; 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?"; case JSEXN_DEBUGGEEWOULDRUN: return "DebugeeWouldRun"; case JSEXN_WASMCOMPILEERROR: return "WASMCompileError"; case JSEXN_WASMRUNTIMEERROR: return "WASMRuntimeError"; } 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 ""; } const char * ircd::js::reflect(const jstype &t) { switch(t) { case jstype::VALUE: return "VALUE"; case jstype::OBJECT: return "OBJECT"; case jstype::FUNCTION: return "FUNCTION"; case jstype::SCRIPT: return "SCRIPT"; case jstype::STRING: return "STRING"; case jstype::SYMBOL: return "SYMBOL"; case jstype::ID: return "ID"; } return ""; } /////////////////////////////////////////////////////////////////////////////// // // ircd/js/type.h // /////////////////////////////////////////////////////////////////////////////// // // ircd/js/version.h // /////////////////////////////////////////////////////////////////////////////// // // Misc / Fixes / Temp // #if defined(IRCD_JS_FIX) void __attribute__((noreturn)) js::ReportOutOfMemory(ExclusiveContext *const c) { ircd::js::log.critical("jsalloc(): Reported out of memory (ExclusiveContext: %p)", (const void *)c); ircd::terminate(); } #endif //IRCD_JS_FIX // // This DEBUG section is a fix for linkage errors when SpiderMonkey is compiled // in debug mode. // #if defined(JS_DEBUG) && defined(IRCD_JS_FIX) namespace js { namespace oom { extern mozilla::detail::ThreadLocal threadType; uint32_t GetThreadType() { assert(0); return threadType.get(); } } // namespace oom } // namespace js #endif // JS_DEBUG && IRCD_JS_FIX