Skip to content

Commit

Permalink
[Java.Interop] Type & Member Remapping Support (#936)
Browse files Browse the repository at this point in the history
Context: #867

There are two scenarios for which a "generalized" type & member
remapping solution would be useful:

 1. Desugaring
 2. Microsoft Intune MAM

Note: this new support is only present when targeting .NET 6+,
and will not (currently) be present in Classic Xamarin.Android.

~~ Desugaring ~~

Context: dotnet/android#4574 (comment)
Context: dotnet/android#6142 (comment)

The [Android Runtime][0] is responsible for running the Dalvik
bytecode within e.g. `classes.dex`.  The Android runtime and Dalvik
bytecode formats have apparently changed over time in order to
support newer Java language features, such as support for static
interface methods:

	package org.webrtc;

	public /* partial */ interface EGLBase {
	    public static EGLBase create() { /* … */}
	}

Support for static interface methods was added in Java 8 and in
Android v8.0 (API-26).  If you want to use static interface methods
on an Android device running API-25 or earlier, you must [desugar][1]
the Java Bytecode.  The result of [desugaring][2] the above
`org.webrtc.EGLBase` type is that the static methods are moved to a
*new* type, with a `$-CC` suffix, "as if" the Java code were instead:

	package org.webrtc;

	public /* partial */ interface EGLBase {
	    // …
	}

	public final /* partial */ class EGLBase$-CC {
	    public static EGLBase create { /* … */ }
	}

While this works for pure Java code, this *does not work* with
Xamarin.Android, because our bindings currently expect (require) that
the types & members our binding process detects don't "move":

	// C# Binding for EGLBase
	namespace Org.Webrtc {
	    public partial interface IEGLBase {
	        private static readonly JniPeerMembers _members = new JniPeerMembers ("org/webrtc/EGLBase", typeof (IEGLBase));
	        public static IEGLBase Create()
	        {
	            const string __id = "create.()Lorg/webrtc/EglBase;"
	            var __rm = _members.StaticMethods.InvokeObjectMethod (__id, null);
	            return Java.Lang.Object.GetObject<IEGLBase>(__rm.Handle, JniHandleOwnership.TransferLocalRef);
	        }
	    }
	}

The `new JniPeerMembers(…)` invocation will use `JNIEnv::FindClass()`
to lookup the `org/webrtc/EGLBase` type, and `IEGLBase.Create()` will
use `JNIEnv::GetStaticMethodID()` and `JNIEnv::CallStaticObjectMethod()`
to lookup and invoke the `EGLBase.create()` method.

However, when desugaring is used, *there is no* `EGLBase.create()`
method.  Consequently, in a "desugared" environment,
`Java.Lang.NoSuchMethodError` will be thrown when `IEGLBase.Create()`
is invoked, as `EGLBase.create()` doesn't exist; it "should" instead
be looking for `EGLBase$-CC.create()`!

Introduce partial support for this scenario by adding the new method:

	namespace Java.Interop {
	    public partial class JniRuntime {
	        public partial class JniTypeManager {
	            public IReadOnlyList<string>? GetStaticMethodFallbackTypes (string jniSimpleReference) =>
	                GetStaticMethodFallbackTypesCore (jniSimpleReference);
	            protected virtual IReadOnlyList<string>? GetStaticMethodFallbackTypesCore (string jniSimpleReference) => null;
	        }
	    }
	}

`JniPeerMembers.JniStaticMethods.GetMethodInfo()` will attempt to
lookup the static method, "as normal".  If the method cannot be
found, then `JniRuntime.JniTypeManager.GetStaticMethodFallbackTypes()`
will be called, and we'll attempt to resolve the static method on each
type returned by `GetStaticMethodFallbackTypes()`.
If `GetStaticMethodFallbackTypes()` returns `null` or no fallback type
provides the method, then a `NoSuchMethodError` will be thrown.

TODO: Update xamarin-android to override
`GetStaticMethodFallbackTypes()`, to return
`$"{jniSimpleReference}$-CC"`.

~~ Microsoft Intune MAM ~~

Context: https://www.nuget.org/packages/Microsoft.Intune.MAM.Remapper.Tasks/
Context: https://docs.microsoft.com/en-us/mem/intune/developer/app-sdk-xamarin#remapper

The [Microsoft Intune App SDK Xamarin Bindings][3] is in a
similar-yet-different position: it involves Java & Dalvik Bytecode
manipulation for various security purposes, and in order to make that
work reasonably from Xamarin.Android, they *also* rewrite
Xamarin.Android IL so that it's consistent with the manipulated
Dalvik bytecode.

See `readme.md` and `content/MonoAndroid10/remapping-config.json`
within the [`Microsoft.Intune.MAM.Remapper.Tasks NuGet package`][4]
for some additional details/tidbits such as:

> This task operates on assemblies to replace the base type classes
> which Microsoft Intune requires to implement its SDK (for example,
> Intune requires using a `MAMActivity` in place of `Activity` and
> methods such as `OnMAMCreate` instead of `OnCreate`).

	"ClassRewrites": [
	  {
	    "Class": {
	      "From": "android.app.Activity",
	      "To": "com.microsoft.intune.mam.client.app.MAMActivity"
	    },
	    "Methods": [
	      {
	        "MakeStatic": false,
	        "OriginalName": "onCreate",
	        "NewName": "onMAMCreate",
	        "OriginalParams": [
	          "android.os.Bundle"
	        ]
	      }
	    ]
	  },
	  {	
	    "Class": {
	      "From": "android.content.pm.PackageManager",
	      "To": "com.microsoft.intune.mam.client.content.pm.MAMPackageManagement"
	    },
	    "Methods": [
	      {
	        "MakeStatic": true,
	        "OriginalName": "checkPermission",
	      }
	    ]
	  }
	]
	"GlobalMethodCalls": [
	  {
	    "Class": {
	      "From": "android.app.PendingIntent",
	      "To": "com.microsoft.intune.mam.client.app.MAMPendingIntent"
	    },
	    "Methods": [
	      {
	        "MakeStatic": false,
	        "OriginalName": "getActivities"
	      },
	    ]
	  }
	]

While what the InTune team has *works*, it suffers from a number of
"workflow" problems, e.g. incremental builds are problematic (they
might try to rewrite assemblies which were already rewritten), IL
rewriting is *always* "fun", and if we change IL binding patterns,
they'll need to support previous "binding styles" and newer styles.

Introduce partial support for this scenario by adding the following
members to `Java.Interop.JniRuntime.JniTypeManager`:

	namespace Java.Interop {
	    public partial class JniRuntime {

	        public struct ReplacementMethodInfo {
	            public  string? SourceJniType                   {get; set;}
	            public  string? SourceJniMethodName             {get; set;}
	            public  string? SourceJniMethodSignature        {get; set;}
	            public  string? TargetJniType                   {get; set;}
	            public  string? TargetJniMethodName             {get; set;}
	            public  string? TargetJniMethodSignature        {get; set;}
	            public  int?    TargetJniMethodParameterCount   {get; set;}
	            public  bool    TargetJniMethodIsStatic         {get; set;}
	        }

	        public partial class JniTypeManager {

	            public string? GetReplacementType (string jniSimpleReference) =>
	                GetReplacementTypeCore (jniSimpleReference);
	            protected virtual string? GetReplacementTypeCore (string jniSimpleReference) => null;

	            public ReplacementMethodInfo? GetReplacementMethodInfo (string jniSimpleReference, string jniMethodName, string jniMethodSignature) =>
	                GetReplacementMethodInfoCore (jniSimpleReference, jniMethodName,jniMethodSignature);
	            protected virtual ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSimpleReference, string jniMethodName, string jniMethodSignature) => null;
	        }
	    }
	}

These new methods are used by `JniPeerMembers`.

Consider "normal" usage of `JniPeerMembers`, within a binding:

	partial class Activity {
	    static readonly JniPeerMembers _members = new XAPeerMembers (jniPeerTypeName:"android/app/Activity", managedPeerType:typeof (Activity));
	}

`JniRuntime.JniTypeManager.GetReplacementType()` allows "replacing"
the `jniPeerTypeName` value with a "replacement" JNI type name.
The "replacement" type will be used for all field and method lookups.
This allows supporting the `ClassRewrites[0].Class.From` / 
`ClassRewrites[0].Class.To` semantics.

`JniRuntime.JniTypeManager.GetReplacementMethodInfo()` is the key
integration for all other "replacement" semantics.  Given the
source type, source method name, and source JNI signature, it is
responsible for providing an *overriding* target type, target method
name, and target JNI signature.

For `ClassRewrites[0].Methods[0]`, this allows "renaming"
`Activity.onCreate()` to `MAMActivity.onMAMCreate()`:

	var r = JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (
	    // Note: must match GetReplacementType() *output*, and since
	    // `Activity` is mapped to `MAMActivity`…
	    "com/microsoft/intune/mam/client/app/MAMActivity",
	    "onCreate",
	    "(Landroid/os/Bundle;)V"
	);
	// is equivalent to:
	var r = new JniRuntime.ReplacementMethodInfo {
	    SourceJniType                   = "com/microsoft/intune/mam/client/app/MAMActivity",
	                                                                // from input parameter
	    SourceJniMethodName             = "onCreate",               // from input parameter
	    SourceJniMethodSignature        = "(Landroid/os/Bundle;)V", // from input parameter

	    TargetJniType                   = "com/microsoft/intune/mam/client/app/MAMActivity",
	    TargetJniMethodName             = "onMAMCreate",
	    TargetJniMethodSignature        = null,                     // "use original value"
	    TargetJniMethodParameterCount   = null,                     // unchanged
	    TargetJniMethodIsStatic         = false,
	}

