diff options
Diffstat (limited to 'toolkit/components/uniffi-js/ScaffoldingCall.h')
-rw-r--r-- | toolkit/components/uniffi-js/ScaffoldingCall.h | 260 |
1 files changed, 260 insertions, 0 deletions
diff --git a/toolkit/components/uniffi-js/ScaffoldingCall.h b/toolkit/components/uniffi-js/ScaffoldingCall.h new file mode 100644 index 0000000000..02b016c513 --- /dev/null +++ b/toolkit/components/uniffi-js/ScaffoldingCall.h @@ -0,0 +1,260 @@ +/* -*- 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/. */ + +#ifndef mozilla_ScaffoldingCall_h +#define mozilla_ScaffoldingCall_h + +#include <tuple> +#include <type_traits> +#include "nsIGlobalObject.h" +#include "nsPrintfCString.h" +#include "mozilla/MozPromise.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/dom/OwnedRustBuffer.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/ScaffoldingConverter.h" +#include "mozilla/dom/UniFFIBinding.h" +#include "mozilla/dom/UniFFIRust.h" + +namespace mozilla::uniffi { + +// Low-level result of calling a scaffolding function +// +// This stores what Rust returned in order to convert it into +// UniFFIScaffoldingCallResult +template <typename ReturnType> +struct RustCallResult { + ReturnType mReturnValue; + RustCallStatus mCallStatus = {}; +}; + +template <> +struct RustCallResult<void> { + RustCallStatus mCallStatus = {}; +}; + +// Does the work required to call a scaffolding function +// +// This class is generic over the type signature of the scaffolding function. +// This seems better than being generic over the functions themselves, since it +// saves space whenever 2 functions share a signature. +template <typename ReturnConverter, typename... ArgConverters> +class ScaffoldingCallHandler { + public: + // Pointer to a scaffolding function that can be called by this + // ScaffoldingConverter + using ScaffoldingFunc = typename ReturnConverter::RustType (*)( + typename ArgConverters::RustType..., RustCallStatus*); + + // Perform an async scaffolding call + static already_AddRefed<dom::Promise> CallAsync( + ScaffoldingFunc aScaffoldingFunc, const dom::GlobalObject& aGlobal, + const dom::Sequence<dom::ScaffoldingType>& aArgs, + const nsLiteralCString& aFuncName, ErrorResult& aError) { + auto convertResult = ConvertJsArgs(aArgs); + if (convertResult.isErr()) { + aError.ThrowUnknownError(aFuncName + convertResult.unwrapErr()); + return nullptr; + } + auto convertedArgs = convertResult.unwrap(); + + // Create the promise that we return to JS + nsCOMPtr<nsIGlobalObject> xpcomGlobal = + do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<dom::Promise> 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 typename TaskPromiseType::Private(aFuncName.get()); + nsresult dispatchResult = NS_DispatchBackgroundTask( + NS_NewRunnableFunction(aFuncName.get(), + [args = std::move(convertedArgs), taskPromise, + aScaffoldingFunc, aFuncName]() mutable { + auto callResult = CallScaffoldingFunc( + aScaffoldingFunc, std::move(args)); + taskPromise->Resolve(std::move(callResult), + aFuncName.get()); + }), + NS_DISPATCH_EVENT_MAY_BLOCK); + if (NS_FAILED(dispatchResult)) { + taskPromise->Reject(dispatchResult, aFuncName.get()); + } + + // When the background task promise completes, resolve the JS promise + taskPromise->Then( + GetCurrentSerialEventTarget(), aFuncName.get(), + [xpcomGlobal, returnPromise, + aFuncName](typename TaskPromiseType::ResolveOrRejectValue&& aResult) { + if (!aResult.IsResolve()) { + returnPromise->MaybeRejectWithUnknownError(aFuncName); + return; + } + + dom::AutoEntryScript aes(xpcomGlobal, aFuncName.get()); + dom::RootedDictionary<dom::UniFFIScaffoldingCallResult> returnValue( + aes.cx()); + + ReturnResult(aes.cx(), aResult.ResolveValue(), returnValue, + aFuncName); + returnPromise->MaybeResolve(returnValue); + }); + + // Return the JS promise, using forget() to convert it to already_AddRefed + return returnPromise.forget(); + } + + // Perform an sync scaffolding call + // + // aFuncName should be a literal C string + static void CallSync( + ScaffoldingFunc aScaffoldingFunc, const dom::GlobalObject& aGlobal, + const dom::Sequence<dom::ScaffoldingType>& aArgs, + dom::RootedDictionary<dom::UniFFIScaffoldingCallResult>& aReturnValue, + const nsLiteralCString& aFuncName, ErrorResult& aError) { + auto convertResult = ConvertJsArgs(aArgs); + if (convertResult.isErr()) { + aError.ThrowUnknownError(aFuncName + convertResult.unwrapErr()); + return; + } + + auto callResult = CallScaffoldingFunc(aScaffoldingFunc, + std::move(convertResult.unwrap())); + + ReturnResult(aGlobal.Context(), callResult, aReturnValue, aFuncName); + } + + private: + using RustArgs = std::tuple<typename ArgConverters::RustType...>; + using IntermediateArgs = + std::tuple<typename ArgConverters::IntermediateType...>; + using CallResult = RustCallResult<typename ReturnConverter::RustType>; + using TaskPromiseType = MozPromise<CallResult, nsresult, true>; + + template <size_t I> + using NthArgConverter = + typename std::tuple_element<I, std::tuple<ArgConverters...>>::type; + + // Convert arguments from JS + // + // This should be called in the main thread + static Result<IntermediateArgs, nsCString> ConvertJsArgs( + const dom::Sequence<dom::ScaffoldingType>& aArgs) { + IntermediateArgs convertedArgs; + if (aArgs.Length() != std::tuple_size_v<IntermediateArgs>) { + return mozilla::Err("Wrong argument count"_ns); + } + auto result = PrepareArgsHelper<0>(aArgs, convertedArgs); + return result.map([&](auto _) { return std::move(convertedArgs); }); + } + + // Helper function for PrepareArgs that uses c++ magic to help with iteration + template <size_t I = 0> + static Result<mozilla::Ok, nsCString> PrepareArgsHelper( + const dom::Sequence<dom::ScaffoldingType>& aArgs, + IntermediateArgs& aConvertedArgs) { + if constexpr (I >= sizeof...(ArgConverters)) { + // Iteration complete + return mozilla::Ok(); + } else { + // Single iteration step + auto result = NthArgConverter<I>::FromJs(aArgs[I]); + if (result.isOk()) { + // The conversion worked, store our result and move on to the next + std::get<I>(aConvertedArgs) = result.unwrap(); + return PrepareArgsHelper<I + 1>(aArgs, aConvertedArgs); + } else { + // The conversion failed, return an error and don't continue + return mozilla::Err(result.unwrapErr() + + nsPrintfCString(" (arg %zu)", I)); + } + } + } + + // Call the scaffolding function + // + // For async calls this should be called in the worker thread + static CallResult CallScaffoldingFunc(ScaffoldingFunc aFunc, + IntermediateArgs&& aArgs) { + return CallScaffoldingFuncHelper( + aFunc, std::move(aArgs), std::index_sequence_for<ArgConverters...>()); + } + + // Helper function for CallScaffoldingFunc that uses c++ magic to help with + // iteration + template <size_t... Is> + static CallResult CallScaffoldingFuncHelper(ScaffoldingFunc aFunc, + IntermediateArgs&& aArgs, + std::index_sequence<Is...> seq) { + CallResult result; + + auto makeCall = [&]() mutable { + return aFunc( + NthArgConverter<Is>::IntoRust(std::move(std::get<Is>(aArgs)))..., + &result.mCallStatus); + }; + if constexpr (std::is_void_v<typename ReturnConverter::RustType>) { + makeCall(); + } else { + result.mReturnValue = makeCall(); + } + return result; + } + + // Return the result of the scaffolding call back to JS + // + // This should be called on the main thread + static void ReturnResult( + JSContext* aContext, CallResult& aCallResult, + dom::RootedDictionary<dom::UniFFIScaffoldingCallResult>& aReturnValue, + const nsLiteralCString& aFuncName) { + switch (aCallResult.mCallStatus.code) { + case RUST_CALL_SUCCESS: { + aReturnValue.mCode = dom::UniFFIScaffoldingCallCode::Success; + if constexpr (!std::is_void_v<typename ReturnConverter::RustType>) { + auto convertResult = + ReturnConverter::FromRust(aCallResult.mReturnValue); + if (convertResult.isOk()) { + ReturnConverter::IntoJs(aContext, std::move(convertResult.unwrap()), + aReturnValue.mData.Construct()); + } else { + aReturnValue.mCode = dom::UniFFIScaffoldingCallCode::Internal_error; + aReturnValue.mInternalErrorMessage.Construct( + aFuncName + " converting result: "_ns + + convertResult.unwrapErr()); + } + } + break; + } + + case RUST_CALL_ERROR: { + // Rust Err() value. Populate data with the `RustBuffer` containing the + // error + aReturnValue.mCode = dom::UniFFIScaffoldingCallCode::Error; + aReturnValue.mData.Construct().SetAsArrayBuffer().Init( + OwnedRustBuffer(aCallResult.mCallStatus.error_buf) + .IntoArrayBuffer(aContext)); + break; + } + + default: { + // This indicates a RustError, which shouldn't happen in practice since + // FF sets panic=abort + aReturnValue.mCode = dom::UniFFIScaffoldingCallCode::Internal_error; + aReturnValue.mInternalErrorMessage.Construct(aFuncName + + " Unexpected Error"_ns); + break; + } + } + } +}; + +} // namespace mozilla::uniffi + +#endif // mozilla_ScaffoldingCall_h |