diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index cb1e2777b1bd4..08cc4ae5d4c5e 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -98,6 +98,8 @@ FILE: ../../../flutter/fml/command_line_unittest.cc FILE: ../../../flutter/fml/compiler_specific.h FILE: ../../../flutter/fml/concurrent_message_loop.cc FILE: ../../../flutter/fml/concurrent_message_loop.h +FILE: ../../../flutter/fml/dart/dart_converter.cc +FILE: ../../../flutter/fml/dart/dart_converter.h FILE: ../../../flutter/fml/delayed_task.cc FILE: ../../../flutter/fml/delayed_task.h FILE: ../../../flutter/fml/eintr_wrapper.h diff --git a/fml/BUILD.gn b/fml/BUILD.gn index 32f58eddf6056..68b9725fa6b2e 100644 --- a/fml/BUILD.gn +++ b/fml/BUILD.gn @@ -249,8 +249,9 @@ executable("fml_unittests") { deps = [ ":fml_fixtures", "$flutter_root/fml", + "$flutter_root/fml/dart", + "$flutter_root/runtime:libdart", "$flutter_root/testing", - "//third_party/dart/runtime:libdart_jit", ] } diff --git a/fml/dart/BUILD.gn b/fml/dart/BUILD.gn new file mode 100644 index 0000000000000..c6b3883955e81 --- /dev/null +++ b/fml/dart/BUILD.gn @@ -0,0 +1,19 @@ +# 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. + +# Utilities for working with FML types with the Dart API. Targets that use FML +# as well as Dart must explicitly include this target as FML itself cannot +# depend on Dart. +source_set("dart") { + sources = [ + "dart_converter.cc", + "dart_converter.h", + ] + + public_deps = [ + "$flutter_root/fml", + "$flutter_root/runtime:libdart", + "//third_party/tonic", + ] +} diff --git a/fml/dart/dart_converter.cc b/fml/dart/dart_converter.cc new file mode 100644 index 0000000000000..7e17bd8bb3c30 --- /dev/null +++ b/fml/dart/dart_converter.cc @@ -0,0 +1,11 @@ +// 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/fml/dart/dart_converter.h" + +namespace fml { + +// + +} // namespace fml diff --git a/fml/dart/dart_converter.h b/fml/dart/dart_converter.h new file mode 100644 index 0000000000000..159ec44e25c31 --- /dev/null +++ b/fml/dart/dart_converter.h @@ -0,0 +1,123 @@ +// 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_FML_DART_DART_CONVERTER_H_ +#define FLUTTER_FML_DART_DART_CONVERTER_H_ + +#include +#include + +#include "flutter/fml/mapping.h" +#include "third_party/dart/runtime/include/dart_api.h" +#include "third_party/tonic/converter/dart_converter.h" + +namespace tonic { + +using DartConverterMapping = std::unique_ptr; + +template <> +struct DartConverter { + static Dart_Handle ToDart(const DartConverterMapping& val) { + if (!val) { + return Dart_Null(); + } + + auto dart_list_handle = Dart_NewListOf(Dart_CoreType_Int, val->GetSize()); + + if (Dart_IsError(dart_list_handle)) { + FML_LOG(ERROR) << "Error while attempting to allocate a list: " + << Dart_GetError(dart_list_handle); + return dart_list_handle; + } + + if (val->GetSize() == 0) { + // Nothing to copy. Just return the zero sized list. + return dart_list_handle; + } + + auto result = Dart_ListSetAsBytes(dart_list_handle, // list + 0, // offset + val->GetMapping(), // native array, + val->GetSize() // length + ); + + if (Dart_IsError(result)) { + FML_LOG(ERROR) << "Error while attempting to create a Dart list: " + << Dart_GetError(result); + return result; + } + + return dart_list_handle; + } + + static void SetReturnValue(Dart_NativeArguments args, + const DartConverterMapping& val) { + Dart_SetReturnValue(args, ToDart(val)); + } + + static DartConverterMapping FromDart(Dart_Handle dart_list) { + if (Dart_IsNull(dart_list)) { + return nullptr; + } + + if (Dart_IsError(dart_list)) { + FML_LOG(ERROR) << "Cannot convert an error handle to a list: " + << Dart_GetError(dart_list); + return nullptr; + } + + if (!Dart_IsList(dart_list)) { + FML_LOG(ERROR) << "Dart handle was not a list."; + return nullptr; + } + + intptr_t length = 0; + auto handle = Dart_ListLength(dart_list, &length); + + if (Dart_IsError(handle)) { + FML_LOG(ERROR) << "Could not get the length of the Dart list: " + << Dart_GetError(handle); + return nullptr; + } + + if (length == 0) { + // Return a valid zero sized mapping. + return std::make_unique(nullptr, 0); + } + + auto mapping_buffer = ::malloc(length); + + if (!mapping_buffer) { + FML_LOG(ERROR) + << "Out of memory while attempting to allocate a mapping of size: " + << length; + return nullptr; + } + + auto mapping = std::make_unique( + static_cast(mapping_buffer), length, + [](const uint8_t* data, size_t size) { + ::free(const_cast(data)); + }); + + handle = Dart_ListGetAsBytes( + dart_list, // list + 0, // offset + static_cast(mapping_buffer), // native array + length // length + ); + + if (Dart_IsError(handle)) { + FML_LOG(ERROR) << "Could not copy Dart list to native buffer: " + << Dart_GetError(handle); + return nullptr; + } + + return mapping; + } +}; + +} // namespace tonic + +#endif // FLUTTER_FML_DART_DART_CONVERTER_H_ diff --git a/shell/common/BUILD.gn b/shell/common/BUILD.gn index 9d57ee5ff5ecf..1564547bd34d1 100644 --- a/shell/common/BUILD.gn +++ b/shell/common/BUILD.gn @@ -173,6 +173,7 @@ if (current_toolchain == host_toolchain) { ":shell_unittests_gpu_configuration", "$flutter_root/common", "$flutter_root/flow", + "$flutter_root/fml/dart", "$flutter_root/lib/ui:ui", "$flutter_root/shell", "$flutter_root/testing:dart", diff --git a/shell/common/fixtures/shell_test.dart b/shell/common/fixtures/shell_test.dart index c6873995b4885..8f8dcab38cbda 100644 --- a/shell/common/fixtures/shell_test.dart +++ b/shell/common/fixtures/shell_test.dart @@ -121,3 +121,20 @@ void canAccessIsolateLaunchData() { } void notifyMessage(String string) native 'NotifyMessage'; + +@pragma('vm:entry-point') +void canConvertMappings() { + sendFixtureMapping(getFixtureMapping()); +} + +List getFixtureMapping() native 'GetFixtureMapping'; +void sendFixtureMapping(List list) native 'SendFixtureMapping'; + +@pragma('vm:entry-point') +void canDecompressImageFromAsset() { + decodeImageFromList(Uint8List.fromList(getFixtureImage()), (Image result) { + notifyWidthHeight(result.width, result.height); + }); +} + +List getFixtureImage() native 'GetFixtureImage'; diff --git a/shell/common/shell_unittests.cc b/shell/common/shell_unittests.cc index b69bf226b5c61..636b879178d88 100644 --- a/shell/common/shell_unittests.cc +++ b/shell/common/shell_unittests.cc @@ -13,6 +13,7 @@ #include "flutter/flow/layers/picture_layer.h" #include "flutter/flow/layers/transform_layer.h" #include "flutter/fml/command_line.h" +#include "flutter/fml/dart/dart_converter.h" #include "flutter/fml/make_copyable.h" #include "flutter/fml/message_loop.h" #include "flutter/fml/synchronization/count_down_latch.h" @@ -1014,5 +1015,143 @@ 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; + + uint8_t* buffer = static_cast(::malloc(buffer_size)); + ASSERT_NE(buffer, nullptr); + ASSERT_TRUE(MemsetPatternSetOrCheck( + buffer, buffer_size, MemsetPatternOp::kMemsetPatternOpSetBuffer)); + + std::unique_ptr mapping = + std::make_unique( + buffer, buffer_size, [](const uint8_t* buffer, size_t size) { + ::free(const_cast(buffer)); + }); + + ASSERT_EQ(mapping->GetSize(), buffer_size); + + fml::AutoResetWaitableEvent latch; + AddNativeCallback( + "SendFixtureMapping", CREATE_NATIVE_ENTRY([&](auto args) { + auto mapping_from_dart = + tonic::DartConverter>::FromDart( + Dart_GetNativeArgument(args, 0)); + ASSERT_NE(mapping_from_dart, nullptr); + ASSERT_EQ(mapping_from_dart->GetSize(), buffer_size); + ASSERT_TRUE(MemsetPatternSetOrCheck( + const_cast(mapping_from_dart->GetMapping()), // buffer + mapping_from_dart->GetSize(), // size + MemsetPatternOp::kMemsetPatternOpCheckBuffer // op + )); + latch.Signal(); + })); + + AddNativeCallback( + "GetFixtureMapping", CREATE_NATIVE_ENTRY([&](auto args) { + tonic::DartConverter::SetReturnValue( + args, mapping); + })); + + auto settings = CreateSettingsForFixture(); + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("canConvertMappings"); + std::unique_ptr shell = CreateShell(settings); + ASSERT_NE(shell.get(), nullptr); + RunEngine(shell.get(), std::move(configuration)); + latch.Wait(); + DestroyShell(std::move(shell)); +} + +TEST_F(ShellTest, CanDecompressImageFromAsset) { + fml::AutoResetWaitableEvent latch; + AddNativeCallback("NotifyWidthHeight", CREATE_NATIVE_ENTRY([&](auto args) { + auto width = tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 0)); + auto height = tonic::DartConverter::FromDart( + Dart_GetNativeArgument(args, 1)); + ASSERT_EQ(width, 100); + ASSERT_EQ(height, 100); + latch.Signal(); + })); + + AddNativeCallback( + "GetFixtureImage", CREATE_NATIVE_ENTRY([](auto args) { + auto fixture = OpenFixtureAsMapping("shelltest_screenshot.png"); + tonic::DartConverter::SetReturnValue( + args, fixture); + })); + + auto settings = CreateSettingsForFixture(); + auto configuration = RunConfiguration::InferFromSettings(settings); + configuration.SetEntrypoint("canDecompressImageFromAsset"); + std::unique_ptr shell = CreateShell(settings); + ASSERT_NE(shell.get(), nullptr); + RunEngine(shell.get(), std::move(configuration)); + latch.Wait(); + DestroyShell(std::move(shell)); +} + } // namespace testing } // namespace flutter diff --git a/testing/testing.cc b/testing/testing.cc index 303ff388d6464..b5e229f06bb5c 100644 --- a/testing/testing.cc +++ b/testing/testing.cc @@ -49,5 +49,9 @@ fml::UniqueFD OpenFixture(std::string fixture_name) { return fixture_fd; } +std::unique_ptr OpenFixtureAsMapping(std::string fixture_name) { + return fml::FileMapping::CreateReadOnly(OpenFixture(fixture_name)); +} + } // namespace testing } // namespace flutter diff --git a/testing/testing.h b/testing/testing.h index 9f0fba0349247..da8571b18b76e 100644 --- a/testing/testing.h +++ b/testing/testing.h @@ -8,21 +8,58 @@ #include #include "flutter/fml/file.h" +#include "flutter/fml/mapping.h" #include "flutter/testing/assertions.h" #include "gtest/gtest.h" namespace flutter { namespace testing { -// Returns the directory containing the test fixture for the target if this -// target has fixtures configured. If there are no fixtures, this is a link -// error. +//------------------------------------------------------------------------------ +/// @brief Returns the directory containing the test fixture for the target +/// if this target has fixtures configured. If there are no +/// fixtures, this is a link error. If you see a linker error on +/// this symbol, the unit-test target needs to depend on a +/// `test_fixtures` target. +/// +/// @return The fixtures path. +/// const char* GetFixturesPath(); +//------------------------------------------------------------------------------ +/// @brief Opens the fixtures directory for the unit-test harness. +/// +/// @return The file descriptor of the fixtures directory. +/// fml::UniqueFD OpenFixturesDirectory(); +//------------------------------------------------------------------------------ +/// @brief Opens a fixture of the given file name. +/// +/// @param[in] fixture_name The fixture name +/// +/// @return The file descriptor of the given fixture. An invalid file +/// descriptor is returned in case the fixture is not found. +/// fml::UniqueFD OpenFixture(std::string fixture_name); +//------------------------------------------------------------------------------ +/// @brief Opens a fixture of the given file name and returns a mapping to +/// its contents. +/// +/// @param[in] fixture_name The fixture name +/// +/// @return A mapping to the contents of fixture or null if the fixture does +/// not exist or its contents cannot be mapped in. +/// +std::unique_ptr OpenFixtureAsMapping(std::string fixture_name); + +//------------------------------------------------------------------------------ +/// @brief Gets the name of the currently running test. This is useful in +/// generating logs or assets based on test name. +/// +/// @return The current test name. +/// std::string GetCurrentTestName(); } // namespace testing