Skip to content

[Mono.Android] call new Java "GC Bridge" APIs #10125

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

jonathanpeppers
Copy link
Member

@jonathanpeppers jonathanpeppers commented May 8, 2025

Context: dotnet/runtime#114184
Context: https://github.com/jonathanpeppers/BridgeSandbox
Context: dotnet/runtime@main...BrzVlad:runtime:feature-clr-gcbridge

So far, I:

  • Built dotnet/runtime, I built this branch:

https://github.com/jonathanpeppers/runtime/tree/gcbridge_impl

  • Put the relevant arm64 packs in packages/ folder and configured a
    NuGet.config to use them.

  • Setup build-tools\scripts\custom-runtime.targets to use the
    10.0.0-dev dotnet/runtime packs.

  • In ManagedValueManager.cs...

  • Call a new native method on startup:

var mark_cross_references_ftn = RuntimeNativeMethods.clr_initialize_gc_bridge (&BridgeProcessingFinished);
  • Pass the returned function pointer to:
JavaMarshal.Initialize (mark_cross_references_ftn);
  • In AddPeer(IJavaPeerable value) call JavaMarshal.CreateReferenceTrackingHandle()

  • In RemovePeer(IJavaPeerable value) call:

static unsafe void FreeHandle (GCHandle handle)
{
    IntPtr context = JavaMarshal.GetContext (handle);
    NativeMemory.Free((void*)context);
}

Update

I added a second commit, of what it could look like if everything was managed/C# code.

@jonathanpeppers jonathanpeppers force-pushed the dev/peppers/gcbridge branch 2 times, most recently from bbc1c64 to 58739d3 Compare May 8, 2025 20:09
}
}
return peers;
}
}

static GCHandle CreateReferenceTrackingHandle (IJavaPeerable value) =>
JavaMarshal.CreateReferenceTrackingHandle (value, value.PeerReference.Handle);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This concerns me, because this value will change over time!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To elaborate, with MonoVM -- and thus I assume eventually with dotnet/runtime#114184 -- when a "bridged" instance is no longer reachable from the .NET GC, we:

  1. "Toggle" JavaObject.PeerReference from a JNI Global Reference to a JNI Weak Global Reference.
  2. Trigger a Java-side GC.
  3. If the instance survives the Java-side GC, we "toggle" the JNI Weak Global Reference to a JNI Global Reference.

This means the value of value.PeerReference changes, because 1...3 will result in a different JNI Global Reference value.

Meaning using "value.PeerReference.Handle` is immediately "bad".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also wonder if using value.PeerReference is necessary? It's for the userContext parameter; perhaps it could just be null?

E.g. CreateCrossReferenceHandle() calls CreateHandleWithExtraInfo() and providing userContext as the "extra info", and JavaMarshal.GetContext() returns GetExtraInfoFromHandle(), i.e. appears to return "userContext".

Aside: if true, this makes me think that JavaMarshal.GetContext() is mis-named, and should instead be called JavaMarshal.GetUserContext(), which would make the relationship more apparent.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

…though, to be fair, it's CreateReferenceTrackingHandle(object obj, System.IntPtr context), so GetContext() does make sense…

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where @jonpryor got to in the end is how this should be used. He is correct in that the PeerReference (assumed to be a JNI handle) should be contained within the allocated memory that is the "context". Renaming to "UserContext" is a reasonable request and we can workshop that in API review.

The PeerReference property could dereference the context pointer and get the JNI handle if desired. In fact, the context could be a pointer sized allocation or any size, store any addtiional details you want there.

Context: dotnet/runtime#114184
Context: https://github.com/jonathanpeppers/BridgeSandbox
Context: dotnet/runtime@main...BrzVlad:runtime:feature-clr-gcbridge

So far, I:

* Built dotnet/runtime, I built this branch:

https://github.com/jonathanpeppers/runtime/tree/gcbridge_impl

* Put the relevant arm64 packs in `packages/` folder and configured a
  `NuGet.config` to use them.

* Setup `build-tools\scripts\custom-runtime.targets` to use the
  10.0.0-dev dotnet/runtime packs.

* In `ManagedValueManager.cs`...

* Call a new native method on startup:

    var mark_cross_references_ftn = RuntimeNativeMethods.clr_initialize_gc_bridge (&BridgeProcessingFinished);

* Pass the returned function pointer to:

    JavaMarshal.Initialize (mark_cross_references_ftn);

* In `AddPeer(IJavaPeerable value)` call `JavaMarshal.CreateReferenceTrackingHandle()`

* In `RemovePeer(IJavaPeerable value)` call:

    static unsafe void FreeHandle (GCHandle handle)
    {
        IntPtr context = JavaMarshal.GetContext (handle);
        NativeMemory.Free((void*)context);
    }
@jonathanpeppers jonathanpeppers force-pushed the dev/peppers/gcbridge branch from ab51219 to f4a2148 Compare May 8, 2025 21:45
@jonpryor
Copy link
Member

jonpryor commented May 8, 2025

What I suspect needs to happen is that JavaObject and JavaException need to be updated to have:

partial class JavaObject {
	// Remove the following members
	[NonSerialized] IntPtr                  handle;
	[NonSerialized] JniObjectReferenceType  handle_type;
	// Used by JavaInteropGCBridge
	[NonSerialized] IntPtr                  weak_handle;
	[NonSerialized] int                     refs_added;
}

and replace with:

internal struct JniObjectInfo {
	public IntPtr                  handle;
	public JniObjectReferenceType  handle_type;
	// Used by JavaInteropGCBridge
	public IntPtr                  weak_handle;
	public int                     refs_added;
}

partial class JavaObject {
	IntPtr JniObjectInfo = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(JniObjectInfo)));
}

…then update everything within JavaObject and JavaException to go "through" JniObjectInfo, e.g. instead of this.handle, do ((JniObjectInfo)this.JniObjectInfo).handle.

This would allow CreateReferenceTrackingHandle() to pass in value.JniObjectInfo, which is a non-moving block of per-instance memory, which would allow the GC bridge callback to reasonably access all needed members.

Further note: this in turn means that the appropriate starting point for this PR is in dotnet/java-interop, not dotnet/android.

@jonathanpeppers
Copy link
Member Author

The new APIs are all decorated with:

[SupportedOSPlatform("android")]

We can use them in java.interop, but would have to wrap them all with #pragma warning disable CA1416.

java.interop is also not building with TargetFramework=net10.0:

The new APIs will only be in .NET 10.

So, maybe we have to do a branch of java.interop that just updates JavaObject and JavaException combined with this PR?

[UnmanagedCallersOnly]
internal static unsafe void FinishBridgeProcessing (nint sccsLen, StronglyConnectedComponent* sccs, nint ccrsLen, ComponentCrossReference* ccrs)
{
Java.Lang.JavaSystem.Gc ();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the C# binding of java.lang.System.gc(): https://developer.android.com/reference/java/lang/System#gc()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants