From 9e579eaf7950cf2e443fdded588c82dddf5825ab Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Jul 2026 00:52:27 +0200 Subject: [PATCH 1/5] [NativeAOT] Use POSIX primitives for GC bridge processing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 187b207a-083b-461e-9071-e9aab61c9d07 --- src/native/clr/host/gc-bridge.cc | 22 ++++++++++++++++---- src/native/clr/include/host/gc-bridge.hh | 26 ++++++++++++++---------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/native/clr/host/gc-bridge.cc b/src/native/clr/host/gc-bridge.cc index 34cc3ccc252..2d7bbf850da 100644 --- a/src/native/clr/host/gc-bridge.cc +++ b/src/native/clr/host/gc-bridge.cc @@ -1,3 +1,5 @@ +#include + #include #include #include @@ -55,8 +57,9 @@ void GCBridge::mark_cross_references (MarkCrossReferencesArgs *args) noexcept abort_unless (args->CrossReferences != nullptr || args->CrossReferenceCount == 0, "CrossReferences must not be null if CrossReferenceCount is greater than 0"); log_mark_cross_references_args_if_enabled (args); - shared_args.store (args); - shared_args_semaphore.release (); + __atomic_store_n (&shared_args, args, __ATOMIC_RELEASE); + int ret = sem_post (&shared_args_semaphore); + abort_unless (ret == 0, "Failed to release GC bridge semaphore"); } void GCBridge::bridge_processing () noexcept @@ -66,8 +69,13 @@ void GCBridge::bridge_processing () noexcept while (true) { // wait until mark cross references args are set by the GC callback - shared_args_semaphore.acquire (); - MarkCrossReferencesArgs *args = shared_args.load (); + int ret; + do { + ret = sem_wait (&shared_args_semaphore); + } while (ret == -1 && errno == EINTR); + abort_unless (ret == 0, "Failed to acquire GC bridge semaphore"); + + MarkCrossReferencesArgs *args = __atomic_load_n (&shared_args, __ATOMIC_ACQUIRE); bridge_processing_started_callback (args); @@ -78,6 +86,12 @@ void GCBridge::bridge_processing () noexcept } } +auto GCBridge::bridge_processing_thread_entry ([[maybe_unused]] void *arg) noexcept -> void* +{ + bridge_processing (); + return nullptr; +} + [[gnu::always_inline]] void GCBridge::log_mark_cross_references_args_if_enabled (MarkCrossReferencesArgs *args) noexcept { diff --git a/src/native/clr/include/host/gc-bridge.hh b/src/native/clr/include/host/gc-bridge.hh index 80c0ac68132..f9f4972e212 100644 --- a/src/native/clr/include/host/gc-bridge.hh +++ b/src/native/clr/include/host/gc-bridge.hh @@ -1,11 +1,9 @@ #pragma once -#include +#include +#include + #include -#include -#include -#include -#include #include @@ -72,8 +70,14 @@ namespace xamarin::android { GCBridge::bridge_processing_started_callback = bridge_processing_started; GCBridge::bridge_processing_finished_callback = bridge_processing_finished; - bridge_processing_thread = std::thread { GCBridge::bridge_processing }; - bridge_processing_thread.detach (); + int ret = sem_init (&shared_args_semaphore, 0, 0); + abort_unless (ret == 0, "Failed to initialize GC bridge semaphore"); + + ret = pthread_create (&bridge_processing_thread, nullptr, GCBridge::bridge_processing_thread_entry, nullptr); + abort_unless (ret == 0, "Failed to create GC bridge processing thread"); + + ret = pthread_detach (bridge_processing_thread); + abort_unless (ret == 0, "Failed to detach GC bridge processing thread"); return mark_cross_references; } @@ -81,10 +85,9 @@ namespace xamarin::android { static void trigger_java_gc (JNIEnv *env) noexcept; private: - static inline std::thread bridge_processing_thread {}; - - static inline std::binary_semaphore shared_args_semaphore{0}; - static inline std::atomic shared_args; + static inline pthread_t bridge_processing_thread {}; + static inline sem_t shared_args_semaphore {}; + static inline MarkCrossReferencesArgs *shared_args = nullptr; static inline jobject Runtime_instance = nullptr; static inline jmethodID Runtime_gc = nullptr; @@ -93,6 +96,7 @@ namespace xamarin::android { static inline BridgeProcessingFinishedFtn bridge_processing_finished_callback = nullptr; static void bridge_processing () noexcept; + static auto bridge_processing_thread_entry (void *arg) noexcept -> void*; static void mark_cross_references (MarkCrossReferencesArgs *args) noexcept; static void log_mark_cross_references_args_if_enabled (MarkCrossReferencesArgs *args) noexcept; From 8d263b1875b5fe6b993c975fc5b1ea5d9b574784 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Jul 2026 08:41:46 +0200 Subject: [PATCH 2/5] [NativeAOT] Encapsulate POSIX GC bridge operations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 187b207a-083b-461e-9071-e9aab61c9d07 --- src/native/clr/host/gc-bridge.cc | 72 ++++++++++++++++++++---- src/native/clr/include/host/gc-bridge.hh | 29 +--------- 2 files changed, 63 insertions(+), 38 deletions(-) diff --git a/src/native/clr/host/gc-bridge.cc b/src/native/clr/host/gc-bridge.cc index 2d7bbf850da..7f849db10be 100644 --- a/src/native/clr/host/gc-bridge.cc +++ b/src/native/clr/host/gc-bridge.cc @@ -1,4 +1,6 @@ #include +#include +#include #include #include @@ -9,6 +11,46 @@ using namespace xamarin::android; +namespace { + sem_t shared_args_semaphore {}; + MarkCrossReferencesArgs *shared_args = nullptr; + + void initialize_shared_args_semaphore () noexcept + { + int ret = sem_init (&shared_args_semaphore, 0, 0); + abort_unless (ret == 0, "Failed to initialize GC bridge semaphore"); + } + + void start_bridge_processing_thread (void *(*thread_entry)(void*)) noexcept + { + pthread_t thread {}; + int ret = pthread_create (&thread, nullptr, thread_entry, nullptr); + abort_unless (ret == 0, "Failed to create GC bridge processing thread"); + + ret = pthread_detach (thread); + abort_unless (ret == 0, "Failed to detach GC bridge processing thread"); + } + + void publish_shared_args (MarkCrossReferencesArgs *args) noexcept + { + __atomic_store_n (&shared_args, args, __ATOMIC_RELEASE); + + int ret = sem_post (&shared_args_semaphore); + abort_unless (ret == 0, "Failed to release GC bridge semaphore"); + } + + auto wait_for_shared_args () noexcept -> MarkCrossReferencesArgs* + { + int ret; + do { + ret = sem_wait (&shared_args_semaphore); + } while (ret == -1 && errno == EINTR); + abort_unless (ret == 0, "Failed to acquire GC bridge semaphore"); + + return __atomic_load_n (&shared_args, __ATOMIC_ACQUIRE); + } +} + void GCBridge::initialize_on_onload (JNIEnv *env) noexcept { abort_if_invalid_pointer_argument (env, "env"); @@ -36,6 +78,24 @@ void GCBridge::initialize_on_runtime_init (JNIEnv *env, jclass runtimeClass) noe BridgeProcessing::initialize_on_runtime_init (env, runtimeClass); } +BridgeProcessingFtn GCBridge::initialize_callback ( + BridgeProcessingStartedFtn bridge_processing_started, + BridgeProcessingFinishedFtn bridge_processing_finished) noexcept +{ + abort_if_invalid_pointer_argument (bridge_processing_started, "bridge_processing_started"); + abort_if_invalid_pointer_argument (bridge_processing_finished, "bridge_processing_finished"); + abort_unless (bridge_processing_started_callback == nullptr, "GC bridge processing started callback is already set"); + abort_unless (bridge_processing_finished_callback == nullptr, "GC bridge processing finished callback is already set"); + + bridge_processing_started_callback = bridge_processing_started; + bridge_processing_finished_callback = bridge_processing_finished; + + initialize_shared_args_semaphore (); + start_bridge_processing_thread (bridge_processing_thread_entry); + + return mark_cross_references; +} + void GCBridge::trigger_java_gc (JNIEnv *env) noexcept { abort_if_invalid_pointer_argument (env, "env"); @@ -57,9 +117,7 @@ void GCBridge::mark_cross_references (MarkCrossReferencesArgs *args) noexcept abort_unless (args->CrossReferences != nullptr || args->CrossReferenceCount == 0, "CrossReferences must not be null if CrossReferenceCount is greater than 0"); log_mark_cross_references_args_if_enabled (args); - __atomic_store_n (&shared_args, args, __ATOMIC_RELEASE); - int ret = sem_post (&shared_args_semaphore); - abort_unless (ret == 0, "Failed to release GC bridge semaphore"); + publish_shared_args (args); } void GCBridge::bridge_processing () noexcept @@ -69,13 +127,7 @@ void GCBridge::bridge_processing () noexcept while (true) { // wait until mark cross references args are set by the GC callback - int ret; - do { - ret = sem_wait (&shared_args_semaphore); - } while (ret == -1 && errno == EINTR); - abort_unless (ret == 0, "Failed to acquire GC bridge semaphore"); - - MarkCrossReferencesArgs *args = __atomic_load_n (&shared_args, __ATOMIC_ACQUIRE); + MarkCrossReferencesArgs *args = wait_for_shared_args (); bridge_processing_started_callback (args); diff --git a/src/native/clr/include/host/gc-bridge.hh b/src/native/clr/include/host/gc-bridge.hh index f9f4972e212..da9a3666ead 100644 --- a/src/native/clr/include/host/gc-bridge.hh +++ b/src/native/clr/include/host/gc-bridge.hh @@ -1,8 +1,5 @@ #pragma once -#include -#include - #include #include @@ -60,35 +57,11 @@ namespace xamarin::android { static BridgeProcessingFtn initialize_callback ( BridgeProcessingStartedFtn bridge_processing_started, - BridgeProcessingFinishedFtn bridge_processing_finished) noexcept - { - abort_if_invalid_pointer_argument (bridge_processing_started, "bridge_processing_started"); - abort_if_invalid_pointer_argument (bridge_processing_finished, "bridge_processing_finished"); - abort_unless (GCBridge::bridge_processing_started_callback == nullptr, "GC bridge processing started callback is already set"); - abort_unless (GCBridge::bridge_processing_finished_callback == nullptr, "GC bridge processing finished callback is already set"); - - GCBridge::bridge_processing_started_callback = bridge_processing_started; - GCBridge::bridge_processing_finished_callback = bridge_processing_finished; - - int ret = sem_init (&shared_args_semaphore, 0, 0); - abort_unless (ret == 0, "Failed to initialize GC bridge semaphore"); - - ret = pthread_create (&bridge_processing_thread, nullptr, GCBridge::bridge_processing_thread_entry, nullptr); - abort_unless (ret == 0, "Failed to create GC bridge processing thread"); - - ret = pthread_detach (bridge_processing_thread); - abort_unless (ret == 0, "Failed to detach GC bridge processing thread"); - - return mark_cross_references; - } + BridgeProcessingFinishedFtn bridge_processing_finished) noexcept; static void trigger_java_gc (JNIEnv *env) noexcept; private: - static inline pthread_t bridge_processing_thread {}; - static inline sem_t shared_args_semaphore {}; - static inline MarkCrossReferencesArgs *shared_args = nullptr; - static inline jobject Runtime_instance = nullptr; static inline jmethodID Runtime_gc = nullptr; From a8d4e860f2f74872bffd77a117f39ce86da51d03 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Jul 2026 08:54:50 +0200 Subject: [PATCH 3/5] [NativeAOT] Make POSIX helpers private GCBridge methods Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 187b207a-083b-461e-9071-e9aab61c9d07 --- src/native/clr/host/gc-bridge.cc | 61 +++++++++++------------- src/native/clr/include/host/gc-bridge.hh | 10 ++++ 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/src/native/clr/host/gc-bridge.cc b/src/native/clr/host/gc-bridge.cc index 7f849db10be..4aef32391cc 100644 --- a/src/native/clr/host/gc-bridge.cc +++ b/src/native/clr/host/gc-bridge.cc @@ -11,44 +11,39 @@ using namespace xamarin::android; -namespace { - sem_t shared_args_semaphore {}; - MarkCrossReferencesArgs *shared_args = nullptr; - - void initialize_shared_args_semaphore () noexcept - { - int ret = sem_init (&shared_args_semaphore, 0, 0); - abort_unless (ret == 0, "Failed to initialize GC bridge semaphore"); - } +void GCBridge::initialize_shared_args_semaphore () noexcept +{ + int ret = sem_init (&shared_args_semaphore, 0, 0); + abort_unless (ret == 0, "Failed to initialize GC bridge semaphore"); +} - void start_bridge_processing_thread (void *(*thread_entry)(void*)) noexcept - { - pthread_t thread {}; - int ret = pthread_create (&thread, nullptr, thread_entry, nullptr); - abort_unless (ret == 0, "Failed to create GC bridge processing thread"); +void GCBridge::start_bridge_processing_thread () noexcept +{ + pthread_t thread {}; + int ret = pthread_create (&thread, nullptr, bridge_processing_thread_entry, nullptr); + abort_unless (ret == 0, "Failed to create GC bridge processing thread"); - ret = pthread_detach (thread); - abort_unless (ret == 0, "Failed to detach GC bridge processing thread"); - } + ret = pthread_detach (thread); + abort_unless (ret == 0, "Failed to detach GC bridge processing thread"); +} - void publish_shared_args (MarkCrossReferencesArgs *args) noexcept - { - __atomic_store_n (&shared_args, args, __ATOMIC_RELEASE); +void GCBridge::publish_shared_args (MarkCrossReferencesArgs *args) noexcept +{ + __atomic_store_n (&shared_args, args, __ATOMIC_RELEASE); - int ret = sem_post (&shared_args_semaphore); - abort_unless (ret == 0, "Failed to release GC bridge semaphore"); - } + int ret = sem_post (&shared_args_semaphore); + abort_unless (ret == 0, "Failed to release GC bridge semaphore"); +} - auto wait_for_shared_args () noexcept -> MarkCrossReferencesArgs* - { - int ret; - do { - ret = sem_wait (&shared_args_semaphore); - } while (ret == -1 && errno == EINTR); - abort_unless (ret == 0, "Failed to acquire GC bridge semaphore"); +auto GCBridge::wait_for_shared_args () noexcept -> MarkCrossReferencesArgs* +{ + int ret; + do { + ret = sem_wait (&shared_args_semaphore); + } while (ret == -1 && errno == EINTR); + abort_unless (ret == 0, "Failed to acquire GC bridge semaphore"); - return __atomic_load_n (&shared_args, __ATOMIC_ACQUIRE); - } + return __atomic_load_n (&shared_args, __ATOMIC_ACQUIRE); } void GCBridge::initialize_on_onload (JNIEnv *env) noexcept @@ -91,7 +86,7 @@ BridgeProcessingFtn GCBridge::initialize_callback ( bridge_processing_finished_callback = bridge_processing_finished; initialize_shared_args_semaphore (); - start_bridge_processing_thread (bridge_processing_thread_entry); + start_bridge_processing_thread (); return mark_cross_references; } diff --git a/src/native/clr/include/host/gc-bridge.hh b/src/native/clr/include/host/gc-bridge.hh index da9a3666ead..b0561927179 100644 --- a/src/native/clr/include/host/gc-bridge.hh +++ b/src/native/clr/include/host/gc-bridge.hh @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -62,12 +64,20 @@ namespace xamarin::android { static void trigger_java_gc (JNIEnv *env) noexcept; private: + static inline sem_t shared_args_semaphore {}; + static inline MarkCrossReferencesArgs *shared_args = nullptr; + static inline jobject Runtime_instance = nullptr; static inline jmethodID Runtime_gc = nullptr; static inline BridgeProcessingStartedFtn bridge_processing_started_callback = nullptr; static inline BridgeProcessingFinishedFtn bridge_processing_finished_callback = nullptr; + static void initialize_shared_args_semaphore () noexcept; + static void start_bridge_processing_thread () noexcept; + static void publish_shared_args (MarkCrossReferencesArgs *args) noexcept; + static auto wait_for_shared_args () noexcept -> MarkCrossReferencesArgs*; + static void bridge_processing () noexcept; static auto bridge_processing_thread_entry (void *arg) noexcept -> void*; static void mark_cross_references (MarkCrossReferencesArgs *args) noexcept; From 58dd1b6a4dbf17e330279bf19886b1f872dfa0ec Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Jul 2026 09:01:36 +0200 Subject: [PATCH 4/5] [NativeAOT] Keep GC bridge callback inline Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 187b207a-083b-461e-9071-e9aab61c9d07 --- src/native/clr/host/gc-bridge.cc | 18 ------------------ src/native/clr/include/host/gc-bridge.hh | 16 +++++++++++++++- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/native/clr/host/gc-bridge.cc b/src/native/clr/host/gc-bridge.cc index 4aef32391cc..3d8cf0942dd 100644 --- a/src/native/clr/host/gc-bridge.cc +++ b/src/native/clr/host/gc-bridge.cc @@ -73,24 +73,6 @@ void GCBridge::initialize_on_runtime_init (JNIEnv *env, jclass runtimeClass) noe BridgeProcessing::initialize_on_runtime_init (env, runtimeClass); } -BridgeProcessingFtn GCBridge::initialize_callback ( - BridgeProcessingStartedFtn bridge_processing_started, - BridgeProcessingFinishedFtn bridge_processing_finished) noexcept -{ - abort_if_invalid_pointer_argument (bridge_processing_started, "bridge_processing_started"); - abort_if_invalid_pointer_argument (bridge_processing_finished, "bridge_processing_finished"); - abort_unless (bridge_processing_started_callback == nullptr, "GC bridge processing started callback is already set"); - abort_unless (bridge_processing_finished_callback == nullptr, "GC bridge processing finished callback is already set"); - - bridge_processing_started_callback = bridge_processing_started; - bridge_processing_finished_callback = bridge_processing_finished; - - initialize_shared_args_semaphore (); - start_bridge_processing_thread (); - - return mark_cross_references; -} - void GCBridge::trigger_java_gc (JNIEnv *env) noexcept { abort_if_invalid_pointer_argument (env, "env"); diff --git a/src/native/clr/include/host/gc-bridge.hh b/src/native/clr/include/host/gc-bridge.hh index b0561927179..91e92d7e599 100644 --- a/src/native/clr/include/host/gc-bridge.hh +++ b/src/native/clr/include/host/gc-bridge.hh @@ -59,7 +59,21 @@ namespace xamarin::android { static BridgeProcessingFtn initialize_callback ( BridgeProcessingStartedFtn bridge_processing_started, - BridgeProcessingFinishedFtn bridge_processing_finished) noexcept; + BridgeProcessingFinishedFtn bridge_processing_finished) noexcept + { + abort_if_invalid_pointer_argument (bridge_processing_started, "bridge_processing_started"); + abort_if_invalid_pointer_argument (bridge_processing_finished, "bridge_processing_finished"); + abort_unless (GCBridge::bridge_processing_started_callback == nullptr, "GC bridge processing started callback is already set"); + abort_unless (GCBridge::bridge_processing_finished_callback == nullptr, "GC bridge processing finished callback is already set"); + + GCBridge::bridge_processing_started_callback = bridge_processing_started; + GCBridge::bridge_processing_finished_callback = bridge_processing_finished; + + initialize_shared_args_semaphore (); + start_bridge_processing_thread (); + + return mark_cross_references; + } static void trigger_java_gc (JNIEnv *env) noexcept; From 12bc25740add0a6a029018b22a5590fd4eecd9ef Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 17 Jul 2026 10:04:08 +0200 Subject: [PATCH 5/5] [NativeAOT] Document GC bridge handoff invariant Clarify that JavaMarshal permits only one outstanding bridge round, so shared_args is intentionally a single-slot handoff. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 70f63eb7-6599-414c-a947-d860705aa0fa --- src/native/clr/include/host/gc-bridge.hh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/native/clr/include/host/gc-bridge.hh b/src/native/clr/include/host/gc-bridge.hh index 91e92d7e599..2ec7bade985 100644 --- a/src/native/clr/include/host/gc-bridge.hh +++ b/src/native/clr/include/host/gc-bridge.hh @@ -79,6 +79,9 @@ namespace xamarin::android { private: static inline sem_t shared_args_semaphore {}; + // JavaMarshal serializes bridge rounds: it does not publish another argument block until + // bridge_processing_finished_callback has completed. This is therefore a single-slot + // handoff; the semaphore signals availability but does not queue distinct argument blocks. static inline MarkCrossReferencesArgs *shared_args = nullptr; static inline jobject Runtime_instance = nullptr;