Changeset 249058 in webkit


Ignore:
Timestamp:
Aug 23, 2019 11:51:20 AM (5 years ago)
Author:
Ross Kirsling
Message:

JSC should have public API for unhandled promise rejections
https://bugs.webkit.org/show_bug.cgi?id=197172

Reviewed by Keith Miller.

Source/JavaScriptCore:

This patch makes it possible to register a unhandled promise rejection callback via the JSC API.
Since there is no event loop in such an environment, this callback fires off of the microtask queue.
The callback receives the promise and rejection reason as arguments and its return value is ignored.

  • API/JSContextRef.cpp:

(JSGlobalContextSetUnhandledRejectionCallback): Added.

  • API/JSContextRefPrivate.h:

Add new C++ API call.

  • API/tests/testapi.cpp:

(TestAPI::promiseResolveTrue): Clean up test output.
(TestAPI::promiseRejectTrue): Clean up test output.
(TestAPI::promiseUnhandledRejection): Added.
(TestAPI::promiseUnhandledRejectionFromUnhandledRejectionCallback): Added.
(TestAPI::promiseEarlyHandledRejections): Added.
(testCAPIViaCpp):
Add new C++ API test.

  • jsc.cpp:

(GlobalObject::finishCreation):
(functionSetUnhandledRejectionCallback): Added.
Add corresponding global to JSC shell.

  • runtime/JSGlobalObject.h:

(JSC::JSGlobalObject::setUnhandledRejectionCallback): Added.
(JSC::JSGlobalObject::unhandledRejectionCallback const): Added.
Keep a strong reference to the callback.

  • runtime/JSGlobalObjectFunctions.cpp:

(JSC::globalFuncHostPromiseRejectionTracker):
Add default behavior.

  • runtime/VM.cpp:

