diff --git a/src/native/common/include/runtime-base/mainthread-dso-loader.hh b/src/native/common/include/runtime-base/mainthread-dso-loader.hh index eaa7c12526d..b97cca64679 100644 --- a/src/native/common/include/runtime-base/mainthread-dso-loader.hh +++ b/src/native/common/include/runtime-base/mainthread-dso-loader.hh @@ -2,10 +2,11 @@ #include #include +#include +#include #include #include -#include #include #include @@ -23,6 +24,17 @@ namespace xamarin::android { public: explicit MainThreadDsoLoader () noexcept { + // Not shared between processes, initially unsignalled. Can only fail if the initial value + // exceeds `SEM_VALUE_MAX`, which 0 clearly does not. + if (sem_init (&load_complete_sem, 0, 0) != 0) { + Helpers::abort_applicationf ( + LOG_ASSEMBLY, + std::source_location::current (), + "Failed to initialize the DSO load semaphore. %s", + strerror (errno) + ); + } + if (pipe (pipe_fds) != 0) { Helpers::abort_applicationf ( LOG_ASSEMBLY, @@ -49,7 +61,10 @@ namespace xamarin::android { MainThreadDsoLoader (const MainThreadDsoLoader&) = delete; MainThreadDsoLoader (MainThreadDsoLoader&&) = delete; - virtual ~MainThreadDsoLoader () noexcept + // Not `virtual` on purpose. The class is never derived from nor destroyed through a base class + // pointer and a virtual destructor would make the compiler emit the deleting destructor, which + // pulls in `operator delete` and, with it, a dependency on `libc++`. + ~MainThreadDsoLoader () noexcept { if (pipe_fds[0] != -1) { ALooper_removeFd (main_thread_looper, pipe_fds[0]); @@ -60,6 +75,8 @@ namespace xamarin::android { close (pipe_fds[1]); } + sem_destroy (&load_complete_sem); + // No need to release the looper, it needs to stay acquired. } @@ -90,12 +107,10 @@ namespace xamarin::android { return false; } - // Wait for the callback to complete - using namespace std::literals; + // Wait for the callback to complete. 3s should be more than enough time for the library to load. + constexpr time_t LoadTimeoutSeconds = 3; - // We'll wait for up to 3s, it should be more than enough time for the library to load - bool success = load_complete_sem.try_acquire_for (3s); - if (!success) { + if (!try_acquire_for (LoadTimeoutSeconds)) { log_warnf (LOG_ASSEMBLY, "Timeout while waiting for shared library '%.*s' to load.", static_cast(full_name.length ()), full_name.data ()); return false; } @@ -117,6 +132,30 @@ namespace xamarin::android { private: + // Waits up to `timeout_seconds` for the main thread callback to signal that it is done. + // Returns `false` if it didn't within that time. + [[nodiscard]] auto try_acquire_for (time_t timeout_seconds) noexcept -> bool + { + // `sem_timedwait` takes an absolute deadline and, until API 28, only supports + // `CLOCK_REALTIME`. A wall clock adjustment inside the timeout window could cut the wait + // short or stretch it, which is harmless for a sanity timeout like this one. + timespec deadline {}; + clock_gettime (CLOCK_REALTIME, &deadline); + deadline.tv_sec += timeout_seconds; + + // The deadline is absolute, so retrying after a signal cannot extend the total wait. + int ret; + do { + ret = sem_timedwait (&load_complete_sem, &deadline); + } while (ret == -1 && errno == EINTR); + + if (ret != 0 && errno != ETIMEDOUT) [[unlikely]] { + log_warnf (LOG_ASSEMBLY, "Failed to wait for the DSO load to complete. %s", strerror (errno)); + } + + return ret == 0; + } + static auto load_cb ([[maybe_unused]] int fd, [[maybe_unused]] int events, void *data) noexcept -> int { auto self = reinterpret_cast (data); @@ -126,7 +165,7 @@ namespace xamarin::android { auto over_and_out = [&self]() -> int { // We're one-shot, 0 means just that - self->load_complete_sem.release (); + sem_post (&self->load_complete_sem); return 0; }; @@ -149,7 +188,7 @@ namespace xamarin::android { private: int pipe_fds[2] = {-1, -1}; - std::binary_semaphore load_complete_sem {0}; + sem_t load_complete_sem {}; std::string_view undecorated_library_name {}; bool load_success = false; diff --git a/src/native/common/include/runtime-base/system-loadlibrary-wrapper.hh b/src/native/common/include/runtime-base/system-loadlibrary-wrapper.hh index 21625cec219..e20a063b37b 100644 --- a/src/native/common/include/runtime-base/system-loadlibrary-wrapper.hh +++ b/src/native/common/include/runtime-base/system-loadlibrary-wrapper.hh @@ -1,6 +1,7 @@ #pragma once -#include +#include +#include #include #include @@ -29,17 +30,49 @@ namespace xamarin::android { Helpers::abort_application ("System.loadeLibrary wrapper class not initialized properly."sv); } - // std::string is needed because we must pass a NUL-terminated string to Java, otherwise - // strange things happen (and std::string_view is not necessarily such a string) - const std::string lib_name { undecorated_lib_name }; - log_debugf (LOG_ASSEMBLY, "Undecorated library name: %s", lib_name.c_str ()); + // We must pass a NUL-terminated string to Java, otherwise strange things happen, and a + // `std::string_view` is not necessarily such a string. Library names are short, so the copy + // will practically always fit in the stack buffer. + constexpr size_t StackBufferSize = 256uz; - jstring java_lib_name = jni_env->NewStringUTF (lib_name.c_str ()); + char stack_buffer[StackBufferSize]; + size_t needed_size = Helpers::add_with_overflow_check (undecorated_lib_name.length (), 1uz); + char *lib_name = stack_buffer; + + if (needed_size > StackBufferSize) [[unlikely]] { + lib_name = static_cast (std::malloc (needed_size)); + if (lib_name == nullptr) [[unlikely]] { + Helpers::abort_application ("Unable to allocate memory for the shared library name."sv); + } + } + + std::memcpy (lib_name, undecorated_lib_name.data (), undecorated_lib_name.length ()); + lib_name[undecorated_lib_name.length ()] = '\0'; + + bool ret = load (jni_env, lib_name); + + if (lib_name != stack_buffer) { + std::free (lib_name); + } + + return ret; + } + + private: + static auto load (JNIEnv *jni_env, const char *lib_name) noexcept -> bool + { + log_debugf (LOG_ASSEMBLY, "Undecorated library name: %s", lib_name); + + jstring java_lib_name = jni_env->NewStringUTF (lib_name); if (java_lib_name == nullptr) [[unlikely]] { // It's an OOM, there's nothing better we can do Helpers::abort_application ("Java string allocation failed while loading a shared library."); } jni_env->CallStaticVoidMethod (systemKlass, System_loadLibrary, java_lib_name); + // This runs while loading the application's shared libraries, before returning to Java, so + // the local reference has to be released here or the local reference table fills up. + jni_env->DeleteLocalRef (java_lib_name); + if (jni_env->ExceptionCheck ()) { log_debugf (LOG_ASSEMBLY, "System.loadLibrary threw a Java exception. Will attempt to log it."); jni_env->ExceptionDescribe (); @@ -51,7 +84,6 @@ namespace xamarin::android { return true; } - private: static inline jmethodID System_loadLibrary = nullptr; static inline jclass systemKlass = nullptr; };