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
6 changes: 6 additions & 0 deletions Documentation/docs-mobile/building-apps/build-items.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,12 @@ proper initialization. However, in some cases it might not be the desired
behavior and this item group allows exclusion of libraries from this process
on an individual basis.

When an exempted JNI library is later loaded on demand, its `JNI_OnLoad`
function runs synchronously on the thread that triggered the load. Libraries
whose `JNI_OnLoad` implementation requires the main thread (for example,
because it expects a prepared main-thread `Looper` or `ALooper`) must remain
preloaded at startup and should not be added to this item group.

Some framework libraries which must be loaded at application startup will not
be affected if included in this item group.

Expand Down
6 changes: 6 additions & 0 deletions Documentation/docs-mobile/building-apps/build-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,12 @@ will be loaded by the runtime early during application startup in order to
assure their proper initialization. However, in some cases it might not be the
desired behavior and this property allows you to effectively disable it.

When JNI preloading is disabled, each exempted library's `JNI_OnLoad` function
runs synchronously on the thread that first loads the library. Libraries whose
`JNI_OnLoad` implementation requires the main thread (for example, because it
expects a prepared main-thread `Looper` or `ALooper`) must be preloaded on the
main thread instead of relying on on-demand worker-thread loading.

Some framework libraries which must be loaded at application startup will not
be affected by this property.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
-keep class mono.MonoRuntimeProvider*
-keep class mono.MonoPackageManager
-keep class mono.MonoPackageManager_Resources
-keep class mono.NativeLibraryHelper { public static void loadLibrary(java.lang.String); }
# Native hosts resolve Runtime and its GC peer interfaces by name.
-keep class mono.android.Runtime
-keep interface mono.android.IGCUserPeer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
-keep class mono.MonoRuntimeProvider* { *; }
-keep class mono.MonoPackageManager { *; }
-keep class mono.MonoPackageManager_Resources { *; }
-keep class mono.NativeLibraryHelper { public static void loadLibrary(java.lang.String); }
-keep class mono.android.Runtime { *; }
-keep interface mono.android.IGCUserPeer { *; }
-keep class mono.android.GCUserPeer { *; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
-keep class mono.MonoRuntimeProvider* { *; <init>(...); }
-keep class mono.MonoPackageManager { *; <init>(...); }
-keep class mono.MonoPackageManager_Resources { *; <init>(...); }
-keep class mono.NativeLibraryHelper { public static void loadLibrary(java.lang.String); }
-keep class mono.android.** { *; <init>(...); }
-keep class mono.java.** { *; <init>(...); }
-keep class mono.javax.** { *; <init>(...); }
Expand Down
6 changes: 6 additions & 0 deletions src/java-runtime/java/mono/android/NativeLibraryHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ private NativeLibraryHelper ()
{
}

// Called through JNI so System.loadLibrary observes an application-owned Java caller.
public static void loadLibrary (String libraryName)
{
System.loadLibrary (libraryName);
}

public static void loadLibrary (String libraryName, Context context)
{
ApplicationInfo applicationInfo = context.getApplicationInfo ();
Expand Down
2 changes: 1 addition & 1 deletion src/java-runtime/java/mono/android/Runtime.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import java.lang.Throwable;

public class Runtime {
static java.lang.Class java_lang_System = java.lang.System.class;
static java.lang.Class mono_NativeLibraryHelper = mono.NativeLibraryHelper.class;
static java.lang.Class mono_android_IGCUserPeer = mono.android.IGCUserPeer.class;

static {
Expand Down
6 changes: 1 addition & 5 deletions src/native/clr/host/host.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
#include <cstring>
#include <unistd.h>

#include <android/looper.h>

#include <coreclrhost.h>

#include <xamarin-app.hh>
Expand Down Expand Up @@ -428,9 +426,7 @@ void Host::Java_mono_android_Runtime_initInternal (

DsoLoader::init (
env,
RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, "java_lang_System", true),
ALooper_forThread (), // main thread looper
gettid ()
RuntimeUtil::get_class_from_runtime_field (env, runtimeClass, "mono_NativeLibraryHelper", true)
);

preload_jni_libraries ();
Expand Down
35 changes: 35 additions & 0 deletions src/native/clr/host/os-bridge.cc
Original file line number Diff line number Diff line change
@@ -1,12 +1,47 @@
#include <pthread.h>

#include <host/os-bridge.hh>

using namespace xamarin::android;

namespace {
pthread_key_t jni_env_key;
pthread_once_t jni_env_key_once = PTHREAD_ONCE_INIT;
}

void OSBridge::create_jni_env_key () noexcept
{
int ret = pthread_key_create (&jni_env_key, detach_current_thread);
abort_unless (ret == 0, "Unable to create TLS key for JNI thread detachment");
}

void OSBridge::register_thread_detach (JNIEnv *env) noexcept
{
int ret = pthread_once (&jni_env_key_once, create_jni_env_key);
abort_unless (ret == 0, "Unable to initialize TLS key for JNI thread detachment");
ret = pthread_setspecific (jni_env_key, env);
abort_unless (ret == 0, "Unable to register JNI thread detachment callback");
}

void OSBridge::detach_current_thread ([[maybe_unused]] void *value) noexcept
{
if (jvm == nullptr) [[unlikely]] {
return;
}

JNIEnv *env = nullptr;
if (jvm->GetEnv (reinterpret_cast<void**> (&env), JNI_VERSION_1_6) == JNI_OK && env != nullptr) {
jvm->DetachCurrentThread ();
}
}

void OSBridge::initialize_on_onload (JavaVM *vm) noexcept
{
abort_if_invalid_pointer_argument (vm, "vm");

jvm = vm;
int ret = pthread_once (&jni_env_key_once, create_jni_env_key);
abort_unless (ret == 0, "Unable to initialize TLS key for JNI thread detachment");
}

auto OSBridge::lref_to_gref (JNIEnv *env, jobject lref) noexcept -> jobject
Expand Down
11 changes: 8 additions & 3 deletions src/native/clr/include/host/os-bridge.hh
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,25 @@ namespace xamarin::android {
static auto ensure_jnienv () noexcept -> JNIEnv*
{
JNIEnv *env = nullptr;
jvm->GetEnv ((void**)&env, JNI_VERSION_1_6);
jvm->GetEnv (reinterpret_cast<void**> (&env), JNI_VERSION_1_6);
if (env == nullptr) {
JavaVMAttachArgs args;
args.version = JNI_VERSION_1_6;
args.name = nullptr;
args.group = nullptr;
jvm->AttachCurrentThread (&env, &args);
abort_unless (env != nullptr, "Unable to get a valid pointer to JNIEnv");
jint status = jvm->AttachCurrentThread (&env, &args);
abort_unless (status == JNI_OK && env != nullptr, "Unable to get a valid pointer to JNIEnv");
register_thread_detach (env);
}

return env;
}

private:
static void create_jni_env_key () noexcept;
static void register_thread_detach (JNIEnv *env) noexcept;
static void detach_current_thread (void *value) noexcept;

static inline JavaVM *jvm = nullptr;
};
}
134 changes: 121 additions & 13 deletions src/native/clr/include/runtime-base/monodroid-dl.hh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <string_view>

#include <dlfcn.h>
#include <unistd.h>
#include <android/dlext.h>

#include <runtime-base/mutex.hh>
Expand All @@ -14,13 +15,67 @@
#include <runtime-base/crc32.hh>
#include <runtime-base/dso-loader.hh>
#include <runtime-base/search.hh>
#include "startup-aware-lock.hh"

namespace xamarin::android
{
class MonodroidDl
{
struct InProgressLoad
{
uint32_t name_index;
pid_t owner_tid;
void *provisional_handle;
InProgressLoad *next;
};

static inline pthread_mutex_t dso_handle_write_lock = PTHREAD_MUTEX_INITIALIZER;
static inline pthread_cond_t dso_load_cond = PTHREAD_COND_INITIALIZER;
static inline InProgressLoad *in_progress_loads = nullptr;

static auto find_in_progress_load (uint32_t name_index) noexcept -> InProgressLoad*
{
for (InProgressLoad *cur = in_progress_loads; cur != nullptr; cur = cur->next) {
if (cur->name_index == name_index) {
return cur;
}
}

return nullptr;
}

static void remove_in_progress_load (InProgressLoad *target) noexcept
{
InProgressLoad **cur = &in_progress_loads;
while (*cur != nullptr) {
if (*cur == target) {
*cur = target->next;
return;
}
cur = &((*cur)->next);
}
}

static void publish_completed_handle (DSOCacheEntry *dso, void *handle) noexcept
{
uint32_t name_index = dso->name_index;
for (size_t i = 0; i < CoreClrBootstrap::config.number_of_dso_cache_entries; i++) {
DSOCacheEntry &entry = CoreClrBootstrap::dso_cache[i];
if (entry.name_index == name_index) {
__atomic_store_n (&entry.handle, handle, __ATOMIC_RELEASE);
}
}
__atomic_store_n (&dso->handle, handle, __ATOMIC_RELEASE);
}

static auto load_dso (DSOCacheEntry *dso, std::string_view const& name, int flags) noexcept -> void*
{
std::string_view dso_name = get_dso_name (dso);
void *handle = AndroidSystem::load_dso_from_any_directories (dso_name, flags, dso->is_jni_library);
if (handle == nullptr) {
handle = AndroidSystem::load_dso_from_any_directories (name, flags, dso->is_jni_library);
}
return handle;
}

[[gnu::always_inline]]
static constexpr auto ascii_to_lower (char c) noexcept -> char
Expand Down Expand Up @@ -152,14 +207,17 @@ namespace xamarin::android

if (dso == nullptr) {
// Packaged libraries have build-time JNI metadata in the DSO cache. A cache miss is therefore
// assumed to be a platform or other non-JNI library, avoiding a synchronous System.loadLibrary
// dispatch to the main thread.
// assumed to be a platform or other non-JNI library, avoiding an unnecessary JNI System.loadLibrary
// call.
constexpr bool IsJniLibrary = false;
constexpr bool SkipExistsCheck = true;
return DsoLoader::load<SkipExistsCheck> (name, flags, IsJniLibrary);
} else if (dso->handle != nullptr) {
log_debugf (LOG_ASSEMBLY, "monodroid_dlopen: library %.*s already loaded, returning handle %p", static_cast<int>(name.length ()), name.data (), dso->handle);
return dso->handle;
}

void *cached_handle = __atomic_load_n (&dso->handle, __ATOMIC_ACQUIRE);
if (cached_handle != nullptr) {
log_debugf (LOG_ASSEMBLY, "monodroid_dlopen: library %.*s already loaded, returning handle %p", static_cast<int>(name.length ()), name.data (), cached_handle);
return cached_handle;
}

if (dso->ignore) {
Expand All @@ -168,16 +226,66 @@ namespace xamarin::android
return nullptr;
}

std::string_view dso_name = get_dso_name (dso);
StartupAwareLock lock (dso_handle_write_lock);
dso->handle = AndroidSystem::load_dso_from_any_directories (dso_name, flags, dso->is_jni_library);
// Perform the actual load outside dso_handle_write_lock so synchronous JNI_OnLoad callbacks
// can re-enter managed code and resolve other P/Invokes without deadlocking on the cache mutex.
// Track in-progress loads per library: same-thread recursive calls receive a provisional handle
// without publishing it to dso->handle, concurrent threads wait outside the load for the
// outermost initialization to finish, and dso->handle is published only after the outermost
// load succeeds.
pid_t current_tid = gettid ();
InProgressLoad current_load {
.name_index = dso->name_index,
.owner_tid = current_tid,
.provisional_handle = nullptr,
.next = nullptr,
};
bool is_recursive_load = false;

{
lock_guard lock (dso_handle_write_lock);
for (;;) {
cached_handle = __atomic_load_n (&dso->handle, __ATOMIC_RELAXED);
if (cached_handle != nullptr) {
log_debugf (LOG_ASSEMBLY, "monodroid_dlopen: library %.*s already loaded, returning handle %p", static_cast<int>(name.length ()), name.data (), cached_handle);
return cached_handle;
}

InProgressLoad *active_load = find_in_progress_load (dso->name_index);
if (active_load == nullptr) {
current_load.next = in_progress_loads;
in_progress_loads = &current_load;
break;
}

if (active_load->owner_tid == current_tid) {
if (active_load->provisional_handle != nullptr) {
return active_load->provisional_handle;
}
is_recursive_load = true;
break;
}

pthread_cond_wait (&dso_load_cond, &dso_handle_write_lock);
}
}

void *handle = load_dso (dso, name, flags);

if (dso->handle != nullptr) {
return dso->handle;
lock_guard lock (dso_handle_write_lock);
if (is_recursive_load) {
InProgressLoad *active_load = find_in_progress_load (dso->name_index);
if (active_load != nullptr && active_load->owner_tid == current_tid && active_load->provisional_handle == nullptr) {
active_load->provisional_handle = handle;
}
return handle;
}

dso->handle = AndroidSystem::load_dso_from_any_directories (name, flags, dso->is_jni_library);
return dso->handle;
remove_in_progress_load (&current_load);
if (handle != nullptr) {
publish_completed_handle (dso, handle);
}
pthread_cond_broadcast (&dso_load_cond);
return handle;
}

static auto monodroid_dlopen (std::string_view const& name, int flags) noexcept -> void*
Expand Down
Loading