diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs index 28f8a57809e..a0c31d0291d 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs @@ -817,6 +817,7 @@ public void WriteValue (GeneratorWriteContext context, Type type, object? value, void WriteStringBlobArray (GeneratorWriteContext context, LlvmIrStringBlob blob) { + // The stride determines how many elements are written on a single line before a newline is added. const uint stride = 16; Type elementType = typeof(byte); diff --git a/src/native/clr/host/CMakeLists.txt b/src/native/clr/host/CMakeLists.txt index d53734eaf8c..d2338f4f072 100644 --- a/src/native/clr/host/CMakeLists.txt +++ b/src/native/clr/host/CMakeLists.txt @@ -41,6 +41,12 @@ set(XAMARIN_MONODROID_SOURCES xamarin_getifaddrs.cc ) +if(DEBUG_BUILD) + list(APPEND XAMARIN_MONODROID_SOURCES + fastdev-assemblies.cc + ) +endif() + list(APPEND LOCAL_CLANG_CHECK_SOURCES ${XAMARIN_MONODROID_SOURCES} ) diff --git a/src/native/clr/host/fastdev-assemblies.cc b/src/native/clr/host/fastdev-assemblies.cc new file mode 100644 index 00000000000..2a38ef06f19 --- /dev/null +++ b/src/native/clr/host/fastdev-assemblies.cc @@ -0,0 +1,113 @@ +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +using namespace xamarin::android; + +auto FastDevAssemblies::open_assembly (std::string_view const& name, int64_t &size) noexcept -> void* +{ + size = 0; + + std::string const& override_dir_path = AndroidSystem::get_primary_override_dir (); + if (!Util::dir_exists (override_dir_path)) [[unlikely]] { + log_debug (LOG_ASSEMBLY, "Override directory '{}' does not exist", override_dir_path); + return nullptr; + } + + // NOTE: override_dir will be kept open, we have no way of knowing when it will be no longer + // needed + if (override_dir_fd < 0) [[unlikely]] { + std::lock_guard dir_lock { override_dir_lock }; + if (override_dir_fd < 0) [[likely]] { + override_dir = opendir (override_dir_path.c_str ()); + if (override_dir == nullptr) [[unlikely]] { + log_warn (LOG_ASSEMBLY, "Failed to open override dir '{}'. {}", override_dir_path, strerror (errno)); + return nullptr; + } + override_dir_fd = dirfd (override_dir); + } + } + + log_debug ( + LOG_ASSEMBLY, + "Attempting to load FastDev assembly '{}' from override directory '{}'", + name, + override_dir_path + ); + + if (!Util::file_exists (override_dir_fd, name)) { + log_warn (LOG_ASSEMBLY, "FastDev assembly '{}' not found.", name); + return nullptr; + } + log_debug (LOG_ASSEMBLY, "Found FastDev assembly '{}'", name); + + auto file_size = Util::get_file_size_at (override_dir_fd, name); + if (!file_size) [[unlikely]] { + log_warn (LOG_ASSEMBLY, "Unable to determine FastDev assembly '{}' file size", name); + return nullptr; + } + + constexpr size_t MAX_SIZE = std::numeric_limits>::max (); + if (file_size.value () > MAX_SIZE) [[unlikely]] { + Helpers::abort_application ( + LOG_ASSEMBLY, + std::format ( + "FastDev assembly '{}' size exceeds the maximum supported value of {}", + name, + MAX_SIZE + ) + ); + } + + size = static_cast(file_size.value ()); + int asm_fd = openat (override_dir_fd, name.data (), O_RDONLY); + if (asm_fd < 0) { + log_warn ( + LOG_ASSEMBLY, + "Failed to open FastDev assembly '{}' for reading. {}", + name, + strerror (errno) + ); + + size = 0; + return nullptr; + } + + // TODO: consider who owns the pointer - we allocate the data, but we have no way of knowing when + // the allocated space is no longer (if ever) needed by CoreCLR. Probably would be best if + // CoreCLR notified us when it wants to free the data, as that eliminates any races as well + // as ambiguity. + auto buffer = new uint8_t[file_size.value ()]; + ssize_t nread = 0; + do { + nread = read (asm_fd, reinterpret_cast(buffer), file_size.value ()); + } while (nread == -1 && errno == EINTR); + close (asm_fd); + + if (nread != size) [[unlikely]] { + delete[] buffer; + + log_warn ( + LOG_ASSEMBLY, + "Failed to read FastDev assembly '{}' data. {}", + name, + strerror (errno) + ); + + size = 0; + return nullptr; + } + log_debug (LOG_ASSEMBLY, "Read {} bytes of FastDev assembly '{}'", nread, name); + + return reinterpret_cast(buffer); +} diff --git a/src/native/clr/host/host.cc b/src/native/clr/host/host.cc index 8103c70030b..67cf27465dd 100644 --- a/src/native/clr/host/host.cc +++ b/src/native/clr/host/host.cc @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -46,22 +47,40 @@ bool Host::clr_external_assembly_probe (const char *path, void **data_start, int internal_timing.start_event (TimingEventKind::AssemblyLoad); } - *data_start = AssemblyStore::open_assembly (path, *size); + auto log_and_return = [](const char *name, void *data_start, int64_t size) { + if (FastTiming::enabled ()) [[unlikely]] { + internal_timing.end_event (true /* uses_more_info */); + internal_timing.add_more_info (name); + } - if (FastTiming::enabled ()) [[unlikely]] { - internal_timing.end_event (true /* uses_more_info */); - internal_timing.add_more_info (path); + log_debug ( + LOG_ASSEMBLY, + "Assembly '{}' data {}mapped ({:p}, {} bytes)", + optional_string (name), + data_start == nullptr ? "not "sv : ""sv, + data_start, + size + ); + + return data_start != nullptr && size > 0; + }; + + if constexpr (Constants::is_debug_build) { + *data_start = FastDevAssemblies::open_assembly (path, *size); + if (*data_start != nullptr && *size > 0) { + return log_and_return (path, *data_start, *size); + } + + log_warn ( + LOG_ASSEMBLY, + "Assembly '{}' not found in FastDev override directory. Attempting to load from assembly store", + optional_string (path) + ); } - log_debug ( - LOG_ASSEMBLY, - "Assembly data {}mapped ({:p}, {} bytes)", - *data_start == nullptr ? "not "sv : ""sv, - *data_start, - *size - ); + *data_start = AssemblyStore::open_assembly (path, *size); - return *data_start != nullptr && *size > 0; + return log_and_return (path, *data_start, *size); } auto Host::zip_scan_callback (std::string_view const& apk_path, int apk_fd, dynamic_local_string const& entry_name, uint32_t offset, uint32_t size) -> bool diff --git a/src/native/clr/include/constants.hh b/src/native/clr/include/constants.hh index 5e4d5adcab2..e29c2fa5c32 100644 --- a/src/native/clr/include/constants.hh +++ b/src/native/clr/include/constants.hh @@ -41,6 +41,7 @@ namespace xamarin::android { public: static constexpr std::string_view NEWLINE { "\n" }; static constexpr std::string_view EMPTY { "" }; + static constexpr std::string_view DIR_SEP { "/" }; // .data() must be used otherwise string_view length will include the trailing \0 in the array static constexpr std::string_view RUNTIME_CONFIG_BLOB_NAME { RUNTIME_CONFIG_BLOB_NAME_ARRAY.data () }; diff --git a/src/native/clr/include/host/fastdev-assemblies.hh b/src/native/clr/include/host/fastdev-assemblies.hh new file mode 100644 index 00000000000..51f1945fce3 --- /dev/null +++ b/src/native/clr/include/host/fastdev-assemblies.hh @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include +#include +#include + +namespace xamarin::android { + class FastDevAssemblies + { + public: +#if defined(DEBUG) + static auto open_assembly (std::string_view const& name, int64_t &size) noexcept -> void*; +#else + static auto open_assembly ([[maybe_unused]] std::string_view const& name, [[maybe_unused]] int64_t &size) noexcept -> void* + { + return nullptr; + } +#endif + + private: +#if defined(DEBUG) + static inline DIR *override_dir = nullptr; + static inline int override_dir_fd = -1; + static inline std::mutex override_dir_lock {}; +#endif + }; +} diff --git a/src/native/clr/include/runtime-base/util.hh b/src/native/clr/include/runtime-base/util.hh index 0dc4316f24b..a3512304e11 100644 --- a/src/native/clr/include/runtime-base/util.hh +++ b/src/native/clr/include/runtime-base/util.hh @@ -73,19 +73,38 @@ namespace xamarin::android { return (log_categories & category) != 0; } - static auto file_exists (const char *file) noexcept -> bool + private: + static auto fs_entry_is_mode (struct stat const& s, mode_t mode) noexcept -> bool { - if (file == nullptr) { - return false; - } + return (s.st_mode & S_IFMT) == mode; + } + static auto exists_and_is_mode (std::string_view const& path, mode_t mode) noexcept -> bool + { struct stat s; - if (::stat (file, &s) == 0 && (s.st_mode & S_IFMT) == S_IFREG) { + + if (::stat (path.data (), &s) == 0 && fs_entry_is_mode (s, mode)) { return true; } + return false; } + public: + static auto dir_exists (std::string_view const& dir_path) noexcept -> bool + { + return exists_and_is_mode (dir_path, S_IFDIR); + } + + static auto file_exists (const char *file) noexcept -> bool + { + if (file == nullptr) { + return false; + } + + return exists_and_is_mode (file, S_IFREG); + } + template static auto file_exists (dynamic_local_string const& file) noexcept -> bool { @@ -96,6 +115,12 @@ namespace xamarin::android { return file_exists (file.get ()); } + static auto file_exists (int dirfd, std::string_view const& file) noexcept -> bool + { + struct stat sbuf; + return fstatat (dirfd, file.data (), &sbuf, 0) == 0 && fs_entry_is_mode (sbuf, S_IFREG); + } + static auto get_file_size_at (int dirfd, const char *file_name) noexcept -> std::optional { struct stat sbuf; @@ -107,6 +132,11 @@ namespace xamarin::android { return static_cast(sbuf.st_size); } + static auto get_file_size_at (int dirfd, std::string_view const& file_name) noexcept -> std::optional + { + return get_file_size_at (dirfd, file_name.data ()); + } + static void set_environment_variable (std::string_view const& name, jstring_wrapper& value) noexcept { ::setenv (name.data (), value.get_cstr (), 1); diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 246287cae37..d179740e994 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -565,8 +565,18 @@ public void SingleProject_ApplicationId ([Values (false, true)] bool testOnly) } [Test] - public void AppWithStyleableUsageRuns ([Values (true, false)] bool isRelease, [Values (true, false)] bool linkResources) + public void AppWithStyleableUsageRuns ([Values (true, false)] bool useCLR, [Values (true, false)] bool isRelease, + [Values (true, false)] bool linkResources, [Values (true, false)] bool useStringTypeMaps) { + // Not all combinations are valid, ignore those that aren't + if (!useCLR && useStringTypeMaps) { + Assert.Ignore ("String-based typemaps mode is used only in CoreCLR apps"); + } + + if (useCLR && isRelease && useStringTypeMaps) { + Assert.Ignore ("String-based typemaps mode is available only in Debug CoreCLR builds"); + } + var rootPath = Path.Combine (Root, "temp", TestName); var lib = new XamarinAndroidLibraryProject () { ProjectName = "Styleable.Library" @@ -615,6 +625,7 @@ public MyLibraryLayout (Android.Content.Context context, Android.Util.IAttribute proj = new XamarinAndroidApplicationProject () { IsRelease = isRelease, }; + proj.SetProperty ("UseMonoRuntime", useCLR ? "false" : "true"); proj.AddReference (lib); proj.AndroidResources.Add (new AndroidItem.AndroidResource ("Resources\\values\\styleables.xml") { @@ -652,15 +663,27 @@ public MyLayout (Android.Content.Context context, Android.Util.IAttributeSet att } "); - var abis = new string [] { "armeabi-v7a", "arm64-v8a", "x86", "x86_64" }; + string[] abis = useCLR switch { + true => new string [] { "arm64-v8a", "x86_64" }, + false => new string [] { "armeabi-v7a", "arm64-v8a", "x86", "x86_64" }, + }; + proj.SetAndroidSupportedAbis (abis); var libBuilder = CreateDllBuilder (Path.Combine (rootPath, lib.ProjectName)); Assert.IsTrue (libBuilder.Build (lib), "Library should have built succeeded."); builder = CreateApkBuilder (Path.Combine (rootPath, proj.ProjectName)); - Assert.IsTrue (builder.Install (proj), "Install should have succeeded."); - RunProjectAndAssert (proj, builder); + + Dictionary? environmentVariables = null; + if (useCLR && !isRelease && useStringTypeMaps) { + // The variable must have content to enable string-based typemaps + environmentVariables = new (StringComparer.Ordinal) { + {"CI_TYPEMAP_DEBUG_USE_STRINGS", "yes"} + }; + } + + RunProjectAndAssert (proj, builder, environmentVariables: environmentVariables); var didStart = WaitForActivityToStart (proj.PackageName, "MainActivity", Path.Combine (Root, builder.ProjectDirectory, "startup-logcat.log"));