Skip to content

Conversation

@jpobst
Copy link
Contributor

@jpobst jpobst commented Aug 5, 2024

Fixes: #1155

When binding Java methods, we can come across "bridge" methods that have the same name and parameters as a "real" method. They only differ by return type.

<method abstract="false" deprecated="not deprecated" final="false" name="setDetectorMode" jni-signature="(I)Lcom/google/mlkit/vision/objects/defaults/ObjectDetectorOptions$Builder;" bridge="false" native="false" return="com.google.mlkit.vision.objects.defaults.ObjectDetectorOptions.Builder" jni-return="Lcom/google/mlkit/vision/objects/defaults/ObjectDetectorOptions$Builder;" static="false" synchronized="false" synthetic="false" visibility="public" return-not-null="true">
  <parameter name="detectorMode" type="int" jni-type="I" />
</method>
<method abstract="false" deprecated="not deprecated" final="true" name="setDetectorMode" jni-signature="(I)Lcom/google/mlkit/vision/objects/ObjectDetectorOptionsBase$Builder;" bridge="true" native="false" return="java.lang.Object" jni-return="Lcom/google/mlkit/vision/objects/ObjectDetectorOptionsBase$Builder;" static="false" synchronized="false" synthetic="true" visibility="public" return-not-null="true">
  <parameter name="p0" type="int" jni-type="I" />
</method>

When bound in C#, this causes an issue because 2 methods cannot have the same name and parameters.

using Com.Google.Mlkit.Vision.Objects;

public Defaults.ObjectDetectorOptions.Builder SetDetectorMode (int detectorMode) { ... }
public ObjectDetectorOptions.Builder SetDetectorMode (int p0) { ... }

We can use managedName metadata to rename one of these methods, however we generate the callback delegate field with the algorithm $"cb_{JavaName}{IDSignature}", so this name is still used twice, preventing C# compilation.

static Delegate cb_setDetectorMode_I;
static Delegate cb_setDetectorMode_I;

The solution is to add both the managed name and the return type to the callback naming algorithm to ensure that it will always be unique for a method. ($"cb_{JavaName}_{Name}{IDSignatureWithReturnType}")

static Delegate cb_setDetectorMode_SetDetectorMode_I_Lcom_google_mlkit_vision_objects_defaults_ObjectDetectorOptions$Builder_;
static Delegate cb_setDetectorMode_SetDetectorMode2_I_Lcom_google_mlkit_vision_objects_ObjectDetectorOptions$Builder_;

XA test PR: dotnet/android#9172

@jpobst jpobst marked this pull request as ready for review August 6, 2024 19:25
@jpobst jpobst requested a review from jonpryor August 6, 2024 19:25
@jonpryor
Copy link
Contributor

jonpryor commented Aug 9, 2024

Aside on this: why don't we encounter this in Mono.Android.dll?

So I look at a build of java-interop, at src/Java.Base/obj/Debug-net8.0/mcw/api.xml, and look for all bridge="true" methods:

xpath -e '//class[method[@bridge="true"]]/@jni-signature' src/Java.Base/obj/Debug-net8.0/mcw/api.xml

which returns hundreds of matches, including java.lang.Long for compareTo():

% javap java.lang.Long
public final class java.lang.Long extends java.lang.Number implements java.lang.Comparable<java.lang.Long> …

  public int compareTo(java.lang.Long);

  public int compareTo(java.lang.Object);

OK, this makes sense to me. So I look at src/Java.Base/obj/Debug-net8.0/mcw/api.xml.adjusted.fixed, and those bridge="true" methods are missing! Instead of 2331 matches for bridge="true" in api.xml, there are 24 in api.xml.adjusted.fixed. The Long.compareTo(Object) bridge method has been removed.

I assume this is due to some fixups within generator which I've forgotten about?

Assuming that this is due to parameter changes, what about return type changes? Alas, I can't find a public type with a bridge method that has a return type change in Java.Base.dll.

So instead of desktop Java, what about android.jar?

