BoundFunctionObject.cpp (17108B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * vim: set ts=8 sts=2 et sw=2 tw=80: 3 * This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "vm/BoundFunctionObject.h" 8 9 #include <string_view> 10 11 #include "util/StringBuilder.h" 12 #include "vm/Interpreter.h" 13 #include "vm/Shape.h" 14 #include "vm/Stack.h" 15 16 #include "gc/ObjectKind-inl.h" 17 #include "vm/JSFunction-inl.h" 18 #include "vm/JSObject-inl.h" 19 #include "vm/NativeObject-inl.h" 20 #include "vm/Shape-inl.h" 21 22 using namespace js; 23 24 // Helper function to initialize `args` with all bound arguments + the arguments 25 // supplied in `callArgs`. 26 template <typename Args> 27 static MOZ_ALWAYS_INLINE void FillArguments(Args& args, 28 BoundFunctionObject* bound, 29 size_t numBoundArgs, 30 const CallArgs& callArgs) { 31 MOZ_ASSERT(args.length() == numBoundArgs + callArgs.length()); 32 33 if (numBoundArgs <= BoundFunctionObject::MaxInlineBoundArgs) { 34 for (size_t i = 0; i < numBoundArgs; i++) { 35 args[i].set(bound->getInlineBoundArg(i)); 36 } 37 } else { 38 ArrayObject* boundArgs = bound->getBoundArgsArray(); 39 for (size_t i = 0; i < numBoundArgs; i++) { 40 args[i].set(boundArgs->getDenseElement(i)); 41 } 42 } 43 44 for (size_t i = 0; i < callArgs.length(); i++) { 45 args[numBoundArgs + i].set(callArgs[i]); 46 } 47 } 48 49 // ES2023 10.4.1.1 [[Call]] 50 // https://tc39.es/ecma262/#sec-bound-function-exotic-objects-call-thisargument-argumentslist 51 // static 52 bool BoundFunctionObject::call(JSContext* cx, unsigned argc, Value* vp) { 53 CallArgs args = CallArgsFromVp(argc, vp); 54 Rooted<BoundFunctionObject*> bound(cx, 55 &args.callee().as<BoundFunctionObject>()); 56 57 // Step 1. 58 Rooted<Value> target(cx, bound->getTargetVal()); 59 60 // Step 2. 61 Rooted<Value> boundThis(cx, bound->getBoundThis()); 62 63 // Steps 3-4. 64 size_t numBoundArgs = bound->numBoundArgs(); 65 InvokeArgs args2(cx); 66 if (!args2.init(cx, uint64_t(numBoundArgs) + args.length())) { 67 return false; 68 } 69 FillArguments(args2, bound, numBoundArgs, args); 70 71 // Step 5. 72 return Call(cx, target, boundThis, args2, args.rval()); 73 } 74 75 // ES2023 10.4.1.2 [[Construct]] 76 // https://tc39.es/ecma262/#sec-bound-function-exotic-objects-construct-argumentslist-newtarget 77 // static 78 bool BoundFunctionObject::construct(JSContext* cx, unsigned argc, Value* vp) { 79 CallArgs args = CallArgsFromVp(argc, vp); 80 Rooted<BoundFunctionObject*> bound(cx, 81 &args.callee().as<BoundFunctionObject>()); 82 83 MOZ_ASSERT(bound->isConstructor(), 84 "shouldn't have called this hook if not a constructor"); 85 86 // Step 1. 87 Rooted<Value> target(cx, bound->getTargetVal()); 88 89 // Step 2. 90 MOZ_ASSERT(IsConstructor(target)); 91 92 // Steps 3-4. 93 size_t numBoundArgs = bound->numBoundArgs(); 94 ConstructArgs args2(cx); 95 if (!args2.init(cx, uint64_t(numBoundArgs) + args.length())) { 96 return false; 97 } 98 FillArguments(args2, bound, numBoundArgs, args); 99 100 // Step 5. 101 Rooted<Value> newTarget(cx, args.newTarget()); 102 if (newTarget == ObjectValue(*bound)) { 103 newTarget = target; 104 } 105 106 // Step 6. 107 Rooted<JSObject*> res(cx); 108 if (!Construct(cx, target, args2, newTarget, &res)) { 109 return false; 110 } 111 args.rval().setObject(*res); 112 return true; 113 } 114 115 // static 116 JSString* BoundFunctionObject::funToString(JSContext* cx, Handle<JSObject*> obj, 117 bool isToSource) { 118 // Implementation of the funToString hook used by Function.prototype.toString. 119 120 // For the non-standard toSource extension, we include "bound" to indicate 121 // it's a bound function. 122 if (isToSource) { 123 static constexpr std::string_view nativeCodeBound = 124 "function bound() {\n [native code]\n}"; 125 return NewStringCopy<CanGC>(cx, nativeCodeBound); 126 } 127 128 static constexpr std::string_view nativeCode = 129 "function() {\n [native code]\n}"; 130 return NewStringCopy<CanGC>(cx, nativeCode); 131 } 132 133 // static 134 SharedShape* BoundFunctionObject::assignInitialShape( 135 JSContext* cx, Handle<BoundFunctionObject*> obj) { 136 MOZ_ASSERT(obj->empty()); 137 138 constexpr PropertyFlags propFlags = {PropertyFlag::Configurable}; 139 if (!NativeObject::addPropertyInReservedSlot(cx, obj, cx->names().length, 140 LengthSlot, propFlags)) { 141 return nullptr; 142 } 143 if (!NativeObject::addPropertyInReservedSlot(cx, obj, cx->names().name, 144 NameSlot, propFlags)) { 145 return nullptr; 146 } 147 148 SharedShape* shape = obj->sharedShape(); 149 if (shape->proto() == TaggedProto(&cx->global()->getFunctionPrototype())) { 150 cx->global()->setBoundFunctionShapeWithDefaultProto(shape); 151 } 152 return shape; 153 } 154 155 static MOZ_ALWAYS_INLINE bool ComputeLengthValue( 156 JSContext* cx, Handle<BoundFunctionObject*> bound, Handle<JSObject*> target, 157 size_t numBoundArgs, double* length) { 158 *length = 0.0; 159 160 // Try to avoid invoking the JSFunction resolve hook. 161 if (target->is<JSFunction>() && 162 !target->as<JSFunction>().hasResolvedLength()) { 163 uint16_t targetLength; 164 if (!JSFunction::getUnresolvedLength(cx, target.as<JSFunction>(), 165 &targetLength)) { 166 return false; 167 } 168 169 if (size_t(targetLength) > numBoundArgs) { 170 *length = size_t(targetLength) - numBoundArgs; 171 } 172 return true; 173 } 174 175 // Use a fast path for getting the .length value if the target is a bound 176 // function with its initial shape. 177 Value targetLength; 178 if (target->is<BoundFunctionObject>() && target->shape() == bound->shape()) { 179 BoundFunctionObject* targetFn = &target->as<BoundFunctionObject>(); 180 targetLength = targetFn->getLengthForInitialShape(); 181 } else { 182 bool hasLength; 183 Rooted<PropertyKey> key(cx, NameToId(cx->names().length)); 184 if (!HasOwnProperty(cx, target, key, &hasLength)) { 185 return false; 186 } 187 188 if (!hasLength) { 189 return true; 190 } 191 192 Rooted<Value> targetLengthRoot(cx); 193 if (!GetProperty(cx, target, target, key, &targetLengthRoot)) { 194 return false; 195 } 196 targetLength = targetLengthRoot; 197 } 198 199 if (targetLength.isNumber()) { 200 *length = std::max( 201 0.0, JS::ToInteger(targetLength.toNumber()) - double(numBoundArgs)); 202 } 203 return true; 204 } 205 206 static MOZ_ALWAYS_INLINE JSAtom* AppendBoundFunctionPrefix(JSContext* cx, 207 JSString* str) { 208 auto& cache = cx->zone()->boundPrefixCache(); 209 210 JSAtom* strAtom = str->isAtom() ? &str->asAtom() : nullptr; 211 if (strAtom) { 212 if (auto p = cache.lookup(strAtom)) { 213 return p->value(); 214 } 215 } 216 217 StringBuilder sb(cx); 218 if (!sb.append("bound ") || !sb.append(str)) { 219 return nullptr; 220 } 221 JSAtom* atom = sb.finishAtom(); 222 if (!atom) { 223 return nullptr; 224 } 225 226 if (strAtom) { 227 (void)cache.putNew(strAtom, atom); 228 } 229 return atom; 230 } 231 232 static MOZ_ALWAYS_INLINE JSAtom* ComputeNameValue( 233 JSContext* cx, Handle<BoundFunctionObject*> bound, 234 Handle<JSObject*> target) { 235 // Try to avoid invoking the JSFunction resolve hook. 236 JSString* name = nullptr; 237 if (target->is<JSFunction>() && !target->as<JSFunction>().hasResolvedName()) { 238 JSFunction* targetFn = &target->as<JSFunction>(); 239 name = targetFn->getUnresolvedName(cx); 240 if (!name) { 241 return nullptr; 242 } 243 } else { 244 // Use a fast path for getting the .name value if the target is a bound 245 // function with its initial shape. 246 Value targetName; 247 if (target->is<BoundFunctionObject>() && 248 target->shape() == bound->shape()) { 249 BoundFunctionObject* targetFn = &target->as<BoundFunctionObject>(); 250 targetName = targetFn->getNameForInitialShape(); 251 } else { 252 Rooted<Value> targetNameRoot(cx); 253 if (!GetProperty(cx, target, target, cx->names().name, &targetNameRoot)) { 254 return nullptr; 255 } 256 targetName = targetNameRoot; 257 } 258 if (!targetName.isString()) { 259 return cx->names().boundWithSpace_; 260 } 261 name = targetName.toString(); 262 } 263 264 return AppendBoundFunctionPrefix(cx, name); 265 } 266 267 // ES2023 20.2.3.2 Function.prototype.bind 268 // https://tc39.es/ecma262/#sec-function.prototype.bind 269 // static 270 bool BoundFunctionObject::functionBind(JSContext* cx, unsigned argc, 271 Value* vp) { 272 CallArgs args = CallArgsFromVp(argc, vp); 273 274 // Steps 1-2. 275 if (!IsCallable(args.thisv())) { 276 ReportIncompatibleMethod(cx, args, &FunctionClass); 277 return false; 278 } 279 280 if (MOZ_UNLIKELY(args.length() > ARGS_LENGTH_MAX)) { 281 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, 282 JSMSG_TOO_MANY_ARGUMENTS); 283 return false; 284 } 285 286 Rooted<JSObject*> target(cx, &args.thisv().toObject()); 287 288 BoundFunctionObject* bound = 289 functionBindImpl(cx, target, args.array(), args.length(), nullptr); 290 if (!bound) { 291 return false; 292 } 293 294 // Step 11. 295 args.rval().setObject(*bound); 296 return true; 297 } 298 299 // ES2023 20.2.3.2 Function.prototype.bind 300 // https://tc39.es/ecma262/#sec-function.prototype.bind 301 // 302 // ES2023 10.4.1.3 BoundFunctionCreate 303 // https://tc39.es/ecma262/#sec-boundfunctioncreate 304 // 305 // BoundFunctionCreate has been inlined in Function.prototype.bind for 306 // performance reasons. 307 // 308 // static 309 BoundFunctionObject* BoundFunctionObject::functionBindImpl( 310 JSContext* cx, Handle<JSObject*> target, Value* args, uint32_t argc, 311 Handle<BoundFunctionObject*> maybeBound) { 312 MOZ_ASSERT(target->isCallable()); 313 314 // Make sure the arguments on the stack are rooted when we're called directly 315 // from JIT code. 316 RootedExternalValueArray argsRoot(cx, argc, args); 317 318 size_t numBoundArgs = argc > 0 ? argc - 1 : 0; 319 MOZ_ASSERT(numBoundArgs <= ARGS_LENGTH_MAX, "ensured by callers"); 320 321 // If this assertion fails, make sure we use the correct AllocKind and that we 322 // use all of its slots (consider increasing MaxInlineBoundArgs). 323 static_assert(gc::GetGCKindSlots(allocKind) == SlotCount); 324 static_assert(gc::GetFinalizeKind(allocKind) == gc::FinalizeKind::None); 325 326 // ES2023 10.4.1.3 BoundFunctionCreate 327 // Steps 1-5. 328 Rooted<BoundFunctionObject*> bound(cx); 329 if (maybeBound) { 330 // We allocated a bound function in JIT code. In the uncommon case of the 331 // target not having Function.prototype as proto, we have to set the right 332 // proto here. 333 bound = maybeBound; 334 if (MOZ_UNLIKELY(bound->staticPrototype() != target->staticPrototype())) { 335 Rooted<JSObject*> proto(cx, target->staticPrototype()); 336 if (!SetPrototype(cx, bound, proto)) { 337 return nullptr; 338 } 339 } 340 } else { 341 // Step 1. 342 Rooted<JSObject*> proto(cx); 343 if (!GetPrototype(cx, target, &proto)) { 344 return nullptr; 345 } 346 347 // Steps 2-5. 348 if (proto == &cx->global()->getFunctionPrototype() && 349 cx->global()->maybeBoundFunctionShapeWithDefaultProto()) { 350 Rooted<SharedShape*> shape( 351 cx, cx->global()->maybeBoundFunctionShapeWithDefaultProto()); 352 bound = NativeObject::create<BoundFunctionObject>( 353 cx, allocKind, gc::Heap::Default, shape); 354 if (!bound) { 355 return nullptr; 356 } 357 } else { 358 bound = NewObjectWithGivenProto<BoundFunctionObject>(cx, proto); 359 if (!bound) { 360 return nullptr; 361 } 362 if (!SharedShape::ensureInitialCustomShape<BoundFunctionObject>(cx, 363 bound)) { 364 return nullptr; 365 } 366 } 367 } 368 369 MOZ_ASSERT(bound->lookupPure(cx->names().length)->slot() == LengthSlot); 370 MOZ_ASSERT(bound->lookupPure(cx->names().name)->slot() == NameSlot); 371 372 // Steps 6 and 9. 373 bound->initFlags(numBoundArgs, target->isConstructor()); 374 375 // Step 7. 376 bound->initReservedSlot(TargetSlot, ObjectValue(*target)); 377 378 // Step 8. 379 if (argc > 0) { 380 bound->initReservedSlot(BoundThisSlot, args[0]); 381 } 382 383 if (numBoundArgs <= MaxInlineBoundArgs) { 384 for (size_t i = 0; i < numBoundArgs; i++) { 385 bound->initReservedSlot(BoundArg0Slot + i, args[i + 1]); 386 } 387 } else { 388 ArrayObject* arr = NewDenseCopiedArray(cx, numBoundArgs, args + 1); 389 if (!arr) { 390 return nullptr; 391 } 392 bound->initReservedSlot(BoundArg0Slot, ObjectValue(*arr)); 393 } 394 395 // ES2023 20.2.3.2 Function.prototype.bind 396 // Step 4. 397 double length = 0.0; 398 399 // Steps 5-6. 400 if (!ComputeLengthValue(cx, bound, target, numBoundArgs, &length)) { 401 return nullptr; 402 } 403 404 // Step 7. 405 bound->initLength(length); 406 407 // Steps 8-9. 408 JSAtom* name = ComputeNameValue(cx, bound, target); 409 if (!name) { 410 return nullptr; 411 } 412 413 // Step 10. 414 bound->initName(name); 415 416 // Step 11. 417 return bound; 418 } 419 420 // static 421 BoundFunctionObject* BoundFunctionObject::createWithTemplate( 422 JSContext* cx, Handle<BoundFunctionObject*> templateObj) { 423 Rooted<SharedShape*> shape(cx, templateObj->sharedShape()); 424 auto* bound = NativeObject::create<BoundFunctionObject>( 425 cx, allocKind, gc::Heap::Default, shape); 426 if (!bound) { 427 return nullptr; 428 } 429 bound->initFlags(templateObj->numBoundArgs(), templateObj->isConstructor()); 430 bound->initLength(templateObj->getLengthForInitialShape().toInt32()); 431 bound->initName(&templateObj->getNameForInitialShape().toString()->asAtom()); 432 return bound; 433 } 434 435 // static 436 BoundFunctionObject* BoundFunctionObject::functionBindSpecializedBaseline( 437 JSContext* cx, Handle<JSObject*> target, Value* args, uint32_t argc, 438 Handle<BoundFunctionObject*> templateObj) { 439 // Root the Values on the stack. 440 RootedExternalValueArray argsRoot(cx, argc, args); 441 442 MOZ_ASSERT(target->is<JSFunction>() || target->is<BoundFunctionObject>()); 443 MOZ_ASSERT(target->isCallable()); 444 MOZ_ASSERT(target->isConstructor() == templateObj->isConstructor()); 445 MOZ_ASSERT(target->staticPrototype() == templateObj->staticPrototype()); 446 447 size_t numBoundArgs = argc > 0 ? argc - 1 : 0; 448 MOZ_ASSERT(numBoundArgs <= MaxInlineBoundArgs); 449 450 BoundFunctionObject* bound = createWithTemplate(cx, templateObj); 451 if (!bound) { 452 return nullptr; 453 } 454 455 MOZ_ASSERT(bound->lookupPure(cx->names().length)->slot() == LengthSlot); 456 MOZ_ASSERT(bound->lookupPure(cx->names().name)->slot() == NameSlot); 457 458 bound->initReservedSlot(TargetSlot, ObjectValue(*target)); 459 if (argc > 0) { 460 bound->initReservedSlot(BoundThisSlot, args[0]); 461 } 462 for (size_t i = 0; i < numBoundArgs; i++) { 463 bound->initReservedSlot(BoundArg0Slot + i, args[i + 1]); 464 } 465 return bound; 466 } 467 468 // static 469 BoundFunctionObject* BoundFunctionObject::createTemplateObject(JSContext* cx) { 470 Rooted<JSObject*> proto(cx, &cx->global()->getFunctionPrototype()); 471 Rooted<BoundFunctionObject*> bound( 472 cx, NewTenuredObjectWithGivenProto<BoundFunctionObject>(cx, proto)); 473 if (!bound) { 474 return nullptr; 475 } 476 if (!SharedShape::ensureInitialCustomShape<BoundFunctionObject>(cx, bound)) { 477 return nullptr; 478 } 479 return bound; 480 } 481 482 bool BoundFunctionObject::initTemplateSlotsForSpecializedBind( 483 JSContext* cx, uint32_t numBoundArgs, bool targetIsConstructor, 484 uint32_t targetLength, JSAtom* targetName) { 485 size_t len = 0; 486 if (targetLength > numBoundArgs) { 487 len = targetLength - numBoundArgs; 488 } 489 490 JSAtom* name = AppendBoundFunctionPrefix(cx, targetName); 491 if (!name) { 492 return false; 493 } 494 495 initFlags(numBoundArgs, targetIsConstructor); 496 initLength(len); 497 initName(name); 498 return true; 499 } 500 501 static const JSClassOps classOps = { 502 nullptr, // addProperty 503 nullptr, // delProperty 504 nullptr, // enumerate 505 nullptr, // newEnumerate 506 nullptr, // resolve 507 nullptr, // mayResolve 508 nullptr, // finalize 509 BoundFunctionObject::call, // call 510 BoundFunctionObject::construct, // construct 511 nullptr, // trace 512 }; 513 514 static const ObjectOps objOps = { 515 nullptr, // lookupProperty 516 nullptr, // qdefineProperty 517 nullptr, // hasProperty 518 nullptr, // getProperty 519 nullptr, // setProperty 520 nullptr, // getOwnPropertyDescriptor 521 nullptr, // deleteProperty 522 nullptr, // getElements 523 BoundFunctionObject::funToString, // funToString 524 }; 525 526 const JSClass BoundFunctionObject::class_ = { 527 "BoundFunctionObject", 528 // Note: bound functions don't have their own constructor or prototype (they 529 // use the prototype of the target object), but we give them a JSProtoKey 530 // because that's what Xray wrappers use to identify builtin objects. 531 JSCLASS_HAS_CACHED_PROTO(JSProto_BoundFunction) | 532 JSCLASS_HAS_RESERVED_SLOTS(BoundFunctionObject::SlotCount), 533 &classOps, 534 JS_NULL_CLASS_SPEC, 535 JS_NULL_CLASS_EXT, 536 &objOps, 537 };