diff --git a/assets/asset_resolver.h b/assets/asset_resolver.h index 4bf1eeb23e6f1..e5da2a2eb4a9a 100644 --- a/assets/asset_resolver.h +++ b/assets/asset_resolver.h @@ -26,7 +26,8 @@ class AssetResolver { enum AssetResolverType { kAssetManager, kApkAssetProvider, - kDirectoryAssetBundle + kDirectoryAssetBundle, + kEmbedderAssetResolver }; virtual bool IsValid() const = 0; diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index e0b43fca37cf5..70aa98611bd53 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1296,6 +1296,8 @@ FILE: ../../../flutter/shell/platform/embedder/assets/EmbedderInfo.plist FILE: ../../../flutter/shell/platform/embedder/assets/embedder.modulemap FILE: ../../../flutter/shell/platform/embedder/embedder.cc FILE: ../../../flutter/shell/platform/embedder/embedder.h +FILE: ../../../flutter/shell/platform/embedder/embedder_asset_resolver.cc +FILE: ../../../flutter/shell/platform/embedder/embedder_asset_resolver.h FILE: ../../../flutter/shell/platform/embedder/embedder_engine.cc FILE: ../../../flutter/shell/platform/embedder/embedder_engine.h FILE: ../../../flutter/shell/platform/embedder/embedder_external_texture_gl.cc diff --git a/shell/platform/embedder/BUILD.gn b/shell/platform/embedder/BUILD.gn index 529f48a7ebd41..9caf914af3a08 100644 --- a/shell/platform/embedder/BUILD.gn +++ b/shell/platform/embedder/BUILD.gn @@ -32,6 +32,8 @@ template("embedder_source_set") { source_set(target_name) { sources = [ "embedder.cc", + "embedder_asset_resolver.cc", + "embedder_asset_resolver.h", "embedder_engine.cc", "embedder_engine.h", "embedder_external_texture_resolver.cc", diff --git a/shell/platform/embedder/embedder.cc b/shell/platform/embedder/embedder.cc index f8496ffc0b0a9..c7f8c1fa6f030 100644 --- a/shell/platform/embedder/embedder.cc +++ b/shell/platform/embedder/embedder.cc @@ -47,6 +47,7 @@ extern const intptr_t kPlatformStrongDillSize; #include "flutter/shell/common/rasterizer.h" #include "flutter/shell/common/switches.h" #include "flutter/shell/platform/embedder/embedder.h" +#include "flutter/shell/platform/embedder/embedder_asset_resolver.h" #include "flutter/shell/platform/embedder/embedder_engine.h" #include "flutter/shell/platform/embedder/embedder_external_texture_resolver.h" #include "flutter/shell/platform/embedder/embedder_platform_message_response.h" @@ -926,10 +927,11 @@ FlutterEngineResult FlutterEngineInitialize(size_t version, "The Flutter project arguments were missing."); } - if (SAFE_ACCESS(args, assets_path, nullptr) == nullptr) { - return LOG_EMBEDDER_ERROR( - kInvalidArguments, - "The assets path in the Flutter project arguments was missing."); + if (SAFE_ACCESS(args, assets_path, nullptr) == nullptr && + SAFE_ACCESS(args, asset_resolver, nullptr) == nullptr) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, + "The assets path or asset resolver in the " + "Flutter project arguments was missing."); } if (SAFE_ACCESS(args, main_path__unused__, nullptr) != nullptr) { @@ -987,19 +989,23 @@ FlutterEngineResult FlutterEngineInitialize(size_t version, PopulateSnapshotMappingCallbacks(args, settings); settings.icu_data_path = icu_data_path; - settings.assets_path = args->assets_path; + if (args->assets_path) + settings.assets_path = args->assets_path; settings.leak_vm = !SAFE_ACCESS(args, shutdown_dart_vm_when_done, false); settings.old_gen_heap_size = SAFE_ACCESS(args, dart_old_gen_heap_size, -1); if (!flutter::DartVM::IsRunningPrecompiledCode()) { // Verify the assets path contains Dart 2 kernel assets. + // NOTE: This check is skipped if using a custom asset resolver. const std::string kApplicationKernelSnapshotFileName = "kernel_blob.bin"; - std::string application_kernel_path = fml::paths::JoinPaths( - {settings.assets_path, kApplicationKernelSnapshotFileName}); - if (!fml::IsFile(application_kernel_path)) { - return LOG_EMBEDDER_ERROR( - kInvalidArguments, - "Not running in AOT mode but could not resolve the kernel binary."); + if (args->assets_path) { + std::string application_kernel_path = fml::paths::JoinPaths( + {settings.assets_path, kApplicationKernelSnapshotFileName}); + if (!fml::IsFile(application_kernel_path)) { + return LOG_EMBEDDER_ERROR( + kInvalidArguments, + "Not running in AOT mode but could not resolve the kernel binary."); + } } settings.application_kernel_asset = kApplicationKernelSnapshotFileName; } @@ -1296,8 +1302,38 @@ FlutterEngineResult FlutterEngineInitialize(size_t version, "Task runner configuration was invalid."); } - auto run_configuration = - flutter::RunConfiguration::InferFromSettings(settings); + auto asset_manager = std::make_shared(); + + if (SAFE_ACCESS(args, asset_resolver, nullptr) != nullptr) { + auto resolver = args->asset_resolver; + + auto user_data = SAFE_ACCESS(resolver, user_data, nullptr); + auto get_asset = SAFE_ACCESS(resolver, get_asset, nullptr); + + if (get_asset == nullptr) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, + "Asset Resolver get_asset is null."); + } + + asset_manager->PushBack( + flutter::CreateEmbedderAssetResolver([=](const char* asset_name) { + FlutterMapping mapping = get_asset(asset_name, user_data); + return std::unique_ptr( + reinterpret_cast(mapping)); + })); + } + + if (args->assets_path) { + asset_manager->PushBack(std::make_unique( + fml::OpenDirectory(args->assets_path, false, + fml::FilePermission::kRead), + true)); + } + + auto run_configuration = flutter::RunConfiguration( + flutter::IsolateConfiguration::InferFromSettings(settings, asset_manager, + nullptr), + asset_manager); if (SAFE_ACCESS(args, custom_dart_entrypoint, nullptr) != nullptr) { auto dart_entrypoint = std::string{args->custom_dart_entrypoint}; @@ -2360,6 +2396,44 @@ FlutterEngineResult FlutterEngineNotifyDisplayUpdate( } } +FlutterEngineResult FlutterEngineCreateMapping( + const FlutterMappingCreateInfo* create_info, + FlutterMapping* out_mapping) { + auto data = SAFE_ACCESS(create_info, data, nullptr); + auto data_size = SAFE_ACCESS(create_info, data_size, 0); + if (data == nullptr && data_size > 0) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, "Invalid mapping specified."); + } + + auto user_data = SAFE_ACCESS(create_info, user_data, nullptr); + auto destruction_callback = + SAFE_ACCESS(create_info, destruction_callback, nullptr); + + auto mapping = std::make_unique( + data, data_size, + [user_data, destruction_callback](const uint8_t* data, size_t size) { + if (destruction_callback) { + destruction_callback(data, size, user_data); + } + }); + + *out_mapping = reinterpret_cast(mapping.release()); + + return kSuccess; +} + +void FlutterEngineDestroyMapping(FlutterMapping mapping) { + delete reinterpret_cast(mapping); +} + +const uint8_t* FlutterEngineGetMappingData(FlutterMapping mapping, + size_t* out_size) { + auto fml_mapping = reinterpret_cast(mapping); + if (out_size != nullptr) + *out_size = fml_mapping->GetSize(); + return fml_mapping->GetMapping(); +} + FlutterEngineResult FlutterEngineGetProcAddresses( FlutterEngineProcTable* table) { if (!table) { @@ -2410,6 +2484,9 @@ FlutterEngineResult FlutterEngineGetProcAddresses( SET_PROC(PostCallbackOnAllNativeThreads, FlutterEnginePostCallbackOnAllNativeThreads); SET_PROC(NotifyDisplayUpdate, FlutterEngineNotifyDisplayUpdate); + SET_PROC(CreateMapping, FlutterEngineCreateMapping); + SET_PROC(DestroyMapping, FlutterEngineDestroyMapping); + SET_PROC(GetMappingData, FlutterEngineGetMappingData); #undef SET_PROC return kSuccess; diff --git a/shell/platform/embedder/embedder.h b/shell/platform/embedder/embedder.h index e8b769ef4cbef..bbd251a1fb598 100644 --- a/shell/platform/embedder/embedder.h +++ b/shell/platform/embedder/embedder.h @@ -1345,12 +1345,90 @@ typedef void (*FlutterLogMessageCallback)(const char* /* tag */, /// FlutterEngine instance in AOT mode. typedef struct _FlutterEngineAOTData* FlutterEngineAOTData; +// Callback called by the Engine to destroy a previously created FlutterMapping. +// +// The callback may be called on any thread, so it must be thread-safe. +// +// The `data`, `data_size`, and `user_data` parameters are the same values +// passed in `FlutterEngineCreateMapping`. +typedef void (*FlutterMappingDestroyCallback)(const uint8_t* /* data */, + size_t /* data_size */, + void* /* user_data */); + +/// An immutable buffer of potentially shared data. +typedef struct { + /// The size of the struct. Must be sizeof(FlutterMappingCreateInfo). + size_t struct_size; + + /// A pointer to the data accessed by the Flutter Engine. The data will not be + /// mutated by the Flutter Engine, and must not be mutated by the Embedder + /// until the mapping is destroyed. + const uint8_t* data; + /// The size of the data backed by this mapping. The data must be valid for + /// reading until this point. Bytes past the end are undefined and not + /// accessed by the Engine. + size_t data_size; + + /// An opaque baton passed back to the embedder when the destruction_callback + /// is invoked. The engine does not interpret this field in any way. + void* user_data; + /// Called once by the engine to destroy this mapping. The `data`, + /// `data_size`, and `user_data` are passed into the given callback. This call + /// may mutate/free/unmap the data, as it will no longer be accessed by the + /// Flutter Engine. + FlutterMappingDestroyCallback destruction_callback; +} FlutterMappingCreateInfo; + +typedef struct FlutterMappingPrivate* FlutterMapping; + +/// Callback for fetching assets for a `FlutterEngineAssetResolver`. +/// +/// This may be called by multiple threads. +/// +/// The `asset_name` parameter contains the path to the asset to load. +/// `user_data` is the user data from `FlutterEngineAssetResolver`, registered +/// via `FlutterProjectArgs`. +/// +/// If the asset was found and successfully loaded, return a valid +/// `FlutterMapping`. Otherwise return NULL to indicate an error occurred +/// while loading the asset. +/// +/// Note that the returned `FlutterMapping` is owned by the Engine and +/// shouldn't be cached or reused. Each callback invocation MUST return a new +/// FlutterMapping. Multiple mappings may refer to the same area in +/// memory, but proper book-keeping is up to the Embedder. +typedef FlutterMapping (*FlutterAssetResolverGetAssetCallback)( + const char* /* asset_name */, + void* /* user_data */); + +/// Resolves assets on the behalf of the Flutter Engine, instead of accessing +/// the filesystem directly. +typedef struct { + /// The size of the struct. Must be sizeof(FlutterEngineAssetResolver). + size_t struct_size; + + /// An opaque baton passed back to the embedder when a callback is + /// invoked. The engine does not interpret this field in any way. + void* user_data; + + /// Required. Gets and returns an asset if available. See the documentation on + /// `FlutterAssetResolverGetAssetCallback` for more information. + FlutterAssetResolverGetAssetCallback get_asset; +} FlutterEngineAssetResolver; + typedef struct { /// The size of this struct. Must be sizeof(FlutterProjectArgs). size_t struct_size; /// The path to the Flutter assets directory containing project assets. The /// string can be collected after the call to `FlutterEngineRun` returns. The /// string must be NULL terminated. + /// + /// If `asset_resolver` is provided, may be NULL. + /// + /// If both `asset_resolver` and `assets_path` are provided, the + /// `asset_resolver` comes first in the asset search order, then + /// `assets_path`. This effectively makes `asset_resolver` an overlay over + /// `assets_path`. const char* assets_path; /// The path to the Dart file containing the `main` entry point. /// The string can be collected after the call to `FlutterEngineRun` returns. @@ -1589,6 +1667,16 @@ typedef struct { // // The first argument is the `user_data` from `FlutterEngineInitialize`. OnPreEngineRestartCallback on_pre_engine_restart_callback; + + /// An asset resolver that fetches assets for the Engine. + /// + /// If `assets_path` is provided, may be NULL. + /// + /// If both `asset_resolver` and `assets_path` are provided, the + /// `asset_resolver` comes first in the asset search order, then + /// `assets_path`. This effectively makes `asset_resolver` an overlay over + /// `assets_path`. + const FlutterEngineAssetResolver* asset_resolver; } FlutterProjectArgs; #ifndef FLUTTER_ENGINE_NO_PROTOTYPES @@ -2251,6 +2339,52 @@ FlutterEngineResult FlutterEngineNotifyDisplayUpdate( const FlutterEngineDisplay* displays, size_t display_count); +//------------------------------------------------------------------------------ +/// @brief Creates a `FlutterMapping` using the given creation info. +/// The returned mapping is owned by the Embedder until passed to +/// the Engine. +/// +/// When owned by the Engine: +/// +/// The backing memory is never mutated by the Engine, and must not +/// be mutated by the Embedder until the mapping is destroyed. +/// The Engine may access the mapping from multiple threads, and may +/// destroy the mapping from any thread. +/// +/// @param[in] create_info Struct describing the mapping to create. +/// @param[out] out_mapping The created mapping, if successful. +/// +/// @return Whether the mapping was successfully created. +/// +FLUTTER_EXPORT +FlutterEngineResult FlutterEngineCreateMapping( + const FlutterMappingCreateInfo* create_info, + FlutterMapping* out_mapping); + +//------------------------------------------------------------------------------ +/// @brief Destroys a `FlutterMapping` owned by the Embedder. +/// This may be called by any thread, as long as the Engine doesn't +/// own the mapping. +/// +/// @param[in] mapping The mapping owned by the Embedder to destroy. +/// +FLUTTER_EXPORT +void FlutterEngineDestroyMapping(FlutterMapping mapping); + +//------------------------------------------------------------------------------ +/// @brief Gets the data and size of a `FlutterMapping`. +/// The mapping must be owned by the Embedder. +/// +/// @param[in] mapping The mapping to get the data and size of. +/// @param[out] out_size Returns the mapping's size, if provided. +/// May be NULL. +/// +/// @return The address to the mapping's data. +/// +FLUTTER_EXPORT +const uint8_t* FlutterEngineGetMappingData(FlutterMapping mapping, + size_t* out_size); + #endif // !FLUTTER_ENGINE_NO_PROTOTYPES // Typedefs for the function pointers in FlutterEngineProcTable. @@ -2367,6 +2501,13 @@ typedef FlutterEngineResult (*FlutterEngineNotifyDisplayUpdateFnPtr)( FlutterEngineDisplaysUpdateType update_type, const FlutterEngineDisplay* displays, size_t display_count); +typedef FlutterEngineResult (*FlutterEngineCreateMappingFnPtr)( + const FlutterMappingCreateInfo* create_info, + FlutterMapping* out_mapping); +typedef void (*FlutterEngineDestroyMappingFnPtr)(FlutterMapping mapping); +typedef const uint8_t* (*FlutterEngineGetMappingDataFnPtr)( + FlutterMapping mapping, + size_t* out_size); /// Function-pointer-based versions of the APIs above. typedef struct { @@ -2411,6 +2552,9 @@ typedef struct { FlutterEnginePostCallbackOnAllNativeThreadsFnPtr PostCallbackOnAllNativeThreads; FlutterEngineNotifyDisplayUpdateFnPtr NotifyDisplayUpdate; + FlutterEngineCreateMappingFnPtr CreateMapping; + FlutterEngineDestroyMappingFnPtr DestroyMapping; + FlutterEngineGetMappingDataFnPtr GetMappingData; } FlutterEngineProcTable; //------------------------------------------------------------------------------ diff --git a/shell/platform/embedder/embedder_asset_resolver.cc b/shell/platform/embedder/embedder_asset_resolver.cc new file mode 100644 index 0000000000000..6000f06e3ce12 --- /dev/null +++ b/shell/platform/embedder/embedder_asset_resolver.cc @@ -0,0 +1,49 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "flutter/shell/platform/embedder/embedder_asset_resolver.h" +#include "flutter/shell/platform/embedder/embedder_struct_macros.h" + +namespace flutter { + +class EmbedderAssetResolver final : public flutter::AssetResolver { + public: + explicit EmbedderAssetResolver(EmbedderAssetResolverGetAsset get_asset) + : get_asset_(get_asset) {} + + ~EmbedderAssetResolver() = default; + + // |AssetResolver| + bool IsValid() const override { return (bool)get_asset_; } + + // |AssetResolver| + bool IsValidAfterAssetManagerChange() const override { return true; } + + // |AssetResolver| + AssetResolverType GetType() const override { + return AssetResolverType::kEmbedderAssetResolver; + } + + // |AssetResolver| + std::unique_ptr GetAsMapping( + const std::string& asset_name) const override { + if (!get_asset_) { + return nullptr; + } + + return get_asset_(asset_name.c_str()); + } + + private: + EmbedderAssetResolverGetAsset get_asset_; + + FML_DISALLOW_COPY_AND_ASSIGN(EmbedderAssetResolver); +}; + +std::unique_ptr CreateEmbedderAssetResolver( + EmbedderAssetResolverGetAsset get_asset) { + return std::make_unique(std::move(get_asset)); +} + +} // namespace flutter diff --git a/shell/platform/embedder/embedder_asset_resolver.h b/shell/platform/embedder/embedder_asset_resolver.h new file mode 100644 index 0000000000000..df73ee8baad25 --- /dev/null +++ b/shell/platform/embedder/embedder_asset_resolver.h @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_EMBEDDER_EMBEDDER_ASSET_RESOLVER_H_ +#define FLUTTER_SHELL_PLATFORM_EMBEDDER_EMBEDDER_ASSET_RESOLVER_H_ + +#include + +#include "flutter/assets/asset_resolver.h" +#include "flutter/shell/platform/embedder/embedder.h" + +namespace flutter { + +using EmbedderAssetResolverGetAsset = + std::function(const char* /* asset_name */)>; + +std::unique_ptr CreateEmbedderAssetResolver( + EmbedderAssetResolverGetAsset get_asset); + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_EMBEDDER_EMBEDDER_ASSET_RESOLVER_H_ diff --git a/shell/platform/embedder/fixtures/main.dart b/shell/platform/embedder/fixtures/main.dart index 5196f37727597..7649378e75144 100644 --- a/shell/platform/embedder/fixtures/main.dart +++ b/shell/platform/embedder/fixtures/main.dart @@ -299,6 +299,28 @@ void null_platform_messages() { signalNativeTest(); } +const String channel = 'flutter/assets'; +ByteData encodePath(String path) => ByteData.sublistView(utf8.encoder.convert(path)); + +void handleAssetReply(ByteData? reply) { + if (reply != null) { + signalNativeCount(reply.lengthInBytes); + signalNativeMessage(utf8.decode(Uint8List.sublistView(reply))); + } else { + signalNativeCount(-1); + } +} + +@pragma('vm:entry-point') +void existing_asset() { + PlatformDispatcher.instance.sendPlatformMessage(channel, encodePath('existing_asset'), handleAssetReply); +} + +@pragma('vm:entry-point') +void invalid_asset() { + PlatformDispatcher.instance.sendPlatformMessage(channel, encodePath('invalid_asset'), handleAssetReply); +} + Picture CreateSimplePicture() { Paint blackPaint = Paint(); PictureRecorder baseRecorder = PictureRecorder(); diff --git a/shell/platform/embedder/tests/embedder_config_builder.cc b/shell/platform/embedder/tests/embedder_config_builder.cc index d04188e0d3070..95bf7c5269fa4 100644 --- a/shell/platform/embedder/tests/embedder_config_builder.cc +++ b/shell/platform/embedder/tests/embedder_config_builder.cc @@ -174,6 +174,12 @@ void EmbedderConfigBuilder::SetAssetsPath() { project_args_.assets_path = context_.GetAssetsPath().c_str(); } +void EmbedderConfigBuilder::SetAssetResolver( + const FlutterEngineAssetResolver* resolver) { + asset_resolver_ = *resolver; + project_args_.asset_resolver = &asset_resolver_; +} + void EmbedderConfigBuilder::SetSnapshots() { if (auto mapping = context_.GetVMSnapshotData()) { project_args_.vm_snapshot_data = mapping->GetMapping(); diff --git a/shell/platform/embedder/tests/embedder_config_builder.h b/shell/platform/embedder/tests/embedder_config_builder.h index ee1fb514bb611..1938299d2d390 100644 --- a/shell/platform/embedder/tests/embedder_config_builder.h +++ b/shell/platform/embedder/tests/embedder_config_builder.h @@ -65,6 +65,8 @@ class EmbedderConfigBuilder { void SetAssetsPath(); + void SetAssetResolver(const FlutterEngineAssetResolver* resolver); + void SetSnapshots(); void SetAOTDataElf(); @@ -124,6 +126,7 @@ class EmbedderConfigBuilder { std::string dart_entrypoint_; FlutterCustomTaskRunners custom_task_runners_ = {}; FlutterCompositor compositor_ = {}; + FlutterEngineAssetResolver asset_resolver_ = {}; std::vector command_line_arguments_; std::vector dart_entrypoint_arguments_; std::string log_tag_; diff --git a/shell/platform/embedder/tests/embedder_unittests.cc b/shell/platform/embedder/tests/embedder_unittests.cc index 3108cff91e200..390d017b45708 100644 --- a/shell/platform/embedder/tests/embedder_unittests.cc +++ b/shell/platform/embedder/tests/embedder_unittests.cc @@ -474,6 +474,107 @@ TEST_F(EmbedderTest, InvalidPlatformMessages) { ASSERT_EQ(result, kInvalidArguments); } +//------------------------------------------------------------------------------ +/// Tests that an asset can be loaded from a custom asset resolver. +/// +TEST_F(EmbedderTest, CustomAssetResolverReturnsValidAsset) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kSoftwareContext); + EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); + builder.SetDartEntrypoint("existing_asset"); + + fml::AutoResetWaitableEvent destroy; + + FlutterEngineAssetResolver resolver = {}; + resolver.struct_size = sizeof(FlutterEngineAssetResolver); + resolver.user_data = reinterpret_cast(&destroy); + resolver.get_asset = [](const char* asset, + void* user_data) -> FlutterMapping { + if (strcmp(asset, "existing_asset") == 0) { + // Return an asset with the string "hello" as its contents. + // When the mapping is destroyed (on GC or Engine shutdown) the + // `destroy` event is signaled. + FlutterMappingCreateInfo create_info = {}; + FlutterMapping out = nullptr; + create_info.struct_size = sizeof(FlutterMappingCreateInfo); + create_info.data = reinterpret_cast("hello"); + create_info.data_size = 5; + create_info.user_data = user_data; + create_info.destruction_callback = [](const uint8_t* data, size_t size, + void* user_data) { + reinterpret_cast(user_data)->Signal(); + }; + auto result = FlutterEngineCreateMapping(&create_info, &out); + EXPECT_EQ(result, kSuccess); + return out; + } + return nullptr; + }; + + builder.SetAssetResolver(&resolver); + + fml::AutoResetWaitableEvent message; + context.AddNativeCallback( + "SignalNativeCount", CREATE_NATIVE_ENTRY([](Dart_NativeArguments args) { + auto count = tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 0)); + // Dart should receive a 5 byte message reply. + EXPECT_EQ(5, count); + })); + context.AddNativeCallback( + "SignalNativeMessage", + CREATE_NATIVE_ENTRY(([&message](Dart_NativeArguments args) { + auto received_message = tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 0)); + // The message received from the asset channel should be the string + // "hello". + EXPECT_EQ("hello", received_message); + message.Signal(); + }))); + + { + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + message.Wait(); + } + // Before or during Engine shutdown, the mapping should be destroyed. + destroy.Wait(); +} + +//------------------------------------------------------------------------------ +/// Tests that an asset that aren't found return null responses. +/// +TEST_F(EmbedderTest, CustomAssetResolverReturnsInvalidAsset) { + auto& context = GetEmbedderContext(EmbedderTestContextType::kSoftwareContext); + EmbedderConfigBuilder builder(context); + builder.SetSoftwareRendererConfig(); + builder.SetDartEntrypoint("invalid_asset"); + + FlutterEngineAssetResolver resolver = {}; + resolver.struct_size = sizeof(FlutterEngineAssetResolver); + resolver.get_asset = [](const char* asset, + void* user_data) -> FlutterMapping { + return nullptr; + }; + + builder.SetAssetResolver(&resolver); + + fml::AutoResetWaitableEvent message; + context.AddNativeCallback( + "SignalNativeCount", + CREATE_NATIVE_ENTRY([&message](Dart_NativeArguments args) { + auto count = tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 0)); + // Dart should receive a null reply. + EXPECT_EQ(-1, count); + message.Signal(); + })); + + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + message.Wait(); +} + //------------------------------------------------------------------------------ /// Tests that setting a custom log callback works as expected and defaults to /// using tag "flutter".