Repository navigation
[API Proposal]: Java Marshalling API #115506
Copy link
Copy link
Closed
Labels
api-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implementedarea-System.Runtime.InteropServices
Milestone
Description
Background and motivation
For interop with JVM based languages, an interaction model must be made to facilitate lifetime managements through GC coordination. This support is primary for Android for .NET, but there is no technical limitation to this support being enabled for non-Android scenarios.
See #114184 and https://github.com/BrzVlad/runtime/tree/feature-clr-gcbridge
API Proposal
namespace System.Runtime.InteropServices.Java;
[SupportedOSPlatform("android")]
public struct ComponentCrossReference
{
public nint SourceGroupIndex;
public nint DestinationGroupIndex;
}
[SupportedOSPlatform("android")]
public unsafe struct StronglyConnectedComponent
{
public nint Count;
public System.IntPtr* Contexts;
}
[SupportedOSPlatform("android")]
public unsafe struct MarkCrossReferences
{
public nint ComponentsLen;
public StronglyConnectedComponent* Components;
public nint CrossReferencesLen;
public ComponentCrossReference* CrossReferences;
}
[SupportedOSPlatform("android")]
partial static class JavaMarshal
{
public static unsafe void Initialize(delegate* unmanaged<MarkCrossReferences*, void> markCrossReferences);
public static GCHandle CreateCrossReferenceHandle(object obj, IntPtr context);
public static IntPtr GetContext(GCHandle obj);
public static unsafe void FinishCrossReferenceProcessing(
MarkCrossReferences* crossReferences,
Span<GCHandle> unreachableObjectHandles);
}API Usage
// Early in application start-up, a callback is provided.
JavaMarshal.Initialize(...);
object javaProxy = ...
var userContext = (IntPtr)NativeMemory.Alloc(...);
// Store data in context.
GCHandle handle = JavaMarshal.CreateCrossReferenceHandle(javaProxy, userContext);
// Store handle in a map from JNI handle to Object for fast retrieval
// Get GCHandle associated with JNI handle
GCHandle handle = ...
var userContext = JavaMarshal.GetContext(handle);
// Extract data from context.
// Free memory passed to callback registered in JavaMarshal.Initialize().
MarkCrossReferences* markRefsMem = ...;
// Collect all GCHandles marked as "unreachable"
Span<GCHandle> unreachableObjects = ...;
JavaMarshal.FinishCrossReferenceProcessing(markRefsMem, unreachableObjects);Case example can be found at #114184
Alternative Designs
Place under System.Runtime.InteropServices.GCBridge
Do not limit to SupportedOSPlatform("android")
Risks
Risk is missing some scenario/semantic in order to ensure Android for .NET is successful. This would be a new interop stack and be narrow in scope, following our Swift, ObjectiveC and WinRT interop solutions in .NET Core.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
api-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implementedarea-System.Runtime.InteropServices
Type
Projects
- StatusShow more project fieldsNo status
Activity
dotnet-policy-service commentedon May 13, 2025
Tagging subscribers to this area: @dotnet/interop-contrib
See info in area-owners.md if you want to be subscribed.
AaronRobinsonMSFT commentedon May 13, 2025
/cc @jonpryor @BrzVlad @steveisok @vitek-karas @simonrozsival @ivanpovazan @jkotas @jonathanpeppers
BrzVlad commentedon May 13, 2025
In addition to the computation and passing of the SCCs and CCRs, that are handled by this API, there are a few implementation details currently present on mono.
After the original rounds of discussions around design, the conclusion was that we will have the fetching of a WeakReference target wait for bridge processing and the finishing step of the bridge processing would clear the weak refs for bridge objects with dead java peers. It seems that the only way to achieve this would to fully iterate over the weakrefs and null them, every single time a ReferenceTrackingHandle is freed. I don't think we can do this when we get back via
ReleaseMarkCrossReferenceResourcessince we have no information about which objects were collected and which not.Given this might be somewhat expensive, it raises the question on how relevant this nulling of weakrefs really is, considering that, even with the current design, resurrection could still happen and we can end up, in theory, with a C# object that had its java peer collected. I couldn't find a detailed reason for its existence, it seems to have been added in
https://github.com/mono/mono/commit/572bd8b01721c4cf423ce1d66f96a195c54f983abut the reference to what exact kind of bug it was fixing is lost. It would surely be cleaner if the WeakReference would only have knowledge of the C# side of things. The WeakReference would then be nulled at the following collection. Calling the Target method could bring the object back alive, but is it really a problem ? I would assume the object would have its own state indicating whether the java peer is collected or not anyway, doesn't seem like it needs to rely on the weak refs for this.So as potential changes for this api:
ReleaseMarkCrossReferenceResourcesto also receive the set of to be freed crossref handles so we can iterate over the weakrefs only once ?WaitForBridgeProcessingapi here ? And not have the WeakReference.Target block.filipnavara commentedon May 13, 2025
The
WaitForBridgeProcessingbarrier is necessary for reintroducing references. We must prevent any mutations on the C# side that would invalidate the compressed graph that was sent to the Java side until it is fully processed.For a more visual explanation, please watch https://youtu.be/h57uvjMQMgU?t=534 which describes the same problem albeit with a slightly more eleborate (and less blocking) solution.
Add JavaBridgedValueManager
jonpryor commentedon May 13, 2025
@AaronRobinsonMSFT: what should the
markCrossReferencescallback do? This should be documented.I had thought that it would be"mirror"
MonoGCBridgeCallbacks::cross_referencescallback, along with:ComponentCrossReferencemirroringMonoGCBridgeXRef:runtime/src/native/public/mono/metadata/details/sgen-bridge-types.h
Lines 32 to 35 in 9bcd3e7
StronglyConnectedComponentmirroringMonoGCBridgeSCC:runtime/src/native/public/mono/metadata/details/sgen-bridge-types.h
Lines 26 to 30 in 9bcd3e7
However, one field is missing in this world view:
MonoGCBridgeSCC::is_alive:runtime/src/native/public/mono/metadata/details/sgen-bridge-types.h
Line 27 in 9bcd3e7
Thus, how does
markCrossReferencestell the GC "this object is dead/still alive"? Do we need to add aStronglyConnectedComponent.IsAlivefield?jonpryor commentedon May 13, 2025
Naming:
StronglyConnectedComponent.Contextshould probably beStronglyConnectedComponent.Contexts(plural), is it containsStronglyConnectedComponent.CountContext values (plural).AaronRobinsonMSFT commentedon May 13, 2025
@BrzVlad
That seems reasonable to me. @jkotas and I also previously talked about another callback, but replacing the "Release" with a broader "Finished" that serves both for memory clean-up and passing along the freed handles works too.
An explicit WaitForBridgeProcessing API shouldn't be needed as we can handle this as an implementation detail where the aforementioned "Finished" API must be called to track the bridge is now done.
@jonpryor
Yep, it will be.
The
markCrossReferencescallback's job is to do the actual freeing of the associatedGCHandles in the JNI value map. See the proof-of-concept's map clean-up here and corresponding bridge implementation here.jonpryor commentedon May 13, 2025
@AaronRobinsonMSFT wrote:
which doesn't actually quite make sense to me: the bridge impl has a
JVMTriggerGC()invocation, i.e. it contains JNI calls, after which it can check to see if the the Java GC collected the JNI weak ref.#114184 , meanwhile, has no Java
System.gc()invocations at all.I thus don't fully understand how it works.
AaronRobinsonMSFT commentedon May 13, 2025
Right. Any calls to JVM APIs are the domain of the GC Bridge (2nd stage) and not needed in the dotnet/runtime repo. Only the SCC building (1st stage) is handled by runtime and that is passed to the 2nd stage which will do the JVM interaction.
AaronRobinsonMSFT commentedon May 13, 2025
@BrzVlad
It sounds like the workflow you're invisioning goes something like this:
WeakReferenceinstances.GCHandles to the dead objects from some hashmap (perhaps,Dictionary<nint, GCHandle>?).GCHandleinstances are collected and passed, along with theMarkCrossReferences*, to some "Finished" API that can then free theGCHandleand null out the interestingWeakReferenceinstances collected in (1).Steps (2) through (6) would all occur under what is now considered the "bridge processing" lock.
15 remaining items
AaronRobinsonMSFT commentedon May 20, 2025
@BrzVlad The overall flow/architecture look good. There are two issues that need to be addressed.
There is a GC hole in the call to
FinishCrossReferenceProcessing. We're passing avoid*to the byref contained within aSpan<T>. That should really be anObjectOnStackinstance and extracted in a cooperative block.The
GCHandleInternalGetBridgeWaitwill need a fast/slow split instead of a single FCall that switches to preemptive mode. The FCall should return immediately with "true" if it is free, otherwise it should return "false" and then call a QCall that does the actual work in preemptive mode.Once the branch is in a PR, I can make a few changes on the VM side. Otherwise, this looks like what we've talked about.
Add JavaBridgedValueManager
bartonjs commentedon Jun 3, 2025
Video
void*sLensuffixes becameCountNinoFloris commentedon Jun 4, 2025
IMO
void*should stay asnint.Changing context to void pointer is a narrowing change to the communicated semantics. For example, an implementation might really want to store an index, and now it needs to check how this
void*value is used. If the marshaling infra only ever considers it opaque data it should not be void* typed (C# does not, and has no need to, mirror historical C-isms around void* vs intptr_t).[CoreCLR] Implement GC bridge (#10198)
agocke commentedon Jul 28, 2025
Can this be closed as done?
filipnavara commentedon Jul 28, 2025
I believe so.
For CoreCLR both the dotnet/runtime and dotnet/android support has landed. I tried it on a local build on a real-world app and it passed the smoke tests.
For NativeAOT the dotnet/runtime part has landed. Now we are waiting for the proper runtime packs to be produced so the workload part can be finished.