Skip to content

Generator: <add-node> ignores method jni-signature and parameter jni-type overrides #1488

Description

@jonathanpeppers

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:

  1. If <method jni-signature="..."> is set, use it verbatim for the Register JNI signature and __id suffix.
  2. If <parameter jni-type="..."> is set, use it as the parameter slot in the JNI signature (instead of deriving from type).
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions