-
Notifications
You must be signed in to change notification settings - Fork 551
[WIP] GC bridge integration for CoreCLR #10185
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
if (peer.Target is IDisposable disposable) | ||
disposable.Dispose (); | ||
if (handle.IsAllocated) | ||
(handle.Target as IDisposable)?.Dispose (); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems a bit weird, but I guess this method is only called from Tests so we don't care too much ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's only used in tests. I wonder if we actually need it, and if we could obsolete this as a public API so that people reach out to us if they have actual use-cases for this method. /cc @grendello @jonathanpeppers
} | ||
|
||
public override void AddPeer (IJavaPeerable value) | ||
{ | ||
if (RegisteredInstances == null) | ||
throw new ObjectDisposedException (nameof (ManagedValueManager)); | ||
|
||
WaitForGCBridgeProcessing (); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what race this wait is meant to prevent. Even if we do the wait here, the code below could still race with a bridge collection ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's not doing anything really. I added it a while ago and I just never removed it. Let me get rid of all the calls to that method in ManagedValueManager.
} | ||
} | ||
|
||
static unsafe void FreeReferenceTrackingHandle (GCHandle handle) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally, we shouldn't free the handle ourselves, rather let the runtime do it for us, so we don't run into races with the GC.
I expect we might still want to free it early via Dispose. If that is the case, we would need to have certainty that this handle/context is not part of a current bridge GC. This would be the case if the C# object that this handle points to is not dead. So if we get hold of this GCHandle from the IJavaPeerable, then it is ok. If we just traverse the RegisteredInstances
and free some handles from there, then this sounds potentially problematic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If that is the case, we would need to have certainty that this handle/context is not part of a current bridge GC.
Right, that's what the naive WaitForBridgeProcessing
method would ideally do, but it really cannot. We would need a ReaderWriterLockSlim
or something.
So if we get hold of this GCHandle from the IJavaPeerable, then it is ok. If we just traverse the RegisteredInstances and free some handles from there, then this sounds potentially problematic.
This shouldn't be a problem for DisposePeer where we have a live reference to the IJavaPeerable value
object. We could maybe use a combination of GC.KeepAlive and GC.SuppressFinalize to ensure it's not collected while we're inside of DisposePeer
?
I suppose this could be a real problem in the case of AddPeer
when we're replacing some existing handle stored in the dictionary. We should only call FreeReferenceTrackingHandle
if that handle has a valid target (my understanding is that accessing the .Target
would block until the current GC processing finishes) and if it has a valid target, we can use GC.KeepAlive (target)
to ensure that this object isn't collected 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(my understanding is that accessing the
.Target
would block until the current GC processing finishes)
That is the problem. It doesn't block for GCHandle.Target
, only for WeakReference.Target
. I think adding blocking to GCHandle
as well adds some complexity and we would rather avoid it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. OK. I will look into a better reader-writer lock mechanism we could use in managed code.
if (p.Target is not IJavaPeerable peer) | ||
continue; | ||
if (!JniEnvironment.Types.IsSameObject (peer.PeerReference, value.PeerReference)) | ||
continue; | ||
if (Replaceable (p)) { | ||
FreeReferenceTrackingHandle (p); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean here that if we have a cross GCHandle C#1 -> Java1. And we try to add a new bridge object C#2 -> Java1. Then we attempt to free the first GCHandle and create a new one instead ? I don't fully understand the reasoning behind this behavior. Also, as described in the comment for FreeReferenceTrackingHandle
it seems like the first GCHandle could be part of the gcbridge machinery, if C#1 is dead in managed world and we would race with the GC here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then we attempt to free the first GCHandle and create a new one instead ? I don't fully understand the reasoning behind this behavior.
Based on the existing code in the mono bridge (
android/src/Mono.Android/Android.Runtime/AndroidRuntime.cs
Lines 744 to 781 in 20de949
bool ShouldReplaceMapping (WeakReference<IJavaPeerable> current, JniObjectReference reference, IJavaPeerable value, out IJavaPeerable? target) | |
{ | |
target = null; | |
if (current == null) | |
return true; | |
// Target has been GC'd; see also FIXME, above, in finalizer | |
if (!current.TryGetTarget (out target) || target == null) | |
return true; | |
// It's possible that the instance was GC'd, but the finalizer | |
// hasn't executed yet, so the `instances` entry is stale. | |
if (!target.PeerReference.IsValid) | |
return true; | |
if (!JniEnvironment.Types.IsSameObject (target.PeerReference, reference)) | |
return false; | |
// JNIEnv.NewObject/JNIEnv.CreateInstance() compatibility. | |
// When two MCW's are created for one Java instance [0], | |
// we want the 2nd MCW to replace the 1st, as the 2nd is | |
// the one the dev created; the 1st is an implicit intermediary. | |
// | |
// Meanwhile, a new "replaceable" instance should *not* replace an | |
// existing "replaceable" instance; see dotnet/android#9862. | |
// | |
// [0]: If Java ctor invokes overridden virtual method, we'll | |
// transition into managed code w/o a registered instance, and | |
// thus will create an "intermediary" via | |
// (IntPtr, JniHandleOwnership) .ctor. | |
if (target.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable) && | |
!value.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable)) { | |
return true; | |
} | |
return false; | |
} |
new MyObject()
to be the actual object stored in the RegisteredInstances
dictionary.
I replied to the other comment earlier and I believe you are right and this is a problematic spot and it will need extra care.
if (RegisteredInstances == null) | ||
throw new ObjectDisposedException (nameof (ManagedValueManager)); | ||
|
||
WaitForGCBridgeProcessing (); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if this wait achieves something. In general, for key synchronization pieces with the GC, I think we should add explicit comments regarding what we are trying to achieve, what race we try to prevent. Later, when we are smarter, we could see whether we actually need it or not, or have another solution for these problems.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I mentioned in another comment, this was added very early, and I just never revisited it. Removing.
foreach (int i in indexesToRemove) { | ||
// Remove the peer from the list | ||
var handle = peers[i]; | ||
FreeReferenceTrackingHandle (handle); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think here we are only freeing handles that have value
as the Target, which, if it was not obtained from the gchandle weak ref, then we know it shouldn't be part of the current bridge. This would mean that this should be safe, in theory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I remember correctly, this method is called from the base Dispose
of all bridge objects. My understanding is that it should just remove dead handles from the RegisteredInstances
.
Alternatively, we could go over the whole RegisteredInstances
collection during GC processing and drop all previously freed handles. The downside of this would be that this behavior would now diverge from what we do in the mono counterpart of this code.
@@ -181,6 +282,8 @@ public override void RemovePeer (IJavaPeerable value) | |||
|
|||
public override void FinalizePeer (IJavaPeerable value) | |||
{ | |||
WaitForGCBridgeProcessing (); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
There are a lot of waits for bridge processing which I don't think are needed. I think the only place we need to wait for bridge processing is when we obtain a C# object ref from the java object (JniObjectReference?), not exactly sure where this location is. This is because by doing this we could insert into C# world an object that we thought was dead during last GC, end up calling Dispose on it racing with the GC. |
|
||
void GCBridge::wait_for_bridge_processing () noexcept | ||
{ | ||
std::shared_lock<std::shared_mutex> lock (processing_mutex); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't seem correct. In theory we could obtain this mutex, before the bridge worker thread actually acquires it, doing the BP2 stage. We would need at least an additional variable to mark whether we have a bridge in progress. Probably makes sense to use a condition variable for this.
The runtime implementation contains implicit wait for bridge processing when obtaining the Target of a WeakReference. If we would use this mechanism when obtaining the C# object from a Java object, then we probably won't need our own implementation in |
} | ||
} | ||
|
||
public void Dispose () |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that this should be legal to call only if the caller holds a reference to the Target
.
Work in progress.