Diff
Modified: trunk/Source/_javascript_Core/API/JSContextRef.cpp (249057 => 249058)
--- trunk/Source/_javascript_Core/API/JSContextRef.cpp 2019-08-23 18:29:59 UTC (rev 249057)
+++ trunk/Source/_javascript_Core/API/JSContextRef.cpp 2019-08-23 18:51:20 UTC (rev 249058)
@@ -28,6 +28,7 @@
#include "JSContextRefInternal.h"
#include "APICast.h"
+#include "APIUtils.h"
#include "CallFrame.h"
#include "InitializeThreading.h"
#include "JSAPIGlobalObject.h"
@@ -37,6 +38,7 @@
#include "JSCInlines.h"
#include "SourceProvider.h"
#include "StackVisitor.h"
+#include "StrongInlines.h"
#include "Watchdog.h"
#include <wtf/text/StringBuilder.h>
#include <wtf/text/StringHash.h>
@@ -252,7 +254,26 @@
vm.vmEntryGlobalObject(exec)->setName(name ? name->string() : String());
}
+void JSGlobalContextSetUnhandledRejectionCallback(JSGlobalContextRef ctx, JSObjectRef function, JSValueRef* exception)
+{
+ if (!ctx) {
+ ASSERT_NOT_REACHED();
+ return;
+ }
+ ExecState* exec = toJS(ctx);
+ VM& vm = exec->vm();
+ JSLockHolder locker(vm);
+
+ JSObject* object = toJS(function);
+ if (!object->isFunction(vm)) {
+ *exception = toRef(createTypeError(exec));
+ return;
+ }
+
+ vm.vmEntryGlobalObject(exec)->setUnhandledRejectionCallback(vm, object);
+}
+
class BacktraceFunctor {
public:
BacktraceFunctor(StringBuilder& builder, unsigned remainingCapacityForFrameCapture)
Modified: trunk/Source/_javascript_Core/API/JSContextRefPrivate.h (249057 => 249058)
--- trunk/Source/_javascript_Core/API/JSContextRefPrivate.h 2019-08-23 18:29:59 UTC (rev 249057)
+++ trunk/Source/_javascript_Core/API/JSContextRefPrivate.h 2019-08-23 18:51:20 UTC (rev 249058)
@@ -128,6 +128,16 @@
*/
JS_EXPORT void JSGlobalContextSetIncludesNativeCallStackWhenReportingExceptions(JSGlobalContextRef ctx, bool includesNativeCallStack) JSC_API_AVAILABLE(macos(10.10), ios(8.0));
+/*!
+@function
+@abstract Sets the unhandled promise rejection callback for a context.
+@discussion Similar to window.addEventListener('unhandledrejection'), but for contexts not associated with a web view.
+@param ctx The JSGlobalContext to set the callback on.
+@param function The callback function to set, which receives the promise and rejection reason as arguments.
+@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.
+*/
+JS_EXPORT void JSGlobalContextSetUnhandledRejectionCallback(JSGlobalContextRef ctx, JSObjectRef function, JSValueRef* exception) JSC_API_AVAILABLE(macos(JSC_MAC_TBA), ios(JSC_IOS_TBA));
+
#ifdef __cplusplus
}
#endif
Modified: trunk/Source/_javascript_Core/API/tests/testapi.cpp (249057 => 249058)
--- trunk/Source/_javascript_Core/API/tests/testapi.cpp 2019-08-23 18:29:59 UTC (rev 249057)
+++ trunk/Source/_javascript_Core/API/tests/testapi.cpp 2019-08-23 18:51:20 UTC (rev 249058)
@@ -29,6 +29,7 @@
#include "JSCJSValueInlines.h"
#include "JSObject.h"
+#include <_javascript_Core/JSContextRefPrivate.h>
#include <_javascript_Core/JSObjectRefPrivate.h>
#include <_javascript_Core/_javascript_.h>
#include <wtf/DataLog.h>
@@ -137,6 +138,9 @@
void symbolsDeletePropertyForKey();
void promiseResolveTrue();
void promiseRejectTrue();
+ void promiseUnhandledRejection();
+ void promiseUnhandledRejectionFromUnhandledRejectionCallback();
+ void promiseEarlyHandledRejections();
int failed() const { return m_failed; }
@@ -454,7 +458,7 @@
auto trueValue = JSValueMakeBoolean(context, true);
JSObjectCallAsFunction(context, resolve, resolve, 1, &trueValue, &exception);
- check(!exception, "No exception should be thrown resolve promise");
+ check(!exception, "No exception should be thrown resolving promise");
check(passedTrueCalled, "then response function should have been called.");
}
@@ -479,7 +483,7 @@
APIString catchString("catch");
JSValueRef catchFunction = JSObjectGetProperty(context, promise, catchString, &exception);
- check(!exception && catchFunction && JSValueIsObject(context, catchFunction), "Promise should have a then object property");
+ check(!exception && catchFunction && JSValueIsObject(context, catchFunction), "Promise should have a catch object property");
JSValueRef passedTrueFunction = JSObjectMakeFunctionWithCallback(context, trueString, passedTrue);
JSObjectCallAsFunction(context, const_cast<JSObjectRef>(catchFunction), promise, 1, &passedTrueFunction, &exception);
@@ -487,10 +491,80 @@
auto trueValue = JSValueMakeBoolean(context, true);
JSObjectCallAsFunction(context, reject, reject, 1, &trueValue, &exception);
- check(!exception, "No exception should be thrown resolve promise");
- check(passedTrueCalled, "then response function should have been called.");
+ check(!exception, "No exception should be thrown rejecting promise");
+ check(passedTrueCalled, "catch response function should have been called.");
}
+void TestAPI::promiseUnhandledRejection()
+{
+ JSObjectRef reject;
+ JSValueRef exception = nullptr;
+ static auto promise = JSObjectMakeDeferredPromise(context, nullptr, &reject, &exception);
+ check(!exception, "creating a (reject-only) deferred promise should not throw");
+ static auto reason = JSValueMakeString(context, APIString("reason"));
+
+ static TestAPI* tester = this;
+ static bool callbackCalled = false;
+ auto callback = [](JSContextRef ctx, JSObjectRef, JSObjectRef, size_t argumentCount, const JSValueRef arguments[], JSValueRef*) -> JSValueRef {
+ tester->check(argumentCount && JSValueIsStrictEqual(ctx, arguments[0], promise), "callback should receive rejected promise as first argument");
+ tester->check(argumentCount > 1 && JSValueIsStrictEqual(ctx, arguments[1], reason), "callback should receive rejection reason as second argument");
+ tester->check(argumentCount == 2, "callback should not receive a third argument");
+ callbackCalled = true;
+ return JSValueMakeUndefined(ctx);
+ };
+ auto callbackFunction = JSObjectMakeFunctionWithCallback(context, APIString("callback"), callback);
+
+ JSGlobalContextSetUnhandledRejectionCallback(context, callbackFunction, &exception);
+ check(!exception, "setting unhandled rejection callback should not throw");
+
+ JSObjectCallAsFunction(context, reject, reject, 1, &reason, &exception);
+ check(!exception && callbackCalled, "unhandled rejection callback should be called upon unhandled rejection");
+}
+
+void TestAPI::promiseUnhandledRejectionFromUnhandledRejectionCallback()
+{
+ static JSObjectRef reject;
+ static JSValueRef exception = nullptr;
+ JSObjectMakeDeferredPromise(context, nullptr, &reject, &exception);
+ check(!exception, "creating a (reject-only) deferred promise should not throw");
+
+ static auto callbackCallCount = 0;
+ auto callback = [](JSContextRef ctx, JSObjectRef, JSObjectRef, size_t, const JSValueRef[], JSValueRef*) -> JSValueRef {
+ if (!callbackCallCount)
+ JSObjectCallAsFunction(ctx, reject, reject, 0, nullptr, &exception);
+ callbackCallCount++;
+ return JSValueMakeUndefined(ctx);
+ };
+ auto callbackFunction = JSObjectMakeFunctionWithCallback(context, APIString("callback"), callback);
+
+ JSGlobalContextSetUnhandledRejectionCallback(context, callbackFunction, &exception);
+ check(!exception, "setting unhandled rejection callback should not throw");
+
+ callFunction("(function () { Promise.reject(); })");
+ check(!exception && callbackCallCount == 2, "unhandled rejection from unhandled rejection callback should also trigger the callback");
+}
+
+void TestAPI::promiseEarlyHandledRejections()
+{
+ JSValueRef exception = nullptr;
+
+ static bool callbackCalled = false;
+ auto callback = [](JSContextRef ctx, JSObjectRef, JSObjectRef, size_t, const JSValueRef[], JSValueRef*) -> JSValueRef {
+ callbackCalled = true;
+ return JSValueMakeUndefined(ctx);
+ };
+ auto callbackFunction = JSObjectMakeFunctionWithCallback(context, APIString("callback"), callback);
+
+ JSGlobalContextSetUnhandledRejectionCallback(context, callbackFunction, &exception);
+ check(!exception, "setting unhandled rejection callback should not throw");
+
+ callFunction("(function () { const p = Promise.reject(); p.catch(() => {}); })");
+ check(!callbackCalled, "unhandled rejection callback should not be called for synchronous early-handled rejection");
+
+ callFunction("(function () { const p = Promise.reject(); Promise.resolve().then(() => { p.catch(() => {}); }); })");
+ check(!callbackCalled, "unhandled rejection callback should not be called for asynchronous early-handled rejection");
+}
+
#define RUN(test) do { \
if (!shouldRun(#test)) \
break; \
@@ -521,6 +595,9 @@
RUN(symbolsDeletePropertyForKey());
RUN(promiseResolveTrue());
RUN(promiseRejectTrue());
+ RUN(promiseUnhandledRejection());
+ RUN(promiseUnhandledRejectionFromUnhandledRejectionCallback());
+ RUN(promiseEarlyHandledRejections());
if (tasks.isEmpty()) {
dataLogLn("Filtered all tests: ERROR");
Modified: trunk/Source/_javascript_Core/ChangeLog (249057 => 249058)
--- trunk/Source/_javascript_Core/ChangeLog 2019-08-23 18:29:59 UTC (rev 249057)
+++ trunk/Source/_javascript_Core/ChangeLog 2019-08-23 18:51:20 UTC (rev 249058)
@@ -1,3 +1,55 @@
+2019-08-23 Ross Kirsling <ross.kirsl...@sony.com>
+
+ JSC should have public API for unhandled promise rejections
+ https://bugs.webkit.org/show_bug.cgi?id=197172
+
+ Reviewed by Keith Miller.
+
+ 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.)
+
2019-08-22 Mark Lam <mark....@apple.com>
VirtualRegister::dump() can use more informative CallFrame header slot names.
Modified: trunk/Source/_javascript_Core/jsc.cpp (249057 => 249058)
--- trunk/Source/_javascript_Core/jsc.cpp 2019-08-23 18:29:59 UTC (rev 249057)
+++ trunk/Source/_javascript_Core/jsc.cpp 2019-08-23 18:51:20 UTC (rev 249058)
@@ -384,6 +384,8 @@
static EncodedJSValue JSC_HOST_CALL functionMallocInALoop(ExecState*);
static EncodedJSValue JSC_HOST_CALL functionTotalCompileTime(ExecState*);
+static EncodedJSValue JSC_HOST_CALL functionSetUnhandledRejectionCallback(ExecState*);
+
struct Script {
enum class StrictMode {
Strict,
@@ -638,6 +640,8 @@
addFunction(vm, "disableRichSourceInfo", functionDisableRichSourceInfo, 0);
addFunction(vm, "mallocInALoop", functionMallocInALoop, 0);
addFunction(vm, "totalCompileTime", functionTotalCompileTime, 0);
+
+ addFunction(vm, "setUnhandledRejectionCallback", functionSetUnhandledRejectionCallback, 1);
}
void addFunction(VM& vm, JSObject* object, const char* name, NativeFunction function, unsigned arguments)
@@ -2384,6 +2388,19 @@
#endif // ENABLE(WEBASSEMBLY)
+EncodedJSValue JSC_HOST_CALL functionSetUnhandledRejectionCallback(ExecState* exec)
+{
+ VM& vm = exec->vm();
+ JSObject* object = exec->argument(0).getObject();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+
+ if (!object || !object->isFunction(vm))
+ return throwVMTypeError(exec, scope);
+
+ exec->lexicalGlobalObject()->setUnhandledRejectionCallback(vm, object);
+ return JSValue::encode(jsUndefined());
+}
+
// Use SEH for Release builds only to get rid of the crash report dialog
// (luckily the same tests fail in Release and Debug builds so far). Need to
// be in a separate main function because the jscmain function requires object
Modified: trunk/Source/_javascript_Core/runtime/JSGlobalObject.h (249057 => 249058)
--- trunk/Source/_javascript_Core/runtime/JSGlobalObject.h 2019-08-23 18:29:59 UTC (rev 249057)
+++ trunk/Source/_javascript_Core/runtime/JSGlobalObject.h 2019-08-23 18:51:20 UTC (rev 249058)
@@ -425,6 +425,8 @@
String m_name;
+ Strong<JSObject> m_unhandledRejectionCallback;
+
Debugger* m_debugger;
VM& m_vm;
@@ -807,6 +809,9 @@
void setName(const String&);
const String& name() const { return m_name; }
+ void setUnhandledRejectionCallback(VM& vm, JSObject* function) { m_unhandledRejectionCallback.set(vm, function); }
+ JSObject* unhandledRejectionCallback() const { return m_unhandledRejectionCallback.get(); }
+
JSObject* arrayBufferConstructor() const { return m_arrayBufferStructure.constructor(this); }
JSObject* arrayBufferPrototype(ArrayBufferSharingMode sharingMode) const
Modified: trunk/Source/_javascript_Core/runtime/JSGlobalObjectFunctions.cpp (249057 => 249058)
--- trunk/Source/_javascript_Core/runtime/JSGlobalObjectFunctions.cpp 2019-08-23 18:29:59 UTC (rev 249057)
+++ trunk/Source/_javascript_Core/runtime/JSGlobalObjectFunctions.cpp 2019-08-23 18:51:20 UTC (rev 249058)
@@ -766,10 +766,12 @@
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
- if (!globalObject->globalObjectMethodTable()->promiseRejectionTracker)
+ JSPromise* promise = jsCast<JSPromise*>(exec->argument(0));
+
+ // InternalPromises should not be exposed to user scripts.
+ if (jsDynamicCast<JSInternalPromise*>(vm, promise))
return JSValue::encode(jsUndefined());
- JSPromise* promise = jsCast<JSPromise*>(exec->argument(0));
JSValue operationValue = exec->argument(1);
ASSERT(operationValue.isNumber());
@@ -777,7 +779,18 @@
ASSERT(operation == JSPromiseRejectionOperation::Reject || operation == JSPromiseRejectionOperation::Handle);
scope.assertNoException();
- globalObject->globalObjectMethodTable()->promiseRejectionTracker(globalObject, exec, promise, operation);
+ if (globalObject->globalObjectMethodTable()->promiseRejectionTracker)
+ globalObject->globalObjectMethodTable()->promiseRejectionTracker(globalObject, exec, promise, operation);
+ else {
+ switch (operation) {
+ case JSPromiseRejectionOperation::Reject:
+ vm.promiseRejected(promise);
+ break;
+ case JSPromiseRejectionOperation::Handle:
+ // do nothing
+ break;
+ }
+ }
RETURN_IF_EXCEPTION(scope, { });
return JSValue::encode(jsUndefined());
Modified: trunk/Source/_javascript_Core/runtime/VM.cpp (249057 => 249058)
--- trunk/Source/_javascript_Core/runtime/VM.cpp 2019-08-23 18:29:59 UTC (rev 249057)
+++ trunk/Source/_javascript_Core/runtime/VM.cpp 2019-08-23 18:51:20 UTC (rev 249058)
@@ -1090,13 +1090,51 @@
m_microtaskQueue.append(makeUnique<QueuedTask>(*this, &globalObject, WTFMove(task)));
}
+void VM::callPromiseRejectionCallback(Strong<JSPromise>& promise)
+{
+ JSObject* callback = promise->globalObject()->unhandledRejectionCallback();
+ if (!callback)
+ return;
+
+ auto scope = DECLARE_CATCH_SCOPE(*this);
+
+ CallData callData;
+ CallType callType = getCallData(*this, callback, callData);
+ ASSERT(callType != CallType::None);
+
+ MarkedArgumentBuffer args;
+ args.append(promise.get());
+ args.append(promise->result(*this));
+ call(promise->globalObject()->globalExec(), callback, callType, callData, jsNull(), args);
+ scope.clearException();
+}
+
+void VM::didExhaustMicrotaskQueue()
+{
+ auto unhandledRejections = WTFMove(m_aboutToBeNotifiedRejectedPromises);
+ for (auto& promise : unhandledRejections) {
+ if (promise->isHandled(*this))
+ continue;
+
+ callPromiseRejectionCallback(promise);
+ }
+}
+
+void VM::promiseRejected(JSPromise* promise)
+{
+ m_aboutToBeNotifiedRejectedPromises.constructAndAppend(*this, promise);
+}
+
void VM::drainMicrotasks()
{
- while (!m_microtaskQueue.isEmpty()) {
- m_microtaskQueue.takeFirst()->run();
- if (m_onEachMicrotaskTick)
- m_onEachMicrotaskTick(*this);
- }
+ do {
+ while (!m_microtaskQueue.isEmpty()) {
+ m_microtaskQueue.takeFirst()->run();
+ if (m_onEachMicrotaskTick)
+ m_onEachMicrotaskTick(*this);
+ }
+ didExhaustMicrotaskQueue();
+ } while (!m_microtaskQueue.isEmpty());
finalizeSynchronousJSExecution();
}
Modified: trunk/Source/_javascript_Core/runtime/VM.h (249057 => 249058)
--- trunk/Source/_javascript_Core/runtime/VM.h 2019-08-23 18:29:59 UTC (rev 249057)
+++ trunk/Source/_javascript_Core/runtime/VM.h 2019-08-23 18:51:20 UTC (rev 249058)
@@ -123,6 +123,7 @@
class JSDestructibleObjectHeapCellType;
class JSGlobalObject;
class JSObject;
+class JSPromise;
class JSPropertyNameEnumerator;
class JSRunLoopTimer;
class JSStringHeapCellType;
@@ -922,6 +923,8 @@
void notifyNeedTermination() { m_traps.fireTrap(VMTraps::NeedTermination); }
void notifyNeedWatchdogCheck() { m_traps.fireTrap(VMTraps::NeedWatchdogCheck); }
+ void promiseRejected(JSPromise*);
+
#if ENABLE(EXCEPTION_SCOPE_VERIFICATION)
StackTrace* nativeStackTraceOfLastThrow() const { return m_nativeStackTraceOfLastThrow.get(); }
Thread* throwingThread() const { return m_throwingThread.get(); }
@@ -1008,6 +1011,9 @@
static void primitiveGigacageDisabledCallback(void*);
void primitiveGigacageDisabled();
+ void callPromiseRejectionCallback(Strong<JSPromise>&);
+ void didExhaustMicrotaskQueue();
+
#if ENABLE(GC_VALIDATION)
const ClassInfo* m_initializingObjectClass;
#endif
@@ -1063,6 +1069,9 @@
std::unique_ptr<ShadowChicken> m_shadowChicken;
std::unique_ptr<BytecodeIntrinsicRegistry> m_bytecodeIntrinsicRegistry;
+ // FIXME: We should remove handled promises from this list at GC flip. <https://webkit.org/b/201005>
+ Vector<Strong<JSPromise>> m_aboutToBeNotifiedRejectedPromises;
+
WTF::Function<void(VM&)> m_onEachMicrotaskTick;
uintptr_t m_currentWeakRefVersion { 0 };
Modified: trunk/Source/WebCore/ChangeLog (249057 => 249058)
--- trunk/Source/WebCore/ChangeLog 2019-08-23 18:29:59 UTC (rev 249057)
+++ trunk/Source/WebCore/ChangeLog 2019-08-23 18:51:20 UTC (rev 249058)
@@ -1,3 +1,14 @@
+2019-08-23 Ross Kirsling <ross.kirsl...@sony.com>
+
+ JSC should have public API for unhandled promise rejections
+ https://bugs.webkit.org/show_bug.cgi?id=197172
+
+ Reviewed by Keith Miller.
+
+ * bindings/js/JSDOMGlobalObject.cpp:
+ (WebCore::JSDOMGlobalObject::promiseRejectionTracker):
+ Move JSInternalPromise early-out to JSC side.
+
2019-08-23 Kate Cheney <katherine_che...@apple.com>
Support ITP on a per-session basis (198923)
Modified: trunk/Source/WebCore/bindings/js/JSDOMGlobalObject.cpp (249057 => 249058)
--- trunk/Source/WebCore/bindings/js/JSDOMGlobalObject.cpp 2019-08-23 18:29:59 UTC (rev 249057)
+++ trunk/Source/WebCore/bindings/js/JSDOMGlobalObject.cpp 2019-08-23 18:51:20 UTC (rev 249058)
@@ -216,16 +216,11 @@
{
// https://html.spec.whatwg.org/multipage/webappapis.html#the-hostpromiserejectiontracker-implementation
- VM& vm = exec->vm();
auto& globalObject = *JSC::jsCast<JSDOMGlobalObject*>(jsGlobalObject);
auto* context = globalObject.scriptExecutionContext();
if (!context)
return;
- // InternalPromises should not be exposed to user scripts.
- if (JSC::jsDynamicCast<JSC::JSInternalPromise*>(vm, promise))
- return;
-
// FIXME: If script has muted errors (cross origin), terminate these steps.
// <https://webkit.org/b/171415> Implement the `muted-errors` property of Scripts to avoid onerror/onunhandledrejection for cross-origin scripts