Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
92 changes: 92 additions & 0 deletions src/native/clr/host/fastdev-assemblies.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <cerrno>
#include <cstring>
#include <limits>
#include <string>

#include <constants.hh>
#include <host/fastdev-assemblies.hh>
Expand All @@ -18,6 +19,23 @@ auto FastDevAssemblies::open_assembly (std::string_view const& name, int64_t &si
{
size = 0;

// When the override directory was used to build a `TRUSTED_PLATFORM_ASSEMBLIES`
// list (see `build_tpa_list`), the external probe should yield to TPA-based
// loading so that CoreCLR opens the assembly from disk via `PEImage::OpenImage`
// and `Assembly.Location` ends up populated. Otherwise sibling portable PDB
// lookup (used by `StackTraceSymbols`) returns an empty path and stack frames
// render without file/line info.
//
// The CoreLib bootstrap is a special case: CoreCLR loads
// `System.Private.CoreLib.dll` via the external probe (not through the
// regular TPA-aware binder), so we must keep returning the bytes for it
// even when TPA is in use. CoreLib has no user code we'd symbolicate, so
// the resulting bare-filename `Assembly.Location` does not matter.
constexpr std::string_view corelib_name { "System.Private.CoreLib.dll" };
if (tpa_in_use && name != corelib_name) {
return nullptr;
}

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"sv, override_dir_path);
Expand Down Expand Up @@ -111,3 +129,77 @@ auto FastDevAssemblies::open_assembly (std::string_view const& name, int64_t &si

return reinterpret_cast<void*>(buffer);
}

auto FastDevAssemblies::build_tpa_list (std::string &tpa_list) noexcept -> bool
{
tpa_list.clear ();

std::string const& override_dir_path = AndroidSystem::get_primary_override_dir ();
if (!Util::dir_exists (override_dir_path)) {
return false;
}

DIR *dir = opendir (override_dir_path.c_str ());
if (dir == nullptr) {
log_warn (LOG_ASSEMBLY, "FastDev: failed to open override dir '{}'. {}"sv, override_dir_path, std::strerror (errno));
return false;
}

constexpr std::string_view dll_ext { ".dll" };
constexpr std::string_view r2r_ext { ".r2r.dll" };
constexpr std::string_view corelib_name { "System.Private.CoreLib.dll" };
bool found_corelib = false;
bool found_r2r = false;
size_t count = 0;
dirent *e;
while ((e = readdir (dir)) != nullptr) {
std::string_view name { e->d_name };
if (name.size () <= dll_ext.size () || !name.ends_with (dll_ext)) {
continue;
}
if (name.ends_with (r2r_ext)) {
// Release+EmbedAssembliesIntoApk=false deploys ReadyToRun
// composites named `Foo.r2r.dll`. CoreCLR's binder probes for
// these by filename and we don't have a clean way to satisfy
// those probes via TPA, so we leave Release-style deployments
// on the legacy probe-only path.
found_r2r = true;
break;
}

if (!tpa_list.empty ()) {
tpa_list.append (":");
}
tpa_list.append (override_dir_path);
tpa_list.append ("/");
tpa_list.append (name);
if (name == corelib_name) {
found_corelib = true;
}
count++;
}
closedir (dir);

log_debug (
LOG_ASSEMBLY,
"FastDev: built TPA list with {} assemblies from '{}' (corelib={}, r2r={})"sv,
count,
override_dir_path,
found_corelib,
found_r2r
);

// We can only safely hand a TPA list to CoreCLR when it contains
// `System.Private.CoreLib.dll`. Passing TPA without CoreLib changes the
// CLR binder mode such that CoreLib is searched via TPA/probe instead of
// the built-in bootstrap, which fails on incomplete FastDev deployments
// (e.g. tests that only sync a handful of user assemblies). We also skip
// TPA when ReadyToRun variants are present (Release+nonembed), since
// CoreCLR's `.r2r.dll` probes aren't compatible with our TPA path.
if (count > 0 && found_corelib && !found_r2r) {
tpa_in_use = true;
return true;
}
tpa_list.clear ();
return false;
}
37 changes: 34 additions & 3 deletions src/native/clr/host/host.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#include <unistd.h>

#include <android/looper.h>
Expand Down Expand Up @@ -450,12 +452,41 @@ void Host::Java_mono_android_Runtime_initInternal (
// The first entry in the property arrays is for the host contract pointer. Application build makes sure
// of that.
init_runtime_property_values[0] = host_contract_ptr_buffer.data ();

const char **prop_names = init_runtime_property_names;
const char **prop_values = const_cast<const char**>(init_runtime_property_values);
int prop_count = static_cast<int>(application_config.number_of_runtime_properties);

// In Debug builds with FastDev, append `TRUSTED_PLATFORM_ASSEMBLIES` with full
// paths to the assemblies pushed into `.__override__/<arch>/`. CoreCLR then
// opens those files from disk so `Assembly.Location` is populated and
// `StackTraceSymbols` can find sibling `.pdb` files for runtime-rendered
// managed stack traces (file/line).
if constexpr (Constants::is_debug_build) {
// Storage must outlive `coreclr_initialize`; function-local statics
// give us process lifetime without polluting global namespace.
static std::string fastdev_tpa_list;
static std::vector<const char*> fastdev_prop_names;
static std::vector<const char*> fastdev_prop_values;

if (FastDevAssemblies::build_tpa_list (fastdev_tpa_list)) {
fastdev_prop_names.assign (prop_names, prop_names + prop_count);
fastdev_prop_values.assign (prop_values, prop_values + prop_count);
fastdev_prop_names.push_back (HOST_PROPERTY_TRUSTED_PLATFORM_ASSEMBLIES);
fastdev_prop_values.push_back (fastdev_tpa_list.c_str ());

prop_names = fastdev_prop_names.data ();
prop_values = fastdev_prop_values.data ();
prop_count = static_cast<int>(fastdev_prop_names.size ());
}
}

int hr = FastTiming::time_call ("coreclr_initialize"sv, coreclr_initialize,
application_config.android_package_name,
"Xamarin.Android",
(int)application_config.number_of_runtime_properties,
init_runtime_property_names,
const_cast<const char**>(init_runtime_property_values),
prop_count,
prop_names,
prop_values,
&clr_host,
&domain_id
);
Expand Down
14 changes: 14 additions & 0 deletions src/native/clr/include/host/fastdev-assemblies.hh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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

namespace xamarin::android {
Expand All @@ -12,18 +13,31 @@ namespace xamarin::android {
public:
#if defined(DEBUG)
static auto open_assembly (std::string_view const& name, int64_t &size) noexcept -> void*;
static auto build_tpa_list (std::string &tpa_list) noexcept -> bool;
#else
static auto open_assembly ([[maybe_unused]] std::string_view const& name, [[maybe_unused]] int64_t &size) noexcept -> void*
{
return nullptr;
}

static auto build_tpa_list ([[maybe_unused]] std::string &tpa_list) noexcept -> bool
{
return false;
}
#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 {};
// Set by `build_tpa_list` when assemblies in the override directory are
// passed to CoreCLR via `TRUSTED_PLATFORM_ASSEMBLIES`. When true, the
// external assembly probe yields to TPA-based loading so that
// `Assembly.Location` is populated with the full disk path (needed for
// `StackTraceSymbols` to find sibling portable PDB files).
public:
static inline bool tpa_in_use = false;
#endif
};
}
50 changes: 50 additions & 0 deletions tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Xml.Linq;
using System.Xml.XPath;
Expand Down Expand Up @@ -2142,6 +2143,55 @@ public void FastDeployEnvironmentFiles ([Values] bool isRelease, [Values] bool e
}
}

[Test]
public void StackTraceContainsLineNumbers ()
{
// FastDev (Debug + assemblies on disk in .__override__) wires up
// portable PDB lookup for runtime-rendered stack traces on CoreCLR
// via the TPA list passed to coreclr_initialize.
AndroidRuntime runtime = AndroidRuntime.CoreCLR;
if (IgnoreUnsupportedConfiguration (runtime, release: false)) {
return;
}

var proj = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (runtime)) {
ProjectName = nameof (StackTraceContainsLineNumbers),
RootNamespace = nameof (StackTraceContainsLineNumbers),
IsRelease = false,
EmbedAssembliesIntoApk = false,
EnableDefaultItems = true,
};
proj.SetRuntime (runtime);
proj.MainActivity = proj.DefaultMainActivity.Replace ("//${AFTER_ONCREATE}", """
Console.WriteLine ("#STACKTRACE-BEGIN#");
Console.WriteLine (Environment.StackTrace);
Console.WriteLine ("#STACKTRACE-END#");
""");
using var builder = CreateApkBuilder ();
Assert.IsTrue (builder.Install (proj), "App should have installed.");
RunProjectAndAssert (proj, builder);

var appStartupLogcatFile = Path.Combine (Root, builder.ProjectDirectory, "stacktrace-logcat.log");
Assert.IsTrue (
MonitorAdbLogcat (line => line.Contains ("#STACKTRACE-END#"), appStartupLogcatFile, timeout: 60),
"Stack trace end marker not found in logcat (output may be missing or truncated)."
);

var logcatOutput = File.ReadAllText (appStartupLogcatFile);
Comment thread
jonathanpeppers marked this conversation as resolved.
StringAssert.Contains ("#STACKTRACE-BEGIN#", logcatOutput, "Stack trace start marker not found in logcat");

// Expect a frame in MainActivity.OnCreate to include
// "in <path>MainActivity.cs:line <N>" on a single line.
var match = Regex.Match (
logcatOutput,
@"at\s+\S*MainActivity\.OnCreate.*\sin\s+\S+MainActivity\.cs:line\s+\d+"
);
Assert.IsTrue (
match.Success,
$"Expected MainActivity.OnCreate frame to include file/line info. Logcat:\n{logcatOutput}"
);
}

[Test]
public void DotNetRunEnvironmentVariables ()
{
Expand Down
Loading