diff --git a/src/native/clr/host/fastdev-assemblies.cc b/src/native/clr/host/fastdev-assemblies.cc index dea16523aaf..dbc2097a7d7 100644 --- a/src/native/clr/host/fastdev-assemblies.cc +++ b/src/native/clr/host/fastdev-assemblies.cc @@ -6,8 +6,10 @@ #include #include #include +#include #include +#include #include #include #include @@ -111,3 +113,62 @@ auto FastDevAssemblies::open_assembly (std::string_view const& name, int64_t &si return reinterpret_cast(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; + } + int dir_fd = dirfd (dir); + if (dir_fd < 0) { + log_warn (LOG_ASSEMBLY, "FastDev: failed to obtain fd for override dir '{}'. {}"sv, override_dir_path, std::strerror (errno)); + closedir (dir); + return false; + } + + size_t count = 0; + // NOTE: The TPA list is sourced from `type_map_unique_assemblies`, which is + // only populated when `_AndroidTypeMapImplementation=llvm-ir` (the Debug + // default). With `managed` or `trimmable` typemaps the native typemap is + // empty, so no TPA paths are added and stack frames won't carry file/line + // info even under FastDev. + uint64_t expected_count = type_map.unique_assemblies_count; + for (uint64_t i = 0; i < expected_count; i++) { + TypeMapAssembly const &asm_entry = type_map_unique_assemblies[i]; + std::string_view name { + &type_map_assembly_names[asm_entry.name_offset], + static_cast(asm_entry.name_length) + }; + + // `Name` is the simple assembly name (e.g. "Mono.Android"), no extension. + std::string file_name; + file_name.reserve (name.size () + 4); + file_name.append (name); + file_name.append (".dll"); + + if (!Util::file_exists (dir_fd, file_name)) { + continue; + } + + if (!tpa_list.empty ()) { + tpa_list.append (":"); + } + tpa_list.append (override_dir_path); + tpa_list.append ("/"); + tpa_list.append (file_name); + count++; + } + closedir (dir); + + log_debug (LOG_ASSEMBLY, "FastDev: built TPA list with {} assemblies from '{}'"sv, count, override_dir_path); + return count > 0; +} diff --git a/src/native/clr/host/host.cc b/src/native/clr/host/host.cc index 3e84ef930d8..8193474eb96 100644 --- a/src/native/clr/host/host.cc +++ b/src/native/clr/host/host.cc @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include @@ -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(init_runtime_property_values); + int prop_count = static_cast(application_config.number_of_runtime_properties); + + // In Debug builds with FastDev, append `TRUSTED_PLATFORM_ASSEMBLIES` with full + // paths to the assemblies pushed into `.__override__//`. 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 fastdev_prop_names; + static std::vector 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(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(init_runtime_property_values), + prop_count, + prop_names, + prop_values, &clr_host, &domain_id ); diff --git a/src/native/clr/include/host/fastdev-assemblies.hh b/src/native/clr/include/host/fastdev-assemblies.hh index 51f1945fce3..fffcca1e650 100644 --- a/src/native/clr/include/host/fastdev-assemblies.hh +++ b/src/native/clr/include/host/fastdev-assemblies.hh @@ -4,6 +4,7 @@ #include #include +#include #include namespace xamarin::android { @@ -12,11 +13,17 @@ 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: diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 322a8d5034b..6cbb273088c 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -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; @@ -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); + StringAssert.Contains ("#STACKTRACE-BEGIN#", logcatOutput, "Stack trace start marker not found in logcat"); + + // Expect a frame in MainActivity.OnCreate to include + // "in MainActivity.cs:line " 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 () {