% dotnet bin/Debug-net8.0/class-parse.dll ~/android-toolchain/sdk/platforms/android-35/android.jar > android-35.xml
% grep 'bridge="true"' android-35.xml | wc -l
     712

Compare to 56 bridge methods within dotnet/android/src/Mono.Android/Profiles/api-35.xml.

So now I'm further confused: how are we generating api-35.xml that it "loses" so many bridge methods? Yes, there's api-merge in there, but I wouldn't expect api-merge to remove bridge methods…

By the time we get to Mono.Android.dll output, there are 4 bridge methods left:

% grep 'bridge="true"' src/Mono.Android/obj/Debug/net9.0/android-35/mcw/api-35.xml.fixed
      <method abstract="false" deprecated="not deprecated" final="false" name="equals" jni-signature="(Ljava/lang/Object;)Z" bridge="true" native="false" return="boolean" jni-return="Z" static="false" synchronized="false" synthetic="true" visibility="public" merge.SourceFile="..\..\bin\BuildDebug\api\api-19.xml.in" removed-since="29">
      <method abstract="false" deprecated="not deprecated" final="false" name="hashCode" jni-signature="()I" bridge="true" native="false" return="int" jni-return="I" static="false" synchronized="false" synthetic="true" visibility="public" merge.SourceFile="..\..\bin\BuildDebug\api\api-19.xml.in" removed-since="29" />
      <method abstract="false" deprecated="not deprecated" final="false" name="equals" jni-signature="(Ljava/lang/Object;)Z" bridge="true" native="false" return="boolean" jni-return="Z" static="false" synchronized="false" synthetic="true" visibility="public" merge.SourceFile="..\..\bin\BuildDebug\api\api-19.xml.in" removed-since="29">
      <method abstract="false" deprecated="not deprecated" final="false" name="hashCode" jni-signature="()I" bridge="true" native="false" return="int" jni-return="I" static="false" synchronized="false" synthetic="true" visibility="public" merge.SourceFile="..\..\bin\BuildDebug\api\api-19.xml.in" removed-since="29" />

None of these appear to make it into bindings: one of those is for SpannableStringInternal.equals(Object), and the binding for SpannableStringInternal contains no Equals() methods at all.

