Fan

 

JavaFFI

Overview

The Java Foreign Function Interface (or Java FFI) is a feature which allows Fan code to easily utilize normal Java libraries. The Java FFI is basically a mapping of the Java type system into the Fan type system:

  • Java packages => Fan pods
  • Java classes => Fan classes
  • Java interfaces => Fan mixins
  • Java fields => Fan fields
  • Java methods => Fan methods

Fan was designed to run on the JVM, so mapping between the two worlds is fairly straight foward with a high level of interoperability. However, there is a level of impedance mismatch between the two type systems. Features supported by the Java FFI:

  • Interop with any Java API except those which use multi-dimensional arrays
  • Static type checking
  • Call overloaded Java methods from Fan
  • Construct Java classes using the Fan constructor syntax
  • Extend Java classes (only one level of inheritance allowed right now)
  • Implement Java interfaces
  • Implicit coercion between Java primitives and sys::Int, sys::Float
  • Implicit coercion between one-dimensional Object arrays and sys::List
  • Direct mappings for Java one-dimensional primitive arrays
  • Fan reflection support for Java members
  • Dynamic invoke support against Java classes

Features which are not yet available in Java FFI:

  • Multi-dimensional arrays
  • Subclassing a Java class more than one level deep
  • Attempting to override a Java overloaded method; this means you cannot subclass or extend from a type with abstract overloaded methods

Features which are not supported result in a compile time error.

Interop Summary

The following table summarizes the mapping of Java types to Fan types:

Java Type            Fan Type
-----------          ----------
foo.bar.Baz          [java]foo.bar::Baz
boolean              sys::Bool
byte                 sys::Int
short                sys::Int
char                 sys::Int
int                  sys::Int
long                 sys::Int
float                sys::Float
double               sys::Float
java.lang.Object     sys::Obj
java.lang.String     sys::Str
java.lang.Boolean    sys::Bool?
java.lang.Long       sys::Int?
java.lang.Double     sys::Float?
java.math.BigDecimal sys::Decimal
Foo[]                Foo[]  // sys::List parameterized with Foo
boolean[]            [java]fanx.interop::BooleanArray
byte[]               [java]fanx.interop::ByteArray
short[]              [java]fanx.interop::ShortArray
char[]               [java]fanx.interop::CharArray
int[]                [java]fanx.interop::IntArray
long[]               [java]fanx.interop::LongArray
float[]              [java]fanx.interop::FloatArray
double[]             [java]fanx.interop::DoubleArray
Foo[][]             // unsupported for both primitivies and Objects

Quick reference for mapping Java code to Fan code:

Java                         Fan
--------------------------   --------------------------
import javax.swing           using [java] javax.swing
import java.util.Map.Entry   using [java] java.util::Map$Entry as Entry
JFrame f = new JFrame(...)   f := JFrame(...)
array.length                 array.size
array[i]                     array[i]
array[i] = val               array[i] = val
int[] x = new int[5]         x := IntArray(5)

How it Works

The Java FFI does not use any special Fan syntax. Java APIs are imported into the Fan type system via the normal using statement with a special syntax for pod names. Java packages are mapped to Fan pods by prefixing the string "[java]". For example the Java package javax.swing has the Fan pod name of [java]javax.swing.

The Fan compiler itself has no knowledge of Java libraries, rather it supports FFI plugins based on pods being prefixed with "[ffi]". In the case of the Java FFI, the compilerJava pod is the compiler plugin for importing the Java type system into Fan.

Fan code using the Java FFI results in a normal pod file compiled down into fcode. The only difference is that the fcode contains type and member references which are Java specific. This means that Fan pods with Java FFI calls are not necessarily portable; attempting to use a Java FFI call on .NET will fail.

Class Path

