Skip to content

Commit

Permalink
[Mono.Android] Java.Interop Unification!
Browse files Browse the repository at this point in the history
Context: xamarin/monodroid@e318861
Context: 130905e
Context: de04316
Context: #9636

In the beginning there was Mono for Android, which had a set of
`Mono.Android.dll` assemblies (one per supported API level), each of
which contained "duplicated" binding logic: each API level had its
own `Java.Lang.Object`, `Android.Runtime.JNIEnv`, etc.

dotnet/java-interop started, in part, as a way to "split out" the
core integration logic, so that it *wouldn't* need to be duplicated
across every assembly.  As part of this, it introduced its own core
abstractions, notably `Java.Interop.IJavaPeerable` and
`Java.Interop.JavaObject`.

When dotnet/java-interop was first introduced into Xamarin.Android,
with xamarin/monodroid@e318861e, the integration was incomplete.
Integration continued with 130905e, allowing unit tests within
`Java.Interop-Tests.dll` to run within Xamarin.Android and
construction of instances of e.g. `JavaInt32Array`, but one large
piece of integration remained:

Moving GC bridge code *out* of `Java.Lang.Object`, and instead
relying on `Java.Interop.JavaObject`, turning this:

	namespace Java.Lang {
	    public partial class Object : System.Object, IJavaPeerable /* … */ {
	    }
	}

into this:

	namespace Java.Lang {
	    public partial class Object : Java.Interop.JavaObject, IJavaPeerable /* … */ {
	    }
	}

*Why*?  In part because @jonpryor has wanted to do this for literal
years at this point, but also in part because of #9636
and related efforts to use Native AOT, which involves avoiding /
bypassing `DllImportAttribute` invocations (for now, everything
touched by Native AOT becomes a single `.so` binary, which we don't
know the name of).  Avoiding P/Invoke means *embracing* and extending
existing Java.Interop constructs (e.g. de04316).

In addition to altering the base types of `Java.Lang.Object` and
`Java.Lang.Throwable`:

  * Remove `handle` and related fields from `Java.Lang.Object` and
    `Java.Lang.Throwable`.

  * Update `PreserveLists/Mono.Android.xml` so that the removed
    fields are note preserved.

  * Rename `JNIenvInit.AndroidValueManager` to
    `JNIEnvInit.ValueManager`, and change its type to
    `JniRuntime.JniValueManager`.  This is to help "force" usage of
    `JnIRuntime.JniValueManager` in more places, as we can't
    currently use `AndroidValueManager` in Native AOT (P/Invokes!).

  * Cleanup: Remove `JNIEnv.Exit()` and related code.  These were
    used by the Android Designer, which is no longer supported.

  * Update (`internal`) interface `IJavaObjectEx` to remove
    constructs present on `IJavaPeerable.`

Known issues:

  * `Java.Lang.Throwable(IntPtr, JniHandleOwnership)` invocation
    will result in construction of an "extra" `java.lang.Throwable`
    instance on the Java side, which will be immediately discarded.
    This is because it uses the `JavaException(string, Exception)`
    constructor, which implicitly creates a Java peer.

    We may need dotnet/java-interop changes to better address this.
  • Loading branch information
