Skip to content

Commit

Permalink
[Java.Interop-Tests] Initial dotnet test support (dotnet#801)
Browse files Browse the repository at this point in the history
Context: dotnet/android#5592

With introductory support for `Java.Interop.dll` on .NET Core via
commit 2a299eb, what about running the unit tests within
`Java.Interop-Tests.dll` on .NET Core?

Update `src/java-interop` so that the `java-interop` native library
is copied to `$(OutputPath)` of referencing projects.  This allows
e.g. `libjava-interop.dylib` to be implicitly copied into the
appropriate output dir, e.g. `bin/TestDebug-netcoreapp3.1`.
Further update `src/java-interop` so that `$(IntermediateOutputPath)`
correctly differs between net472 and netcoreapp3.1 builds, and
generate `jni.c` into `$(IntermediateOutputPath)`.  This ensures that
e.g. `bin/Debug/libjava-interop.dylib` reliably uses Mono on net472,
instead of occasionally/accidentally being the netstandard version.

Update `tests/Java.Interop-Tests` and `tests/TestJVM/TestJVM.csproj`
to multitarget between net472 and netcoreapp3.1.

Update `Java.Runtime.Environment.csproj` to reference
`java-interop.csproj`, which causes all projects which reference
`Java.Runtime.Environment.csproj` to get the `java-interop` native
library copied to the appropriate `$(OutputPath)`.

Update `JreRuntime` to use "dummy" `JniRuntime.JniValueManager` and
`JniRuntime.JniObjectReferenceManager` subclasses when *not* running
under Mono.  This avoids an `ArgumentException` when creating the
`Java.InteropTests.JavaVMFixture` instance used by the unit tests.

Update `tests/TestJVM` to reference
`Xamarin.Android.Tools.AndroidSdk.csproj`, allowing the `TestJVM` type
to use `JdkInfo` to find a JVM to use.  This allows us to avoid a
requirement to set the `JI_JVM_PATH` environment variable in order to
create `TestJVM` instances.

Update `src/Java.Interop` so that generic delegate types are not
marshaled via P/Invoke.  .NET Core doesn't support this.

Update `JniEnvironment.Types.FindClass()` to call
`info.Runtime.GetExceptionForThrowable()` as soon as possible after
calling `JNIEnv::ExceptionClear()`, instead of potentially after
calling `ClassLoader.loadClass()`.  The problem is Java-side stack-
frames: if `info.Runtime.GetExceptionForThrowable()` is delayed, then
the exception created by `info.Runtime.GetExceptionForThrowable()`
only contains the Java message text, but no stack trace, and no
"cause" information.  This meant that when generic delegate types
were still being used, `dotnet test` failures would show:

	java.lang.NoClassDefFoundError: Could not initialize class com.xamarin.interop.CallVirtualFromConstructorDerive

with *no* additional information about *why* it failed.
(It failed because P/Invoke was trying to marshal a generic delegate,
so the `ManagedPeer` static constructor threw an exception, which
was "within" a `static{ManagedPeer.registerNativeMembers(…)}` Java
block, and thus the Java type initializer failed, so nothing worked.)

Update `**/*.java` to remove Version, Culture, and PublicKeyToken
information from the assembly qualified name, for two reasons:

 1. The default Version value appears to differ between
    net472 (0.0.0.0) and netcoreapp3.1 (1.0.0.0).

 2. Wrt dotnet/android#5592, certain "security products"
    believe that the Version number is an *IP address*, and emit a
    warning about "IP Address disclosure".

Since we don't strong-name any of these assemblies (unit tests!)
*or* the expected execution environment is a "self-contained app"
in which you can't have multiple versions of the same assembly
(Xamarin.Android), remove the Version, Culture, and PublicKeyToken
information from these Java Callable Wrappers.
  • Loading branch information
jonpryor authored Feb 18, 2021
1 parent a4a2c13 commit 89a5a22
Show file tree
Hide file tree
Showing 25 changed files with 166 additions and 110 deletions.
8 changes: 0 additions & 8 deletions build-tools/scripts/RunNUnitTests.targets
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,6 @@
</ItemGroup>
<Target Name="RunTests"
Outputs="$(_TopDir)\TestResult-%(_TestAssembly.Filename).xml">
<ItemGroup>
<_JavaInteropNativeLibrary Include="$(_TopDir)\bin\$(Configuration)\libjava-interop.*" />
</ItemGroup>
<Copy
SourceFiles="@(_JavaInteropNativeLibrary)"
DestinationFolder="$(_TopDir)\bin\Test$(Configuration)"
SkipUnchangedFiles="True"
/>
<SetEnvironmentVariable Name="ANDROID_SDK_PATH" Value="$(AndroidSdkDirectory)" Condition=" '$(AndroidSdkDirectory)' != '' " />
<SetEnvironmentVariable Name="MONO_TRACE_LISTENER" Value="Console.Out" />
<SetEnvironmentVariable Name="JAVA_INTEROP_GREF_LOG" Value="bin\Test$(Configuration)\g-%(_TestAssembly.Filename).txt" />
Expand Down
9 changes: 6 additions & 3 deletions src/Java.Interop/Java.Interop/JavaProxyObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ sealed class JavaProxyObject : JavaObject, IEquatable<JavaProxyObject>
[JniAddNativeMethodRegistrationAttribute]
static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args)
{
args.Registrations.Add (new JniNativeMethodRegistration ("equals", "(Ljava/lang/Object;)Z", (Func<IntPtr, IntPtr, IntPtr, bool>)Equals));
args.Registrations.Add (new JniNativeMethodRegistration ("hashCode", "()I", (Func<IntPtr, IntPtr, int>)GetHashCode));
args.Registrations.Add (new JniNativeMethodRegistration ("toString", "()Ljava/lang/String;", (Func<IntPtr, IntPtr, IntPtr>)ToString));
args.Registrations.Add (new JniNativeMethodRegistration ("equals", "(Ljava/lang/Object;)Z", (EqualsMarshalMethod)Equals));
args.Registrations.Add (new JniNativeMethodRegistration ("hashCode", "()I", (GetHashCodeMarshalMethod)GetHashCode));
args.Registrations.Add (new JniNativeMethodRegistration ("toString", "()Ljava/lang/String;", (ToStringMarshalMethod)ToString));
}

