/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set ts=2 sw=2 sts=2 et cindent: */ /* 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 "nsThreadUtils.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/RootedDictionary.h" #include "mozilla/uniffi/Call.h" namespace mozilla::uniffi { extern mozilla::LazyLogModule gUniffiLogger; using dom::GlobalObject; using dom::OwningUniFFIScaffoldingValue; using dom::RootedDictionary; using dom::Sequence; using dom::UniFFIScaffoldingCallResult; void UniffiSyncCallHandler::CallSync( UniquePtr aHandler, const GlobalObject& aGlobal, const Sequence& aArgs, RootedDictionary& aReturnValue, ErrorResult& aError) { MOZ_ASSERT(NS_IsMainThread()); aHandler->PrepareRustArgs(aArgs, aError); if (aError.Failed()) { return; } RustCallStatus callStatus{}; aHandler->MakeRustCall(&callStatus); aHandler->mUniffiCallStatusCode = callStatus.code; if (callStatus.error_buf.data) { aHandler->mUniffiCallStatusErrorBuf = FfiValueRustBuffer(callStatus.error_buf); } aHandler->ExtractCallResult(aGlobal.Context(), aReturnValue, aError); } already_AddRefed UniffiSyncCallHandler::CallAsyncWrapper( UniquePtr aHandler, const dom::GlobalObject& aGlobal, const dom::Sequence& aArgs, ErrorResult& aError) { MOZ_ASSERT(NS_IsMainThread()); aHandler->PrepareRustArgs(aArgs, aError); if (aError.Failed()) { return nullptr; } // Create the promise that we return to JS nsCOMPtr xpcomGlobal = do_QueryInterface(aGlobal.GetAsSupports()); RefPtr returnPromise = dom::Promise::Create(xpcomGlobal, aError); if (aError.Failed()) { return nullptr; } // Create a second promise that gets resolved by a background task that // calls the scaffolding function RefPtr taskPromise = new MozPromise, nsresult, true>::Private( __func__); nsresult dispatchResult = NS_DispatchBackgroundTask( NS_NewRunnableFunction( __func__, [handler = std::move(aHandler), taskPromise]() mutable { RustCallStatus callStatus{}; handler->MakeRustCall(&callStatus); handler->mUniffiCallStatusCode = callStatus.code; if (callStatus.error_buf.data) { handler->mUniffiCallStatusErrorBuf = FfiValueRustBuffer(callStatus.error_buf); } taskPromise->Resolve(std::move(handler), __func__); }), NS_DISPATCH_EVENT_MAY_BLOCK); if (NS_FAILED(dispatchResult)) { taskPromise->Reject(dispatchResult, __func__); } // When the background task promise completes, resolve the JS promise taskPromise->Then( GetCurrentSerialEventTarget(), __func__, [xpcomGlobal, returnPromise]( typename MozPromise, nsresult, true>::ResolveOrRejectValue&& aResult) { if (!aResult.IsResolve()) { returnPromise->MaybeRejectWithUnknownError(__func__); return; } auto handler = std::move(aResult.ResolveValue()); dom::AutoEntryScript aes(xpcomGlobal, "UniffiSyncCallHandler::CallAsyncWrapper"); dom::RootedDictionary returnValue( aes.cx()); ErrorResult error; handler->ExtractCallResult(aes.cx(), returnValue, error); error.WouldReportJSException(); if (error.Failed()) { returnPromise->MaybeReject(std::move(error)); } else { returnPromise->MaybeResolve(returnValue); } }); // Return the JS promise, using forget() to convert it to already_AddRefed return returnPromise.forget(); } void UniffiCallHandlerBase::ExtractCallResult( JSContext* aCx, dom::RootedDictionary& aDest, ErrorResult& aError) { switch (mUniffiCallStatusCode) { case RUST_CALL_SUCCESS: { aDest.mCode = dom::UniFFIScaffoldingCallCode::Success; ExtractSuccessfulCallResult(aCx, aDest.mData, aError); break; } case RUST_CALL_ERROR: { // Rust Err() value. Populate data with the `RustBuffer` containing the // error aDest.mCode = dom::UniFFIScaffoldingCallCode::Error; mUniffiCallStatusErrorBuf.Lift(aCx, &aDest.mData.Construct(), aError); break; } default: { // This indicates a RustError, which should rarely happen in practice. // The normal case is a Rust panic, but FF sets panic=abort. aDest.mCode = dom::UniFFIScaffoldingCallCode::Internal_error; if (mUniffiCallStatusErrorBuf.IsSet()) { mUniffiCallStatusErrorBuf.Lift(aCx, &aDest.mData.Construct(), aError); } break; } } } already_AddRefed UniffiAsyncCallHandler::CallAsync( UniquePtr aHandler, const dom::GlobalObject& aGlobal, const dom::Sequence& aArgs, ErrorResult& aError) { MOZ_ASSERT(NS_IsMainThread()); // Async calls return a Future rather than doing any work. This means we can // make the call right now on the JS main thread without fear of blocking it. aHandler->PrepareArgsAndMakeRustCall(aArgs, aError); if (aError.Failed()) { return nullptr; } // Create the promise that the handler will resolve nsCOMPtr global(do_QueryInterface(aGlobal.GetAsSupports())); aHandler->mPromise = dom::Promise::Create(global, aError); // Also get a copy to return to JS RefPtr returnPromise(aHandler->mPromise); if (aError.Failed()) { aError.ThrowUnknownError("[UniFFI] dom::Promise::Create failed"_ns); return nullptr; } // Schedule a poll for the future in a background thread. nsresult dispatchResult = NS_DispatchBackgroundTask(NS_NewRunnableFunction( __func__, [handler = std::move(aHandler)]() mutable { UniffiAsyncCallHandler::Poll(std::move(handler)); })); if (NS_FAILED(dispatchResult)) { aError.ThrowUnknownError( "[UniFFI] UniffiAsyncCallHandler::CallAsync - Error scheduling background task"_ns); return nullptr; } // Return a copy of the JS promise, using forget() to convert it to // already_AddRefed return returnPromise.forget(); } // Callback function for async calls // // This is passed to Rust when we poll the future alongside a 64-bit handle that // represents the callback data. For uniffi-bindgen-gecko-js, the handle is a // `UniffiAsyncCallHandler*` casted to an int. // // Rust calls this when either the future is ready or when it's time to poll it // again. void UniffiAsyncCallHandler::FutureCallback(uint64_t aCallHandlerHandle, int8_t aCode) { // Recreate the UniquePtr we previously released UniquePtr handler( reinterpret_cast( static_cast(aCallHandlerHandle))); switch (aCode) { case UNIFFI_FUTURE_READY: { // `Future::poll` returned a `Ready` value on the Rust side. nsresult dispatchResult = NS_DispatchToMainThread(NS_NewRunnableFunction( __func__, [handler = std::move(handler)]() mutable { UniffiAsyncCallHandler::Finish(std::move(handler)); })); if (NS_FAILED(dispatchResult)) { MOZ_LOG(gUniffiLogger, LogLevel::Error, ("[UniFFI] NS_DispatchToMainThread failed in " "UniffiAsyncCallHandler::FutureCallback")); } break; } case UNIFFI_FUTURE_MAYBE_READY: { // The Rust waker was invoked after `poll` returned a `Pending` value. // Poll the future again soon in a background task. nsresult dispatchResult = NS_DispatchBackgroundTask( NS_NewRunnableFunction(__func__, [handler = std::move(handler)]() mutable { UniffiAsyncCallHandler::Poll( std::move(handler)); }), NS_DISPATCH_NORMAL); if (NS_FAILED(dispatchResult)) { MOZ_LOG(gUniffiLogger, LogLevel::Error, ("[UniFFI] NS_DispatchBackgroundTask failed in " "UniffiAsyncCallHandler::FutureCallback")); } break; } default: { // Invalid poll code, this should never happen, but if it does log an // error and reject the promise. MOZ_LOG(gUniffiLogger, LogLevel::Error, ("[UniFFI] Invalid poll code in " "UniffiAsyncCallHandler::FutureCallback %d", aCode)); handler->mPromise->MaybeRejectWithUndefined(); break; } }; } void UniffiAsyncCallHandler::Poll(UniquePtr aHandler) { auto futureHandle = aHandler->mFutureHandle; auto pollFn = aHandler->mPollFn; // Release the UniquePtr into a raw pointer and convert it to a handle // so that we can pass it as a handle to the UniFFI code. It gets converted // back in `UniffiAsyncCallHandler::FutureCallback()`, which the Rust code // guarentees will be called if the future makes progress. uint64_t selfHandle = static_cast(reinterpret_cast(aHandler.release())); pollFn(futureHandle, UniffiAsyncCallHandler::FutureCallback, selfHandle); } // Complete the Rust future, extract the call result and resolve/reject the JS // promise void UniffiAsyncCallHandler::Finish( UniquePtr aHandler) { RefPtr promise = aHandler->mPromise; if (!promise) { return; } dom::AutoEntryScript aes(promise->GetGlobalObject(), "UniffiAsyncCallHandler::Finish"); dom::RootedDictionary returnValue(aes.cx()); ErrorResult error; RustCallStatus callStatus{}; aHandler->CallCompleteFn(&callStatus); aHandler->mUniffiCallStatusCode = callStatus.code; if (callStatus.error_buf.data) { aHandler->mUniffiCallStatusErrorBuf = FfiValueRustBuffer(callStatus.error_buf); } aHandler->ExtractCallResult(aes.cx(), returnValue, error); error.WouldReportJSException(); if (error.Failed()) { aHandler->mPromise->MaybeReject(std::move(error)); } else { aHandler->mPromise->MaybeResolve(returnValue); } } UniffiAsyncCallHandler::~UniffiAsyncCallHandler() { mFreeFn(mFutureHandle); } } // namespace mozilla::uniffi