Summary
The <add-node> metadata transform does not honor explicit JNI overrides on the inserted method. When a <method> declares jni-signature="..." and its <parameter> declares jni-type="...", the generator ignores both and derives the JNI signature from the managed parameter type instead. This makes it impossible to bind Java methods whose erased signature differs from the managed parameter type (a common pattern with generic Java interfaces).
Repro
Java (Material Slider, simplified):
// @RestrictTo(LIBRARY_GROUP) — public API consumers should use the typed sub-interface
public interface BaseOnChangeListener<S extends BaseSlider<?, ?, ?>> {
void onValueChange(S slider, float value, boolean fromUser);
}
public interface Slider.OnChangeListener extends BaseOnChangeListener<Slider> { }
public class Slider extends BaseSlider<Slider, ...> {
// Real Java signature uses the erased base type:
public void addOnChangeListener(BaseOnChangeListener listener) { ... }
}
Metadata.xml:
<add-node path="/api/package[@name='com.google.android.material.slider']/class[@name='Slider']">
<method visibility="public" static="false" abstract="false" final="false"
deprecated="not deprecated" bridge="false" native="false"
synchronized="false" synthetic="false"
return="void" jni-return="V" name="addOnChangeListener"
jni-signature="(Lcom/google/android/material/slider/BaseOnChangeListener;)V">
<parameter type="com.google.android.material.slider.Slider.OnChangeListener" name="listener"
jni-type="Lcom/google/android/material/slider/BaseOnChangeListener;" />
</method>
</add-node>
Expected
Generated C# uses the JNI signature from jni-signature / parameter jni-type:
[Register ("addOnChangeListener", "(Lcom/google/android/material/slider/BaseOnChangeListener;)V", ...)]
public virtual unsafe void AddOnChangeListener (Slider.IOnChangeListener? listener)
{
const string __id = "addOnChangeListener.(Lcom/google/android/material/slider/BaseOnChangeListener;)V";
...
}
Actual
Generated C# derives the JNI signature from the managed type attribute on the parameter:
[Register ("addOnChangeListener", "(Lcom/google/android/material/slider/Slider$OnChangeListener;)V", ...)]
public virtual unsafe void AddOnChangeListener (Slider.IOnChangeListener? listener)
{
const string __id = "addOnChangeListener.(Lcom/google/android/material/slider/Slider$OnChangeListener;)V";
...
}
That __id does not exist on the Java class, so any call would throw java.lang.NoSuchMethodError at runtime.
Why this matters
This pattern is exactly what we hit on Slider / RangeSlider in dotnet/android-libraries PR #1486 (issue #1484). The base interface BaseOnChangeListener is @RestrictTo(LIBRARY_GROUP) and uses erased generics, so we want to:
- Hide the restricted base interface from the public binding API.
- Expose
addOnChangeListener(Slider.IOnChangeListener) as the discoverable, typed entry point.
- Still dispatch through the real Java method, whose erased signature is
(Lcom/google/android/material/slider/BaseOnChangeListener;)V.
Without an honored override, this is only solvable by hand-writing the JNI plumbing in an Additions/*.cs partial class — see source/com.google.android.material/material/Additions/SliderListenerMethods.cs in PR #1486. That hand-written file would be deleted the moment this generator gap is closed.
Existing <add-node> call sites that already shape methods with jni-signature (but happen to not exercise the divergence, so they go unnoticed):
Proposed behavior
When emitting [Register(...)] and the local __id constant for an <add-node> method:
- If
<method jni-signature="..."> is set, use it verbatim for the Register JNI signature and __id suffix.
- If
<parameter jni-type="..."> is set, use it as the parameter slot in the JNI signature (instead of deriving from type).
- The managed parameter
type continues to drive the C# parameter type (so the public API stays strongly typed).
This matches how jni-signature / jni-type already work on the existing-method <attr> path; <add-node>-inserted methods just need the same hookup.
Workaround
Hand-written partial class in Additions/ that invokes _members.InstanceMethods.InvokeVirtualVoidMethod with the correct __id. Maintained per-method, easy to drift.
Environment
Summary
The
<add-node>metadata transform does not honor explicit JNI overrides on the inserted method. When a<method>declaresjni-signature="..."and its<parameter>declaresjni-type="...", the generator ignores both and derives the JNI signature from the managed parametertypeinstead. This makes it impossible to bind Java methods whose erased signature differs from the managed parameter type (a common pattern with generic Java interfaces).Repro
Java (Material Slider, simplified):
Metadata.xml:Expected
Generated C# uses the JNI signature from
jni-signature/ parameterjni-type:Actual
Generated C# derives the JNI signature from the managed
typeattribute on the parameter:That
__iddoes not exist on the Java class, so any call would throwjava.lang.NoSuchMethodErrorat runtime.Why this matters
This pattern is exactly what we hit on
Slider/RangeSliderindotnet/android-librariesPR #1486 (issue #1484). The base interfaceBaseOnChangeListeneris@RestrictTo(LIBRARY_GROUP)and uses erased generics, so we want to:addOnChangeListener(Slider.IOnChangeListener)as the discoverable, typed entry point.(Lcom/google/android/material/slider/BaseOnChangeListener;)V.Without an honored override, this is only solvable by hand-writing the JNI plumbing in an
Additions/*.cspartial class — seesource/com.google.android.material/material/Additions/SliderListenerMethods.csin PR #1486. That hand-written file would be deleted the moment this generator gap is closed.Existing
<add-node>call sites that already shape methods withjni-signature(but happen to not exercise the divergence, so they go unnoticed):source/androidx.camera/camera-video/Transforms/Metadata.xml#L311-L321source/androidx.activity/activity/Transforms/Metadata.xml#L339-L348Proposed behavior
When emitting
[Register(...)]and the local__idconstant for an<add-node>method:<method jni-signature="...">is set, use it verbatim for theRegisterJNI signature and__idsuffix.<parameter jni-type="...">is set, use it as the parameter slot in the JNI signature (instead of deriving fromtype).typecontinues to drive the C# parameter type (so the public API stays strongly typed).This matches how
jni-signature/jni-typealready work on the existing-method<attr>path;<add-node>-inserted methods just need the same hookup.Workaround
Hand-written partial class in
Additions/that invokes_members.InstanceMethods.InvokeVirtualVoidMethodwith the correct__id. Maintained per-method, easy to drift.Environment
dotnet/android-librariesmain, .NET for Androidnet10.0-android36.0