`ClassRewrites[1].Methods[0]` is "weird", in that it has
`MakeStatic: true`.  The semantics for when `MakeStatic: true` exists
is that the "original" method is an *instance* method, and the target
method is a *`static`* method, *and* the `this` instance now becomes
a *parameter* to the method.  This is likewise supported via
`JniRuntime.JniTypeManager.GetReplacementMethodInfo()`, and is
identified by:

 1. `ReplacementMethodInfo.TargetJniMethodParameterCount` being a
    non-`null` value, and

 2. `ReplacementMethodInfo.TargetJniMethodIsStatic` is true.

Thus, `ClassRewrites[1].Methods[0]` is akin to:

	var r = JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (
	    "android/content/pm/PackageManager",
	    "checkPermission",
	    "(Ljava/lang/String;Ljava/lang/String;)I"
	);
	// is equivalent to:
	var r = new JniRuntime.ReplacementMethodInfo {
	    SourceJniType                   = "android/content/pm/PackageManager",          // from input parameter
	    SourceJniMethodName             = "checkPermission",                            // from input parameter
	    SourceJniMethodSignature        = "(Ljava/lang/String;Ljava/lang/String;)I",    // from input parameter

	    TargetJniType                   = "com/microsoft/intune/mam/client/content/pm/MAMPackageManagement",
	    TargetJniMethodName             = "CheckPermission",
	    TargetJniMethodSignature        = "(Landroid/content/pm/PackageManager;Ljava/lang/String;Ljava/lang/String;)I",
	                                      // Note: `PackageManager` is inserted as new first parameter
	    TargetJniMethodParameterCount   = 3,
	    TargetJniMethodIsStatic         = true,
	}

(Note that the subclass is responsible for providing this data.)

`ReplacementMethodInfo.TargetJniMethodParameterCount` must be the
number of parameters that the target method accepts.  This is needed
so that `JniPeerMembers.JniInstanceMethods.TryInvoke*StaticRedirect()`
can appropriately reallocate the `JniArgumentValue*` array, so that
the `this` parameter can be added to the beginning of the parameters.

