Introduction

This article is the second of a series of 4 articles:

  1. Preparation
  2. Writing the binding
  3. Testing the binding
  4. Publishing the binding as a NuGet package

In the previous article, we have seen what we need to prepare ourselves to bind a library.

Now, I will explain some key concepts and then showcase the common errors you might encounter.

For this article, I also suppose you have some basic knowledge of the binding process as written in the official documentation.

The purpose of this article is not replacing any online documentation (links at the end) but more how to deal with common errors.

Structure

A Xamarin Android binding project is structured in the following way:

  • Additions
    • Additions.cs
  • Jars
    • Your Jar or Aar file
  • Properties
  • Transforms
    • EnumFields.xml
    • EnumMethods.xml
    • Metadata.xml

Beside the actual library you are trying to bind, the most important file is the Metadata.xml file.

This is were most of the magic happens. Most of the time, you will not touch the other files. That is why it is going to be my focus here.

Dependencies

I suppose that you know about the dependencies needed for your library, if not, please read the preparation part of this series.

You have basically three ways to handle native dependencies in a binding project:

  • Include them
  • Reference them
  • Reference a NuGet package containing a binding on them

For the first two, I invite you to read the official documentation about binding a jar and binding an aar.

The last one is more useful for big libraries like the AndroidX libraries, the Huawei Mobile Services or any library with a dependency on an existing one.

For example, if the library you need to bind has a reference to OkHttp, it’s not useful to create the binding for OkHttp as it already exists. Just reference the NuGet in your binding project and you are covered.

Compilation

We now have an empty project and have added the jar or aar file we need to bind.

We compile the project then we can have compilation errors and warnings.

So here what I usually do next:

  1. Error management phase
  2. First test
  3. Warning management phase
  4. Test again
  5. Polishing phase
  6. Yet another test
  7. Packaging and publishing the binding (in an upcoming article)

The little game of binding creation will be to loop between 1 and 6 until we finally reach step 7 :-)

Fixing compilation errors

Errors are usually a good sign. This is what I expect when I do the first compilation. They mostly indicate that the compiler needs some help to generate the binding.

What you will read next is the process I apply to solve those binding errors. As stated in the previous article, this series does not aim to provide a solution to every problem but show you how I deal with them.

The most usual one is naming conflicts. They often occur because some member methods or classes have the same name as their parent class. Fixing those errors are pretty straightforward as you need to either rename the class or the method.

Here are a few exemples of naming conflicts:

Com.MySdk.ISomeInterface.cs(80,80): Error CS0102: The type 'ErrorEventArgs' already contains a definition for 'error' (CS0102) (MySDK)
Com.MySDK.Ma.cs(24,24): Error CS0542: 'Ma': member names cannot be the same as their enclosing type (CS0542) (MySDK)
Com.MySDK.IMyListener.cs(10,10): Error CS0111: Type 'ErrorEventArgs' already defines a member called 'ErrorEventArgs' with the same parameter types (CS0111) (MySDK)

Fun fact: Most of the errors come from the fact that C# does not allow some of the things Java is fine with :)

Looking for the culprit

Double-clicking on the error in Visual Studio will redirect you to the generated code where you can see some comments:

// event args for com.mysdk.SomeListener.onError
public partial class ErrorEventArgs : global::System.EventArgs {

  public ErrorEventArgs (global::Com.MySDK.ISDKError error)
  {
    this.error = error;
  }

  global::Com.MySDK.MyConfig.ISDKError error;
  public global::Com.MySDK.MyConfig.ISDKError Error {
    get { return error; }
  }
}

As you can see, the comment gives you a java type name. That might prove to be useful.

Scrolling up you might notice comments starting with Metadata.xml:

// Metadata.xml XPath interface reference: path="/api/package[@name='com.mysdk.listener']/interface[@name='SomeListener']"
[Register ("com/mysdk/SomeListener", "", "...")]
public partial interface ISomeListener : IJavaObject, IJavaPeerable {

  // Metadata.xml XPath method reference: path="/api/package[@name='com.mysdk.listener']/interface[@name='SomeListener']/method[@name='onError' and count(parameter)=1 and parameter[1][@type='com.mysdk.sdkconfig.SDKError<com.mysdk.model.someErrorCode>']]"
  [Register ("onError", "...", "...")]
  void OnError (global::Com.MySDK.MyConfig.ISDKError error);
  ...
}

Those comments give the correct syntax to add into the Metadata.xml file to reference classes or methods. You can be as precise as referencing a specific parameter as well.

In the Metadata.xml file, I want to reference the OnError method of the ISomeListener interface.

I will therefore copy the path defined in the comment into an attr attribute:

<attr path="/api/package[@name='com.mysdk.listener']/interface[@name='SomeListener']/method[@name='onError' and count(parameter)=1 and parameter[1][@type='com.mysdk.sdkconfig.SDKError&lt;com.com.mysdk.model.someErrorCode&gt;']]" ...></attr>

Basically, we are using an XPath syntax to query our native library and apply some modifications on them.

If there is only one overload of a method, specifying the parameters arguments is not necessary:

<attr path="/api/package[@name='com.mysdk.listener']/interface[@name='SomeListener']/method[@name='onError']" ...></attr>

Renaming

If I want to rename the interface I can use the managedName operator:

<attr path="/api/package[@name='com.mysdk.listener']/interface[@name='SomeListener']" 
      name="managedName">ISomeOtherName</attr>

This operator works on every element so you can apply it on namespaces, classes, interfaces or methods.

<attr path="/api/package[@name='com.mylibrary.mycomponent.cde']/class[@name='abc']" name="managedName">AbcObject</attr>

Event args

Sometimes, event args conversion fails because of naming conflicts too.

In that case, I use the argsType operator to change the parameter’s type.

<attr path="/api/package[@name='com.mylibrary.mycomponent.listener']/interface[@name='ChangeListener']/method[@name='onError']" name="argsType">ChangeListenerOnErrorEventArgs</attr>

There are many operators available which you can have a list in the famous Gist from Jon Douglas referenced links.

Most of the time, fixing those naming issues will make your binding compilation pass. But it does not mean it is over yet!

Test

I now need to look at the generated binding. If it compiles, it is testable!

The testing phase will be described in details in the next article of this series.

At that step of the process, I go to the C# sample project to check for any missing methods or types. I look at my Java sample and try to find the types and methods I need with Intellisense in my C# project. I find the process much faster that way.

If no classes or methods are missing then perfect, I go straight to the cleanup phase. But if I have some missing elements, I need to find why.

To help me with the investigation here are the questions I try to find answers for:

  1. If a class is missing, does this type inherits from or implements another type that is also missing?
  2. If a method is missing, does it have a parameter whose type is also missing?
  3. Does the Java code reference some Java generic types?
  4. Is the type obfuscated?

Repeat those questions for every type helps to have a clear view of what needs fixing.

Now that I know what is missing for my sample, I can work on fixing the binding.

Managing warnings

Warnings in Xamarin Android projects are important as they indicate something went wrong but not wrong enough to prevent the compilation.

Usually, I try to fix most of them starting with the infamous:

BINDINGSGENERATOR: Warning BG8503: Invalidating SomeNamespace.SomeType and all nested types because some of its methods were invalid. (BG8503)

Invalidating means that a big bunch of classes and method have been removed. Therefore I always treat those warnings as errors to fix my issues.

Chances are, that if we fix those errors, we will get back some of the missing members needed.

So how can we fix them?

That depends on the error hence we need to understand very well the native library.

Obfuscation

My experience shows that most of the time when dependencies are referenced correctly, obfuscation is a probable culprit.

Writing this kind of code is no fun. Hopefully, we only need to fix the types causing the issues.

Also, if the library is obfuscated in a somewhat predictable way, it is possible to write scripts to generate those obfuscation elements.

<attr path="/api/package[@name='util.z']/class[@name='a.b']" name="obfuscated">false</attr>

For those interested here is a dummy PowerShell sample I made. Please note that it requires the jd-cli command-line tool.

Missing members

Sometimes, even if we have all the references needed, some methods or classes might fail to be generated.

The first thing I do is check the api.xml file to verify that the class or method I need is there.

