Skip to content

[CoreCLR] Support for fastdev assemblies #10113

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4e32f8f
Changes from https://github.com/dotnet/android/pull/10065
grendello Apr 22, 2025
39931bc
WIP
grendello Apr 24, 2025
12e6277
Fix after rebase
grendello Apr 25, 2025
8156edb
Use string blobs to decrease number of relocations
grendello Apr 25, 2025
2510637
Optimize debug maps a bit
grendello Apr 25, 2025
ed0dbbc
Implement fallback matching when hash clashes are detected
grendello Apr 28, 2025
b9d04d1
java-to-managed map should be fully functional now
grendello Apr 28, 2025
4fa897c
A handful of tweaks for the Debug build
grendello Apr 29, 2025
57d0b80
Cleanup
grendello Apr 29, 2025
2f74c7a
Add on-device test for string-based typemaps
grendello Apr 29, 2025
e943a4f
Fix after rebase
grendello Apr 30, 2025
626796f
Fix nullable issues after rebase on `main`
grendello May 5, 2025
6406817
This test requires more changes in the runtime, will be enabled in an…
grendello May 5, 2025
cf50b0d
Test the debug typemaps
grendello May 6, 2025
a99cb00
Implement support for fastdev assemblies
grendello May 7, 2025
b6cfb67
Update src/Xamarin.Android.Build.Tasks/Utilities/LlvmIrGenerator/Llvm…
grendello May 7, 2025
a728f2b
Be more generic (even though it looks fugly)
grendello May 7, 2025
bb7b04b
A handful of cosmetic changes
grendello May 8, 2025
37ed99f
Use better looking code
grendello May 8, 2025
a8e6925
Drop the `typemap_` prefix
grendello May 12, 2025
c096b28
Fix after rebase
grendello May 13, 2025
aec4156
Cleanup
grendello May 13, 2025
e5a13a3
Cleanup
grendello May 13, 2025
29bef2d
Fix after rebase
grendello May 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
6 changes: 6 additions & 0 deletions src/native/clr/host/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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}
)
Expand Down
113 changes: 113 additions & 0 deletions src/native/clr/host/fastdev-assemblies.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#include <sys/types.h>
#include <dirent.h>
#include <fcntl.h>
#include <unistd.h>

#include <cerrno>
#include <cstring>
#include <limits>

#include <constants.hh>
#include <host/fastdev-assemblies.hh>
#include <runtime-base/android-system.hh>
#include <runtime-base/util.hh>

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<std::remove_reference_t<decltype(size)>>::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<int64_t>(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<void*>(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<void*>(buffer);
}
43 changes: 31 additions & 12 deletions src/native/clr/host/host.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include <xamarin-app.hh>
#include <host/assembly-store.hh>
#include <host/fastdev-assemblies.hh>
#include <host/host.hh>
#include <host/host-jni.hh>
#include <host/host-util.hh>
Expand Down Expand Up @@ -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<SENSIBLE_PATH_MAX> const& entry_name, uint32_t offset, uint32_t size) -> bool
Expand Down
1 change: 1 addition & 0 deletions src/native/clr/include/constants.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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 () };
Expand Down
29 changes: 29 additions & 0 deletions src/native/clr/include/host/fastdev-assemblies.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#pragma once

#include <dirent.h>

#include <cstdint>
#include <mutex>
#include <string_view>

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
};
}
40 changes: 35 additions & 5 deletions src/native/clr/include/runtime-base/util.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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<size_t MaxStackSize>
static auto file_exists (dynamic_local_string<MaxStackSize> const& file) noexcept -> bool
{
Expand All @@ -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<size_t>
{
struct stat sbuf;
Expand All @@ -107,6 +132,11 @@ namespace xamarin::android {
return static_cast<size_t>(sbuf.st_size);
}

static auto get_file_size_at (int dirfd, std::string_view const& file_name) noexcept -> std::optional<size_t>
{
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);
Expand Down
31 changes: 27 additions & 4 deletions tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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<string, string>? 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"));
Expand Down
Loading