Skip to content

Understanding the Binding Pipeline

Jonathan Pobst edited this page Apr 1, 2021 · 6 revisions

In order to troubleshoot issues with binding a specific library, it can be helpful to understand the bindings pipeline, and how to verify results at each step in order to narrow down the cause of an issue.

There are 4 main stages in the bindings pipeline:

Extracting the Java API from a .jar/.aar ("class-parse")

This step decompiles compiled Java, extracts the Java API, and writes it to an XML file. Additionally, if the Java library is actually a Kotlin library, it will read the additional annotations that Kotlin places in the API, and updates the Java API to be closer to the Kotlin API. (Kotlin supports more features than Java (like internal classes), which are encoding in the Kotlin annotations.)

If the input binary is an .aar, this file is unzipped and the classes.jar file inside is used as the input.

The output of this step is an XML file in the /obj/$(Configuration)/$(TargetFramework) directory called api.xml.class-parse. There is a lot of noise in this file needed by future steps, but if you ignore the noise you can see it is a description of the Java API:

<api
  api-source="class-parse">
  <package
    name="com.example">
    <interface
      deprecated="not deprecated"
      name="MyInterface"
      visibility="public">
      <method
        abstract="true"
        deprecated="not deprecated"
        final="false"
        name="doStuff"
        return="void"
        static="false"
        visibility="public">
        <parameter
          name="p0"
          type="java.lang.String" />
      </method>
    </interface>
  </package>
</api>

There isn't much interesting output in a diagnostic MSBuild log from this step. However if you are binding a Kotlin library you will see messages describing the changes made due to the Kotlin annotations:

Kotlin: Hiding internal class com.example.MyInternalClass
Kotlin: Renaming parameter com.example.MyClass - getByName - p0 -> name

Resolving the Java types in the API ("ApiXmlAdjuster")

The next step takes the api.xml.class-parse created in the previous step, attempts to resolve all Java types mentioned in the API, removes types that rely on other types it cannot resolve, and outputs the result as another XML file called api.xml in the /obj/$(Configuration)/$(TargetFramework) directory.

For example, imagine the following method was found in the previous step:

<method
  abstract="true"
  deprecated="not deprecated"
  final="false"
  name="getFragmentActivityByName"
  return="androidx.fragment.app.FragmentActivity"
  static="false"
  visibility="public">
    <parameter
      name="p0"
      type="java.lang.String" />
</method>

We need to ensure that we can find the definitions for all Java types this method requires:

  • Parameter with type java.lang.String
  • Return type androidx.fragment.app.FragmentActivity

The type androidx.fragment.app.FragmentActivity exists in AndroidX's androidx.fragment.app library, which is not referenced by default in a Bindings project. Thus if you try to compile, this method will be removed with the message:

Error while processing type 'com.example.MyClass': Type 'androidx.fragment.app.FragmentActivity' was not found.

If you look in this step's output: api.xml, you will see the method is no longer included.

Applying metadata ("metadata"/"Fixups")

There are many scenarios where the process does not have enough data to correctly bind the Java code, or there are bugs in the process that need to be worked around, or the user may want the resulting C# API to look different from the default. For this, we have a step called "metadata" or "fixups" that allow changes to be made to the binding process.

This takes the form of metadata.xml, which contains XML elements that define how to transform the api.xml file. That is, this step merely transforms the api.xml file into a different XML file. This transformed XML file is in the /obj/$(Configuration)/$(TargetFramework) directory called api.xml.fixed.

For example, let's go back to our previous method example. The full XML tree looks like this:

<api>
  <package name="com.example">
    <class name="MyClass" visibility="public">
      <method name="getFragmentActivityByName" return="androidx.fragment.app.FragmentActivity" visibility="public">
        <parameter name="name" type="java.lang.String" />
      </method>
    </class>
  </package>
</api>

Suppose we did not want to this method to be public, instead we want it to be private. To do this we would want to change the visibility="public" attribute of the <method> to be private. This could be done with the following metadata:

<attr path="/api/package[@name='com.example']/class[@name='MyClass']/method[@name='getFragmentActivityByName']" name="visibility">private</attr>

A guide to constructing metadata is available here.

If we were to look at this step's output api.xml.fixed, we would now see:

<api>
  <package name="com.example">
    <class name="MyClass" visibility="public">
      <method name="getFragmentActivityByName" return="androidx.fragment.app.FragmentActivity" visibility="private">
        <parameter name="name" type="java.lang.String" />
      </method>
    </class>
  </package>
</api>

An example warning from this step:

Warning BG8A04 Metadata.xml element '<attr path="/api/package[@name='com.example']/class[@name='MyMispelledClass']/method[@name='getFragmentActivityByName']" name="visibility">private</attr>' matched no nodes.

Generating C# code ("generator")

The final step of the binding pipeline is to output and compile C# code that represents the API defined in the api.xml.fixed file. This code is written to the /obj/$(Configuration)/$(TargetFramework)/generated/src directory.

This code may fail to compile due to binding limitations or bugs, which will require writing metadata to fix. Some of those potential issues are outlined in the Common Binding Issues section.