public override JniPeerMembers JniPeerMembers {
Expand Down Expand Up @@ -72,6 +72,7 @@ public override bool Equals (object? obj)
}

// TODO: Keep in sync with the code generated by ExportedMemberBuilder
delegate bool EqualsMarshalMethod (IntPtr jnienv, IntPtr n_self, IntPtr n_value);
static bool Equals (IntPtr jnienv, IntPtr n_self, IntPtr n_value)
{
var envp = new JniTransition (jnienv);
Expand All @@ -91,6 +92,7 @@ static bool Equals (IntPtr jnienv, IntPtr n_self, IntPtr n_value)
}

// TODO: Keep in sync with the code generated by ExportedMemberBuilder
delegate int GetHashCodeMarshalMethod (IntPtr jnienv, IntPtr n_self);
static int GetHashCode (IntPtr jnienv, IntPtr n_self)
{
var envp = new JniTransition (jnienv);
Expand All @@ -107,6 +109,7 @@ static int GetHashCode (IntPtr jnienv, IntPtr n_self)
}
}

delegate IntPtr ToStringMarshalMethod (IntPtr jnienv, IntPtr n_self);
static IntPtr ToString (IntPtr jnienv, IntPtr n_self)
{
var envp = new JniTransition (jnienv);
Expand Down
51 changes: 33 additions & 18 deletions src/Java.Interop/Java.Interop/JniEnvironment.Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,29 +48,37 @@ public static unsafe JniObjectReference FindClass (string classname)
}

NativeMethods.java_interop_jnienv_exception_clear (info.EnvironmentPointer);
var e = new JniObjectReference (thrown, JniObjectReferenceType.Local);
LogCreateLocalRef (e);

var findClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local);
LogCreateLocalRef (findClassThrown);
var pendingException = info.Runtime.GetExceptionForThrowable (ref findClassThrown, JniObjectReferenceOptions.CopyAndDispose);

