Skip to content

Add a JniEnvironment.BeginGetValueScope() method. #11941

Description

@jonpryor

Building upon Issue dotnet/java-interop#3, we expect JavaVM.GetValue() calls to be "buried" within binding code, not directly invoked by developers (most of the time). Thus, how does a developer tell a binding method that a new value should be created instead of retrieving a possibly shared value?

Introduce JniEnvironment.BeginGetValueScope(GetValueScope):

[Flags]
public enum GetValueBehaviors {
    Default                = 0,
    CreateValues           = 1,
    DoNotMarshalExceptions = 2,
}

partial class JniEnvironment {
    public static IDisposable BeginGetValueBehaviors (GetValueBehaviors scope);
}

Calling JniEnviornment.BeginGetValueBehaviors() would alter the behavior of JavaVM.GetValue(): if GetValueBehaviors.CreateValues is specified, then JavaVM.GetValue() will instead behave like JavaVM.CreateValue(). This allows the end user to maintain some degree of control:

using (var scope = JniEnvironment.BeginGetValueBehaviors (GetValueBehaviors.CreateValues))
using (var typeface = Typeface.Create (...)) {
    // use typeface
}

The above allows disposing of the temporary with impunity, as BeginGetValueScope() will ensure that Typeface.Create() returns unique wrappers instead of possibly shared wrappers.

