Skip to content
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

Type coercion and JavaCast<T> support #10

Closed
jonpryor opened this issue Apr 7, 2015 · 0 comments · Fixed by #1234
Closed

Type coercion and JavaCast<T> support #10

jonpryor opened this issue Apr 7, 2015 · 0 comments · Fixed by #1234
Labels
enhancement Proposed change to current functionality java-interop Runtime bridge between .NET and Java

Comments

@jonpryor
Copy link
Member

jonpryor commented Apr 7, 2015

Java.Interop.dll assembly “needs” a .JavaCast extension method, but the question is how to “sanely” provide one when Mono.Android.dll provides one w/o causing compilation errors

jonpryor added a commit that referenced this issue Dec 7, 2015
Context: https://bugzilla.xamarin.com/show_bug.cgi?id=25443
Fixes: #2
Fixes: #3

The problem with JniRuntime.JniValueManager.GetObject() is that it was
constrained to only return IJavaPeerable instances.  This constraint
in turn necessitated the introduction of JniMarshal.GetValue<T>()
(commit 1e14a61) so that "anything" could be stored within a
JavaObjectArray<T>, proxying values if required.  This in turn led to
a dichotomy: should JniRuntime.GetObject<T>() be used, or should
JniMarshal.GetValue<T>() be used?

This dichotomy was partially resolved in commit 77a6bf8, which
removed JniMarshal.GetValue<T>() and added a
JniRuntime.JniValueManager.GetValue<T>() method, but that just
introduced a different dichotomy: should JniValueManager.GetObject()
be used or JniValueManager.GetValue()?

Simplify this mess: kill JniValueManager.GetObject() (and all
overloads), and instead rely on JniValueManager.GetValue().