~~ Overhead? ~~

All these extra invocations into `JniRuntime.JniTypeManager` imply
additional overhead to invoke a Java method.  However, this is all
done during the *first* time a method is looked up, after which the
`JniMethodInfo` instance is *cached* for a given
(type, method, signature) tuple.

To demonstrate overheads, `samples/Hello` has been updated to accept
a new `-t` value; when provided, it invokes the
`java.lang.Object.hashCode()` method 1 million times and calculates
the average invocation overhead:

	% dotnet run --project samples/Hello -- -t
	Object.hashCode: 1000000 invocations. Total=00:00:00.5196758; Average=0.0005196758ms

If we replace the .NET 6 `samples/Hello/bin/Debug/Java.Interop.dll`
assembly with e.g. `bin/Debug/Java.Interop.dll`, we can compare to
performance *without* these new changes, as the changes are only in
the .NET 6 build:

	% \cp bin/Debug/Java.Interop{.dll,.pdb} samples/Hello/bin/Debug
	% dotnet samples/Hello/bin/Debug/Hello.dll -t
	Object.hashCode: 1000000 invocations. Total=00:00:00.5386676; Average=0.0005386676ms

There is a fair bit of variation in `dotnet Hello.dll -t` output,
but they're all roughly similar.  There doesn't appear to be
significant overhead for this particular test.

~~ Other Changes ~~

Cleaned up the `JniPeerMembers` constructors.  The `Debug.Assert()`
calls were duplicative and redundant.

Replace the `Debug.Assert()` with `Debug.WriteLine()`.
`Mono.Android.NET-Tests.apk` was running into an "unfixable" scenario:

	WARNING: ManagedPeerType <=> JniTypeName Mismatch!
	javaVM.GetJniTypeInfoForType(typeof(Android.Runtime.JavaList)).JniTypeName="java/util/ArrayList" != "java/util/List"

This was because of [`Android.Runtime.JavaList`][5] using a
`JniPeerMembers` for `List` while registering `ArrayList`, causing
typemaps to associate `JavaList` with `ArrayList`:

	[Register ("java/util/ArrayList", DoNotGenerateAcw=true)]
	partial class JavaList {
	    internal static readonly JniPeerMembers list_members =
	        new XAPeerMembers ("java/util/List", typeof (JavaList), isInterface: true);
	}

@jonpryor doesn't want to try fixing this right now; turning the
assertion into a diagnostic message feels preferable.

[0]: https://en.wikipedia.org/wiki/Android_Runtime
[1]: https://developer.android.com/studio/write/java8-support
[2]: https://developer.android.com/studio/write/java8-support-table
[3]: https://docs.microsoft.com/en-us/mem/intune/developer/app-sdk-xamarin
[4]: https://www.nuget.org/packages/Microsoft.Intune.MAM.Remapper.Tasks/
[5]: https://github.com/xamarin/xamarin-android/blob/b250c04b09a2b725336ae6d6c5693e0b9f37e4cc/src/Mono.Android/Android.Runtime/JavaList.cs#L9-L13
  • Loading branch information
jonpryor authored May 20, 2022
1 parent 02aa54e commit 1f27ab5
Show file tree
Hide file tree
Showing 24 changed files with 1,760 additions and 305 deletions.
32 changes: 26 additions & 6 deletions samples/Hello/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.Threading;