api.xml

You can find the api.xml in your project build folder: obj/debug/api.xml. It basically shows what the binding process will expose.

In case it is not, I manually add what is missing to the Metadata.xml:

<add-node path="/api/package[@name='com.huawei.hms.support.api.client']/class[@name='ResolvingResultCallbacks']">
    <method name="onSuccess" return="void" abstract="false" native="false" synchronized="false" static="false" final="false" deprecated="not deprecated" visibility="public">
        <parameter name="context" type="java.lang.Object" />
    </method>
</add-node>

Of course, we need to know what to write there. To help, I usually look at similar methods or classes in the api.xml and write the same thing in Metadata.xml. As for the path attribute you can copy it from JD-GUI.

Removing members

Sometimes, some members are generated but not useful to C# and even might create issues.

In those cases I remove them:

<remove-node path="/api/package[@name='com.huawei.hms.common.data']/class[@name='DataHolder']/method[@name='finalize' and count(parameter)=0]" />
<remove-node path="/api/package[@name='com.huawei.hms.support.api.client']/class[@name='ResolvingResultCallbacks']/method[@name='onSuccess' and count(parameter)=1 and parameter[1][@type='R']]" />

Property fixing

In the Huawei binding, I encounter some properties that were generated because a method was called getSomething but was not a getter.

To fix that, I instructed the binding to not generate a property and keep the method by setting its name to empty:

<attr path="/api/package[@name='com.huawei.hms.location']/class[@name='FusedLocationProviderClient']/method[@name='getLastLocation']" name="propertyName"></attr>
<attr path="/api/package[@name='com.huawei.hms.location']/class[@name='FusedLocationProviderClient']/method[@name='getLocationAvailability']" name="propertyName"></attr>
<attr path="/api/package[@name='com.huawei.hms.location']/class[@name='FusedLocationProviderClient']/method[@name='getLastLocationWithAddress']" name="propertyName"></attr>

<attr path="/api/package[@name='com.huawei.hms.ml.scan']/class[@name='HmsScan.AddressInfo']/method[@name='getAddressDetails']" name="propertyName"></attr>
<attr path="/api/package[@name='com.huawei.hms.ml.scan']/class[@name='HmsScan.AddressInfo']/method[@name='getAddressType']" name="propertyName"></attr>

Polishing

We now have all the necessary types and methods. That does not mean the work is over yet.

Usually, I try to clean up a bit the bindings at this step.

Of course, the cleanup process is important when I do a public binding. For private ones, I can accept some rough edges to save time.

I like renaming namespaces that do not feel like .NET at all:

<attr path="/api/package[@name='com.huawei.hmf.tasks']" name="managedName">Huawei.Hmf.Tasks</attr>

This is what I used in the Huawei binding since I would rather not have Com.Something in C#.

I can also rename some parameters if I find them necessary yet usually I stop there with renaming.

Finally, for types that are Java tasks or callbacks, I like to add some code in the Additions.cs.

public class DelegateLocationCallback : LocationCallback
{
    private Action<LocationResult> _onLocationResult;

    public DelegateLocationCallback(Action<LocationResult> onLocationResult)
    {
        _onLocationResult = onLocationResult;
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);

        _onLocationResult = null;
    }

    public override void OnLocationResult(LocationResult locationResult)
    {
        _onLocationResult?.Invoke(locationResult);
    }
}

Adding code in this file will make it available to every consumer of the library easing code sharing.

Wrapping up

By following all these steps, I have working binding.

Of course, the process is not linear so I go back and forth from fixing errors to warnings and so on.

I find this important to apply those steps and it helped me a lot in writing those bindings.

So hopefully that will help you too!

If you want a good example of what I highly I recommend that you go and check my Huawei mobile services binding and the associated article.

In the next article, I will describe how I do to test the bindings I work on.

Keep posted and as always, feel free to read my previous posts and to comment below, I will be more than happy to answer.

References

  1. The official documentation on bindings metadata.
  2. The official documentation on binding a jar
  3. The official documentation on binding an aar
  4. The official documentation on troubleshooting bindings.
  5. The excellent Jon Douglas gist.

Comments