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
57 changes: 48 additions & 9 deletions src/native/common/include/runtime-base/mainthread-dso-loader.hh
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

#include <cerrno>
#include <cstring>
#include <ctime>
#include <semaphore.h>
#include <unistd.h>

#include <array>
#include <semaphore>
#include <string_view>

#include <android/looper.h>
Expand All @@ -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,
Expand All @@ -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]);
Expand All @@ -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.
}

Expand Down Expand Up @@ -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<int>(full_name.length ()), full_name.data ());
return false;
}
Expand All @@ -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<MainThreadDsoLoader*> (data);
Expand All @@ -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;
};

Expand All @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <string>
#include <cstdlib>
#include <cstring>
#include <string_view>

#include <jni.h>
Expand Down Expand Up @@ -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<size_t> (undecorated_lib_name.length (), 1uz);
char *lib_name = stack_buffer;

if (needed_size > StackBufferSize) [[unlikely]] {
lib_name = static_cast<char*> (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.");
Comment thread
simonrozsival marked this conversation as resolved.
}
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 ();
Expand All @@ -51,7 +84,6 @@ namespace xamarin::android {
return true;
}

private:
static inline jmethodID System_loadLibrary = nullptr;
static inline jclass systemKlass = nullptr;
};
Expand Down
Loading