jonpryor committed Dec 20, 2024
1 parent f83583b commit 4926f0c
Show file tree
Hide file tree
Showing 12 changed files with 91 additions and 410 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,7 @@
-->
<type fullname="Java.Interop.TypeManager*JavaTypeManager" />
<type fullname="Java.Lang.Object">
<field name="handle" />
<field name="handle_type" />
<field name="refs_added" />
<method name="SetHandleOnDeserialized" />
</type>
<type fullname="Java.Lang.Throwable">
<field name="handle" />
<field name="handle_type" />
<field name="refs_added" />
</type>
</assembly>
</linker>
18 changes: 13 additions & 5 deletions src/Mono.Android/Android.Runtime/AndroidRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,29 +56,36 @@ public override string GetCurrentManagedThreadStackTrace (int skipFrames, bool f
{
if (!reference.IsValid)
return null;
var peeked = JNIEnvInit.AndroidValueManager?.PeekPeer (reference);
var peeked = JNIEnvInit.ValueManager?.PeekPeer (reference);
var peekedExc = peeked as Exception;
if (peekedExc == null) {
var throwable = Java.Lang.Object.GetObject<Java.Lang.Throwable> (reference.Handle, JniHandleOwnership.DoNotTransfer);
JniObjectReference.Dispose (ref reference, options);
return throwable;
}
JniObjectReference.Dispose (ref reference, options);
var unwrapped = JNIEnvInit.AndroidValueManager?.UnboxException (peeked!);
var unwrapped = UnboxException (peeked!);
if (unwrapped != null) {
return unwrapped;
}
return peekedExc;
}

Exception? UnboxException (IJavaPeerable value)
{
if (JNIEnvInit.ValueManager is AndroidValueManager vm) {
return vm.UnboxException (value);
}
return null;
}

public override void RaisePendingException (Exception pendingException)
{
var je = pendingException as JavaProxyThrowable;
var je = pendingException as JavaException;
if (je == null) {
je = JavaProxyThrowable.Create (pendingException);
}
var r = new JniObjectReference (je.Handle);
JniEnvironment.Exceptions.Throw (r);
JniEnvironment.Exceptions.Throw (je.PeerReference);
}
}

Expand Down Expand Up @@ -470,6 +477,7 @@ static bool CallRegisterMethodByIndex (JniNativeMethodRegistrationArguments argu
}
}