The current implementation of the compilerJava plugin is pretty simple. It looks for packages and classes using the Fan runtime classpath. This includes:

  1. jars found in "sun.boot.class.path"
  2. {java}lib/rt.jar (only if step above fails to find anything)
  3. {java}lib/ext/*.jar
  4. jars found in "java.class.path" system property

In order to use a class found in the classpath, compilerJava uses Java reflection and resolves the class via Class.forName with the current classloader. If you have problems importing classes you can dump the jars and packages found using:

fan compilerJava::ClassPath

Primitives

The special Java primitives boolean, long, and double are implicitly mapped to the Fan types sys::Bool, sys::Int, and sys::Float respectively. The other Java primitives are mapped as follows:

byte    sys::Int
short   sys::Int
char    sys::Int
int     sys::Int
long    sys::Int
float   sys::Float

The special primitives above are not directly supported by the Fan type system. Therefore you cannot use them as local variables or in field or method signatures. They are always coerced to/from their Fan representation.

Arrays

Arrays of Objects are implicitly boxed/unboxed as Fan sys::List. If you call a method which returns an array of Objects it is boxed into a Fan list of the appropriate type. Likewise you pass a Fan list of the appropriate type whenever a Java array is expected.

Primitive arrays are handled specially without any boxing/unboxing. They are represented in the Fan type system via the special types:

boolean[]  [java]fanx.interop::BooleanArray
byte[]     [java]fanx.interop::ByteArray
short[]    [java]fanx.interop::ShortArray
char[]     [java]fanx.interop::CharArray
int[]      [java]fanx.interop::IntArray
long[]     [java]fanx.interop::LongArray
float[]    [java]fanx.interop::FloatArray
double[]   [java]fanx.interop::DoubleArray

The make, get, set, and size methods provide symbolic representations for working with primitive arrays. They are mapped directly to a single opcode in the Java bytecode:

int[] x = new int[4]  =>  x := IntArray(4)
x[2]                  =>  x[3]
x[3] = 5              =>  x[3] = 5
x.length              =>  x.size

Nullable

Any Java API which uses reference types are mapped into the Fan type system as nullable:

// Java methods
String foo(Object x, int y, Foo z)
void bar(String[] x) {}

// Fan representation
Str? foo(Obj? x, Int y, Foo? z)
Void bar(Str?[]? x) {}

Note the case of String[] we assume that the entire array could be null or that any of the array items may be null, so the Fan mapping is Str?[]?.

Overloaded Methods

One impedance mismatch between Java and Fan is that Java permits a field and method to have the same name. Java also allows method overloading where multiple methods with the same name may be declared with different parameter types.

Fan only allows a single definition of a slot for a given name. However when calling out to Java types the compiler will correctly resolve overloaded fields and methods. Let's consider this Java class:

class Foo
{
  String a;
  String a() { return a; }
  void a(String x) { a = x; }
  String b() { return a; }
}

In the class above a is overloaded by a field and two methods. Let's look at how Fan code is resolved against the Java members:

foo.a        // lack of parens indicates field get
foo.a = "x"  // field set
foo.a()      // call Foo.a() method
foo.a("x")   // call Foo.a(String) method
foo.b()      // call Foo.b() method
foo.b        // call Foo.b() method - no ambiguity so we can omit parens

Resolving a call to an overloaded version of the method follows the same rules as the Java Language Specification. It is a compile time error if the arguments do not match any methods or if they match multiple methods ambiguously.

Constructors

Under the covers Fan treats Java constructors just like the Java VM treats them - as special methods with the name of <init>. The standard constructor syntax is used to invoke a Java constructor:

a := Date()
b := Date(millis)
c := Date(2008-1900, 11, 13) // crazy API to create 13-Dec-2008

The constructor call is resolved against the Java constructors using the same rules for resolving overloaded methods.

Subclassing

The Java FFI enables you to both extend from a Java class and implement Java interfaces. For example to create a subclass of java.util.Date:

using [java] java.util::Date as JDate
class FanDate : JDate
{
  new now() : super() {}
  new make(Int millis) : super(millis) {}
}

The standard inheritance syntax is used to extend and implement Java types.

The Fan subclass must define how the Fan constructors call the Java superclass constructors. This is done by calling super as an overloaded constructor (if the base class has multiple constructors). You may not use a this constructor chain.

Constructors for a Fan class which subclasses from a Java class are emitted as true Java constructors. Therefore you may not declare two Fan constructors with the same parameter types as it would result in duplicate Java constructors. For example the following code would result in a compile time error:

class FanDate : Date
{
  new now() : super() {}
  new epoch() : super(0) {}
}

Because of this difference between Fan constructors versus Java constructors there is currently a restriction that you may only subclass from a Java class one level deep. You may not subclass from a Fan class which itself subclasses from a Java class.

Another restriction: because a Fan class may not override overloaded methods, you may not create a concrete Fan class which inherits abstract overloaded methods.

Overrides

When a Fan type subclasses a Java type, you can override the Java methods using the normal Fan syntax with the following restrictions:

  • cannot override static or final methods
  • cannot override a method overloaded by field of same name
  • cannot override a method overloaded by parameter types
  • cannot override a method which uses multi-dimensional arrays in its signature

Consider this example:

// Java class
class Java
{
  void alpha() {}
  void alpha(Java x) {}
  void beta(Java x) {}
  int gamma(String[] x) {}
}

In the example above, the method alpha is overloaded by parameter types, therefore it is not permissible for a Fan class to override it. However we can override beta and gamma:

class Fan : Java
{
  override Void beta(Java? x) {}
  override Int gamma(Str?[]? x) {}
}

When we override a Java method we use the Fan type representation of the signature. In the gamma method we map the return type int to sys::Int and the argument type from String[] to the Fan list Str[].

Inner Classes

Inner classes in Java source are formatted using a dot, but in the Java VM they are represented using the "$" dollar sign. We import inner classes into the Fan type system using the Java VM name:

using [java] java.util::Map$Entry as Entry

Note that since the "$" is not a legal identifier char in Fan, you must rename the Java inner class to a valid Fan identifier with the as keyword.

Dynamic Calls

Normal Fan reflection and dynamic invoke is available with Java classes. The Fan runtime will correctly map reflective calls and dynamic invokes against overloaded methods. Because it is possible for a Java class to have both a field and method of the same name, Type.field and Type.method might return different results for the same name. If an attempt is made to use dynamic invoke on slot which has both a field and method of that name, then the method always hides the field.

Functions as Interfaces

Inner classes are often used in Java as a substitute for closures. The Java FFI allows you to use a function where an interface with one method is expected. For example the interface Runnable defines one method called run, so we can use a Fan function whenever a Runnable is expected:

// Java API
void execute(Runnable r)

// Fan code
execute |,| { echo("run!") }
execute(&run)

The standard rules for coercion between Java and Fan types apply for how the function implements the interface's method.