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

Support C# generics in base types #424

Open
jonpryor opened this issue Apr 2, 2019 · 5 comments
Open

Support C# generics in base types #424

jonpryor opened this issue Apr 2, 2019 · 5 comments
Labels
enhancement Proposed change to current functionality generator Issues binding a Java library (generator, class-parse, etc.)

Comments

@jonpryor
Copy link
Member

jonpryor commented Apr 2, 2019

Consider the BouncyCastle-provided bcprov-ext-jdk15on-161.jar. This Java library contains a type equivalent to:

/* partial */ interface X509AttributeCertificate extends java.security.cert.X509Extension {
}

Which looks fine enough, except that it implements the IX509Extension interface, which has two properties which are generic types:

partial interface IX509Extension {
    ICollection<string> CriticalExtensionOIDs {get;}
    ICollection<string> NonCriticalExtensionOIDs {get;}
}

Unfortunately, generator's support for C# generic methods results in the resulting binding...not being valid C# code:

internal partial class internal class IX509AttributeCertificateInvoker : global::Java.Lang.Object, IX509AttributeCertificate {
	IntPtr id_getCriticalExtensionOIDs;
	public unsafe global::System.Collections.Generic.ICollection`1<global::System.String> CriticalExtensionOIDs {
		get {
			if (id_getCriticalExtensionOIDs == IntPtr.Zero)
				id_getCriticalExtensionOIDs = JNIEnv.GetMethodID (class_ref, "getCriticalExtensionOIDs", "()Ljava/util/Set;");
			return global::Android.Runtime.JavaSet.FromJniHandle (JNIEnv.CallObjectMethod (((global::Java.Lang.Object) this).Handle, id_getCriticalExtensionOIDs), JniHandleOwnership.TransferLocalRef);
		}
	}
}

The string global::System.Collections.Generic.ICollection`1<global::System.String> is not a valid C# type -- the ICollection`1<global::System.String> needs to be ICollection<global::System.String> -- which prevents the binding from compiling. (Along with a host of other issues, all of which should be addressed as well.)

generator needs to be fixed so that this can be properly supported.

@jpobst
Copy link
Contributor

jpobst commented Jul 16, 2019

I tried naively removing the arity from the return type, which at least generates valid C# code, but it still doesn't compile due to:

JavaSet.ToLocalJniHandle (__this.CriticalExtensionOIDs)

ToLocalJniHandle expects an ICollection while CriticalExtensionOIDs is an ICollection<string>.

A similar error occurs here:

public unsafe global::System.Collections.Generic.ICollection<global::System.String> CriticalExtensionOIDs {
	get {
		if (id_getCriticalExtensionOIDs == IntPtr.Zero)
			id_getCriticalExtensionOIDs = JNIEnv.GetMethodID (class_ref, "getCriticalExtensionOIDs", "()Ljava/util/Set;");
		return global::Android.Runtime.JavaSet.FromJniHandle (JNIEnv.CallObjectMethod (((global::Java.Lang.Object) this).Handle, id_getCriticalExtensionOIDs), JniHandleOwnership.TransferLocalRef);
	}
}

because JavaSet.FromJniHandle (...) returns an ICollection and we need the property to return an ICollection<string>.

I'm not sure if that means my change is incorrect, or if that is an additional separate issue that needs to be addressed.

@jonpryor
Copy link
Member Author

but it still doesn't compile due to:

JavaSet.ToLocalJniHandle (__this.CriticalExtensionOIDs)

This should use JavaSet<string>.ToLocalJniHandle (__this.CriticalExtensionOIDs).

See also: https://github.com/xamarin/xamarin-android/blob/a3267e34d35a457f11ec1bb4565c350002f8c332/src/Mono.Android/Android.Runtime/JavaSet.cs#L173-L286

@jpobst
Copy link
Contributor

jpobst commented Jul 17, 2019

Is this something we would expect generator to figure out, and if so, how?

Or is this something where we would expect metadata to be written to change the base class from JavaSet to JavaSet<T>?

jonpryor pushed a commit that referenced this issue Jul 19, 2019
Context: #424

One of the issues trying to bind the BouncyCastle-provided
[`bcprov-ext-jdk15on-161.jar.`][0] in #424 is that we write generic
types with invalid signatures:

	public List`1<string> MyProperty { get; set; }

as this is how a Cecil type reference `FullName` is returned.

Since this notation is not useful to us, strip the "arity"
information when reading the assembly.

This results in the proper type syntax:

	public List<string> MyProperty { get; set; }

[0]: https://www.bouncycastle.org/download/bcprov-ext-jdk15on-161.jar
@jonpryor
Copy link
Member Author

jonpryor commented Jul 19, 2019

Is this something we would expect generator to figure out?

Yes.

and if so, how?

Good question. :-)

Have a more concrete example in mind? There are only ("only") 25 instances of JavaSet.ToLocalJniHandle in API-28:

$ grep -r JavaSet.ToLocalJniH obj/Debug/android-28/mcw
obj/Debug/android-28/mcw/Java.Util.HashMap.cs:			return Android.Runtime.JavaSet.ToLocalJniHandle (__this.EntrySet ());
...

None of the existing declarations seem quite that useful for this discussion, as they all deal with non-generic types, so more "demo code" might be helpful.

That said, within the context of the CriticalExtensionOIDs property declaration, generator can "know" that the return type is ICollection<T> -- it has to, otherwise it wouldn't have emitted that in the first place, right? -- so it seems reasonable for that "generic-ness" information to be obtainable, so that generator could in turn emit JavaSet<T>.ToLocalJniHandle(...).

However, this might also be interpreted as a limitation in our current binding infrastructure. Consider the IX509Extension declaration we currently emit:

// Metadata.xml XPath interface reference: path="/api/package[@name='java.security.cert']/interface[@name='X509Extension']"
[Register ("java/security/cert/X509Extension", "", "Java.Security.Cert.IX509ExtensionInvoker", ApiSince = 1)]
public partial interface IX509Extension : IJavaObject {

  System.Collections.Generic.ICollection<string> CriticalExtensionOIDs {
    // Metadata.xml XPath method reference: path="/api/package[@name='java.security.cert']/interface[@name='X509Extension']/method[@name='getCriticalExtensionOIDs' and count(parameter)=0]"
    [Register ("getCriticalExtensionOIDs", "()Ljava/util/Set;", "GetGetCriticalExtensionOIDsHandler:Java.Security.Cert.IX509ExtensionInvoker, Mono.Android, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null")]
    get;
  }
}

Reflection-wise, the only way anything can "know" that getCriticalExtensionOIDs() returns a Set<string> is by "reading between the lines" between the specified JNI signature (()Ljava/util/Set;) and the managed type (ICollection<string>).

We may want to consider adding additional attributes to remove the need for this inference, e.g. by using a JniGenericTypeArguments custom attribute, a'la:

System.Collections.Generic.ICollection<string> CriticalExtensionOIDs {
  [return:JniGenericTypeReference ("java/util/Set", new[]{"java/lang/String"})]
  [Register ("getCriticalExtensionOIDs", "()Ljava/util/Set;", "GetGetCriticalExtensionOIDsHandler:Java.Security.Cert.IX509ExtensionInvoker, Mono.Android, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null")]
  get;
}

generator could then put [Register] + [JniGenericTypeArguments] "together" to deduce that the Java-side return type is java/util/Set<java/lang/String>, i.e. java.util.Set<java.lang.String> i.e. Set<String>, and carry on without needing to "interrogate" and parse the managed return types.

jonpryor pushed a commit that referenced this issue Jul 29, 2019
…451)

One issue with the BouncyCastle [`bcprov-ext-jdk15on-161.jar`][0] in
Issue #424 is that it contains this field:

	public static final int _3DES_EDE_CBC

We studly-case Java field names, however that removes underscores and
we end up with `3desEdeCbc`, which is invalid C#.  This PR preserves
a starting underscore if there was one and the resulting name starts
with a number.  (`_3desEdeCbc` for this case.)

[0]: https://www.bouncycastle.org/download/bcprov-ext-jdk15on-161.jar
jonpryor pushed a commit to dotnet/android that referenced this issue Aug 13, 2019
Changes: dotnet/java-interop@4e364ba...be58159

Context: dotnet/java-interop#424

`generator` refactoring.

Make the `*Invoker` classes `partial` classes.

Add `generator --lang-features=interface-constants` option, which
emits Java interface constant values into the C# interface
declarations.

`generator` performance improvements.

Allow consumption of `Java.Interop-Tests` by Xamarin.Android.
@mrrozenbergs
Copy link

Is it possible to fix this with metadata.xml?

@jpobst jpobst added enhancement Proposed change to current functionality generator Issues binding a Java library (generator, class-parse, etc.) labels Apr 6, 2020
jonpryor pushed a commit that referenced this issue Apr 19, 2022
Fixes: #967

Context: dotnet/android-libraries#504
Context: #424
Context: #586
Context: #918

Java generics continues to be a "difficulty" in creating bindings.
Consider [`ActivityResultContracts.RequestPermission`][0]:

	// Java
	public abstract /* partial */ class ActivityResultContract<I, O> {
	    public abstract Intent createIntent(Context context, I input);
	}
	public /* partial */ class /* ActivityResultContracts. */ RequestPermission
	    extends ActivityResultContract<String, Boolean>
	{
	    @OverRide public Intent createIntent(Context context, String input) {…}
	}

The JNI Signature for
`ActivityResultContracts.RequestPermission.createIntent()` is
`(Landroid/content/Context;Ljava/lang/String;)Landroid/content/Intent;`,
i.e. `class-parse` & `generator` believe that the `input` parameter
is of type `String`, which we bind by default as `string`.  Thus:

	// C#
	public abstract partial class ActivityResultContract {
	    public abstract Intent CreateIntent (Context? context, Java.Lang.Object? input) => …
	}
	public partial class /* ActivityResultContracts. */ RequestPermission {
	    public override Intent CreateIntent (Context? context, string? input) => …
	}

This fails to compile with a [CS0115][1]:

	'RequestPermission.CreateIntent(Context?, string?)': no suitable method found to override

as the `input` parameter of `RequestPermission.CreateIntent()` changes
from `Java.Lang.Object?` to `string?`.

We can attempt to address this via Metadata:

	<attr
	  path="/api/package[@name='androidx.activity.result.contract']/class[@name='ActivityResultContracts.RequestPermission']/method[@name='createIntent' and count(parameter)=2 and parameter[1][@type='android.content.Context'] and parameter[2][@type='java.lang.String']]"
	  name="managedType"
	>Java.Lang.Object</attr>

This fixes one error, as `generator` now emits:

	public partial class /* ActivityResultContracts. */ RequestPermission {
	    [Register ("createIntent", "(Landroid/content/Context;Ljava/lang/String;)Landroid/content/Intent;", "")]
	    public override unsafe global::Android.Content.Intent CreateIntent (global::Android.Content.Context context, global::Java.Lang.Object input)
	    {
	        const string __id = "createIntent.(Landroid/content/Context;Ljava/lang/String;)Landroid/content/Intent;";
	        IntPtr native_input = JNIEnv.NewString (input);
	        try {
	            JniArgumentValue* __args = stackalloc JniArgumentValue [2];
	            __args [0] = new JniArgumentValue ((context == null) ? IntPtr.Zero : ((global::Java.Lang.Object) context).Handle);
	            __args [1] = new JniArgumentValue (native_input);
	            var __rm = _members.InstanceMethods.InvokeAbstractObjectMethod (__id, this, __args);
	            return global::Java.Lang.Object.GetObject<global::Android.Content.Intent> (__rm.Handle, JniHandleOwnership.TransferLocalRef);
	        } finally {
	            JNIEnv.DeleteLocalRef (native_input);
	            global::System.GC.KeepAlive (context);
	            global::System.GC.KeepAlive (input);
	        }
	    }
	}

The `override` method declaration is correct.

However, this introduces a [CS1503][2] error:

	IntPtr native_input = JNIEnv.NewString (input);
	// Error CS1503 Argument 1: cannot convert from 'Java.Lang.Object' to 'string?'

The workaround is to remove `createIntent()` ~entirely, and manually
bind it, as done in dotnet/android-libraries#512.

Fix this issue by always emitting a cast to `(string)` as
part of the `JNIEnv.NewString()` invocation, instead emitting:

	IntPtr native_input = JNIEnv.NewString ((string?) input);

This works because `Java.Lang.Object` defines an
[explicit conversion to `string?`][3], and if a `Java.Lang.String`
instance is provided to the `input` parameter, it's equivalent to
calling `.ToString()`.

This fix allows the original suggested Metadata solution to work.

[0]: https://developer.android.com/reference/androidx/activity/result/contract/ActivityResultContracts.RequestPermission
[1]: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/cs0115
[2]: https://docs.microsoft.com/en-us/dotnet/csharp/misc/cs1503
[3]: https://github.com/xamarin/xamarin-android/blob/a58d4e9706455227eabb6e5b5103b25da716688b/src/Mono.Android/Java.Lang/Object.cs#L434-L439
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Proposed change to current functionality generator Issues binding a Java library (generator, class-parse, etc.)
Projects
None yet
Development

No branches or pull requests

3 participants