(I'm also at a loss for why hashCode() would ever be a bridge method…)

So I've just managed to raise more unanswered questions. Java.Base.dll and Mono.Android.dll don't suffer from this problem because all "meaningful" bridge methods have been removed, but why were they removed? Why isn't that apparent removal happening in xamarin/AndroidX?

@jonpryor
Copy link
Contributor

jonpryor commented Aug 9, 2024

Commit message for review:

Fixes: https://github.com/dotnet/java-interop/issues/1155

Context: https://docs.oracle.com/javase/tutorial/java/generics/bridgeMethods.html

A *bridge method* is a Java compiler-generated method "overload"
which deals with type coercion, to support Java type erasure:

	package com.google.mlkit.vision.objects;
	// https://developers.google.com/android/reference/com/google/mlkit/vision/objects/ObjectDetectorOptionsBase
	public class ObjectDetectorOptionsBase {
	    /** @hide */ public static class Builder<B extends Builder> {
	        public B setDetectorMode(int detectorMode) {…}
	    }
	}

	package com.google.mlkit.vision.objects.defaults;
	public class ObjectDetectorOptions {
	    // https://developers.google.com/android/reference/com/google/mlkit/vision/objects/defaults/ObjectDetectorOptions.Builder
	    public static class Builder extends ObjectDetectorOptionsBase.Builder<Builder> {
	        public Builder setDetectorMode(int detectorMode) {…}
	    }
	}

It *looks like* there is only one
`ObjectDetectorOptions.Builder.setDetectorMode()` method, but there
are in fact *two*.  The first is the bridge method (`ACC_BRIDGE`),
the second method is the "real" method; they differ on return type:

	% javap -cp classes.jar -s -v 'com/google/mlkit/vision/objects/defaults/ObjectDetectorOptions$Builder'
	
	  public final com.google.mlkit.vision.objects.ObjectDetectorOptionsBase$Builder setDetectorMode(int);
	    descriptor: (I)Lcom/google/mlkit/vision/objects/ObjectDetectorOptionsBase$Builder;
	    flags: (0x1051) ACC_PUBLIC, ACC_FINAL, ACC_BRIDGE, ACC_SYNTHETIC
	
	  public com.google.mlkit.vision.objects.defaults.ObjectDetectorOptions$Builder setDetectorMode(int);
	    descriptor: (I)Lcom/google/mlkit/vision/objects/defaults/ObjectDetectorOptions$Builder;
	    flags: (0x0001) ACC_PUBLIC

This is represented within `class-parse` XML as:

	<method abstract="false" deprecated="not deprecated" final="false" name="setDetectorMode" jni-signature="(I)Lcom/google/mlkit/vision/objects/defaults/ObjectDetectorOptions$Builder;" bridge="false" native="false" return="com.google.mlkit.vision.objects.defaults.ObjectDetectorOptions.Builder" jni-return="Lcom/google/mlkit/vision/objects/defaults/ObjectDetectorOptions$Builder;" static="false" synchronized="false" synthetic="false" visibility="public" return-not-null="true">
	  <parameter name="detectorMode" type="int" jni-type="I" />
	</method>
	<method abstract="false" deprecated="not deprecated" final="true" name="setDetectorMode" jni-signature="(I)Lcom/google/mlkit/vision/objects/ObjectDetectorOptionsBase$Builder;" bridge="true" native="false" return="java.lang.Object" jni-return="Lcom/google/mlkit/vision/objects/ObjectDetectorOptionsBase$Builder;" static="false" synchronized="false" synthetic="true" visibility="public" return-not-null="true">
	  <parameter name="p0" type="int" jni-type="I" />
	</method>

The method with `bridge="true"` is the bridge method.

When bound in C#, this causes an issue because two methods cannot
have the same method name and parameter types:

	// C# binding
	public partial class ObjectDetectorOptions {
	    public partial class Builder {
	        public virtual MLKit.Vision.Objects.Defaults.ObjectDetectorOptions.Builder  SetDetectorMode (int detectorMode);
	        public virtual MLKit.Vision.Objects.ObjectDetectorOptionsBase.Builder       SetDetectorMode (int p0);
	    }
	}

We can set `managedName` metadata to rename one of these methods:

	<attr path="//method[@name='setDetectorMode' and @bridge='true']" name="managedName>SetDetectorMode2</attr>

However, we generate the callback delegate field with the algorithm
`$"cb_{JavaName}{IDSignature}"`, so this identifier is still used
twice, preventing C# compilation:

	// C# binding
	public partial class ObjectDetectorOptions {
	    public partial class Builder {
	        static Delegate cb_setDetectorMode_I;
	        static Delegate cb_setDetectorMode_I;
	    }
	}

The solution is to add both the _managed_ name and the return type to
the callback naming algorithm to ensure that it will always be unique
for a method: `$"cb_{JavaName}_{Name}{IDSignatureWithReturnType}"`:

	// C# binding
	public partial class ObjectDetectorOptions {
	    public partial class Builder {
	        static Delegate cb_setDetectorMode_SetDetectorMode_I_Lcom_google_mlkit_vision_objects_defaults_ObjectDetectorOptions$Builder_;
	        static Delegate cb_setDetectorMode_SetDetectorMode2_I_Lcom_google_mlkit_vision_objects_ObjectDetectorOptions$Builder_;
	    }
	}

@jonpryor jonpryor merged commit a295eab into main Aug 9, 2024
@jonpryor jonpryor deleted the callback_conflict branch August 9, 2024 20:13
@github-actions github-actions bot locked and limited conversation to collaborators Sep 9, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Callback (cb_) name collision when binding methods with identical signatures and Java names

3 participants