if (info.Runtime.ClassLoader_LoadClass != null) {
var java = info.ToJavaName (classname);
var __args = stackalloc JniArgumentValue [1];
__args [0] = new JniArgumentValue (java);

IntPtr ignoreThrown;
c = NativeMethods.java_interop_jnienv_call_object_method_a (info.EnvironmentPointer, out ignoreThrown, info.Runtime.ClassLoader.Handle, info.Runtime.ClassLoader_LoadClass.ID, (IntPtr) __args);
c = NativeMethods.java_interop_jnienv_call_object_method_a (info.EnvironmentPointer, out thrown, info.Runtime.ClassLoader.Handle, info.Runtime.ClassLoader_LoadClass.ID, (IntPtr) __args);
JniObjectReference.Dispose (ref java);
if (ignoreThrown == IntPtr.Zero) {
JniObjectReference.Dispose (ref e);
if (thrown == IntPtr.Zero) {
(pendingException as IJavaPeerable)?.Dispose ();
var r = new JniObjectReference (c, JniObjectReferenceType.Local);
JniEnvironment.LogCreateLocalRef (r);
return r;
}
NativeMethods.java_interop_jnienv_exception_clear (info.EnvironmentPointer);
NativeMethods.java_interop_jnienv_delete_local_ref (info.EnvironmentPointer, ignoreThrown);

if (pendingException != null) {
NativeMethods.java_interop_jnienv_delete_local_ref (info.EnvironmentPointer, thrown);
}
else {
var loadClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local);
LogCreateLocalRef (loadClassThrown);
pendingException = info.Runtime.GetExceptionForThrowable (ref loadClassThrown, JniObjectReferenceOptions.CopyAndDispose);
}
}

throw info.Runtime.GetExceptionForThrowable (ref e, JniObjectReferenceOptions.CopyAndDispose)!;
throw pendingException!;
#endif // !FEATURE_JNIENVIRONMENT_JI_PINVOKES
#if FEATURE_JNIOBJECTREFERENCE_SAFEHANDLES
var c = info.Invoker.FindClass (info.EnvironmentPointer, classname);
Expand All @@ -80,25 +88,32 @@ public static unsafe JniObjectReference FindClass (string classname)
return new JniObjectReference (c, JniObjectReferenceType.Local);
}
info.Invoker.ExceptionClear (info.EnvironmentPointer);
LogCreateLocalRef (thrown);
var findClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local);
LogCreateLocalRef (findClassThrown);
var pendingException = info.Runtime.GetExceptionForThrowable (ref findClassThrown, JniObjectReferenceOptions.CopyAndDispose);

var java = info.ToJavaName (classname);
var __args = stackalloc JniArgumentValue [1];
__args [0] = new JniArgumentValue (java);

c = info.Invoker.CallObjectMethodA (info.EnvironmentPointer, info.Runtime.ClassLoader.SafeHandle, info.Runtime.ClassLoader_LoadClass.ID, __args);
c = info.Invoker.CallObjectMethodA (info.EnvironmentPointer, info.Runtime.ClassLoader.SafeHandle, info.Runtime.ClassLoader_LoadClass.ID, __args);
JniObjectReference.Dispose (ref java);
var ignoreThrown = info.Invoker.ExceptionOccurred (info.EnvironmentPointer);
thrown = info.Invoker.ExceptionOccurred (info.EnvironmentPointer);
if (ignoreThrown.IsInvalid) {
thrown.Dispose ();
JniEnvironment.LogCreateLocalRef (c);
return new JniObjectReference (c, JniObjectReferenceType.Local);
(pendingException as IJavaPeerable)?.Dispose ();
var r = new JniObjectReference (c, JniObjectReferenceType.Local);
JniEnvironment.LogCreateLocalRef (r);
return r;
}
info.Invoker.ExceptionClear (info.EnvironmentPointer);
LogCreateLocalRef (ignoreThrown);
ignoreThrown.Dispose ();
var e = new JniObjectReference (thrown, JniObjectReferenceType.Local);
throw info.Runtime.GetExceptionForThrowable (ref e, JniObjectReferenceOptions.CopyAndDispose);
if (pendingException != null) {
thrown.Dispose ();
throw pendingException;
}
var loadClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local);
LogCreateLocalRef (loadClassThrown);
pendingException = info.Runtime.GetExceptionForThrowable (ref loadClassThrown, JniObjectReferenceOptions.CopyAndDispose);
throw pendingException!;
#endif // !FEATURE_JNIOBJECTREFERENCE_SAFEHANDLES
}

Expand Down
27 changes: 13 additions & 14 deletions src/Java.Interop/Java.Interop/ManagedPeer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,6 @@ namespace Java.Interop {
[JniTypeSignature (JniTypeName)]
/* static */ sealed class ManagedPeer : JavaObject {

delegate void ConstructDelegate (IntPtr jnienv,
IntPtr klass,
IntPtr n_self,
IntPtr n_assemblyQualifiedName,
IntPtr n_constructorSignature,
IntPtr n_constructorArguments);
delegate void RegisterDelegate (IntPtr jnienv,
IntPtr klass,
IntPtr n_nativeClass,
IntPtr n_assemblyQualifiedName,
IntPtr n_methods);

internal const string JniTypeName = "com/xamarin/java_interop/ManagedPeer";


Expand All @@ -37,11 +25,11 @@ static ManagedPeer ()
new JniNativeMethodRegistration (
"construct",
ConstructSignature,
(ConstructDelegate) Construct),
(ConstructMarshalMethod) Construct),
new JniNativeMethodRegistration (
"registerNativeMembers",
RegisterNativeMembersSignature,
(RegisterDelegate) RegisterNativeMembers)
(RegisterMarshalMethod) RegisterNativeMembers)
);
}

