Skip to content

Static Binding Generator implementation overview

Vladimir Mutafov edited this page Apr 22, 2019 · 2 revisions

Overview of the NativeScript Android Generators

Doing a build of a NativeScript application targeting Android, there are multiple generators which are ran and provide information about JS and native code, which would be used later by the runtime. More precisely, there are the Metadata Generator, Static Binding Generator and the DTS Generator. This document describes the way how the Static Binding Generator (SBG) works internally.

Main idea of the SBG

The goal of the SBG is to generate Java classes at build time which are direct mirror representations of the JS objects provided to the extend function when extending native classes/interfaces excluding all new functions declared on the JS side. This is necessary for supporting the case where a user wants to extend a native class/interface and provide an istance of the resulting class to a native API expecting the extended class/interface. For example, if we have the following code:

let ExtendedNativeObject = java.lang.Object.extend("com.natives.ExtendedNativeObject", {
    toString: function(){
        return "overridden toString call";
    },
    someNewFunction(){
    }
});

The SBG would generate a Java class similar to the following: package com.natives;

public class ExtendedNativeObject extends java.lang.Object{
    public ExtendedNativeObject(){
        // ... invoke runtime methods for registering the object instance
    }
    public String toString(){
        // ... invoke runtime methods to trigger the 'toString()' function on the JS instance of this class in V8
    }
}

All generated Java classes can be found in <my-app>/platforms/android/app/src/main/java/com/tns/gen if there was a usage of the extend() function without providing a class name, and <my-app>/platforms/android/app/src/main/java/ if there was a usage of the extend() function with providing a class name. The above shown generated Java class is used only to describe the idea of the SBG without showing the exact produced code.

SBG execution phase

  1. Parse JS code and collect information about possible native class extends
  2. For every case from the previous step, generate a mirroring Java class

Problems

Although at first it seems pretty easy to generate some Java code which invokes runtime methods to delegate the execution to V8, there are some difficulties in generating the right method signature. One of the main problems is collecting the overridable methods from the extended class and its parents. Another big problem is the handling of generic classes with generic methods. Having to determine which methods could be overridden it's best to do the same thing the JVM does when ensuring a method correctly overrides a parent class' method - follow the Java Language Specification (JLS). The basic rules for overriding a method according to the JLS are: An instance method m1, declared in class C, overrides another instance method m2, declared in class A if all of the following are true:

  1. C is a subclass of A.
  2. The signature of m1 is a subsignature of the signature of m2.
  3. Either:
    • m2 is public, protected, or declared with default access in the same package as C, or
    • m1 overrides a method m3 (m3 distinct from m1, m3 distinct from m2), such that m3 overrides m2.

As for the signature of a method, the following descriptions from the JLS gives a good explanation:

  • Two methods have the same signature if they have the same name and argument types.
  • Two method or constructor declarations M and N have the same argument types if all of the following conditions hold:
    • They have the same number of formal parameters (possibly zero)
    • They have the same number of type parameters (possibly zero)
    • Let A1, ..., An be the type parameters of M and let B1, ..., Bn be the type parameters of N. After renaming each occurrence of a Bi in N's type to Ai, the bounds of corresponding type variables are the same, and the formal parameter types of M and N are the same.

The signature of a method m1 is a subsignature of the signature of a method m2 if either:

  • m2 has the same signature as m1, or
  • the signature of m1 is the same as the erasure of the signature of m2.

Two method signatures m1 and m2 are override-equivalent iff either m1 is a subsignature of m2 or m2 is a subsignature of m1.

These rules are pretty straightforward to implement except handling generic methods. The issue is due to the SBG parsing Java class files (not Java source code) and the nature of generics in Java - they don't exist at runtime. Having a generic class like ArrayList at runtime is just ArrayList and the only way to see if a class has generic parameters is to look into the metadata in the Java class file. This affects the logic of finding overridable methods and the problem can be solved by either:

  1. Calculating the erasure of method arguments and return type
  2. Building an inheritance graph of the extended class and calculating how the initially provided generics spread through all parent classes

The first option may seem easier than the second but it has some drawbacks. Even if it is correctly determined which method could be overridden, when it's time to write it in the mirror class, there should be some logic to write the exact generic type and not its erasure. Considering the handling of this drawback we could conclude that option number two would actually be easier to implement. Currently, the SBG does the generic inheritance graph building in the GenericsAwareClassHierarchyParserImpl. Its goal is if having the following classes hierarchy:

class GenericBase<K,V>{}
class SimplifiedGenericBase<T> extends GenericBase<T, java.lang.String>{}
class Concrete extends SimplifiedGenericBase<java.lang.Integer>{}

The parser should determine the exact runtime type which would be in the place of the K and V generic parameters in GenericBase. So, the parser would start from Concrete and walk through its hierarchy replacing all generic parameters with the ones provided in the previously iterated class. In this example when reaching SimplifiedGenericBase, the parameter T would be resolved by looking back and seeing that Concrete extended SimplifiedGenericBase providing java.lang.Integer as a parameter. Doing this technique until we reach the base class (the one whose parent is java.lang.Object) we can reify all generic parameters at build time. After the parser finishes with building the inheritance graph for Concrete, we have the following resolution of generics:

  • T in SimplifiedGenericBase is java.lang.Integer at runtime
  • K in GenericBase is java.lang.Integer at runtime
  • V in GenericBase is java.lang.String at runtime

Having all this information, we can determine if a given method m1 overrides another method m2 following the JLS without much transformations and easily write signatures of methods in the mirror class. Collection of methods for a class is currently done by the InherritedMethodsCollectorImpl class in the SBG. Its goal is, having the generic inheritance graph, to follow the JLS and provide a collection of reified methods which are overridable or abstract. The reification of generic methods is done by replacing the generic parameters in methods with the collected resolved generic parameters for the current class by the GenericsAwareClassHierarchyParserImpl. This step includes some transformations necessary to handle bounded generic parameters and generic wildcards. These transformations can be understood by examining the MethodReificationPipelineImpl class. When the methods which are abstract or overridable are collected, the SBG starts generating the mirror class writing all overridable methods which the user has overriden and all abstract methods. Abstract methods are written even if the user has not implemented them in order to not break the JLS and to support the dynamic nature of JS.

Special cases when generating the mirror class

There are cases where the SBG generates code which not only proxies the execution from Android to V8, but performs other tasks. An example of this is when extending the Android Application or Service class. In those cases there is additional runtime initialization logic generated in the body of the onCreate method.