/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "vm/BoundFunctionObject.h" #include #include "util/StringBuffer.h" #include "vm/Interpreter.h" #include "vm/Shape.h" #include "vm/Stack.h" #include "gc/ObjectKind-inl.h" #include "vm/JSFunction-inl.h" #include "vm/JSObject-inl.h" #include "vm/NativeObject-inl.h" #include "vm/Shape-inl.h" using namespace js; // Helper function to initialize `args` with all bound arguments + the arguments // supplied in `callArgs`. template static MOZ_ALWAYS_INLINE void FillArguments(Args& args, BoundFunctionObject* bound, size_t numBoundArgs, const CallArgs& callArgs) { MOZ_ASSERT(args.length() == numBoundArgs + callArgs.length()); if (numBoundArgs <= BoundFunctionObject::MaxInlineBoundArgs) { for (size_t i = 0; i < numBoundArgs; i++) { args[i].set(bound->getInlineBoundArg(i)); } } else { ArrayObject* boundArgs = bound->getBoundArgsArray(); for (size_t i = 0; i < numBoundArgs; i++) { args[i].set(boundArgs->getDenseElement(i)); } } for (size_t i = 0; i < callArgs.length(); i++) { args[numBoundArgs + i].set(callArgs[i]); } } // ES2023 10.4.1.1 [[Call]] // https://tc39.es/ecma262/#sec-bound-function-exotic-objects-call-thisargument-argumentslist // static bool BoundFunctionObject::call(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); Rooted bound(cx, &args.callee().as()); // Step 1. Rooted target(cx, bound->getTargetVal()); // Step 2. Rooted boundThis(cx, bound->getBoundThis()); // Steps 3-4. size_t numBoundArgs = bound->numBoundArgs(); InvokeArgs args2(cx); if (!args2.init(cx, uint64_t(numBoundArgs) + args.length())) { return false; } FillArguments(args2, bound, numBoundArgs, args); // Step 5. return Call(cx, target, boundThis, args2, args.rval()); } // ES2023 10.4.1.2 [[Construct]] // https://tc39.es/ecma262/#sec-bound-function-exotic-objects-construct-argumentslist-newtarget // static bool BoundFunctionObject::construct(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); Rooted bound(cx, &args.callee().as()); MOZ_ASSERT(bound->isConstructor(), "shouldn't have called this hook if not a constructor"); // Step 1. Rooted target(cx, bound->getTargetVal()); // Step 2. MOZ_ASSERT(IsConstructor(target)); // Steps 3-4. size_t numBoundArgs = bound->numBoundArgs(); ConstructArgs args2(cx); if (!args2.init(cx, uint64_t(numBoundArgs) + args.length())) { return false; } FillArguments(args2, bound, numBoundArgs, args); // Step 5. Rooted newTarget(cx, args.newTarget()); if (newTarget == ObjectValue(*bound)) { newTarget = target; } // Step 6. Rooted res(cx); if (!Construct(cx, target, args2, newTarget, &res)) { return false; } args.rval().setObject(*res); return true; } // static JSString* BoundFunctionObject::funToString(JSContext* cx, Handle obj, bool isToSource) { // Implementation of the funToString hook used by Function.prototype.toString. // For the non-standard toSource extension, we include "bound" to indicate // it's a bound function. if (isToSource) { static constexpr std::string_view nativeCodeBound = "function bound() {\n [native code]\n}"; return NewStringCopy(cx, nativeCodeBound); } static constexpr std::string_view nativeCode = "function() {\n [native code]\n}"; return NewStringCopy(cx, nativeCode); } // static SharedShape* BoundFunctionObject::assignInitialShape( JSContext* cx, Handle obj) { MOZ_ASSERT(obj->empty()); constexpr PropertyFlags propFlags = {PropertyFlag::Configurable}; if (!NativeObject::addPropertyInReservedSlot(cx, obj, cx->names().length, LengthSlot, propFlags)) { return nullptr; } if (!NativeObject::addPropertyInReservedSlot(cx, obj, cx->names().name, NameSlot, propFlags)) { return nullptr; } SharedShape* shape = obj->sharedShape(); if (shape->proto() == TaggedProto(&cx->global()->getFunctionPrototype())) { cx->global()->setBoundFunctionShapeWithDefaultProto(shape); } return shape; } static MOZ_ALWAYS_INLINE bool ComputeLengthValue( JSContext* cx, Handle bound, Handle target, size_t numBoundArgs, double* length) { *length = 0.0; // Try to avoid invoking the JSFunction resolve hook. if (target->is() && !target->as().hasResolvedLength()) { uint16_t targetLength; if (!JSFunction::getUnresolvedLength(cx, target.as(), &targetLength)) { return false; } if (size_t(targetLength) > numBoundArgs) { *length = size_t(targetLength) - numBoundArgs; } return true; } // Use a fast path for getting the .length value if the target is a bound // function with its initial shape. Value targetLength; if (target->is() && target->shape() == bound->shape()) { BoundFunctionObject* targetFn = &target->as(); targetLength = targetFn->getLengthForInitialShape(); } else { bool hasLength; Rooted key(cx, NameToId(cx->names().length)); if (!HasOwnProperty(cx, target, key, &hasLength)) { return false; } if (!hasLength) { return true; } Rooted targetLengthRoot(cx); if (!GetProperty(cx, target, target, key, &targetLengthRoot)) { return false; } targetLength = targetLengthRoot; } if (targetLength.isNumber()) { *length = std::max( 0.0, JS::ToInteger(targetLength.toNumber()) - double(numBoundArgs)); } return true; } static MOZ_ALWAYS_INLINE JSAtom* AppendBoundFunctionPrefix(JSContext* cx, JSString* str) { auto& cache = cx->zone()->boundPrefixCache(); JSAtom* strAtom = str->isAtom() ? &str->asAtom() : nullptr; if (strAtom) { if (auto p = cache.lookup(strAtom)) { return p->value(); } } StringBuffer sb(cx); if (!sb.append("bound ") || !sb.append(str)) { return nullptr; } JSAtom* atom = sb.finishAtom(); if (!atom) { return nullptr; } if (strAtom) { (void)cache.putNew(strAtom, atom); } return atom; } static MOZ_ALWAYS_INLINE JSAtom* ComputeNameValue( JSContext* cx, Handle bound, Handle target) { // Try to avoid invoking the JSFunction resolve hook. JSString* name = nullptr; if (target->is() && !target->as().hasResolvedName()) { JSFunction* targetFn = &target->as(); name = targetFn->getUnresolvedName(cx); if (!name) { return nullptr; } } else { // Use a fast path for getting the .name value if the target is a bound // function with its initial shape. Value targetName; if (target->is() && target->shape() == bound->shape()) { BoundFunctionObject* targetFn = &target->as(); targetName = targetFn->getNameForInitialShape(); } else { Rooted targetNameRoot(cx); if (!GetProperty(cx, target, target, cx->names().name, &targetNameRoot)) { return nullptr; } targetName = targetNameRoot; } if (!targetName.isString()) { return cx->names().boundWithSpace_; } name = targetName.toString(); } return AppendBoundFunctionPrefix(cx, name); } // ES2023 20.2.3.2 Function.prototype.bind // https://tc39.es/ecma262/#sec-function.prototype.bind // static bool BoundFunctionObject::functionBind(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Steps 1-2. if (!IsCallable(args.thisv())) { ReportIncompatibleMethod(cx, args, &FunctionClass); return false; } if (MOZ_UNLIKELY(args.length() > ARGS_LENGTH_MAX)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TOO_MANY_ARGUMENTS); return false; } Rooted target(cx, &args.thisv().toObject()); BoundFunctionObject* bound = functionBindImpl(cx, target, args.array(), args.length(), nullptr); if (!bound) { return false; } // Step 11. args.rval().setObject(*bound); return true; } // ES2023 20.2.3.2 Function.prototype.bind // https://tc39.es/ecma262/#sec-function.prototype.bind // // ES2023 10.4.1.3 BoundFunctionCreate // https://tc39.es/ecma262/#sec-boundfunctioncreate // // BoundFunctionCreate has been inlined in Function.prototype.bind for // performance reasons. // // static BoundFunctionObject* BoundFunctionObject::functionBindImpl( JSContext* cx, Handle target, Value* args, uint32_t argc, Handle maybeBound) { MOZ_ASSERT(target->isCallable()); // Make sure the arguments on the stack are rooted when we're called directly // from JIT code. RootedExternalValueArray argsRoot(cx, argc, args); size_t numBoundArgs = argc > 0 ? argc - 1 : 0; MOZ_ASSERT(numBoundArgs <= ARGS_LENGTH_MAX, "ensured by callers"); // If this assertion fails, make sure we use the correct AllocKind and that we // use all of its slots (consider increasing MaxInlineBoundArgs). static_assert(gc::GetGCKindSlots(allocKind) == SlotCount); // ES2023 10.4.1.3 BoundFunctionCreate // Steps 1-5. Rooted bound(cx); if (maybeBound) { // We allocated a bound function in JIT code. In the uncommon case of the // target not having Function.prototype as proto, we have to set the right // proto here. bound = maybeBound; if (MOZ_UNLIKELY(bound->staticPrototype() != target->staticPrototype())) { Rooted proto(cx, target->staticPrototype()); if (!SetPrototype(cx, bound, proto)) { return nullptr; } } } else { // Step 1. Rooted proto(cx); if (!GetPrototype(cx, target, &proto)) { return nullptr; } // Steps 2-5. if (proto == &cx->global()->getFunctionPrototype() && cx->global()->maybeBoundFunctionShapeWithDefaultProto()) { Rooted shape( cx, cx->global()->maybeBoundFunctionShapeWithDefaultProto()); bound = NativeObject::create( cx, allocKind, gc::Heap::Default, shape); if (!bound) { return nullptr; } } else { bound = NewObjectWithGivenProto(cx, proto); if (!bound) { return nullptr; } if (!SharedShape::ensureInitialCustomShape(cx, bound)) { return nullptr; } } } MOZ_ASSERT(bound->lookupPure(cx->names().length)->slot() == LengthSlot); MOZ_ASSERT(bound->lookupPure(cx->names().name)->slot() == NameSlot); // Steps 6 and 9. bound->initFlags(numBoundArgs, target->isConstructor()); // Step 7. bound->initReservedSlot(TargetSlot, ObjectValue(*target)); // Step 8. if (argc > 0) { bound->initReservedSlot(BoundThisSlot, args[0]); } if (numBoundArgs <= MaxInlineBoundArgs) { for (size_t i = 0; i < numBoundArgs; i++) { bound->initReservedSlot(BoundArg0Slot + i, args[i + 1]); } } else { ArrayObject* arr = NewDenseCopiedArray(cx, numBoundArgs, args + 1); if (!arr) { return nullptr; } bound->initReservedSlot(BoundArg0Slot, ObjectValue(*arr)); } // ES2023 20.2.3.2 Function.prototype.bind // Step 4. double length = 0.0; // Steps 5-6. if (!ComputeLengthValue(cx, bound, target, numBoundArgs, &length)) { return nullptr; } // Step 7. bound->initLength(length); // Steps 8-9. JSAtom* name = ComputeNameValue(cx, bound, target); if (!name) { return nullptr; } // Step 10. bound->initName(name); // Step 11. return bound; } // static BoundFunctionObject* BoundFunctionObject::createWithTemplate( JSContext* cx, Handle templateObj) { Rooted shape(cx, templateObj->sharedShape()); auto* bound = NativeObject::create( cx, allocKind, gc::Heap::Default, shape); if (!bound) { return nullptr; } bound->initFlags(templateObj->numBoundArgs(), templateObj->isConstructor()); bound->initLength(templateObj->getLengthForInitialShape().toInt32()); bound->initName(&templateObj->getNameForInitialShape().toString()->asAtom()); return bound; } // static BoundFunctionObject* BoundFunctionObject::functionBindSpecializedBaseline( JSContext* cx, Handle target, Value* args, uint32_t argc, Handle templateObj) { // Root the Values on the stack. RootedExternalValueArray argsRoot(cx, argc, args); MOZ_ASSERT(target->is() || target->is()); MOZ_ASSERT(target->isCallable()); MOZ_ASSERT(target->isConstructor() == templateObj->isConstructor()); MOZ_ASSERT(target->staticPrototype() == templateObj->staticPrototype()); size_t numBoundArgs = argc > 0 ? argc - 1 : 0; MOZ_ASSERT(numBoundArgs <= MaxInlineBoundArgs); BoundFunctionObject* bound = createWithTemplate(cx, templateObj); if (!bound) { return nullptr; } MOZ_ASSERT(bound->lookupPure(cx->names().length)->slot() == LengthSlot); MOZ_ASSERT(bound->lookupPure(cx->names().name)->slot() == NameSlot); bound->initReservedSlot(TargetSlot, ObjectValue(*target)); if (argc > 0) { bound->initReservedSlot(BoundThisSlot, args[0]); } for (size_t i = 0; i < numBoundArgs; i++) { bound->initReservedSlot(BoundArg0Slot + i, args[i + 1]); } return bound; } // static BoundFunctionObject* BoundFunctionObject::createTemplateObject(JSContext* cx) { Rooted proto(cx, &cx->global()->getFunctionPrototype()); Rooted bound( cx, NewTenuredObjectWithGivenProto(cx, proto)); if (!bound) { return nullptr; } if (!SharedShape::ensureInitialCustomShape(cx, bound)) { return nullptr; } return bound; } bool BoundFunctionObject::initTemplateSlotsForSpecializedBind( JSContext* cx, uint32_t numBoundArgs, bool targetIsConstructor, uint32_t targetLength, JSAtom* targetName) { size_t len = 0; if (targetLength > numBoundArgs) { len = targetLength - numBoundArgs; } JSAtom* name = AppendBoundFunctionPrefix(cx, targetName); if (!name) { return false; } initFlags(numBoundArgs, targetIsConstructor); initLength(len); initName(name); return true; } static const JSClassOps classOps = { nullptr, // addProperty nullptr, // delProperty nullptr, // enumerate nullptr, // newEnumerate nullptr, // resolve nullptr, // mayResolve nullptr, // finalize BoundFunctionObject::call, // call BoundFunctionObject::construct, // construct nullptr, // trace }; static const ObjectOps objOps = { nullptr, // lookupProperty nullptr, // qdefineProperty nullptr, // hasProperty nullptr, // getProperty nullptr, // setProperty nullptr, // getOwnPropertyDescriptor nullptr, // deleteProperty nullptr, // getElements BoundFunctionObject::funToString, // funToString }; const JSClass BoundFunctionObject::class_ = { "BoundFunctionObject", // Note: bound functions don't have their own constructor or prototype (they // use the prototype of the target object), but we give them a JSProtoKey // because that's what Xray wrappers use to identify builtin objects. JSCLASS_HAS_CACHED_PROTO(JSProto_BoundFunction) | JSCLASS_HAS_RESERVED_SLOTS(BoundFunctionObject::SlotCount), &classOps, JS_NULL_CLASS_SPEC, JS_NULL_CLASS_EXT, &objOps, };