(JSC::VM::callPromiseRejectionCallback): Added.
(JSC::VM::didExhaustMicrotaskQueue): Added.
(JSC::VM::promiseRejected): Added.
(JSC::VM::drainMicrotasks):
When microtask queue is exhausted, deal with any pending unhandled rejections
(in a manner based on RejectedPromiseTracker's reportUnhandledRejections),
then make sure this didn't cause any new microtasks to be added to the queue.

  • runtime/VM.h:

Store unhandled rejections.
(This collection will always be empty in the presence of WebCore.)

Source/WebCore:

  • bindings/js/JSDOMGlobalObject.cpp:

(WebCore::JSDOMGlobalObject::promiseRejectionTracker):
Move JSInternalPromise early-out to JSC side.

Location:
trunk/Source
Files:
11 edited

Legend:

Unmodified
Added
Removed
  • trunk/Source/JavaScriptCore/API/JSContextRef.cpp

    r248533 r249058  
    2929
    3030#include "APICast.h"
     31#include "APIUtils.h"
    3132#include "CallFrame.h"
    3233#include "InitializeThreading.h"
     
    3839#include "SourceProvider.h"
    3940#include "StackVisitor.h"
     41#include "StrongInlines.h"
    4042#include "Watchdog.h"
    4143#include <wtf/text/StringBuilder.h>
     
    253255}
    254256
     257void JSGlobalContextSetUnhandledRejectionCallback(JSGlobalContextRef ctx, JSObjectRef function, JSValueRef* exception)
     258{
     259    if (!ctx) {
     260        ASSERT_NOT_REACHED();
     261        return;
     262    }
     263
     264    ExecState* exec = toJS(ctx);
     265    VM& vm = exec->vm();
     266    JSLockHolder locker(vm);
     267
     268    JSObject* object = toJS(function);
     269    if (!object->isFunction(vm)) {
     270        *exception = toRef(createTypeError(exec));
     271        return;
     272    }
     273
     274    vm.vmEntryGlobalObject(exec)->setUnhandledRejectionCallback(vm, object);
     275}
    255276
    256277class BacktraceFunctor {
  • trunk/Source/JavaScriptCore/API/JSContextRefPrivate.h

    r243376 r249058  
    129129JS_EXPORT void JSGlobalContextSetIncludesNativeCallStackWhenReportingExceptions(JSGlobalContextRef ctx, bool includesNativeCallStack) JSC_API_AVAILABLE(macos(10.10), ios(8.0));
    130130
     131/*!
     132@function
     133@abstract Sets the unhandled promise rejection callback for a context.
     134@discussion Similar to window.addEventListener('unhandledrejection'), but for contexts not associated with a web view.
     135@param ctx The JSGlobalContext to set the callback on.
     136@param function The callback function to set, which receives the promise and rejection reason as arguments.
     137@param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception.
     138*/
     139JS_EXPORT void JSGlobalContextSetUnhandledRejectionCallback(JSGlobalContextRef ctx, JSObjectRef function, JSValueRef* exception) JSC_API_AVAILABLE(macos(JSC_MAC_TBA), ios(JSC_IOS_TBA));
     140
    131141#ifdef __cplusplus
    132142}
  • trunk/Source/JavaScriptCore/API/tests/testapi.cpp

    r244996 r249058  
    3030#include "JSObject.h"
    3131
     32#include <JavaScriptCore/JSContextRefPrivate.h>
    3233#include <JavaScriptCore/JSObjectRefPrivate.h>
    3334#include <JavaScriptCore/JavaScript.h>
     
    138139    void promiseResolveTrue();
    139140    void promiseRejectTrue();
     141    void promiseUnhandledRejection();
     142    void promiseUnhandledRejectionFromUnhandledRejectionCallback();
     143    void promiseEarlyHandledRejections();
    140144
    141145    int failed() const { return m_failed; }
     
    455459    auto trueValue = JSValueMakeBoolean(context, true);
    456460    JSObjectCallAsFunction(context, resolve, resolve, 1, &trueValue, &exception);
    457     check(!exception, "No exception should be thrown resolve promise");
     461    check(!exception, "No exception should be thrown resolving promise");
    458462    check(passedTrueCalled, "then response function should have been called.");
    459463}
     
    480484    APIString catchString("catch");
    481485    JSValueRef catchFunction = JSObjectGetProperty(context, promise, catchString, &exception);
    482     check(!exception && catchFunction && JSValueIsObject(context, catchFunction), "Promise should have a then object property");
     486    check(!exception && catchFunction && JSValueIsObject(context, catchFunction), "Promise should have a catch object property");
    483487
    484488    JSValueRef passedTrueFunction = JSObjectMakeFunctionWithCallback(context, trueString, passedTrue);
     
    488492    auto trueValue = JSValueMakeBoolean(context, true);
    489493    JSObjectCallAsFunction(context, reject, reject, 1, &trueValue, &exception);
    490     check(!exception, "No exception should be thrown resolve promise");
    491     check(passedTrueCalled, "then response function should have been called.");
     494    check(!exception, "No exception should be thrown rejecting promise");
     495    check(passedTrueCalled, "catch response function should have been called.");
     496}
     497
     498void TestAPI::promiseUnhandledRejection()
     499{
     500    JSObjectRef reject;
     501    JSValueRef exception = nullptr;
     502    static auto promise = JSObjectMakeDeferredPromise(context, nullptr, &reject, &exception);
     503    check(!exception, "creating a (reject-only) deferred promise should not throw");
     504    static auto reason = JSValueMakeString(context, APIString("reason"));
     505
     506    static TestAPI* tester = this;
     507    static bool callbackCalled = false;
     508    auto callback = [](JSContextRef ctx, JSObjectRef, JSObjectRef, size_t argumentCount, const JSValueRef arguments[], JSValueRef*) -> JSValueRef {
     509        tester->check(argumentCount && JSValueIsStrictEqual(ctx, arguments[0], promise), "callback should receive rejected promise as first argument");
     510        tester->check(argumentCount > 1 && JSValueIsStrictEqual(ctx, arguments[1], reason), "callback should receive rejection reason as second argument");
     511        tester->check(argumentCount == 2, "callback should not receive a third argument");
     512        callbackCalled = true;
     513        return JSValueMakeUndefined(ctx);
     514    };
     515    auto callbackFunction = JSObjectMakeFunctionWithCallback(context, APIString("callback"), callback);
     516
     517    JSGlobalContextSetUnhandledRejectionCallback(context, callbackFunction, &exception);
     518    check(!exception, "setting unhandled rejection callback should not throw");
     519
     520    JSObjectCallAsFunction(context, reject, reject, 1, &reason, &exception);
     521    check(!exception && callbackCalled, "unhandled rejection callback should be called upon unhandled rejection");
     522}
     523
     524void TestAPI::promiseUnhandledRejectionFromUnhandledRejectionCallback()
     525{
     526    static JSObjectRef reject;
     527    static JSValueRef exception = nullptr;
     528    JSObjectMakeDeferredPromise(context, nullptr, &reject, &exception);
     529    check(!exception, "creating a (reject-only) deferred promise should not throw");
     530
     531    static auto callbackCallCount = 0;
     532    auto callback = [](JSContextRef ctx, JSObjectRef, JSObjectRef, size_t, const JSValueRef[], JSValueRef*) -> JSValueRef {
     533        if (!callbackCallCount)
     534            JSObjectCallAsFunction(ctx, reject, reject, 0, nullptr, &exception);
     535        callbackCallCount++;
     536        return JSValueMakeUndefined(ctx);
     537    };
     538    auto callbackFunction = JSObjectMakeFunctionWithCallback(context, APIString("callback"), callback);
     539
     540    JSGlobalContextSetUnhandledRejectionCallback(context, callbackFunction, &exception);
     541    check(!exception, "setting unhandled rejection callback should not throw");
     542
     543    callFunction("(function () { Promise.reject(); })");
     544    check(!exception && callbackCallCount == 2, "unhandled rejection from unhandled rejection callback should also trigger the callback");
     545}
     546
     547void TestAPI::promiseEarlyHandledRejections()
     548{
     549    JSValueRef exception = nullptr;
     550   
     551    static bool callbackCalled = false;
     552    auto callback = [](JSContextRef ctx, JSObjectRef, JSObjectRef, size_t, const JSValueRef[], JSValueRef*) -> JSValueRef {
     553        callbackCalled = true;
     554        return JSValueMakeUndefined(ctx);
     555    };
     556    auto callbackFunction = JSObjectMakeFunctionWithCallback(context, APIString("callback"), callback);
     557
     558    JSGlobalContextSetUnhandledRejectionCallback(context, callbackFunction, &exception);
     559    check(!exception, "setting unhandled rejection callback should not throw");
     560
     561    callFunction("(function () { const p = Promise.reject(); p.catch(() => {}); })");
     562    check(!callbackCalled, "unhandled rejection callback should not be called for synchronous early-handled rejection");
     563
     564    callFunction("(function () { const p = Promise.reject(); Promise.resolve().then(() => { p.catch(() => {}); }); })");
     565    check(!callbackCalled, "unhandled rejection callback should not be called for asynchronous early-handled rejection");
    492566}
    493567
     
    522596    RUN(promiseResolveTrue());
    523597    RUN(promiseRejectTrue());
     598    RUN(promiseUnhandledRejection());
     599    RUN(promiseUnhandledRejectionFromUnhandledRejectionCallback());
     600    RUN(promiseEarlyHandledRejections());
    524601
    525602    if (tasks.isEmpty()) {
  • trunk/Source/JavaScriptCore/ChangeLog

    r249052 r249058  
     12019-08-23  Ross Kirsling  <ross.kirsling@sony.com>
     2
     3        JSC should have public API for unhandled promise rejections
     4        https://bugs.webkit.org/show_bug.cgi?id=197172
     5
     6        Reviewed by Keith Miller.
     7
     8        This patch makes it possible to register a unhandled promise rejection callback via the JSC API.
     9        Since there is no event loop in such an environment, this callback fires off of the microtask queue.
     10        The callback receives the promise and rejection reason as arguments and its return value is ignored.
     11
     12        * API/JSContextRef.cpp:
     13        (JSGlobalContextSetUnhandledRejectionCallback): Added.
     14        * API/JSContextRefPrivate.h:
     15        Add new C++ API call.
     16
     17        * API/tests/testapi.cpp:
     18        (TestAPI::promiseResolveTrue): Clean up test output.
     19        (TestAPI::promiseRejectTrue): Clean up test output.
     20        (TestAPI::promiseUnhandledRejection): Added.
     21        (TestAPI::promiseUnhandledRejectionFromUnhandledRejectionCallback): Added.
     22        (TestAPI::promiseEarlyHandledRejections): Added.
     23        (testCAPIViaCpp):
     24        Add new C++ API test.
     25
     26        * jsc.cpp:
     27        (GlobalObject::finishCreation):
     28        (functionSetUnhandledRejectionCallback): Added.
     29        Add corresponding global to JSC shell.
     30
     31        * runtime/JSGlobalObject.h:
     32        (JSC::JSGlobalObject::setUnhandledRejectionCallback): Added.
     33        (JSC::JSGlobalObject::unhandledRejectionCallback const): Added.
     34        Keep a strong reference to the callback.
     35
     36        * runtime/JSGlobalObjectFunctions.cpp:
     37        (JSC::globalFuncHostPromiseRejectionTracker):
     38        Add default behavior.
     39
     40        * runtime/VM.cpp:
     41        (JSC::VM::callPromiseRejectionCallback): Added.
     42        (JSC::VM::didExhaustMicrotaskQueue): Added.
     43        (JSC::VM::promiseRejected): Added.
     44        (JSC::VM::drainMicrotasks):
     45        When microtask queue is exhausted, deal with any pending unhandled rejections
     46        (in a manner based on RejectedPromiseTracker's reportUnhandledRejections),
     47        then make sure this didn't cause any new microtasks to be added to the queue.
     48
     49        * runtime/VM.h:
     50        Store unhandled rejections.
     51        (This collection will always be empty in the presence of WebCore.)
     52
    1532019-08-22  Mark Lam  <mark.lam@apple.com>
    254
  • trunk/Source/JavaScriptCore/jsc.cpp

    r248929 r249058  
    385385static EncodedJSValue JSC_HOST_CALL functionTotalCompileTime(ExecState*);
    386386
     387static EncodedJSValue JSC_HOST_CALL functionSetUnhandledRejectionCallback(ExecState*);
     388
    387389struct Script {
    388390    enum class StrictMode {
     
    639641        addFunction(vm, "mallocInALoop", functionMallocInALoop, 0);
    640642        addFunction(vm, "totalCompileTime", functionTotalCompileTime, 0);
     643
     644        addFunction(vm, "setUnhandledRejectionCallback", functionSetUnhandledRejectionCallback, 1);
    641645    }
    642646   
     
    23852389#endif // ENABLE(WEBASSEMBLY)
    23862390
     2391EncodedJSValue JSC_HOST_CALL functionSetUnhandledRejectionCallback(ExecState* exec)
     2392{
     2393    VM& vm = exec->vm();
     2394    JSObject* object = exec->argument(0).getObject();
     2395    auto scope = DECLARE_THROW_SCOPE(vm);
     2396
     2397    if (!object || !object->isFunction(vm))
     2398        return throwVMTypeError(exec, scope);
     2399
     2400    exec->lexicalGlobalObject()->setUnhandledRejectionCallback(vm, object);
     2401    return JSValue::encode(jsUndefined());
     2402}
     2403
    23872404// Use SEH for Release builds only to get rid of the crash report dialog
    23882405// (luckily the same tests fail in Release and Debug builds so far). Need to
  • trunk/Source/JavaScriptCore/runtime/JSGlobalObject.h

    r248906 r249058  
    426426    String m_name;
    427427
     428    Strong<JSObject> m_unhandledRejectionCallback;
     429
    428430    Debugger* m_debugger;
    429431
     
    808810    const String& name() const { return m_name; }
    809811
     812    void setUnhandledRejectionCallback(VM& vm, JSObject* function) { m_unhandledRejectionCallback.set(vm, function); }
     813    JSObject* unhandledRejectionCallback() const { return m_unhandledRejectionCallback.get(); }
     814
    810815    JSObject* arrayBufferConstructor() const { return m_arrayBufferStructure.constructor(this); }
    811816
  • trunk/Source/JavaScriptCore/runtime/JSGlobalObjectFunctions.cpp

    r245645 r249058  
    767767    auto scope = DECLARE_THROW_SCOPE(vm);
    768768
    769     if (!globalObject->globalObjectMethodTable()->promiseRejectionTracker)
     769    JSPromise* promise = jsCast<JSPromise*>(exec->argument(0));
     770
     771    // InternalPromises should not be exposed to user scripts.
     772    if (jsDynamicCast<JSInternalPromise*>(vm, promise))
    770773        return JSValue::encode(jsUndefined());
    771774
    772     JSPromise* promise = jsCast<JSPromise*>(exec->argument(0));
    773775    JSValue operationValue = exec->argument(1);
    774776
     
    778780    scope.assertNoException();
    779781
    780     globalObject->globalObjectMethodTable()->promiseRejectionTracker(globalObject, exec, promise, operation);
     782    if (globalObject->globalObjectMethodTable()->promiseRejectionTracker)
     783        globalObject->globalObjectMethodTable()->promiseRejectionTracker(globalObject, exec, promise, operation);
     784    else {
     785        switch (operation) {
     786        case JSPromiseRejectionOperation::Reject:
     787            vm.promiseRejected(promise);
     788            break;
     789        case JSPromiseRejectionOperation::Handle:
     790            // do nothing
     791            break;
     792        }
     793    }
    781794    RETURN_IF_EXCEPTION(scope, { });
    782795
  • trunk/Source/JavaScriptCore/runtime/VM.cpp

    r248846 r249058  
    10911091}
    10921092
     1093void VM::callPromiseRejectionCallback(Strong<JSPromise>& promise)
     1094{
     1095    JSObject* callback = promise->globalObject()->unhandledRejectionCallback();
     1096    if (!callback)
     1097        return;
     1098
     1099    auto scope = DECLARE_CATCH_SCOPE(*this);
     1100
     1101    CallData callData;
     1102    CallType callType = getCallData(*this, callback, callData);
     1103    ASSERT(callType != CallType::None);
     1104
     1105    MarkedArgumentBuffer args;
     1106    args.append(promise.get());
     1107    args.append(promise->result(*this));
     1108    call(promise->globalObject()->globalExec(), callback, callType, callData, jsNull(), args);
     1109    scope.clearException();
     1110}
     1111
     1112void VM::didExhaustMicrotaskQueue()
     1113{
     1114    auto unhandledRejections = WTFMove(m_aboutToBeNotifiedRejectedPromises);
     1115    for (auto& promise : unhandledRejections) {
     1116        if (promise->isHandled(*this))
     1117            continue;
     1118
     1119        callPromiseRejectionCallback(promise);
     1120    }
     1121}
     1122
     1123void VM::promiseRejected(JSPromise* promise)
     1124{
     1125    m_aboutToBeNotifiedRejectedPromises.constructAndAppend(*this, promise);
     1126}
     1127
    10931128void VM::drainMicrotasks()
    10941129{
    1095     while (!m_microtaskQueue.isEmpty()) {
    1096         m_microtaskQueue.takeFirst()->run();
    1097         if (m_onEachMicrotaskTick)
    1098             m_onEachMicrotaskTick(*this);
    1099     }
     1130    do {
     1131        while (!m_microtaskQueue.isEmpty()) {
     1132            m_microtaskQueue.takeFirst()->run();
     1133            if (m_onEachMicrotaskTick)
     1134                m_onEachMicrotaskTick(*this);
     1135        }
     1136        didExhaustMicrotaskQueue();
     1137    } while (!m_microtaskQueue.isEmpty());
    11001138    finalizeSynchronousJSExecution();
    11011139}
  • trunk/Source/JavaScriptCore/runtime/VM.h

    r248546 r249058  
    124124class JSGlobalObject;
    125125class JSObject;
     126class JSPromise;
    126127class JSPropertyNameEnumerator;
    127128class JSRunLoopTimer;
     
    923924    void notifyNeedWatchdogCheck() { m_traps.fireTrap(VMTraps::NeedWatchdogCheck); }
    924925
     926    void promiseRejected(JSPromise*);
     927
    925928#if ENABLE(EXCEPTION_SCOPE_VERIFICATION)
    926929    StackTrace* nativeStackTraceOfLastThrow() const { return m_nativeStackTraceOfLastThrow.get(); }
     
    10081011    static void primitiveGigacageDisabledCallback(void*);
    10091012    void primitiveGigacageDisabled();
     1013
     1014    void callPromiseRejectionCallback(Strong<JSPromise>&);
     1015    void didExhaustMicrotaskQueue();
    10101016
    10111017#if ENABLE(GC_VALIDATION)
     
    10641070    std::unique_ptr<BytecodeIntrinsicRegistry> m_bytecodeIntrinsicRegistry;
    10651071
     1072    // FIXME: We should remove handled promises from this list at GC flip. <https://webkit.org/b/201005>
     1073    Vector<Strong<JSPromise>> m_aboutToBeNotifiedRejectedPromises;
     1074
    10661075    WTF::Function<void(VM&)> m_onEachMicrotaskTick;
    10671076    uintptr_t m_currentWeakRefVersion { 0 };
  • trunk/Source/WebCore/ChangeLog

    r249056 r249058  
     12019-08-23  Ross Kirsling  <ross.kirsling@sony.com>
     2
     3        JSC should have public API for unhandled promise rejections
     4        https://bugs.webkit.org/show_bug.cgi?id=197172
     5
     6        Reviewed by Keith Miller.
     7
     8        * bindings/js/JSDOMGlobalObject.cpp:
     9        (WebCore::JSDOMGlobalObject::promiseRejectionTracker):
     10        Move JSInternalPromise early-out to JSC side.
     11
    1122019-08-23  Kate Cheney  <katherine_cheney@apple.com>
    213
  • trunk/Source/WebCore/bindings/js/JSDOMGlobalObject.cpp

    r244312 r249058  
    217217    // https://html.spec.whatwg.org/multipage/webappapis.html#the-hostpromiserejectiontracker-implementation
    218218
    219     VM& vm = exec->vm();
    220219    auto& globalObject = *JSC::jsCast<JSDOMGlobalObject*>(jsGlobalObject);
    221220    auto* context = globalObject.scriptExecutionContext();
    222221    if (!context)
    223         return;
    224 
    225     // InternalPromises should not be exposed to user scripts.
    226     if (JSC::jsDynamicCast<JSC::JSInternalPromise*>(vm, promise))
    227222        return;
    228223
Note: See TracChangeset for help on using the changeset viewer.