diff --git a/fml/closure.h b/fml/closure.h index 9a8598cdc52..ca7f60fdbbd 100644 --- a/fml/closure.h +++ b/fml/closure.h @@ -31,6 +31,8 @@ using closure = std::function; /// class ScopedCleanupClosure { public: + ScopedCleanupClosure() = default; + ScopedCleanupClosure(const fml::closure& closure) : closure_(closure) {} ~ScopedCleanupClosure() { @@ -39,6 +41,12 @@ class ScopedCleanupClosure { } } + fml::closure SetClosure(const fml::closure& closure) { + auto old_closure = closure_; + closure_ = closure; + return old_closure; + } + fml::closure Release() { fml::closure closure = closure_; closure_ = nullptr; diff --git a/shell/common/shell_unittests.cc b/shell/common/shell_unittests.cc index d1e78eb6925..02b0edf6e07 100644 --- a/shell/common/shell_unittests.cc +++ b/shell/common/shell_unittests.cc @@ -1050,67 +1050,6 @@ TEST_F(ShellTest, Screenshot) { DestroyShell(std::move(shell)); } -enum class MemsetPatternOp { - kMemsetPatternOpSetBuffer, - kMemsetPatternOpCheckBuffer, -}; - -//------------------------------------------------------------------------------ -/// @brief Depending on the operation, either scribbles a known pattern -/// into the buffer or checks if that pattern is present in an -/// existing buffer. This is a portable variant of the -/// memset_pattern class of methods that also happen to do assert -/// that the same pattern exists. -/// -/// @param buffer The buffer -/// @param[in] size The size -/// @param[in] op The operation -/// -/// @return If the result of the operation was a success. -/// -static bool MemsetPatternSetOrCheck(uint8_t* buffer, - size_t size, - MemsetPatternOp op) { - if (buffer == nullptr) { - return false; - } - - auto pattern = reinterpret_cast("dErP"); - constexpr auto pattern_length = 4; - - uint8_t* start = buffer; - uint8_t* p = buffer; - - while ((start + size) - p >= pattern_length) { - switch (op) { - case MemsetPatternOp::kMemsetPatternOpSetBuffer: - memmove(p, pattern, pattern_length); - break; - case MemsetPatternOp::kMemsetPatternOpCheckBuffer: - if (memcmp(pattern, p, pattern_length) != 0) { - return false; - } - break; - }; - p += pattern_length; - } - - if ((start + size) - p != 0) { - switch (op) { - case MemsetPatternOp::kMemsetPatternOpSetBuffer: - memmove(p, pattern, (start + size) - p); - break; - case MemsetPatternOp::kMemsetPatternOpCheckBuffer: - if (memcmp(pattern, p, (start + size) - p) != 0) { - return false; - } - break; - } - } - - return true; -} - TEST_F(ShellTest, CanConvertToAndFromMappings) { const size_t buffer_size = 2 << 20; diff --git a/shell/platform/embedder/embedder.cc b/shell/platform/embedder/embedder.cc index 36a4785954e..bbde2d6c5a2 100644 --- a/shell/platform/embedder/embedder.cc +++ b/shell/platform/embedder/embedder.cc @@ -8,8 +8,10 @@ #include #include "flutter/fml/build_config.h" +#include "flutter/fml/closure.h" #include "flutter/fml/make_copyable.h" #include "flutter/fml/native_library.h" +#include "third_party/dart/runtime/include/dart_native_api.h" #if OS_WIN #define FLUTTER_EXPORT __declspec(dllexport) @@ -1583,3 +1585,128 @@ FlutterEngineResult FlutterEngineUpdateLocales(FLUTTER_API_SYMBOL(FlutterEngine) bool FlutterEngineRunsAOTCompiledDartCode(void) { return flutter::DartVM::IsRunningPrecompiledCode(); } + +FlutterEngineResult FlutterEnginePostDartObject( + FLUTTER_API_SYMBOL(FlutterEngine) engine, + FlutterEngineDartPort port, + const FlutterEngineDartObject* object) { + if (engine == nullptr) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, "Invalid engine handle."); + } + + if (!reinterpret_cast(engine)->IsValid()) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, "Engine not running."); + } + + if (port == ILLEGAL_PORT) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, + "Attempted to post to an illegal port."); + } + + if (object == nullptr) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, + "Invalid Dart object to post."); + } + + Dart_CObject dart_object = {}; + fml::ScopedCleanupClosure typed_data_finalizer; + + switch (object->type) { + case kFlutterEngineDartObjectTypeNull: + dart_object.type = Dart_CObject_kNull; + break; + case kFlutterEngineDartObjectTypeBool: + dart_object.type = Dart_CObject_kBool; + dart_object.value.as_bool = object->bool_value; + break; + case kFlutterEngineDartObjectTypeInt32: + dart_object.type = Dart_CObject_kInt32; + dart_object.value.as_int32 = object->int32_value; + break; + case kFlutterEngineDartObjectTypeInt64: + dart_object.type = Dart_CObject_kInt64; + dart_object.value.as_int64 = object->int64_value; + break; + case kFlutterEngineDartObjectTypeDouble: + dart_object.type = Dart_CObject_kDouble; + dart_object.value.as_double = object->double_value; + break; + case kFlutterEngineDartObjectTypeString: + if (object->string_value == nullptr) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, + "kFlutterEngineDartObjectTypeString must be " + "a null terminated string but was null."); + } + dart_object.type = Dart_CObject_kString; + dart_object.value.as_string = const_cast(object->string_value); + break; + case kFlutterEngineDartObjectTypeBuffer: { + auto* buffer = SAFE_ACCESS(object->buffer_value, buffer, nullptr); + if (buffer == nullptr) { + return LOG_EMBEDDER_ERROR(kInvalidArguments, + "kFlutterEngineDartObjectTypeBuffer must " + "specify a buffer but found nullptr."); + } + auto buffer_size = SAFE_ACCESS(object->buffer_value, buffer_size, 0); + auto callback = + SAFE_ACCESS(object->buffer_value, buffer_collect_callback, nullptr); + auto user_data = SAFE_ACCESS(object->buffer_value, user_data, nullptr); + + // The the user has provided a callback, let them manage the lifecycle of + // the underlying data. If not, copy it out from the provided buffer. + + if (callback == nullptr) { + dart_object.type = Dart_CObject_kTypedData; + dart_object.value.as_typed_data.type = Dart_TypedData_kUint8; + dart_object.value.as_typed_data.length = buffer_size; + dart_object.value.as_typed_data.values = buffer; + } else { + struct ExternalTypedDataPeer { + void* user_data = nullptr; + VoidCallback trampoline = nullptr; + }; + auto peer = new ExternalTypedDataPeer(); + peer->user_data = user_data; + peer->trampoline = callback; + // This finalizer is set so that in case of failure of the + // Dart_PostCObject below, we collect the peer. The embedder is still + // responsible for collecting the buffer in case of non-kSuccess returns + // from this method. This finalizer must be released in case of kSuccess + // returns from this method. + typed_data_finalizer.SetClosure([peer]() { + // This is the tiny object we use as the peer to the Dart call so that + // we can attach the a trampoline to the embedder supplied callback. + // In case of error, we need to collect this object lest we introduce + // a tiny leak. + delete peer; + }); + dart_object.type = Dart_CObject_kExternalTypedData; + dart_object.value.as_external_typed_data.type = Dart_TypedData_kUint8; + dart_object.value.as_external_typed_data.length = buffer_size; + dart_object.value.as_external_typed_data.data = buffer; + dart_object.value.as_external_typed_data.peer = peer; + dart_object.value.as_external_typed_data.callback = + +[](void* unused_isolate_callback_data, + Dart_WeakPersistentHandle unused_handle, void* peer) { + auto typed_peer = reinterpret_cast(peer); + typed_peer->trampoline(typed_peer->user_data); + delete typed_peer; + }; + } + } break; + default: + return LOG_EMBEDDER_ERROR( + kInvalidArguments, + "Invalid FlutterEngineDartObjectType type specified."); + } + + if (!Dart_PostCObject(port, &dart_object)) { + return LOG_EMBEDDER_ERROR(kInternalInconsistency, + "Could not post the object to the Dart VM."); + } + + // On a successful call, the VM takes ownership of and is responsible for + // invoking the finalizer. + typed_data_finalizer.Release(); + return kSuccess; +} diff --git a/shell/platform/embedder/embedder.h b/shell/platform/embedder/embedder.h index 95724c423c2..5c11b403a75 100644 --- a/shell/platform/embedder/embedder.h +++ b/shell/platform/embedder/embedder.h @@ -861,6 +861,79 @@ typedef struct { const char* variant_code; } FlutterLocale; +typedef int64_t FlutterEngineDartPort; + +typedef enum { + kFlutterEngineDartObjectTypeNull, + kFlutterEngineDartObjectTypeBool, + kFlutterEngineDartObjectTypeInt32, + kFlutterEngineDartObjectTypeInt64, + kFlutterEngineDartObjectTypeDouble, + kFlutterEngineDartObjectTypeString, + /// The object will be made available to Dart code as an instance of + /// Uint8List. + kFlutterEngineDartObjectTypeBuffer, +} FlutterEngineDartObjectType; + +typedef struct { + /// The size of this struct. Must be sizeof(FlutterEngineDartBuffer). + size_t struct_size; + /// An opaque baton passed back to the embedder when the + /// buffer_collect_callback is invoked. The engine does not interpret this + /// field in any way. + void* user_data; + /// This is an optional field. + /// + /// When specified, the engine will assume that the buffer is owned by the + /// embedder. When the data is no longer needed by any isolate, this callback + /// will be made on an internal engine managed thread. The embedder is free to + /// collect the buffer here. When this field is specified, it is the embedders + /// responsibility to keep the buffer alive and not modify it till this + /// callback is invoked by the engine. The user data specified in the callback + /// is the value of `user_data` field in this struct. + /// + /// When NOT specified, the VM creates an internal copy of the buffer. The + /// caller is free to modify the buffer as necessary or collect it immediately + /// after the call to `FlutterEnginePostDartObject`. + /// + /// @attention The buffer_collect_callback is will only be invoked by the + /// engine when the `FlutterEnginePostDartObject` method + /// returns kSuccess. In case of non-successful calls to this + /// method, it is the embedders responsibility to collect the + /// buffer. + VoidCallback buffer_collect_callback; + /// A pointer to the bytes of the buffer. When the buffer is owned by the + /// embedder (by specifying the `buffer_collect_callback`), Dart code may + /// modify that embedder owned buffer. For this reason, it is important that + /// this buffer not have page protections that restrict writing to this + /// buffer. + uint8_t* buffer; + /// The size of the buffer. + size_t buffer_size; +} FlutterEngineDartBuffer; + +/// This struct specifies the native representation of a Dart object that can be +/// sent via a send port to any isolate in the VM that has the corresponding +/// receive port. +/// +/// All fields in this struct are copied out in the call to +/// `FlutterEnginePostDartObject` and the caller is free to reuse or collect +/// this struct after that call. +typedef struct { + FlutterEngineDartObjectType type; + union { + bool bool_value; + int32_t int32_value; + int64_t int64_value; + double double_value; + /// A null terminated string. This string will be copied by the VM in the + /// call to `FlutterEnginePostDartObject` and must be collected by the + /// embedder after that call is made. + const char* string_value; + const FlutterEngineDartBuffer* buffer_value; + }; +} FlutterEngineDartObject; + typedef struct { /// The size of this struct. Must be sizeof(FlutterProjectArgs). size_t struct_size; @@ -1535,6 +1608,37 @@ FlutterEngineResult FlutterEngineUpdateLocales(FLUTTER_API_SYMBOL(FlutterEngine) FLUTTER_EXPORT bool FlutterEngineRunsAOTCompiledDartCode(void); +//------------------------------------------------------------------------------ +/// @brief Posts a Dart object to specified send port. The corresponding +/// receive port for send port can be in any isolate running in the +/// VM. This isolate can also be the root isolate for an +/// unrelated engine. The engine parameter is necessary only to +/// ensure the call is not made when no engine (and hence no VM) is +/// running. +/// +/// Unlike the platform messages mechanism, there are no threading +/// restrictions when using this API. Message can be posted on any +/// thread and they will be made available to isolate on which the +/// corresponding send port is listening. +/// +/// However, it is the embedders responsibility to ensure that the +/// call is not made during an ongoing call the +/// `FlutterEngineDeinitialize` or `FlutterEngineShutdown` on +/// another thread. +/// +/// @param[in] engine. A running engine instance. +/// @param[in] port The send port to send the object to. +/// @param[in] object The object to send to the isolate with the +/// corresponding receive port. +/// +/// @return If the message was posted to the send port. +/// +FLUTTER_EXPORT +FlutterEngineResult FlutterEnginePostDartObject( + FLUTTER_API_SYMBOL(FlutterEngine) engine, + FlutterEngineDartPort port, + const FlutterEngineDartObject* object); + #if defined(__cplusplus) } // extern "C" #endif diff --git a/shell/platform/embedder/fixtures/main.dart b/shell/platform/embedder/fixtures/main.dart index a4f08a7bb5d..72430ebd086 100644 --- a/shell/platform/embedder/fixtures/main.dart +++ b/shell/platform/embedder/fixtures/main.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:typed_data'; import 'dart:ui'; +import 'dart:isolate'; +import 'dart:ffi'; import 'dart:core'; import 'dart:convert'; @@ -659,3 +661,14 @@ void scene_builder_with_clips() { }; window.scheduleFrame(); } + + +void sendObjectToNativeCode(dynamic object) native 'SendObjectToNativeCode'; + +@pragma('vm:entry-point') +void objects_can_be_posted() { + ReceivePort port = ReceivePort(); + port.listen((dynamic message){ sendObjectToNativeCode(message); }); + signalNativeCount(port.sendPort.nativePort); +} + diff --git a/shell/platform/embedder/tests/embedder_unittests.cc b/shell/platform/embedder/tests/embedder_unittests.cc index f4c57ccd6f0..6a62563471c 100644 --- a/shell/platform/embedder/tests/embedder_unittests.cc +++ b/shell/platform/embedder/tests/embedder_unittests.cc @@ -3606,5 +3606,204 @@ TEST_F(EmbedderTest, ClipsAreCorrectlyCalculated) { latch.Wait(); } +TEST_F(EmbedderTest, ObjectsCanBePostedViaPorts) { + auto& context = GetEmbedderContext(); + EmbedderConfigBuilder builder(context); + builder.SetOpenGLRendererConfig(SkISize::Make(800, 1024)); + builder.SetDartEntrypoint("objects_can_be_posted"); + + // Synchronously acquire the send port from the Dart end. We will be using + // this to send message. The Dart end will just echo those messages back to us + // for inspection. + FlutterEngineDartPort port = 0; + fml::AutoResetWaitableEvent event; + context.AddNativeCallback("SignalNativeCount", + CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + port = tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 0)); + event.Signal(); + })); + auto engine = builder.LaunchEngine(); + ASSERT_TRUE(engine.is_valid()); + event.Wait(); + ASSERT_NE(port, 0); + + using Trampoline = std::function; + Trampoline trampoline; + + context.AddNativeCallback("SendObjectToNativeCode", + CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) { + FML_CHECK(trampoline); + auto trampoline_copy = trampoline; + trampoline = nullptr; + trampoline_copy(Dart_GetNativeArgument(args, 0)); + })); + + // Check null. + { + FlutterEngineDartObject object = {}; + object.type = kFlutterEngineDartObjectTypeNull; + trampoline = [&](Dart_Handle handle) { + ASSERT_TRUE(Dart_IsNull(handle)); + event.Signal(); + }; + ASSERT_EQ(FlutterEnginePostDartObject(engine.get(), port, &object), + kSuccess); + event.Wait(); + } + + // Check bool. + { + FlutterEngineDartObject object = {}; + object.type = kFlutterEngineDartObjectTypeBool; + object.bool_value = true; + trampoline = [&](Dart_Handle handle) { + ASSERT_TRUE(tonic::DartConverter::FromDart(handle)); + event.Signal(); + }; + ASSERT_EQ(FlutterEnginePostDartObject(engine.get(), port, &object), + kSuccess); + event.Wait(); + } + + // Check int32. + { + FlutterEngineDartObject object = {}; + object.type = kFlutterEngineDartObjectTypeInt32; + object.int32_value = 1988; + trampoline = [&](Dart_Handle handle) { + ASSERT_EQ(tonic::DartConverter::FromDart(handle), 1988); + event.Signal(); + }; + ASSERT_EQ(FlutterEnginePostDartObject(engine.get(), port, &object), + kSuccess); + event.Wait(); + } + + // Check int64. + { + FlutterEngineDartObject object = {}; + object.type = kFlutterEngineDartObjectTypeInt64; + object.int64_value = 1988; + trampoline = [&](Dart_Handle handle) { + ASSERT_EQ(tonic::DartConverter::FromDart(handle), 1988); + event.Signal(); + }; + ASSERT_EQ(FlutterEnginePostDartObject(engine.get(), port, &object), + kSuccess); + event.Wait(); + } + + // Check double. + { + FlutterEngineDartObject object = {}; + object.type = kFlutterEngineDartObjectTypeDouble; + object.double_value = 1988.0; + trampoline = [&](Dart_Handle handle) { + ASSERT_DOUBLE_EQ(tonic::DartConverter::FromDart(handle), 1988.0); + event.Signal(); + }; + ASSERT_EQ(FlutterEnginePostDartObject(engine.get(), port, &object), + kSuccess); + event.Wait(); + } + + // Check string. + { + const char* message = "Hello. My name is Inigo Montoya."; + FlutterEngineDartObject object = {}; + object.type = kFlutterEngineDartObjectTypeString; + object.string_value = message; + trampoline = [&](Dart_Handle handle) { + ASSERT_EQ(tonic::DartConverter::FromDart(handle), + std::string{message}); + event.Signal(); + }; + ASSERT_EQ(FlutterEnginePostDartObject(engine.get(), port, &object), + kSuccess); + event.Wait(); + } + + // Check buffer (copied out). + { + std::vector message; + message.resize(1988); + + ASSERT_TRUE(MemsetPatternSetOrCheck( + message, MemsetPatternOp::kMemsetPatternOpSetBuffer)); + + FlutterEngineDartBuffer buffer = {}; + + buffer.struct_size = sizeof(buffer); + buffer.user_data = nullptr; + buffer.buffer_collect_callback = nullptr; + buffer.buffer = message.data(); + buffer.buffer_size = message.size(); + + FlutterEngineDartObject object = {}; + object.type = kFlutterEngineDartObjectTypeBuffer; + object.buffer_value = &buffer; + trampoline = [&](Dart_Handle handle) { + intptr_t length = 0; + Dart_ListLength(handle, &length); + ASSERT_EQ(length, 1988); + // TODO(chinmaygarde); The std::vector specialization for + // DartConvertor in tonic is broken which is preventing the buffer + // being checked here. Fix tonic and strengthen this check. For now, just + // the buffer length is checked. + event.Signal(); + }; + ASSERT_EQ(FlutterEnginePostDartObject(engine.get(), port, &object), + kSuccess); + event.Wait(); + } + + std::vector message; + fml::AutoResetWaitableEvent buffer_released_latch; + + // Check buffer (caller own buffer with zero copy transfer). + { + message.resize(1988); + + ASSERT_TRUE(MemsetPatternSetOrCheck( + message, MemsetPatternOp::kMemsetPatternOpSetBuffer)); + + FlutterEngineDartBuffer buffer = {}; + + buffer.struct_size = sizeof(buffer); + buffer.user_data = &buffer_released_latch; + buffer.buffer_collect_callback = +[](void* user_data) { + reinterpret_cast(user_data)->Signal(); + }; + buffer.buffer = message.data(); + buffer.buffer_size = message.size(); + + FlutterEngineDartObject object = {}; + object.type = kFlutterEngineDartObjectTypeBuffer; + object.buffer_value = &buffer; + trampoline = [&](Dart_Handle handle) { + intptr_t length = 0; + Dart_ListLength(handle, &length); + ASSERT_EQ(length, 1988); + // TODO(chinmaygarde); The std::vector specialization for + // DartConvertor in tonic is broken which is preventing the buffer + // being checked here. Fix tonic and strengthen this check. For now, just + // the buffer length is checked. + event.Signal(); + }; + ASSERT_EQ(FlutterEnginePostDartObject(engine.get(), port, &object), + kSuccess); + event.Wait(); + } + + engine.reset(); + + // We cannot determine when the VM will GC objects that might have external + // typed data finalizers. Since we need to ensure that we correctly wired up + // finalizers from the embedders, we force the VM to collect all objects but + // just shutting it down. + buffer_released_latch.Wait(); +} + } // namespace testing } // namespace flutter diff --git a/testing/testing.cc b/testing/testing.cc index b5e229f06bb..95852ed149e 100644 --- a/testing/testing.cc +++ b/testing/testing.cc @@ -53,5 +53,50 @@ std::unique_ptr OpenFixtureAsMapping(std::string fixture_name) { return fml::FileMapping::CreateReadOnly(OpenFixture(fixture_name)); } +bool MemsetPatternSetOrCheck(uint8_t* buffer, size_t size, MemsetPatternOp op) { + if (buffer == nullptr) { + return false; + } + + auto pattern = reinterpret_cast("dErP"); + constexpr auto pattern_length = 4; + + uint8_t* start = buffer; + uint8_t* p = buffer; + + while ((start + size) - p >= pattern_length) { + switch (op) { + case MemsetPatternOp::kMemsetPatternOpSetBuffer: + memmove(p, pattern, pattern_length); + break; + case MemsetPatternOp::kMemsetPatternOpCheckBuffer: + if (memcmp(pattern, p, pattern_length) != 0) { + return false; + } + break; + }; + p += pattern_length; + } + + if ((start + size) - p != 0) { + switch (op) { + case MemsetPatternOp::kMemsetPatternOpSetBuffer: + memmove(p, pattern, (start + size) - p); + break; + case MemsetPatternOp::kMemsetPatternOpCheckBuffer: + if (memcmp(pattern, p, (start + size) - p) != 0) { + return false; + } + break; + } + } + + return true; +} + +bool MemsetPatternSetOrCheck(std::vector& buffer, MemsetPatternOp op) { + return MemsetPatternSetOrCheck(buffer.data(), buffer.size(), op); +} + } // namespace testing } // namespace flutter diff --git a/testing/testing.h b/testing/testing.h index da8571b18b7..79d407e2faa 100644 --- a/testing/testing.h +++ b/testing/testing.h @@ -6,6 +6,7 @@ #define TESTING_TESTING_H_ #include +#include #include "flutter/fml/file.h" #include "flutter/fml/mapping.h" @@ -62,6 +63,28 @@ std::unique_ptr OpenFixtureAsMapping(std::string fixture_name); /// std::string GetCurrentTestName(); +enum class MemsetPatternOp { + kMemsetPatternOpSetBuffer, + kMemsetPatternOpCheckBuffer, +}; + +//------------------------------------------------------------------------------ +/// @brief Depending on the operation, either scribbles a known pattern +/// into the buffer or checks if that pattern is present in an +/// existing buffer. This is a portable variant of the +/// memset_pattern class of methods that also happen to do assert +/// that the same pattern exists. +/// +/// @param buffer The buffer +/// @param[in] size The size +/// @param[in] op The operation +/// +/// @return If the result of the operation was a success. +/// +bool MemsetPatternSetOrCheck(uint8_t* buffer, size_t size, MemsetPatternOp op); + +bool MemsetPatternSetOrCheck(std::vector& buffer, MemsetPatternOp op); + } // namespace testing } // namespace flutter