using Mono.Options;
Expand All @@ -9,10 +10,13 @@ namespace Hello
{
class App
{
const int N = 1000000;

public static void Main (string[] args)
{
string? jvmPath = global::Java.InteropTests.TestJVM.GetJvmLibraryPath ();
bool createMultipleVMs = false;
bool reportTiming = false;
bool showHelp = false;
var options = new OptionSet () {
"Using the JVM from C#!",
Expand All @@ -24,6 +28,9 @@ public static void Main (string[] args)
{ "m",
"Create multiple Java VMs. This will likely creash.",
v => createMultipleVMs = v != null },
{ "t",
$"Timing; invoke Object.hashCode() {N} times, print average.",
v => reportTiming = v != null },
{ "h|help",
"Show this message and exit.",
v => showHelp = v != null },
Expand All @@ -33,23 +40,25 @@ public static void Main (string[] args)
options.WriteOptionDescriptions (Console.Out);
return;
}
Console.WriteLine ("Hello World!");
var builder = new JreRuntimeOptions () {
JniAddNativeMethodRegistrationAttributePresent = true,
JvmLibraryPath = jvmPath,
};
builder.AddOption ("-Xcheck:jni");

var jvm = builder.CreateJreVM ();
Console.WriteLine ($"JniRuntime.CurrentRuntime == jvm? {ReferenceEquals (JniRuntime.CurrentRuntime, jvm)}");
foreach (var h in JniRuntime.GetAvailableInvocationPointers ()) {
Console.WriteLine ("PRE: GetCreatedJavaVMHandles: {0}", h);
}

CreateJLO ();
if (reportTiming) {
ReportTiming ();
return;
}

if (createMultipleVMs) {
CreateAnotherJVM ();
return;
}

CreateJLO ();
}

static void CreateJLO ()
Expand All @@ -58,6 +67,17 @@ static void CreateJLO ()
Console.WriteLine ($"binding? {jlo.ToString ()}");
}

static void ReportTiming ()
{
var jlo = new Java.Lang.Object ();
var t = Stopwatch.StartNew ();
for (int i = 0; i < N; ++i) {
jlo.GetHashCode ();
}
t.Stop ();
Console.WriteLine ($"Object.hashCode: {N} invocations. Total={t.Elapsed}; Average={t.Elapsed.TotalMilliseconds / (double) N}ms");
}

static unsafe void CreateAnotherJVM ()
{
Console.WriteLine ("Part 2!");
Expand Down
3 changes: 2 additions & 1 deletion src/Java.Interop/Java.Interop.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
<OutputPath>$(ToolOutputFullPath)</OutputPath>
<DocumentationFile>$(ToolOutputFullPath)Java.Interop.xml</DocumentationFile>
<JNIEnvGenPath>$(BuildToolOutputFullPath)</JNIEnvGenPath>
<LangVersion>8.0</LangVersion>
<LangVersion Condition=" '$(JIBuildingForNetCoreApp)' == 'True' ">9.0</LangVersion>
<LangVersion Condition=" '$(LangVersion)' == '' ">8.0</LangVersion>
<Nullable>enable</Nullable>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
<MSBuildWarningsAsMessages>NU1702</MSBuildWarningsAsMessages>
Expand Down
4 changes: 2 additions & 2 deletions src/Java.Interop/Java.Interop/JavaArray.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,9 @@ bool IList.IsFixedSize {

object? IList.this [int index] {
get {return this [index];}
#pragma warning disable 8601
#pragma warning disable 8600,8601
set {this [index] = (T) value;}
#pragma warning restore 8601
#pragma warning restore 8600,8601
}

void ICollection.CopyTo (Array array, int index)
Expand Down
28 changes: 27 additions & 1 deletion src/Java.Interop/Java.Interop/JniEnvironment.Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ static Types ()
}
}

public static unsafe JniObjectReference FindClass (string classname)
public static JniObjectReference FindClass (string classname)
{
return TryFindClass (classname, throwOnError: true);
}

static unsafe JniObjectReference TryFindClass (string classname, bool throwOnError)
{
if (classname == null)
throw new ArgumentNullException (nameof (classname));
Expand Down Expand Up @@ -85,6 +90,10 @@ public static unsafe JniObjectReference FindClass (string classname)
}
}

if (!throwOnError) {
(pendingException as IJavaPeerable)?.Dispose ();
return default;
}
throw pendingException!;
#endif // !FEATURE_JNIENVIRONMENT_JI_PINVOKES
#if FEATURE_JNIOBJECTREFERENCE_SAFEHANDLES
Expand Down Expand Up @@ -120,10 +129,27 @@ public static unsafe JniObjectReference FindClass (string classname)
var loadClassThrown = new JniObjectReference (thrown, JniObjectReferenceType.Local);
LogCreateLocalRef (loadClassThrown);
pendingException = info.Runtime.GetExceptionForThrowable (ref loadClassThrown, JniObjectReferenceOptions.CopyAndDispose);
if (!throwOnError) {
(pendingException as IJavaPeerable)?.Dispose ();
return default;
}
throw pendingException!;
#endif // !FEATURE_JNIOBJECTREFERENCE_SAFEHANDLES
}

#if NET
public static bool TryFindClass (string classname, out JniObjectReference instance)
{
if (classname == null)
throw new ArgumentNullException (nameof (classname));
if (classname.Length == 0)
throw new ArgumentException ("'classname' cannot be a zero-length string.", nameof (classname));

instance = TryFindClass (classname, throwOnError: false);
return instance.IsValid;
}
#endif // NET

public static JniType? GetTypeFromInstance (JniObjectReference instance)
{
if (!instance.IsValid)
Expand Down
134 changes: 134 additions & 0 deletions src/Java.Interop/Java.Interop/JniMemberSignature.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
#nullable enable

#if NET

using System;
using System.Diagnostics.CodeAnalysis;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

namespace Java.Interop
{
public struct JniMemberSignature : IEquatable<JniMemberSignature>
{
public static readonly JniMemberSignature Empty;

string? memberName;
string? memberSignature;

public string MemberName => memberName ?? throw new InvalidOperationException ();
public string MemberSignature => memberSignature ?? throw new InvalidOperationException ();

public JniMemberSignature (string memberName, string memberSignature)
{
if (string.IsNullOrEmpty (memberName)) {
throw new ArgumentNullException (nameof (memberName));
}
if (string.IsNullOrEmpty (memberSignature)) {
throw new ArgumentNullException (nameof (memberSignature));
}
this.memberName = memberName;
this.memberSignature = memberSignature;
}

public static int GetParameterCountFromMethodSignature (string jniMethodSignature)
{
if (jniMethodSignature.Length < "()V".Length || jniMethodSignature [0] != '(' ) {
throw new ArgumentException (
$"Member signature `{jniMethodSignature}` is not a method signature. Method signatures must start with `(`.",
nameof (jniMethodSignature));
}
int count = 0;
int index = 1;
while (index < jniMethodSignature.Length &&
jniMethodSignature [index] != ')') {
ExtractType (jniMethodSignature, ref index);
count++;
}
return count;
}

internal static (int StartIndex, int Length) ExtractType (string signature, ref int index)
{
AssertSignatureIndex (signature, index);
var i = index++;
switch (signature [i]) {
case '[':
if ((i+1) >= signature.Length)
throw new InvalidOperationException ($"Missing array type after '[' at index {i} in: `{signature}`");
var rest = ExtractType (signature, ref index);
return (StartIndex: i, Length: index - i);
case 'B':
case 'C':
case 'D':
case 'F':
case 'I':
case 'J':
case 'S':
case 'V':
case 'Z':
return (StartIndex: i, Length: 1);
case 'L':
int depth = 0;
int e = index;
while (e < signature.Length) {
var c = signature [e++];
if (depth == 0 && c == ';')
break;
}
if (e > signature.Length)
throw new InvalidOperationException ($"Missing reference type after `{signature [i]}` at index {i} in `{signature}`!");
index = e;
return (StartIndex: i, Length: (e - i));
default:
throw new InvalidOperationException ($"Unknown JNI Type `{signature [i]}` within: `{signature}`!");
}
}

internal static void AssertSignatureIndex (string signature, int index)
{
if (signature == null)
throw new ArgumentNullException (nameof (signature));
if (signature.Length == 0)
throw new ArgumentException ("Descriptor cannot be empty string", nameof (signature));
if (index >= signature.Length)
throw new ArgumentException ("index >= descriptor.Length", nameof (index));
}

public override int GetHashCode ()
{
return (memberName?.GetHashCode () ?? 0) ^
(memberSignature?.GetHashCode () ?? 0);
}

public override bool Equals (object? obj)
{
var v = obj as JniMemberSignature?;
if (v.HasValue)
return Equals (v.Value);
return false;
}

public bool Equals (JniMemberSignature other)
{
return memberName == other.memberName &&
memberSignature == other.memberSignature;
}

public override string ToString ()
{
return $"{nameof (JniMemberSignature)} {{ " +
$"{nameof (MemberName)} = {(memberName == null ? "null" : "\"" + memberName + "\"")}" +
$", {nameof (MemberSignature)} = {(memberSignature == null ? "null" : "\"" + memberSignature + "\"")}" +
$"}}";
}

public static bool operator== (JniMemberSignature a, JniMemberSignature b) => a.Equals (b);
public static bool operator!= (JniMemberSignature a, JniMemberSignature b) => !a.Equals (b);
}
}

#endif // NET
5 changes: 5 additions & 0 deletions src/Java.Interop/Java.Interop/JniMethodInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ public sealed class JniMethodInfo

public bool IsStatic {get; private set;}

#if NET
internal JniType? StaticRedirect;
internal int? ParameterCount;
#endif //NET

internal bool IsValid {
get {return ID != IntPtr.Zero;}
}
Expand Down
44 changes: 38 additions & 6 deletions src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,46 @@ internal JniInstanceMethods GetConstructorsForType (Type declaringType)
public JniMethodInfo GetMethodInfo (string encodedMember)
{
lock (InstanceMethods) {
if (!InstanceMethods.TryGetValue (encodedMember, out var m)) {
string method, signature;
JniPeerMembers.GetNameAndSignature (encodedMember, out method, out signature);
m = JniPeerType.GetInstanceMethod (method, signature);
InstanceMethods.Add (encodedMember, m);
if (InstanceMethods.TryGetValue (encodedMember, out var m)) {
return m;
}
return m;
}
string method, signature;
JniPeerMembers.GetNameAndSignature (encodedMember, out method, out signature);
var info = GetMethodInfo (method, signature);
lock (InstanceMethods) {
if (InstanceMethods.TryGetValue (encodedMember, out var m)) {
return m;
}
InstanceMethods.Add (encodedMember, info);
}
return info;
}

JniMethodInfo GetMethodInfo (string method, string signature)
{
#if NET
var m = (JniMethodInfo?) null;
var newMethod = JniEnvironment.Runtime.TypeManager.GetReplacementMethodInfo (Members.JniPeerTypeName, method, signature);
if (newMethod.HasValue) {
var typeName = newMethod.Value.TargetJniType ?? Members.JniPeerTypeName;
var methodName = newMethod.Value.TargetJniMethodName ?? method;
var methodSig = newMethod.Value.TargetJniMethodSignature ?? signature;

using var t = new JniType (typeName);
if (newMethod.Value.TargetJniMethodInstanceToStatic &&
t.TryGetStaticMethod (methodName, methodSig, out m)) {
m.ParameterCount = newMethod.Value.TargetJniMethodParameterCount;
m.StaticRedirect = new JniType (typeName);
return m;
}
if (t.TryGetInstanceMethod (methodName, methodSig, out m)) {
return m;
}
Console.Error.WriteLine ($"warning: For declared method `{Members.JniPeerTypeName}.{method}.{signature}`, could not find requested method `{typeName}.{methodName}.{methodSig}`!");
}
#endif // NET
return JniPeerType.GetInstanceMethod (method, signature);
}

public unsafe JniObjectReference StartCreateInstance (string constructorSignature, Type declaringType, JniArgumentValue* parameters)
Expand Down
Loading

0 comments on commit 1f27ab5

Please sign in to comment.