diff options
Diffstat (limited to 'src/mongo/scripting/mozjs/wraptype.h')
-rw-r--r-- | src/mongo/scripting/mozjs/wraptype.h | 474 |
1 files changed, 474 insertions, 0 deletions
diff --git a/src/mongo/scripting/mozjs/wraptype.h b/src/mongo/scripting/mozjs/wraptype.h new file mode 100644 index 00000000000..c5ca6095e76 --- /dev/null +++ b/src/mongo/scripting/mozjs/wraptype.h @@ -0,0 +1,474 @@ +/** + * Copyright (C) 2015 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#pragma once + +#include <cstddef> +#include <jsapi.h> +#include <type_traits> + +#include "mongo/scripting/mozjs/base.h" +#include "mongo/scripting/mozjs/exception.h" +#include "mongo/scripting/mozjs/objectwrapper.h" +#include "mongo/util/assert_util.h" + +// The purpose of this class is to take in specially crafted types and generate +// a wrapper which installs the type, along with any useful life cycle methods +// and free functions that might be associated with it. The template magic in +// here, along with some useful macros, hides a lot of the implementation +// complexity of exposing C++ code into javascript. Most prominently, we have +// to wrap every function that can be called from javascript to prevent any C++ +// exceptions from leaking out. We do this, with template and macro based +// codegen, and turn mongo exceptions into instances of Status, then convert +// those into javascript exceptions before returning. That allows all consumers +// of this library to throw exceptions freely, with the understanding that +// they'll be visible in javascript. Javascript exceptions are trapped at the +// top level and converted back to mongo exceptions by an error handler on +// ImplScope. + +// MONGO_*_JS_FUNCTION_* macros are public and allow wrapped types to install +// their own functions on types and into the global scope +#define MONGO_DEFINE_JS_FUNCTION(name) \ + static void name(JSContext* cx, JS::CallArgs args); \ + static bool WRAPPER_##name(JSContext* cx, unsigned argc, JS::Value* vp) { \ + try { \ + JS::CallArgs args = JS::CallArgsFromVp(argc, vp); \ + name(cx, args); \ + return true; \ + } catch (...) { \ + mongoToJSException(cx); \ + return false; \ + } \ + } + +#define MONGO_ATTACH_JS_FUNCTION_WITH_FLAGS(name, flags) \ + JS_FS(#name, Functions::WRAPPER_##name, 0, flags) + +#define MONGO_ATTACH_JS_FUNCTION(name) MONGO_ATTACH_JS_FUNCTION_WITH_FLAGS(name, 0) + +namespace mongo { +namespace mozjs { + +namespace smUtils { + +// Now all the spidermonkey type methods +template <typename T> +static bool addProperty(JSContext* cx, + JS::HandleObject obj, + JS::HandleId id, + JS::MutableHandleValue v) { + try { + T::addProperty(cx, obj, id, v); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +template <typename T> +static bool call(JSContext* cx, unsigned argc, JS::Value* vp) { + try { + T::call(cx, JS::CallArgsFromVp(argc, vp)); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +template <typename T> +static bool construct(JSContext* cx, unsigned argc, JS::Value* vp) { + try { + T::construct(cx, JS::CallArgsFromVp(argc, vp)); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +template <typename T> +static bool convert(JSContext* cx, JS::HandleObject obj, JSType type, JS::MutableHandleValue vp) { + try { + T::convert(cx, obj, type, vp); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +template <typename T> +static bool delProperty(JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool* succeeded) { + try { + T::delProperty(cx, obj, id, succeeded); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +template <typename T> +static bool enumerate(JSContext* cx, JS::HandleObject obj, JS::AutoIdVector& properties) { + try { + T::enumerate(cx, obj, properties); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +template <typename T> +static bool getProperty(JSContext* cx, + JS::HandleObject obj, + JS::HandleId id, + JS::MutableHandleValue vp) { + try { + T::getProperty(cx, obj, id, vp); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +template <typename T> +static bool hasInstance(JSContext* cx, JS::HandleObject obj, JS::MutableHandleValue vp, bool* bp) { + try { + T::hasInstance(cx, obj, vp, bp); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +template <typename T> +static bool setProperty( + JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool strict, JS::MutableHandleValue vp) { + try { + T::setProperty(cx, obj, id, strict, vp); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +template <typename T> +static bool resolve(JSContext* cx, JS::HandleObject obj, JS::HandleId id, bool* resolvedp) { + try { + T::resolve(cx, obj, id, resolvedp); + return true; + } catch (...) { + mongoToJSException(cx); + return false; + } +}; + +} // namespace smUtils + +template <typename T> +class WrapType : public T { +public: + WrapType(JSContext* context) + : _context(context), + _proto(), + _jsclass({T::className, + T::classFlags, + T::addProperty != BaseInfo::addProperty ? smUtils::addProperty<T> : nullptr, + T::delProperty != BaseInfo::delProperty ? smUtils::delProperty<T> : nullptr, + T::getProperty != BaseInfo::getProperty ? smUtils::getProperty<T> : nullptr, + T::setProperty != BaseInfo::setProperty ? smUtils::setProperty<T> : nullptr, + // We don't use the regular enumerate because we want the fancy new one + nullptr, + T::resolve != BaseInfo::resolve ? smUtils::resolve<T> : nullptr, + T::convert != BaseInfo::convert ? smUtils::convert<T> : nullptr, + T::finalize != BaseInfo::finalize ? T::finalize : nullptr, + T::call != BaseInfo::call ? smUtils::call<T> : nullptr, + T::hasInstance != BaseInfo::hasInstance ? smUtils::hasInstance<T> : nullptr, + T::construct != BaseInfo::construct ? smUtils::construct<T> : nullptr, + nullptr}) { + _installEnumerate(T::enumerate != BaseInfo::enumerate ? smUtils::enumerate<T> : nullptr); + + // The global object is different. We need it for basic setup + // before the other types are installed. Might as well just do it + // in the constructor. + if (T::classFlags & JSCLASS_GLOBAL_FLAGS) { + JS::RootedObject proto(_context); + + _proto.init(_context, + _assertPtr(JS_NewGlobalObject( + _context, &_jsclass, nullptr, JS::DontFireOnNewGlobalHook))); + + JSAutoCompartment ac(_context, _proto); + _installFunctions(_proto, T::freeFunctions); + } + } + + ~WrapType() { + // Persistent globals don't RAII, you have to reset() them manually + _proto.reset(); + } + + void install(JS::HandleObject global) { + switch (static_cast<InstallType>(T::installType)) { + case InstallType::Global: + _installGlobal(global); + break; + case InstallType::Private: + _installPrivate(global); + break; + case InstallType::OverNative: + _installOverNative(global); + break; + } + } + + /** + * newObject methods don't invoke the constructor. So they're good for + * types without a constructor or inside the constructor + */ + void newObject(JS::MutableHandleObject out) { + // The regular form of JS_NewObject, where we pass proto as the + // third param, actually does a global object lookup for some + // reason. This way allows object creation with non-public + // prototypes and if someone deletes the symbol up the chain. + out.set(_assertPtr(JS_NewObject(_context, &_jsclass, JS::NullPtr()))); + + if (!JS_SetPrototype(_context, out, _proto)) + throwCurrentJSException( + _context, ErrorCodes::JSInterpreterFailure, "Failed to set prototype"); + } + + void newObject(JS::MutableHandleValue out) { + JS::RootedObject obj(_context); + newObject(&obj); + + out.setObjectOrNull(obj); + } + + /** + * newInstance calls the constructor, a la new Type() in js + */ + void newInstance(JS::MutableHandleObject out) { + JS::AutoValueVector args(_context); + + newInstance(args, out); + } + + void newInstance(const JS::HandleValueArray& args, JS::MutableHandleObject out) { + out.set(_assertPtr(JS_New(_context, _proto, args))); + } + + void newInstance(JS::MutableHandleValue out) { + JS::AutoValueVector args(_context); + + newInstance(args, out); + } + + void newInstance(const JS::HandleValueArray& args, JS::MutableHandleValue out) { + out.setObjectOrNull(_assertPtr(JS_New(_context, _proto, args))); + } + + // instanceOf doesn't go up the prototype tree. It's a lower level more specific match + bool instanceOf(JS::HandleObject obj) { + return JS_InstanceOf(_context, obj, &_jsclass, nullptr); + } + + bool instanceOf(JS::HandleValue value) { + if (!value.isObject()) + return false; + + JS::RootedObject obj(_context, value.toObjectOrNull()); + + return instanceOf(obj); + } + + const JSClass* getJSClass() const { + return &_jsclass; + } + + JS::HandleObject getProto() const { + return _proto; + } + +private: + /** + * Use this if you want your types installed visibly in the global scope + */ + void _installGlobal(JS::HandleObject global) { + JS::RootedObject parent(_context); + _inheritFrom(T::inheritFrom, global, &parent); + + _proto.init(_context, + _assertPtr(JS_InitClass( + _context, + global, + parent, + &_jsclass, + T::construct != BaseInfo::construct ? smUtils::construct<T> : nullptr, + 0, + nullptr, + T::methods, + nullptr, + nullptr))); + + _installFunctions(global, T::freeFunctions); + _postInstall(global, T::postInstall); + } + + // Use this if you want your types installed, but not visible in the + // global scope + void _installPrivate(JS::HandleObject global) { + JS::RootedObject parent(_context); + _inheritFrom(T::inheritFrom, global, &parent); + + // See newObject() for why we have to do this dance with the explicit + // SetPrototype + _proto.init(_context, _assertPtr(JS_NewObject(_context, &_jsclass, JS::NullPtr()))); + if (parent.get() && !JS_SetPrototype(_context, _proto, parent)) + throwCurrentJSException( + _context, ErrorCodes::JSInterpreterFailure, "Failed to set prototype"); + + _installFunctions(_proto, T::methods); + _installFunctions(global, T::freeFunctions); + + _installConstructor(T::construct != BaseInfo::construct ? smUtils::construct<T> : nullptr); + + _postInstall(global, T::postInstall); + } + + // Use this to attach things to types that we don't provide like + // Object, or Array + void _installOverNative(JS::HandleObject global) { + JS::RootedValue value(_context); + if (!JS_GetProperty(_context, global, T::className, &value)) + throwCurrentJSException( + _context, ErrorCodes::JSInterpreterFailure, "Couldn't get className property"); + + if (!value.isObject()) + uasserted(ErrorCodes::BadValue, "className isn't object"); + + _proto.init(_context, value.toObjectOrNull()); + + _installFunctions(_proto, T::methods); + _installFunctions(global, T::freeFunctions); + _postInstall(global, T::postInstall); + } + + void _installFunctions(JS::HandleObject global, const JSFunctionSpec* fs) { + if (!fs) + return; + if (JS_DefineFunctions(_context, global, fs)) + return; + + throwCurrentJSException( + _context, ErrorCodes::JSInterpreterFailure, "Failed to define functions"); + } + + // We have to do this awkward dance to set the new style enumeration. + // You used to be able to set this with JSCLASS_NEW_ENUMERATE in class + // flags, in the future you'll probably only set ObjectOps, but for now + // we have this. There are a host of static_asserts in js/Class.h that + // ensure that these two structures are equal. + // + // This is a landmine to watch out for during upgrades + using enumerateT = bool (*)(JSContext*, JS::HandleObject, JS::AutoIdVector&); + void _installEnumerate(enumerateT enumerate) { + if (!enumerate) + return; + + auto implClass = reinterpret_cast<js::Class*>(&_jsclass); + + implClass->ops.enumerate = enumerate; + } + + // This is for inheriting from something other than Object + void _inheritFrom(const char* name, JS::HandleObject global, JS::MutableHandleObject out) { + if (!name) + return; + + JS::RootedValue val(_context); + + if (!JS_GetProperty(_context, global, name, &val)) { + throwCurrentJSException( + _context, ErrorCodes::JSInterpreterFailure, "Failed to get parent"); + } + + if (!val.isObject()) { + uasserted(ErrorCodes::JSInterpreterFailure, "Parent is not an object"); + } + + out.set(val.toObjectOrNull()); + } + + using postInstallT = void (*)(JSContext*, JS::HandleObject, JS::HandleObject); + void _postInstall(JS::HandleObject global, postInstallT postInstall) { + if (!postInstall) + return; + + postInstall(_context, global, _proto); + } + + void _installConstructor(JSNative ctor) { + if (!ctor) + return; + + auto ptr = JS_NewFunction(_context, ctor, 0, JSFUN_CONSTRUCTOR, JS::NullPtr(), nullptr); + if (!ptr) { + throwCurrentJSException( + _context, ErrorCodes::JSInterpreterFailure, "Failed to install constructor"); + } + + JS::RootedObject ctorObj(_context, JS_GetFunctionObject(ptr)); + + if (!JS_LinkConstructorAndPrototype(_context, ctorObj, _proto)) + throwCurrentJSException(_context, + ErrorCodes::JSInterpreterFailure, + "Failed to link constructor and prototype"); + } + + JSObject* _assertPtr(JSObject* ptr) { + if (!ptr) + throwCurrentJSException( + _context, ErrorCodes::JSInterpreterFailure, "Failed to JS_NewX"); + + return ptr; + } + + JSContext* _context; + JS::PersistentRootedObject _proto; + JSClass _jsclass; +}; + +} // namespace mozjs +} // namespace mongo |