Skip to content

Commit 7486928

Browse files
authored
[CoreCLR] Support for fastdev assemblies (#10113)
Context: #10065 Context: #10075 This is the last of the PR series which implement support for `.NET for Android` applications Debug builds. While developing applications using Visual Studio, developers are in the cycle of making frequent changes and testing them on device or emulator. This development loop must be fast enough to not make the experience frustrating. One of the ways `.NET for Android` uses to make it better is the so-called "fastdev" mode in which the application APK is built and deployed without any assemblies in it, while the assemblies are synchronized individually to the device's filesystem, so they can be refreshed individually on changes affecting, thus saving time. The `.NET for Android` runtime must be able to locate those assemblies and load them from the filesystem, instead of using the usual assembly store included in the application package. This commit implements support for finding, enumerating and locating the fastdev assemblies on the filesystem. It also modifies one of the "install and run" unit tests to test the functionality.
1 parent 99b8a37 commit 7486928

File tree

8 files changed

+243
-21
lines changed

8 files changed

+243
-21
lines changed

src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/LlvmIrGenerator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,7 @@ public void WriteValue (GeneratorWriteContext context, Type type, object? value,
817817

818818
void WriteStringBlobArray (GeneratorWriteContext context, LlvmIrStringBlob blob)
819819
{
820+
// The stride determines how many elements are written on a single line before a newline is added.
820821
const uint stride = 16;
821822
Type elementType = typeof(byte);
822823

src/native/clr/host/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ set(XAMARIN_MONODROID_SOURCES
4141
xamarin_getifaddrs.cc
4242
)
4343

44+
if(DEBUG_BUILD)
45+
list(APPEND XAMARIN_MONODROID_SOURCES
46+
fastdev-assemblies.cc
47+
)
48+
endif()
49+
4450
list(APPEND LOCAL_CLANG_CHECK_SOURCES
4551
${XAMARIN_MONODROID_SOURCES}
4652
)
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#include <sys/types.h>
2+
#include <dirent.h>
3+
#include <fcntl.h>
4+
#include <unistd.h>
5+
6+
#include <cerrno>
7+
#include <cstring>
8+
#include <limits>
9+
10+
#include <constants.hh>
11+
#include <host/fastdev-assemblies.hh>
12+
#include <runtime-base/android-system.hh>
13+
#include <runtime-base/util.hh>
14+
15+
using namespace xamarin::android;
16+
17+
auto FastDevAssemblies::open_assembly (std::string_view const& name, int64_t &size) noexcept -> void*
18+
{
19+
size = 0;
20+
21+
std::string const& override_dir_path = AndroidSystem::get_primary_override_dir ();
22+
if (!Util::dir_exists (override_dir_path)) [[unlikely]] {
23+
log_debug (LOG_ASSEMBLY, "Override directory '{}' does not exist", override_dir_path);
24+
return nullptr;
25+
}
26+
27+
// NOTE: override_dir will be kept open, we have no way of knowing when it will be no longer
28+
// needed
29+
if (override_dir_fd < 0) [[unlikely]] {
30+
std::lock_guard dir_lock { override_dir_lock };
31+
if (override_dir_fd < 0) [[likely]] {
32+
override_dir = opendir (override_dir_path.c_str ());
33+
if (override_dir == nullptr) [[unlikely]] {
34+
log_warn (LOG_ASSEMBLY, "Failed to open override dir '{}'. {}", override_dir_path, strerror (errno));
35+
return nullptr;
36+
}
37+
override_dir_fd = dirfd (override_dir);
38+
}
39+
}
40+
41+
log_debug (
42+
LOG_ASSEMBLY,
43+
"Attempting to load FastDev assembly '{}' from override directory '{}'",
44+
name,
45+
override_dir_path
46+
);
47+
48+
if (!Util::file_exists (override_dir_fd, name)) {
49+
log_warn (LOG_ASSEMBLY, "FastDev assembly '{}' not found.", name);
50+
return nullptr;
51+
}
52+
log_debug (LOG_ASSEMBLY, "Found FastDev assembly '{}'", name);
53+
54+
auto file_size = Util::get_file_size_at (override_dir_fd, name);
55+
if (!file_size) [[unlikely]] {
56+
log_warn (LOG_ASSEMBLY, "Unable to determine FastDev assembly '{}' file size", name);
57+
return nullptr;
58+
}
59+
60+
constexpr size_t MAX_SIZE = std::numeric_limits<std::remove_reference_t<decltype(size)>>::max ();
61+
if (file_size.value () > MAX_SIZE) [[unlikely]] {
62+
Helpers::abort_application (
63+
LOG_ASSEMBLY,
64+
std::format (
65+
"FastDev assembly '{}' size exceeds the maximum supported value of {}",
66+
name,
67+
MAX_SIZE
68+
)
69+
);
70+
}
71+
72+
size = static_cast<int64_t>(file_size.value ());
73+
int asm_fd = openat (override_dir_fd, name.data (), O_RDONLY);
74+
if (asm_fd < 0) {
75+
log_warn (
76+
LOG_ASSEMBLY,
77+
"Failed to open FastDev assembly '{}' for reading. {}",
78+
name,
79+
strerror (errno)
80+
);
81+
82+
size = 0;
83+
return nullptr;
84+
}
85+
86+
// TODO: consider who owns the pointer - we allocate the data, but we have no way of knowing when
87+
// the allocated space is no longer (if ever) needed by CoreCLR. Probably would be best if
88+
// CoreCLR notified us when it wants to free the data, as that eliminates any races as well
89+
// as ambiguity.
90+
auto buffer = new uint8_t[file_size.value ()];
91+
ssize_t nread = 0;
92+
do {
93+
nread = read (asm_fd, reinterpret_cast<void*>(buffer), file_size.value ());
94+
} while (nread == -1 && errno == EINTR);
95+
close (asm_fd);
96+
97+
if (nread != size) [[unlikely]] {
98+
delete[] buffer;
99+
100+
log_warn (
101+
LOG_ASSEMBLY,
102+
"Failed to read FastDev assembly '{}' data. {}",
103+
name,
104+
strerror (errno)
105+
);
106+
107+
size = 0;
108+
return nullptr;
109+
}
110+
log_debug (LOG_ASSEMBLY, "Read {} bytes of FastDev assembly '{}'", nread, name);
111+
112+
return reinterpret_cast<void*>(buffer);
113+
}

src/native/clr/host/host.cc

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#include <xamarin-app.hh>
1111
#include <host/assembly-store.hh>
12+
#include <host/fastdev-assemblies.hh>
1213
#include <host/host.hh>
1314
#include <host/host-jni.hh>
1415
#include <host/host-util.hh>
@@ -46,22 +47,40 @@ bool Host::clr_external_assembly_probe (const char *path, void **data_start, int
4647
internal_timing.start_event (TimingEventKind::AssemblyLoad);
4748
}
4849

49-
*data_start = AssemblyStore::open_assembly (path, *size);
50+
auto log_and_return = [](const char *name, void *data_start, int64_t size) {
51+
if (FastTiming::enabled ()) [[unlikely]] {
52+
internal_timing.end_event (true /* uses_more_info */);
53+
internal_timing.add_more_info (name);
54+
}
5055

51-
if (FastTiming::enabled ()) [[unlikely]] {
52-
internal_timing.end_event (true /* uses_more_info */);
53-
internal_timing.add_more_info (path);
56+
log_debug (
57+
LOG_ASSEMBLY,
58+
"Assembly '{}' data {}mapped ({:p}, {} bytes)",
59+
optional_string (name),
60+
data_start == nullptr ? "not "sv : ""sv,
61+
data_start,
62+
size
63+
);
64+
65+
return data_start != nullptr && size > 0;
66+
};
67+
68+
if constexpr (Constants::is_debug_build) {
69+
*data_start = FastDevAssemblies::open_assembly (path, *size);
70+
if (*data_start != nullptr && *size > 0) {
71+
return log_and_return (path, *data_start, *size);
72+
}
73+
74+
log_warn (
75+
LOG_ASSEMBLY,
76+
"Assembly '{}' not found in FastDev override directory. Attempting to load from assembly store",
77+
optional_string (path)
78+
);
5479
}
5580

56-
log_debug (
57-
LOG_ASSEMBLY,
58-
"Assembly data {}mapped ({:p}, {} bytes)",
59-
*data_start == nullptr ? "not "sv : ""sv,
60-
*data_start,
61-
*size
62-
);
81+
*data_start = AssemblyStore::open_assembly (path, *size);
6382

64-
return *data_start != nullptr && *size > 0;
83+
return log_and_return (path, *data_start, *size);
6584
}
6685

6786
auto Host::zip_scan_callback (std::string_view const& apk_path, int apk_fd, dynamic_local_string<SENSIBLE_PATH_MAX> const& entry_name, uint32_t offset, uint32_t size) -> bool

src/native/clr/include/constants.hh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ namespace xamarin::android {
4141
public:
4242
static constexpr std::string_view NEWLINE { "\n" };
4343
static constexpr std::string_view EMPTY { "" };
44+
static constexpr std::string_view DIR_SEP { "/" };
4445

4546
// .data() must be used otherwise string_view length will include the trailing \0 in the array
4647
static constexpr std::string_view RUNTIME_CONFIG_BLOB_NAME { RUNTIME_CONFIG_BLOB_NAME_ARRAY.data () };
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#pragma once
2+
3+
#include <dirent.h>
4+
5+
#include <cstdint>
6+
#include <mutex>
7+
#include <string_view>
8+
9+
namespace xamarin::android {
10+
class FastDevAssemblies
11+
{
12+
public:
13+
#if defined(DEBUG)
14+
static auto open_assembly (std::string_view const& name, int64_t &size) noexcept -> void*;
15+
#else
16+
static auto open_assembly ([[maybe_unused]] std::string_view const& name, [[maybe_unused]] int64_t &size) noexcept -> void*
17+
{
18+
return nullptr;
19+
}
20+
#endif
21+
22+
private:
23+
#if defined(DEBUG)
24+
static inline DIR *override_dir = nullptr;
25+
static inline int override_dir_fd = -1;
26+
static inline std::mutex override_dir_lock {};
27+
#endif
28+
};
29+
}

src/native/clr/include/runtime-base/util.hh

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,19 +73,38 @@ namespace xamarin::android {
7373
return (log_categories & category) != 0;
7474
}
7575

76-
static auto file_exists (const char *file) noexcept -> bool
76+
private:
77+
static auto fs_entry_is_mode (struct stat const& s, mode_t mode) noexcept -> bool
7778
{
78-
if (file == nullptr) {
79-
return false;
80-
}
79+
return (s.st_mode & S_IFMT) == mode;
80+
}
8181

82+
static auto exists_and_is_mode (std::string_view const& path, mode_t mode) noexcept -> bool
83+
{
8284
struct stat s;
83-
if (::stat (file, &s) == 0 && (s.st_mode & S_IFMT) == S_IFREG) {
85+
86+
if (::stat (path.data (), &s) == 0 && fs_entry_is_mode (s, mode)) {
8487
return true;
8588
}
89+
8690
return false;
8791
}
8892

93+
public:
94+
static auto dir_exists (std::string_view const& dir_path) noexcept -> bool
95+
{
96+
return exists_and_is_mode (dir_path, S_IFDIR);
97+
}
98+
99+
static auto file_exists (const char *file) noexcept -> bool
100+
{
101+
if (file == nullptr) {
102+
return false;
103+
}
104+
105+
return exists_and_is_mode (file, S_IFREG);
106+
}
107+
89108
template<size_t MaxStackSize>
90109
static auto file_exists (dynamic_local_string<MaxStackSize> const& file) noexcept -> bool
91110
{
@@ -96,6 +115,12 @@ namespace xamarin::android {
96115
return file_exists (file.get ());
97116
}
98117

118+
static auto file_exists (int dirfd, std::string_view const& file) noexcept -> bool
119+
{
120+
struct stat sbuf;
121+
return fstatat (dirfd, file.data (), &sbuf, 0) == 0 && fs_entry_is_mode (sbuf, S_IFREG);
122+
}
123+
99124
static auto get_file_size_at (int dirfd, const char *file_name) noexcept -> std::optional<size_t>
100125
{
101126
struct stat sbuf;
@@ -107,6 +132,11 @@ namespace xamarin::android {
107132
return static_cast<size_t>(sbuf.st_size);
108133
}
109134

135+
static auto get_file_size_at (int dirfd, std::string_view const& file_name) noexcept -> std::optional<size_t>
136+
{
137+
return get_file_size_at (dirfd, file_name.data ());
138+
}
139+
110140
static void set_environment_variable (std::string_view const& name, jstring_wrapper& value) noexcept
111141
{
112142
::setenv (name.data (), value.get_cstr (), 1);

tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -565,8 +565,18 @@ public void SingleProject_ApplicationId ([Values (false, true)] bool testOnly)
565565
}
566566

567567
[Test]
568-
public void AppWithStyleableUsageRuns ([Values (true, false)] bool isRelease, [Values (true, false)] bool linkResources)
568+
public void AppWithStyleableUsageRuns ([Values (true, false)] bool useCLR, [Values (true, false)] bool isRelease,
569+
[Values (true, false)] bool linkResources, [Values (true, false)] bool useStringTypeMaps)
569570
{
571+
// Not all combinations are valid, ignore those that aren't
572+
if (!useCLR && useStringTypeMaps) {
573+
Assert.Ignore ("String-based typemaps mode is used only in CoreCLR apps");
574+
}
575+
576+
if (useCLR && isRelease && useStringTypeMaps) {
577+
Assert.Ignore ("String-based typemaps mode is available only in Debug CoreCLR builds");
578+
}
579+
570580
var rootPath = Path.Combine (Root, "temp", TestName);
571581
var lib = new XamarinAndroidLibraryProject () {
572582
ProjectName = "Styleable.Library"
@@ -615,6 +625,7 @@ public MyLibraryLayout (Android.Content.Context context, Android.Util.IAttribute
615625
proj = new XamarinAndroidApplicationProject () {
616626
IsRelease = isRelease,
617627
};
628+
proj.SetProperty ("UseMonoRuntime", useCLR ? "false" : "true");
618629
proj.AddReference (lib);
619630

620631
proj.AndroidResources.Add (new AndroidItem.AndroidResource ("Resources\\values\\styleables.xml") {
@@ -652,15 +663,27 @@ public MyLayout (Android.Content.Context context, Android.Util.IAttributeSet att
652663
}
653664
");
654665

655-
var abis = new string [] { "armeabi-v7a", "arm64-v8a", "x86", "x86_64" };
666+
string[] abis = useCLR switch {
667+
true => new string [] { "arm64-v8a", "x86_64" },
668+
false => new string [] { "armeabi-v7a", "arm64-v8a", "x86", "x86_64" },
669+
};
670+
656671
proj.SetAndroidSupportedAbis (abis);
657672
var libBuilder = CreateDllBuilder (Path.Combine (rootPath, lib.ProjectName));
658673
Assert.IsTrue (libBuilder.Build (lib), "Library should have built succeeded.");
659674
builder = CreateApkBuilder (Path.Combine (rootPath, proj.ProjectName));
660675

661-
662676
Assert.IsTrue (builder.Install (proj), "Install should have succeeded.");
663-
RunProjectAndAssert (proj, builder);
677+
678+
Dictionary<string, string>? environmentVariables = null;
679+
if (useCLR && !isRelease && useStringTypeMaps) {
680+
// The variable must have content to enable string-based typemaps
681+
environmentVariables = new (StringComparer.Ordinal) {
682+
{"CI_TYPEMAP_DEBUG_USE_STRINGS", "yes"}
683+
};
684+
}
685+
686+
RunProjectAndAssert (proj, builder, environmentVariables: environmentVariables);
664687

665688
var didStart = WaitForActivityToStart (proj.PackageName, "MainActivity",
666689
Path.Combine (Root, builder.ProjectDirectory, "startup-logcat.log"));

0 commit comments

Comments
 (0)