/* -*- 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 #include #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 struct RustCallResult { ReturnType mReturnValue; RustCallStatus mCallStatus = {}; }; template <> struct RustCallResult { 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 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 CallAsync( ScaffoldingFunc aScaffoldingFunc, const dom::GlobalObject& aGlobal, const dom::Sequence& 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 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 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 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& aArgs, dom::RootedDictionary& 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; using IntermediateArgs = std::tuple; using CallResult = RustCallResult; using TaskPromiseType = MozPromise; template using NthArgConverter = typename std::tuple_element>::type; // Convert arguments from JS // // This should be called in the main thread static Result ConvertJsArgs( const dom::Sequence& aArgs) { IntermediateArgs convertedArgs; if (aArgs.Length() != std::tuple_size_v) { 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 static Result PrepareArgsHelper( const dom::Sequence& aArgs, IntermediateArgs& aConvertedArgs) { if constexpr (I >= sizeof...(ArgConverters)) { // Iteration complete return mozilla::Ok(); } else { // Single iteration step auto result = NthArgConverter::FromJs(aArgs[I]); if (result.isOk()) { // The conversion worked, store our result and move on to the next std::get(aConvertedArgs) = result.unwrap(); return PrepareArgsHelper(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()); } // Helper function for CallScaffoldingFunc that uses c++ magic to help with // iteration template static CallResult CallScaffoldingFuncHelper(ScaffoldingFunc aFunc, IntermediateArgs&& aArgs, std::index_sequence seq) { CallResult result; auto makeCall = [&]() mutable { return aFunc( NthArgConverter::IntoRust(std::move(std::get(aArgs)))..., &result.mCallStatus); }; if constexpr (std::is_void_v) { 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& aReturnValue, const nsLiteralCString& aFuncName) { switch (aCallResult.mCallStatus.code) { case RUST_CALL_SUCCESS: { aReturnValue.mCode = dom::UniFFIScaffoldingCallCode::Success; if constexpr (!std::is_void_v) { 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