Activity

  1. jonpryor commented on Sep 21, 2015

    @jonpryor
    ContributorAuthor

    Another option for GetValueScope is to inhibit exception marshaling: Xamarin.Forms has a request to just have the app exit if an exception is thrown, because it Can't Happen, and if it does there's nothing reasonable to do.

    Consequently, it would be helpful if exception marshaling could be inhibited in a similar manner.

  2. added
    enhancementProposed change to current functionality.
    java-interopIssues migrated from dotnet/java-interop / relates to the Java.Interop subtree
    on Apr 16, 2020
  3. jonpryor commented on Feb 19, 2021

    @jonpryor
    ContributorAuthor

    WRT inhibiting exception marshaling, the whole concern was around needing to have two JNI invocations "everywhere", e.g. https://github.com/xamarin/java.interop/blob/main/tests/invocation-overhead/jni.cs#L6095-L6099

    (in which JniEnvironment.GetExceptionForLastThrowable() called JNIEnv::ExceptionOccurred() occurred.)

    Calling through the delegates is not a "zero-cost" operation, so the idea behind GetValueBehaviors.DoNotMarshalExceptions was as an assertion that "this method will not throw", and thus we could avoid the JNIEnv::ExceptionOccurred() invocation.

    However, with the "new" P/Invoke-based system in 926e4bc (not really "new" anymore), JNIEnv::ExceptionOccurred() is done as part of the C layer, not C#+delegates, and is faster. (Still not zero-cost, but faster.)

    I thus don't see much need for GetValueBehaviors.DoNotMarshalExceptions.

    GetValueBehaviors.CreateValues still seems like a good idea, but needs to be "paired with" a noun to describe existing behavior, e.g. .IdentityValues?

  4. added a commit that references this issue on Jun 17, 2021
  5. jonpryor commented on Oct 27, 2023

    @jonpryor
    ContributorAuthor
  6. added theissue type on Apr 29, 2025
  7. removed
    enhancementProposed change to current functionality.
    on Apr 29, 2025
  8. jonpryor commented on Jun 12, 2025

    @jonpryor
    ContributorAuthor

    A way to "save" dotnet/java-interop#1238 is to take a page out of swift-java: https://youtu.be/QSHO-GUGidA

    1. Introduce an Arena type, and
    2. Add Arena as a constructor parameter.

    Such as:

    // Java.Interop.dll
    namespace Java.Interop {
        public partial class JniInstanceArena : Disposable {
            public static JniInstanecArena Shared {get;}
    
            public JniInstanceArena ();
            public void Dispose ();
        }
    
        public partial class JavaObject {
            public JavaObject (JniInstanceArena? arena);
        }
    }
    
    // Java.Base.dll / Mono.Android.dll
    namespace Java.Lang {
        partial class Object {
            public Object (JniInstanceArenea? arena);
        }
    }

    We would also need to update generator to add this parameter overload to all bound types.

    If you use the existing non-JniInstanceArena constructor, or pass null as the arena parameter, it's "as if" JniInstanceArena.Shared were specified, and existing semantics continue (cross-VM GC bridge, etc.)

    If you allocate a new JniInstanceArena value and provide it to the constructor:

    using var arena = new JniInstanceArena();
    var o = new Java.Lang.Object(arena);

    Then when arena.Dispose() is invoked, all instances that were constructed with that arena are also disposed.

    This avoids the problems in dotnet/java-interop#1238 because it requires explicit "opt-in" to using the specified JniInstanceArena. If no JniInstanceArena is specified, as would be the case for #11856 -style caching, then the instance will only be disposed as part of JniRuntime.Dispose().


    Problems: as an "opt-in" solution, it means to fully use it all dependencies need to be built with a generator that provides that new JniInstanceArena? arena parameter. Non-binding subclasses would also need to add this constructor overload in order to opt-in to JniInstanceArena semantics.

    Which re-introduces the original question: is it worth it? If this were "new", sure. In a world with thousands of existing binding assemblies, none of which would be able to use this new feature without changes… is it worth it? ¯\(ツ)/¯

  9. jonpryor commented on Jun 17, 2025

    @jonpryor
    ContributorAuthor

    Alas, after further consideration, the JniInstanceArena improvement suggested above is still insufficient.

    What do we want? A "reasonable" way to deal with Java-side caching, as with Typeface.create() and dotnet/java-interop#3, so that we can "limit" GREF usage to a scope. The problem is that Java-side caching results in "inadvertent instance sharing" in C#, e.g. this code is bad:

    // DO NOT DO THIS
    using var typeface = Typeface.Create(…);

    As mentioned elsewhere, if you want to explicitly Dispose() of an instance, the only safe and reliable way to do so is if you call the constructor:

    // This is fine
    using var v = new Java.Lang.Integer(42);

    How can we square this circle? We can extend the JniInstanceArena concept by providing it to any method which returns a reference type:

    using (var arena = new JniInstanceArena()) {
        var typeface = Typeface.Create(…, arena);
        // `typeface` is *not* shared with other arenas, and is present only in `arena`
    }
    // `typeface` is `Dispose()`d here

    Which means this is non-viable: every method which returns a reference type would need to have this JniInstanceArena? arena = null parameter added, which would constitute an ABI break, e.g.

    namespace Java.Lang;
    
    public partial interface IAppendable {
        Java.Lang.IAppendable Append (char c, JniInstanceArena? arena = null);
    }

    Which means "fix Typeface.Create()" is not realistically possible; this would need to have been part of the design 15 years ago… :-(

  10. added
    java-interopIssues migrated from dotnet/java-interop / relates to the Java.Interop subtree
    needs-triageIssues that need to be assigned.
    on Jul 1, 2026
  11. dotnet-policy-service commented on Jul 1, 2026

    @dotnet-policy-service

    We suspect this issue is stale and no longer relevant. It will be closed if no further activity occurs within 14 more days. Any new comment (by anyone, not necessarily the author) will undo this process.

  12. dotnet-policy-service commented on Jul 15, 2026

    @dotnet-policy-service

    This issue will now be closed since it had been marked "possibly-stale" but received no further activity in the past 14 days. It is still possible to reopen or comment on the issue, but please note that the issue will be locked if it remains inactive for another 30 days.

  13. locked and limited conversation to collaborators on Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    java-interopIssues migrated from dotnet/java-interop / relates to the Java.Interop subtreeneeds-triageIssues that need to be assigned.possibly-staleIssues that are potentially no longer relevant.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions