xamarin / java.interop

Java.Interop provides open-source bindings of Java's Java Native Interface (JNI) for use with .NET managed languages such as C#

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Discussion: Ways to *actually* fix trimmer warnings around `Type.GetType()`?

jonpryor opened this issue · comments

Context: #1153
Context: #1157

From the PR #1153 README.md:

Known Unknowns

With this sample "done" (-ish), there are several "future research directions" to
make NativeAOT + Java viable.

Type.GetType()

Next, Java.Interop and .NET Android make extensive use of Type.GetType(), which doesn't quite work "the same" in NativeAOT. It works when using a string constant:

var type = Type.GetType ("System.Int32, System.Runtime");

It fails if the string comes from "elsewhere", even if it's a type that exists.

Unfortunately, we do this in key places within Java.Interop. Consider this more complete Java Callable Wrapper fragment:

public class ManagedType
	extends java.lang.Object
	implements
		com.xamarin.java_interop.GCUserPeerable
{
/** @hide */
	public static final String __md_methods;
	static {
		__md_methods = 
			"getString:()Ljava/lang/String;:__export__\n" +
			"";
		com.xamarin.java_interop.ManagedPeer.registerNativeMembers (
				ManagedType.class,
				"Example.ManagedType, Hello-NativeAOTFromJNI",
				__md_methods);
	}


	public ManagedType (int p0)
	{
		super ();
		if (getClass () == ManagedType.class) {
			com.xamarin.java_interop.ManagedPeer.construct (
					this,
					"Example.ManagedType, Hello-NativeAOTFromJNI",
					"System.Int32, System.Runtime",
					new java.lang.Object[] { p0 });
		}
	}


	public native java.lang.String getString ();
}

There are two places that assembly-qualified names are used, both of which normally wind up at Type.GetType():

  • ManagedPeer.RegisterNativeMembers() is given an assembly-qualified name to register the native methods.
  • ManagedPeer.Construct() is given a :-separated list of assembly-qualified names for each parameter type. This is done to lookup a ConstructorInfo.

This sample "fixes" things by adding JniRuntime.JniTypeManager.GetTypeFromAssemblyQualifiedName(), which allows NativeAotTypeManager to override it and support the various assembly-qualified name values which the sample requires.

An alternate idea to avoid some of the new GetTypeFromAssemblyQualifiedName() invocations would be to declare native methods for each constructor overload, but fixing this gets increasingly difficult.

Which brings us to #1157 (comment):

  • var type = Type.GetType (JniEnvironment.Strings.ToString (n_assemblyQualifiedName)!, throwOnError: true)!;

    warning IL2057: Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String, Boolean)'. It's not possible to guarantee the availability of the target type.
    
  • ptypes [i] = Type.GetType (typeNames [i], throwOnError:true)!;

    warning IL2057: Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String, Boolean)'. It's not possible to guarantee the availability of the target type.
    
  • var type = Type.GetType (assemblyQualifiedName!, throwOnError: true)!;

    warning IL2057: Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String, Boolean)'. It's not possible to guarantee the availability of the target type.
    

How do we fix these? These are Type.GetType() invocations for string values which come from Java.

Can they be fixed?

We can just add UnconditionalSuppressMessageAttribute to entirely ignore the warning. This will cause things to break on NativeAOT, though, and the whole point to fixing things is to make things linker friendly and usable on NativeAOT.

PR #1153 implements a "punt" solution to the problem: instead of Type.GetType(), introduce a JniRuntime.JniTypeManager.GetTypeFromAssemblyQualifiedName() method which optionally does Type.GetType(). This would likely still produce IL2057, but we could also [UncondionalSuppressMessage] to silence the warning or somehow require that it be overridden within a NativeAOT app. This permits a path which can work with NativeAOT, but it wouldn't work by default, and would require "extra opt-in logic" in the form of a new method override.

A "proper by default" solution will require separately considering the Type.GetType() calls within ManagedPeer.cs.

Method Registration

Method Registration is done in ManagedPeer.RegisterNativeMembers(), which is invoked from Java:

		__md_methods = 
			"getString:()Ljava/lang/String;:__export__\n" +
			"";
		com.xamarin.java_interop.ManagedPeer.registerNativeMembers (
				ManagedType.class,
				"Example.ManagedType, Hello-NativeAOTFromJNI",
				__md_methods);

Note that ManagedPeer.registerNativeMembers() is given three values:

  1. The java.lang.Class instance of the type to register methods for
  2. The assembly qualified name of the .NET type which corresponds to (1)
  3. The methods to register (which involve Reflection on (2), but Reflection works, so…)

We could have ManagedPeer.RegisterNativeMembers() use JniRuntime.JniTypeManager.GetType(JniTypeSignature) instead of Type.GetType(), a'la:

var type  = JniEnvironment.Runtime.TypeManager.GetType (new JniTypeSignature (r_nativeClass.Name));

This also "punts" on the question to JniRuntime.JniTypeManager.GetType(JniTypeSignature), but that's a pre-existing (and required) extension point:

public Type? GetType (JniTypeSignature typeSignature)
{
AssertValid ();
return GetTypes (typeSignature).FirstOrDefault ();
}

The problem with this approach is that, at present, performance would be "not great", given that JniTypeManager.GetType(JniTypeSignature) uses LINQ.

Constructor Invocation

Constructor Invocation is done in ManagedPeer.Construct(), which is invoked from Java:

	public ManagedType (int p0)
	{
		super ();
		if (getClass () == ManagedType.class) {
			com.xamarin.java_interop.ManagedPeer.construct (
					this,
					"Example.ManagedType, Hello-NativeAOTFromJNI",
					"System.Int32, System.Runtime",
					new java.lang.Object[] { p0 });
		}
	}

ManagedPeer.construct() is given four separate values:

  1. The java.lang.Object instance that we need to construct the .NET side for
  2. The assembly qualified type name of the C# type which corresponds to (1)
  3. A :-separated sequence of assembly qualified type names for the constructor signature to invoke
  4. The values to pass to the constructor.

(2) and (3) both involve Type.GetType() invocations.

As with ManagedPeer.RegisterNativeMembers(), (2) could be replaced with JniEnvironment.Runtime.TypeManager.GetType(), using new JniTypeSignature(JniEnvironment.Types.GetJniTypeNameFromInstance()).

(3) is more problematic.

We could resolve this through one of two mechanisms:

a. Use a JNI method signature, and parse that into System.Type instances at runtime. We would thus invoke ManagedPeer.construct(this, "…now ignored…", "(I)V", …), and turn (I)V into new Type[]{typeof(int)}, again via JniEnvironment.Runtime.TypeManager.GetType().
b. Stop using ManagedPeer.construct() entirely, and instead use a native method declaration.

(b) would result in a Java Callable Wrapper akin to:

/* partial */ class ManagedType {
    public ManagedType (int p0) {
        super ();
        if (getClass () == ManagedType.class) {
            __ctor (p0);
        }
    }
    private native void __ctor (int p0);
}

(a) has the benefit of being easier to implement: jcw-gen already has the JNI method signature, and could emit it. (a) also "feels like" it would have more runtime overhead.

(b) has the benefit of (probably) being faster at runtime, but is more complex. It would requires changes to:

  1. jcw-gen (to declare the new native method)
  2. All JniRuntime.JniTypeManager.RegisterNativeMembers() overrides/implementations would need to figure out how to handle these new native methods.
    .NET Android relies on generator-emitted methods as part of RegisterNativeMembers(), but I don't see how that approach could actually work here. We could pull in System.Reflection.Emit/DynamicMethod, but we're trying to get away from that.
    JavaInterop1 could have jnimarshalmethod-gen implement it, which has the added benefit of increased efficiency, but that's not a solution for .NET Android.
    There be complications here.
  3. jnimarshalmethod-gen (to emit & register these new methods), but only for Desktop Java.Base usage.
  4. Java.Interop.Export
  5. others…?

It should be explicitly noted that at this time ManagedPeer.cs is not used by .NET Android. We can thus fix it any way we wish.

That said, the "core" architecture within Java.Interop.dll and .NET Android rhyme here; .NET Android doesn't use ManagedPeer.registerNativeMembers(); it instead uses Runtime.register(), which has the same semantics (just a different parameter ordering). .NET Android doesn't use ManagedPeer.construct(); it instead uses TypeManager.Activate(), which has the same semantics (just a different parameter ordering).

The benefit to there being two similar-yet-distinct implementations is that we can prototype things in "core" -- e.g. rethink the ManagedPeer.java methods and semantics -- without breaking .NET Android, while keeping the "rhyming" concerns in mind (e.g. anything that requires jnimarshalmethod-gen or LLVM Marshal Methods is a non-starter, which in turn means that native methods for constructors, while appealing, is likely not viable in the short term).