Additionally, add JniValueManager.CreateValue() overloads (Issue #3)
and fix JniValueMarshaler<T>.CreateGenericValue() logic to *not*
lookup registered values, as per commit 77a6bf8:

> clean[] up JniValueMarshaler.Create*Value() methods to always
> create values when able, simplifying the logic of
> JniValueMarshaler implementations.

Cleaning up these semantics is necessary so that
JniValueManager.CreateValue() can be meaningful.  The only time that
JniValueManager.CreateValue() won't create a *new* value is if the
value is a proxy'd instance, at which point we're not going to muck
with it and any resulting bugs are Not Our Problem™.

The primary change of killing JniValueManager.GetObject() required a
few secondary changes:

  * JniValueManager.RegisterObject<T>() can no longer throw a
    NotSupportedException when attempting to create an alias, as
    (1) the NotSupportedException is a semantic change from
    Xamarin.Android that we can't accept -- aliases are a fact of
    life, and will be required for .JavaCast<T> (Issue #10) -- and
    (2) the check was breaking unit tests.
    We now generate a warning to the GREF log when aliases are
    created, similar in spirit to what Xamarin.Android does.

  * ProxyValueMarshaler.CreateGenericValue() now falls back to
    JniValueManager.CreateObjectWrapper() instead of returning `null`.
    This was needed so that ValueManager.GetValue<Exception>(...)
    would work.

  * For a "unified type system"-like experience,
    JniValueManager.CreateObjectWrapper() now maps certain types to
    corresponding types, e.g. object is mapped to JavaObject, and
    Exception is mapped to JavaException.  This is likewise to support
    ValueManager.GetValue<Exception>(...) around a Java instance.
jonpryor added a commit to jonpryor/java.interop that referenced this issue Aug 17, 2016
When `JniRuntime.CreationOptions.DestroyRuntimeOnDispose` is true,
`JavaVM::DestroyJavaVM()` will be invoked when the `JniRuntime`
instance is disposed *or* finalized.

`JreRuntime.CreateJreVM()` would *always* set
`DestroyRuntimeOnDispose` to true, because it called
`JNI_CreateJavaVM()`, so *of course* you'd want to destroy the
Java VM, right?

Which brings us to unit tests. I don't know of any "before all test
fixtures run" and "after all test fixtures run" extension points,
which means:

1. The JVM needs to be created implicitly, "on demand."
2. There's no good way to destroy the JVM created in (1) after all
    tests have finished executing.

Which *really* means that the `JreRuntime` instance is *finalized*,
which sets us up for the unholy trifecta of AppDomain unloads,
finalizers, and JVM shutdown:

For unknown reasons, ~randomly, when running the unit tests (e.g.
`make run-tests`), the test runner will *hang*, indefinitely.

Attaching `lldb` and triggering a backtrace shows the unholy trifecta:

Finalization:

	thread dotnet#4: tid = 0x403831, 0x00007fff9656bdb6 libsystem_kernel.dylib`__psynch_cvwait + 10, name = 'tid_1403'
	  ...
	  frame dotnet#10: 0x00000001001ccb4a mono64`mono_gc_run_finalize(obj=<unavailable>, data=<unavailable>) + 938 at gc.c:256 [opt]
	  frame dotnet#11: 0x00000001001cdd4a mono64`finalizer_thread [inlined] finalize_domain_objects + 51 at gc.c:681 [opt]
	  frame dotnet#12: 0x00000001001cdd17 mono64`finalizer_thread(unused=<unavailable>) + 295 at gc.c:730 [opt]

JVM destruction:

	thread dotnet#4: tid = 0x403831, 0x00007fff9656bdb6 libsystem_kernel.dylib`__psynch_cvwait + 10, name = 'tid_1403'
	  frame #0: 0x00007fff9656bdb6 libsystem_kernel.dylib`__psynch_cvwait + 10
	  frame dotnet#1: 0x00007fffa04d4728 libsystem_pthread.dylib`_pthread_cond_wait + 767
	  frame dotnet#2: 0x000000010ba5bc76 libjvm.dylib`os::PlatformEvent::park() + 192
	  frame dotnet#3: 0x000000010ba38e32 libjvm.dylib`ParkCommon(ParkEvent*, long) + 42
	  frame dotnet#4: 0x000000010ba39708 libjvm.dylib`Monitor::IWait(Thread*, long) + 168
	  frame dotnet#5: 0x000000010ba398f0 libjvm.dylib`Monitor::wait(bool, long, bool) + 246
	  frame dotnet#6: 0x000000010bb3dca2 libjvm.dylib`Threads::destroy_vm() + 80
	  frame dotnet#7: 0x000000010b8fd665 libjvm.dylib`jni_DestroyJavaVM + 254

AppDomain unload:

	thread dotnet#37: tid = 0x4038fb, 0x00007fff9656bdb6 libsystem_kernel.dylib`__psynch_cvwait + 10
	  frame #0: 0x00007fff9656bdb6 libsystem_kernel.dylib`__psynch_cvwait + 10
	  frame dotnet#1: 0x00007fffa04d4728 libsystem_pthread.dylib`_pthread_cond_wait + 767
	  frame dotnet#2: 0x0000000100234a7f mono64`mono_os_cond_timedwait [inlined] mono_os_cond_wait(cond=0x0000000102016e50, mutex=0x0000000102016e10) + 11 at mono-os-mutex.h:105 [opt]
	  frame dotnet#3: 0x0000000100234a74 mono64`mono_os_cond_timedwait(cond=0x0000000102016e50, mutex=0x0000000102016e10, timeout_ms=<unavailable>) + 164 at mono-os-mutex.h:120 [opt]
	  frame dotnet#4: 0x0000000100234828 mono64`_wapi_handle_timedwait_signal_handle(handle=0x0000000000000440, timeout=4294967295, alertable=1, poll=<unavailable>, alerted=0x0000700000a286f4) + 536 at handles.c:1554 [opt]
	  frame dotnet#5: 0x0000000100246370 mono64`wapi_WaitForSingleObjectEx(handle=<unavailable>, timeout=<unavailable>, alertable=<unavailable>) + 592 at wait.c:189 [opt]
	  frame dotnet#6: 0x00000001001c832e mono64`mono_domain_try_unload [inlined] guarded_wait(timeout=4294967295, alertable=1) + 30 at appdomain.c:2390 [opt]
	  frame dotnet#7: 0x00000001001c8310 mono64`mono_domain_try_unload(domain=0x000000010127ccb0, exc=0x0000700000a287a0) + 416 at appdomain.c:2482 [opt]
	  frame dotnet#8: 0x00000001001c7db2 mono64`ves_icall_System_AppDomain_InternalUnload [inlined] mono_domain_unload(domain=<unavailable>) + 20 at appdomain.c:2379 [opt]
	  frame dotnet#9: 0x00000001001c7d9e mono64`ves_icall_System_AppDomain_InternalUnload(domain_id=<unavailable>) + 46 at appdomain.c:2039 [opt]

This randomly results in deadlock, and hung Jenkins bots.

Fix this behavior by altering `JreRuntime.CreateJreVM()` to *not*
override the value of
`JniRuntime.CreationOptions.DestroyRuntimeOnDispose`. This allows the
constructor of the `JreRuntime` instance to decide whether or not the
JVM is destroyed.

In the case of TestJVM, we *don't* want to destroy the JVM.

This prevents the JVM from being destroyed, which in turn prevents the
hang during process shutdown.
jonpryor added a commit that referenced this issue Aug 17, 2016
When `JniRuntime.CreationOptions.DestroyRuntimeOnDispose` is true,
`JavaVM::DestroyJavaVM()` will be invoked when the `JniRuntime`
instance is disposed *or* finalized.

`JreRuntime.CreateJreVM()` would *always* set
`DestroyRuntimeOnDispose` to true, because it called
`JNI_CreateJavaVM()`, so *of course* you'd want to destroy the
Java VM, right?

Which brings us to unit tests. I don't know of any "before all test
fixtures run" and "after all test fixtures run" extension points,
which means:

1. The JVM needs to be created implicitly, "on demand."
2. There's no good way to destroy the JVM created in (1) after all
    tests have finished executing.

Which *really* means that the `JreRuntime` instance is *finalized*,
which sets us up for the unholy trifecta of AppDomain unloads,
finalizers, and JVM shutdown:

For unknown reasons, ~randomly, when running the unit tests (e.g.
`make run-tests`), the test runner will *hang*, indefinitely.

Attaching `lldb` and triggering a backtrace shows the unholy trifecta:

Finalization:

	thread #4: tid = 0x403831, 0x00007fff9656bdb6 libsystem_kernel.dylib`__psynch_cvwait + 10, name = 'tid_1403'
	  ...
	  frame #10: 0x00000001001ccb4a mono64`mono_gc_run_finalize(obj=<unavailable>, data=<unavailable>) + 938 at gc.c:256 [opt]
	  frame #11: 0x00000001001cdd4a mono64`finalizer_thread [inlined] finalize_domain_objects + 51 at gc.c:681 [opt]
	  frame #12: 0x00000001001cdd17 mono64`finalizer_thread(unused=<unavailable>) + 295 at gc.c:730 [opt]

JVM destruction:

	thread #4: tid = 0x403831, 0x00007fff9656bdb6 libsystem_kernel.dylib`__psynch_cvwait + 10, name = 'tid_1403'
	  frame #0: 0x00007fff9656bdb6 libsystem_kernel.dylib`__psynch_cvwait + 10
	  frame #1: 0x00007fffa04d4728 libsystem_pthread.dylib`_pthread_cond_wait + 767
	  frame #2: 0x000000010ba5bc76 libjvm.dylib`os::PlatformEvent::park() + 192
	  frame #3: 0x000000010ba38e32 libjvm.dylib`ParkCommon(ParkEvent*, long) + 42
	  frame #4: 0x000000010ba39708 libjvm.dylib`Monitor::IWait(Thread*, long) + 168
	  frame #5: 0x000000010ba398f0 libjvm.dylib`Monitor::wait(bool, long, bool) + 246
	  frame #6: 0x000000010bb3dca2 libjvm.dylib`Threads::destroy_vm() + 80
	  frame #7: 0x000000010b8fd665 libjvm.dylib`jni_DestroyJavaVM + 254

AppDomain unload:

	thread #37: tid = 0x4038fb, 0x00007fff9656bdb6 libsystem_kernel.dylib`__psynch_cvwait + 10
	  frame #0: 0x00007fff9656bdb6 libsystem_kernel.dylib`__psynch_cvwait + 10
	  frame #1: 0x00007fffa04d4728 libsystem_pthread.dylib`_pthread_cond_wait + 767
	  frame #2: 0x0000000100234a7f mono64`mono_os_cond_timedwait [inlined] mono_os_cond_wait(cond=0x0000000102016e50, mutex=0x0000000102016e10) + 11 at mono-os-mutex.h:105 [opt]
	  frame #3: 0x0000000100234a74 mono64`mono_os_cond_timedwait(cond=0x0000000102016e50, mutex=0x0000000102016e10, timeout_ms=<unavailable>) + 164 at mono-os-mutex.h:120 [opt]
	  frame #4: 0x0000000100234828 mono64`_wapi_handle_timedwait_signal_handle(handle=0x0000000000000440, timeout=4294967295, alertable=1, poll=<unavailable>, alerted=0x0000700000a286f4) + 536 at handles.c:1554 [opt]
	  frame #5: 0x0000000100246370 mono64`wapi_WaitForSingleObjectEx(handle=<unavailable>, timeout=<unavailable>, alertable=<unavailable>) + 592 at wait.c:189 [opt]
	  frame #6: 0x00000001001c832e mono64`mono_domain_try_unload [inlined] guarded_wait(timeout=4294967295, alertable=1) + 30 at appdomain.c:2390 [opt]
	  frame #7: 0x00000001001c8310 mono64`mono_domain_try_unload(domain=0x000000010127ccb0, exc=0x0000700000a287a0) + 416 at appdomain.c:2482 [opt]
	  frame #8: 0x00000001001c7db2 mono64`ves_icall_System_AppDomain_InternalUnload [inlined] mono_domain_unload(domain=<unavailable>) + 20 at appdomain.c:2379 [opt]
	  frame #9: 0x00000001001c7d9e mono64`ves_icall_System_AppDomain_InternalUnload(domain_id=<unavailable>) + 46 at appdomain.c:2039 [opt]

This randomly results in deadlock, and hung Jenkins bots.

Fix this behavior by altering `JreRuntime.CreateJreVM()` to *not*
override the value of
`JniRuntime.CreationOptions.DestroyRuntimeOnDispose`. This allows the
constructor of the `JreRuntime` instance to decide whether or not the
JVM is destroyed.

In the case of TestJVM, we *don't* want to destroy the JVM.

This prevents the JVM from being destroyed, which in turn prevents the
hang during process shutdown.
@jpobst jpobst added enhancement Proposed change to current functionality java-interop Runtime bridge between .NET and Java labels Apr 16, 2020
jonpryor added a commit to jonpryor/java.interop that referenced this issue Oct 29, 2020
jonpryor added a commit that referenced this issue Oct 29, 2020
Changes: mono/LineEditor@3fa0c2e...5d67d34

  * mono/LineEditor@5d67d34: Use the new signing features for RealSigning (#10)
jonpryor added a commit that referenced this issue Jul 8, 2024
Fixes: #10
Fixes: dotnet/android#9038

Context: 1adb796

Imagine the following Java type hierarchy:

	// Java
	public abstract class Drawable {
	    public static Drawable createFromStream(IntputStream is, String srcName) {…}
	    // …
	}
	public interface Animatable {
	    public void start();
	    // …
	}
	/* package */ class SomeAnimatableDrawable extends Drawable implements Animatable {
	    // …
	}

Further imagine that a call to `Drawable.createFromStream()` returns
an instance of `SomeAnimatableDrawable`.

What does the *binding* `Drawable.CreateFromStream()` return?

	// C#
	var drawable = Drawable.CreateFromStream(input, name);
	// What is the runtime type of `drawable`?

The binding `Drawable.CreateFromStream()` look at the runtime type
of the value returned, sees that it's of type `SomeAnimatableDrawable`,
and looks for an existing binding of that type.  If no such binding
is found -- which will be the case here, as `SomeAnimatableDrawable`
is package-private -- then we check the value's base class,
ad infinitum, until we hit a type that we *do* have a binding for
(or fail catastrophically if we can't find a binding for
`java.lang.Object`).  See also [`TypeManager.CreateInstance()`][0],
which is similar to the code within
`JniRuntime.JniValueManager.GetPeerConstructor()`.

Any interfaces implemented by Java value are not consulted, only
the base class hierarchy is consulted.

Consequently, the runtime type of `drawable` would be the
`Drawable` binding; however, as `Drawable` is an `abstract` type,
the runtime type will *actually* be `DrawableInvoker`
(see e.g. 1adb796), akin to:

	// emitted by `generator`…
	internal class DrawableInvoker : Drawable {
	    // …
	}

Further imagine that we want to invoke `Animatable` methods on
`drawable`.  How do we do this?

This is where the [`.JavaCast<TResult>()` extension method][1] comes
in: we can use `.JavaCast<TResult>()` to perform a Java-side type
check for the desired type, which returns a value which can be used
to invoke methods on the specified type:

	var animatable = drawable.JavaCast<IAnimatable>();
	animatable.Start();

The problem with `.JavaCast<TResult>()` is that it always throws on
failure:

	var someOtherIface = drawable.JavaCast<ISomethingElse>();
	// throws some exception…

@mattleibow requests an "exception-free JavaCast overload" so that he
can *easily* use type-specific functionality *optionally*.

Add the following extension methods to `IJavaPeerable`:

	static partial class JavaPeerableExtensions {
	    public static TResult? JavaAs<TResult>(
	            this IJavaPeerable self);
	    public static bool TryJavaCast<TResult>(
	            this IJavaPeerable self,
	            out TResult? result);
	}

The `.JavaAs<TResult>()` extension method mirrors the C# `as`
operator, returning `null` if the the runtime type of `self`
is not implicitly convertible to the Java type corresponding to
`TResult`.  This makes it useful for one-off invocations:

	drawable.JavaAs<IAnimatable>()?.Start();

The `.TryJavaCast<TResult>()` extension method follows the
[`TryParse()` pattern][2], returning true if the type coercion
succeeds and the output `result` parameter is non-null, and false
otherwise.  This allows "nicely scoping" things within an `if`:

	if (drawable.TryJavaCast<IAnimatable>(out var animatable)) {
	    animatable.Start();
	    // …
	    animatable.Stop();
	}

[0]: https://github.com/dotnet/android/blob/06bb1dc6a292ef5618a3bb6ecca3ca869253ff2e/src/Mono.Android/Java.Interop/TypeManager.cs#L276-L291
[1]: https://github.com/dotnet/android/blob/06bb1dc6a292ef5618a3bb6ecca3ca869253ff2e/src/Mono.Android/Android.Runtime/Extensions.cs#L9-L17
[2]: https://learn.microsoft.com/dotnet/standard/design-guidelines/exceptions-and-performance#try-parse-pattern
@github-actions github-actions bot locked and limited conversation to collaborators Aug 9, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement Proposed change to current functionality java-interop Runtime bridge between .NET and Java
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants