Skip to content
72 changes: 72 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,57 @@ 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 corelib_name { "System.Private.CoreLib.dll" };
bool found_corelib = 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 (!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 '{}'"sv, count, override_dir_path);

// 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).
if (count > 0 && found_corelib) {
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 @@ -2227,6 +2228,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