Expand All @@ -62,6 +50,12 @@ public override JniPeerMembers JniPeerMembers {
const string ConstructSignature = "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)V";

// TODO: Keep in sync with the code generated by ExportedMemberBuilder
delegate void ConstructMarshalMethod (IntPtr jnienv,
IntPtr klass,
IntPtr n_self,
IntPtr n_assemblyQualifiedName,
IntPtr n_constructorSignature,
IntPtr n_constructorArguments);
static void Construct (
IntPtr jnienv,
IntPtr klass,
Expand Down Expand Up @@ -183,6 +177,11 @@ static Type[] GetParameterTypes (string? signature)

const string RegisterNativeMembersSignature = "(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/String;)V";

delegate void RegisterMarshalMethod (IntPtr jnienv,
IntPtr klass,
IntPtr n_nativeClass,
IntPtr n_assemblyQualifiedName,
IntPtr n_methods);
static void RegisterNativeMembers (
IntPtr jnienv,
IntPtr klass,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
extends java.lang.Object
implements GCUserPeerable
{
static final String assemblyQualifiedName = "Java.Interop.JavaProxyObject, Java.Interop, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null";
static final String assemblyQualifiedName = "Java.Interop.JavaProxyObject, Java.Interop";
static {
com.xamarin.java_interop.ManagedPeer.registerNativeMembers (
JavaProxyObject.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
extends java.lang.Error
implements GCUserPeerable
{
static final String assemblyQualifiedName = "Java.Interop.JavaProxyThrowable, Java.Interop, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null";
static final String assemblyQualifiedName = "Java.Interop.JavaProxyThrowable, Java.Interop";
static {
com.xamarin.java_interop.ManagedPeer.registerNativeMembers (
JavaProxyThrowable.class,
Expand Down
52 changes: 52 additions & 0 deletions src/Java.Runtime.Environment/Java.Interop/JreRuntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;

Expand Down Expand Up @@ -44,6 +45,10 @@ public JreRuntimeOptions ()
ValueManager = ValueManager ?? new MonoRuntimeValueManager ();
ObjectReferenceManager = ObjectReferenceManager ?? new MonoRuntimeObjectReferenceManager ();
}
else {
ValueManager = ValueManager ?? new DummyValueManager ();
ObjectReferenceManager = ObjectReferenceManager ?? new DummyObjectReferenceManager ();
}
}

public JreRuntimeOptions AddOption (string option)
Expand Down Expand Up @@ -161,5 +166,52 @@ partial class NativeMethods {
[DllImport (JavaInteropLib, CharSet=CharSet.Ansi, CallingConvention=CallingConvention.Cdecl)]
internal static extern int java_interop_jvm_create (out IntPtr javavm, out IntPtr jnienv, ref JavaVMInitArgs args);
}

class DummyValueManager : JniRuntime.JniValueManager {

public override void WaitForGCBridgeProcessing ()
{
}

public override void CollectPeers ()
{
}

public override void AddPeer (IJavaPeerable reference)
{
}

public override void RemovePeer (IJavaPeerable reference)
{
}

public override void FinalizePeer (IJavaPeerable reference)
{
}

public override List<JniSurfacedPeerInfo> GetSurfacedPeers ()
{
return null;
}

public override IJavaPeerable PeekPeer (global::Java.Interop.JniObjectReference reference)
{
return null;
}

public override void ActivatePeer (IJavaPeerable self, JniObjectReference reference, ConstructorInfo cinfo, object [] argumentValues)
{
}
}

class DummyObjectReferenceManager : JniRuntime.JniObjectReferenceManager {
public override int GlobalReferenceCount {
get {return 0;}
}

public override int WeakGlobalReferenceCount {
get {return 0;}
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<ItemGroup>
<ProjectReference Include="..\Java.Interop\Java.Interop.csproj" />
<ProjectReference Include="..\..\src\java-interop\java-interop.csproj" ReferenceOutputAssembly="false" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/java-interop/java-interop.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
</ItemDefinitionGroup>

<ItemGroup>
<ClCompile Include="jni.c" />
<ClCompile Include="$(IntermediateOutputPath)jni.c" />
<ClCompile Include="java-interop.cc" />
<ClCompile Include="java-interop-dlfcn.cc" />
<ClCompile Include="java-interop-jvm.cc" />
Expand Down
21 changes: 16 additions & 5 deletions src/java-interop/java-interop.targets
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,33 @@
<Project>
<Target Name="BuildJni_c"
Inputs="$(_JNIEnvGenPath)"
Outputs="jni.c">
Outputs="$(IntermediateOutputPath)jni.c">
<MakeDir Directories="$(OutputPath)" />
<Exec Command="$(_RunJNIEnvGen) jni.g.cs jni.c" />
<Exec Command="$(_RunJNIEnvGen) $(IntermediateOutputPath)jni.g.cs $(IntermediateOutputPath)jni.c" />
</Target>

<PropertyGroup>
<_MacLib>$(OutputPath)/lib$(OutputName).dylib</_MacLib>
<_UnixLib>$(OutputPath)/lib$(OutputName).so</_UnixLib>
</PropertyGroup>

<ItemGroup Condition=" '$(OS)' != 'Windows_NT' And Exists ('/Library/Frameworks/') ">
<None Include="$(_MacLib)">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup Condition=" '$(OS)' != 'Windows_NT' And !Exists ('/Library/Frameworks/') ">
<None Include="$(_UnixLib)">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemDefinitionGroup>
<ClCompile>
<PreprocessorDefinitions>$([MSBuild]::Unescape($(DefineSymbols.Replace(' ', ';'))))</PreprocessorDefinitions>
<AdditionalIncludeDirectories>$([MSBuild]::Unescape($(_MonoIncludePath)));$([MSBuild]::Unescape($(_JdkIncludePath)))</AdditionalIncludeDirectories>
<Obj Condition=" '$(OS)' != 'Windows_NT' ">obj/$(Configuration)/%(Filename).o</Obj>
<Obj Condition=" '$(OS)' != 'Windows_NT' ">$(IntermediateOutputPath)/%(Filename).o</Obj>
</ClCompile>
</ItemDefinitionGroup>

Expand All @@ -25,7 +37,7 @@
DependsOnTargets="BuildJni_c"
Inputs="@(ClCompile);@(ClInclude)"
Outputs="%(ClCompile.Obj)">
<MakeDir Directories="obj\$(Configuration)" />
<MakeDir Directories="$(IntermediateOutputPath)" />
<ItemGroup>
<_Cl Include="@(ClCompile)">
<Compiler Condition=" '%(Extension)' == '.c' ">gcc -std=c99 -fPIC</Compiler>
Expand Down Expand Up @@ -86,7 +98,6 @@

<Target Name="Clean">
<RemoveDir Directories="obj" />
<Delete Files="jni.c" />
<Delete
Files="$(_MacLib);$(_UnixLib)"
Condition=" '$(OS)' != 'Windows_NT' "
Expand Down
2 changes: 1 addition & 1 deletion tests/Java.Interop-Tests/Java.Interop-Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<TargetFrameworks>net472;netcoreapp3.1</TargetFrameworks>
<IsPackable>false</IsPackable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class CallVirtualFromConstructorDerived : CallVirtualFromConstructorBase
[JniAddNativeMethodRegistrationAttribute]
static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args)
{
args.Registrations.Add (new JniNativeMethodRegistration ("calledFromConstructor", "(I)V", (Action<IntPtr, IntPtr, int>)CalledFromConstructorHandler));
args.Registrations.Add (new JniNativeMethodRegistration ("calledFromConstructor", "(I)V", (CalledFromConstructorMarshalMethod)CalledFromConstructorHandler));
}

public override JniPeerMembers JniPeerMembers {
Expand Down Expand Up @@ -57,6 +57,7 @@ public static unsafe CallVirtualFromConstructorDerived NewInstance (int value)
return JniEnvironment.Runtime.ValueManager.GetValue<CallVirtualFromConstructorDerived> (ref o, JniObjectReferenceOptions.CopyAndDispose);
}

delegate void CalledFromConstructorMarshalMethod (IntPtr jnienv, IntPtr n_self, int value);
static void CalledFromConstructorHandler (IntPtr jnienv, IntPtr n_self, int value)
{
var envp = new JniTransition (jnienv);
Expand Down
Loading

0 comments on commit 89a5a22

Please sign in to comment.