[Obsolete ("Use RegisterNativeMembers(JniType, Type, ReadOnlySpan<char>) instead.")]
public override void RegisterNativeMembers (
JniType nativeClass,
[DynamicallyAccessedMembers (MethodsAndPrivateNested)]
Expand Down
43 changes: 0 additions & 43 deletions src/Mono.Android/Android.Runtime/JNIEnv.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,49 +76,6 @@ internal static bool ShouldWrapJavaException (Java.Lang.Throwable? t, [CallerMem
return wrap;
}

[DllImport ("libc")]
static extern int gettid ();

internal static void Exit ()
{
/* Manually dispose surfaced objects and close the current JniEnvironment to
* avoid ObjectDisposedException thrown on finalizer threads after shutdown
*/
foreach (var surfacedObject in Java.Interop.Runtime.GetSurfacedObjects ()) {
try {
var obj = surfacedObject.Target as IDisposable;
if (obj != null)
obj.Dispose ();
continue;
} catch (Exception e) {
RuntimeNativeMethods.monodroid_log (LogLevel.Warn, LogCategories.Default, $"Couldn't dispose object: {e}");
}
/* If calling Dispose failed, the assumption is that user-code in
* the Dispose(bool) overload is to blame for it. In that case we
* fallback to manual deletion of the surfaced object.
*/
var jobj = surfacedObject.Target as Java.Lang.Object;
if (jobj != null)
ManualJavaObjectDispose (jobj);
}
JniEnvironment.Runtime.Dispose ();
}

/* FIXME: This reproduces the minimal steps in Java.Lang.Object.Dispose
* that needs to be executed so that we don't leak any GREF and prevent
* code execution into an appdomain that we are disposing via a finalizer.
* Ideally it should be done via another more generic mechanism, likely
* from the Java.Interop.Runtime API.
*/
static void ManualJavaObjectDispose (Java.Lang.Object obj)
{
var peer = obj.PeerReference;
var handle = peer.Handle;
var keyHandle = ((IJavaObjectEx)obj).KeyHandle;
Java.Lang.Object.Dispose (obj, ref handle, keyHandle, (JObjectRefType)peer.Type);
GC.SuppressFinalize (obj);
}

internal static void PropagateUncaughtException (IntPtr env, IntPtr javaThreadPtr, IntPtr javaExceptionPtr)
{
if (!JNIEnvInit.PropagateExceptions)
Expand Down
4 changes: 2 additions & 2 deletions src/Mono.Android/Android.Runtime/JNIEnvInit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ internal struct JnienvInitializeArgs {
}
#pragma warning restore 0649

internal static AndroidValueManager? AndroidValueManager;
internal static JniRuntime.JniValueManager? ValueManager;
internal static bool IsRunningOnDesktop;
internal static bool jniRemappingInUse;
internal static bool MarshalMethodsEnabled;
Expand Down Expand Up @@ -94,7 +94,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)

BoundExceptionType = (BoundExceptionType)args->ioExceptionType;
androidRuntime = new AndroidRuntime (args->env, args->javaVm, args->grefLoader, args->Loader_loadClass, args->jniAddNativeMethodRegistrationAttributePresent != 0);
AndroidValueManager = (AndroidValueManager) androidRuntime.ValueManager;
ValueManager = androidRuntime.ValueManager;

IsRunningOnDesktop = args->isRunningOnDesktop == 1;

Expand Down
3 changes: 0 additions & 3 deletions src/Mono.Android/Java.Interop/IJavaObjectEx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
namespace Java.Interop {

interface IJavaObjectEx {
IntPtr KeyHandle {get; set;}
bool IsProxy {get; set;}
bool NeedsActivation {get; set;}
IntPtr ToLocalJniHandle ();
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Mono.Android/Java.Interop/Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public static class Runtime {
[Obsolete ("Please use Java.Interop.JniEnvironment.Runtime.ValueManager.GetSurfacedPeers()")]
public static List<WeakReference> GetSurfacedObjects ()
{
var peers = JNIEnvInit.AndroidValueManager!.GetSurfacedPeers ();
var peers = JNIEnvInit.ValueManager!.GetSurfacedPeers ();
var r = new List<WeakReference> (peers.Count);
foreach (var p in peers) {
if (p.SurfacedPeer.TryGetTarget (out var target))
Expand Down
13 changes: 6 additions & 7 deletions src/Mono.Android/Java.Interop/TypeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,10 @@ static Type[] GetParameterTypes (string? signature)
static void n_Activate (IntPtr jnienv, IntPtr jclass, IntPtr typename_ptr, IntPtr signature_ptr, IntPtr jobject, IntPtr parameters_ptr)
{
var o = Java.Lang.Object.PeekObject (jobject);
var ex = o as IJavaObjectEx;
var ex = o as IJavaPeerable;
if (ex != null) {
if (!ex.NeedsActivation && !ex.IsProxy)
var state = ex.JniManagedPeerState;
if (!state.HasFlag (JniManagedPeerStates.Activatable) && !state.HasFlag (JniManagedPeerStates.Replaceable))
return;
}
if (!ActivationEnabled) {
Expand Down Expand Up @@ -171,10 +172,8 @@ internal static void Activate (IntPtr jobject, ConstructorInfo cinfo, object? []
{
try {
var newobj = RuntimeHelpers.GetUninitializedObject (cinfo.DeclaringType!);
if (newobj is Java.Lang.Object o) {
o.handle = jobject;
} else if (newobj is Java.Lang.Throwable throwable) {
throwable.handle = jobject;
if (newobj is IJavaPeerable peer) {
peer.SetPeerReference (new JniObjectReference (jobject));
} else {
throw new InvalidOperationException ($"Unsupported type: '{newobj}'");
}
Expand Down Expand Up @@ -232,7 +231,7 @@ static Exception CreateJavaLocationException ()
return null;
}

internal static IJavaPeerable CreateInstance (IntPtr handle, JniHandleOwnership transfer)
internal static IJavaPeerable? CreateInstance (IntPtr handle, JniHandleOwnership transfer)
{
return CreateInstance (handle, transfer, null);
}
Expand Down
Loading

0 comments on commit 4926f0c

Please sign in to comment.