/fastFFI

Primary LanguageJavaApache License 2.0Apache-2.0

fastFFI: Modern and Efficient FFI for Java and C++

Basically, fastFFI has three components:

  • FFI: DSL and API used to develop FFI applications.
  • Annotation Processor: the code generator for FFI.
  • LLVM4JNI: LLVM4JNI has two submodules:
    • LLVM4JNI: a tool that translates LLVM bitcode into Java bytecode.
    • LLVM4JNI Runtime: the runtime component used by generated bytecode.

An FFI application must include ffi and llvm4jni-runtime in its class path as runtime dependency.

Build

  1. Checkout source code

    git clone <path-to-fastffi> fastffi
  2. Prepare building environment

    export LLVM11_HOME=<path-to-llvm-11>

    LLVM11_HOME should point to the home of LLVM 11. In Ubuntu, it is at /usr/lib/llvm-11. Basically, the build procedure the following binary:

    • $LLVM11_HOME/bin/clang++
    • $LLVM11_HOME/bin/ld.lld
    • $LLVM11_HOME/lib/cmake/llvm
  3. Use fastFFI with Maven.

    <properties>
        <fastffi.revision>0.1.2</fastffi.revision>
    </properties>
    
    <dependencies>
        <!-- The FFI annotation -->
        <dependency>
            <groupId>com.alibaba.fastffi</groupId>
            <artifactId>ffi</artifactId>
            <version>${fastffi.revision}</version>
        </dependency>
        <!-- The FFI annotation processor for code generation -->
        <dependency>
            <groupId>com.alibaba.fastffi</groupId>
            <artifactId>annotation-processor</artifactId>
            <version>${fastffi.revision}</version>
        </dependency>
    
        <!-- The runtime component of LLVM4JNI -->
        <dependency>
            <groupId>com.alibaba.fastffi</groupId>
            <artifactId>llvm4jni</artifactId>
            <version>${fastffi.revision}</version>
            <classifier>${os.detected.classifier}</classifier>
        </dependency>
        <dependency>
            <groupId>com.alibaba.fastffi</groupId>
            <artifactId>llvm4jni-runtime</artifactId>
            <version>${fastffi.revision}</version>
        </dependency>
    </dependencies>
    
    <plugins>
        <plugin>
            <groupId>kr.motd.maven</groupId>
            <artifactId>os-maven-plugin</artifactId>
            <version>1.7.0</version>
            <executions>
                <execution>
                    <phase>initialize</phase>
                    <goals>
                        <goal>detect</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
  4. Use maven to build your applications.

    The generated code, including Java and C++ code, is available in <project.dir>/target/generated-source/annotations

Options

A Java programming language compiler must support standard options in the format -Akey[=value]. fastFFI provides the following options:

  1. fastffi.handleException: whether generating code to handle C++ exceptions
    • default value: false
  2. fastffi.manualBoxing: using new Integer() or new Long() to box a primitive integer.
    • default value: true
    • Auto boxing uses Integer.valueOf or Long.valueOf, which cannot be properly handled by the escape analysis of C2 compiler.
  3. fastffi.strictTypeCheck
    • default value: false
  4. fastffi.nullReturnValueCheck
    • default value: true
    • insert additional null check for native pointers
  5. fastffi.cxxOutputLocation
    • default value: CLASS_OUTPUT
    • accept values: CLASS_OUTPUT, SOURCE_OUTPUT, NATIVE_HEADER_OUTPUT.
  6. fastffi.traceJNICalls
    • default value: false
    • generate stuffs to trace the invocations of JNI wrappers
  7. fastffi.compactFFINames
    • default value: true
    • generate compact FFI wrapper type names, non-compact names will benefit debugging, but increase the binary size

Usage:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.0</version>
    <configuration>
        <compilerVersion>${javac.target}</compilerVersion>
        <source>${javac.target}</source>
        <target>${javac.target}</target>
        <compilerArgs>
            <arg>-Afastffi.strictTypeCheck=true</arg>
        </compilerArgs>
    </configuration>
</plugin>

Build Mac OS

  1. Install a JDK (JDK 8 and 11)

  2. Install LLVM 11, Maven and CMake

    brew install llvm@11 cmake maven
    
  3. Set ENV

    export LLVM11_HOME=/usr/local/opt/llvm@11
    

FAQ

How to install llvm-11?

In Ubuntu:

sudo apt-get install llvm-11 clang-11 lld-11 libclang-11-dev libz-dev -y

Or use this script with LLVM11_HOME=/usr/lib/llvm-11 and LLVM_VAR=11.0.0 installing from source.

Why You need a CMakeLists?

You need to write a CMakeLists.txt because the auto-generated C++ code depends on JNI library and other library you need.

By collecting what C++ code depend, you can provide a JNI dynamic-link library, so that the auto-generated C++ code can only rely on the JNI dynamic-link library.

How to specify jni library though annotations?

First of all, the basic way to link dynamic library is:

try {
      System.loadLibrary("lib-name");
}

There are some higher level way to link dynamic library by wrapping the basic way.

One way is use @FFIGen(library = "lib-name")

For example, if the original C++ code belongs to a third-party library, and you have linked this library in your JNI library, so you need specify the library as your JNI library like @FFIGen(library = "jni-lib").

@FFIGen(library = "jni-lib")
@FFITypeAlias("ns::Clz")
@CXXHead("xxx.h")
public interface Clz extends CXXPointer {}

An other way is using @FFIApplication(jniLibrary = "lib-name") create a package-info.java file in your package, telling needed library like:

@FFIApplication(jniLibrary = "jni-lib")
package com.alibaba.fastffi.demo.ffi;

import com.alibaba.fastffi.FFIApplication;

So that you don't need to specify library for @FFIGen in the same package. Sample

How to map static functions?

You may use FFILibrary designed for functions defined in a namespace. For any C function, it could be viewed as defined in the global empty namespace. Here is an example:

@FFIGen
@FFILibrary(value = "cmath", namespace = "")
@CXXHead("math.h")
public interface C {
    double fabs(double v);
    double pow(double x, double y);
}

Field value in the @FFILibrary is simply a key used to register an instance of the FFILibrary in FFITypeFactory.

To use the interface C, we need to obtain an instance of the FFILibrary via FFITypeFactory.

import com.alibaba.fastffi.FFITypeFactory;

public class Main {
    public static void main(String[]args) {
        C clib = FFITypeFactory.getLibrary(C.class);
        double a = 1.234;
        double b = 2.345;
        double c = -3.456;
        System.out.println(clib.fabs(a));
        System.out.println(clib.fabs(b));
        System.out.println(clib.fabs(c));
        System.out.println(clib.pow(a, b));
        System.out.println(Math.pow(a, b));
    }
}

Why we reimplement stdcxx?

For example, you need to use std::map which fastffi.stdcxx didn't provide. So you write StdMap like this:

@FFIGen
@CXXHead(
        value = {"stdint.h"},
        system = {"map"})
@FFITypeAlias("std::map")
@CXXTemplate(
        cxx = {"std::string", "value_class"},
        java = {"StdString", "ValueClass"})
public interface StdMap<K, V> extends FFIPointer {

    @CXXOperator("[]")
    @CXXReference V get(@CXXReference K key);

    @FFIFactory
    interface Factory<K, V> {
        StdMap<K, V> create();
    }
}

Unfortunately, you well meet error below:

stdcxx/StdMap.java:[28,8] java.lang.IllegalArgumentException: Cannot find type 'StdString' in the context of stdcxx.StdMap

To solve this, you must implement all stdcxx you need manually.

Beside, template in C++ is not same as generic in Java, so you need use @CXXTemplate to specific needed class manually.

How to get and set member directly?

For example, there is a public string member in C++ called name without set and get function.

Use @FFISetter and @FFIGetter like this:

@FFIGetter
@FFINameAlias("name")
@CXXValue
StdString getName();

@FFISetter
@FFINameAlias("name")
void setName(@CXXValue StdString name);

How to get generic factory?

Pass C++ full name to method FFITypeFactory.getFactory like this:

Factory<Long> LONG_FACTORY = FFITypeFactory.getFactory(StdVector.class, "std::vector<int64_t>");

How to specify value of enum's menber?

In common case(enum's value is from 0 to n), you only need to implement getValue method with ordinal method like this:

@Override
public int getValue() {
    return ordinal();
}

But for special C++ enum like this:

enum class AdjListType : std::uint8_t {
  /// collection of edges by source, but unordered, can represent COO format
  unordered_by_source = 0b00000001,
  /// collection of edges by destination, but unordered, can represent COO
  /// format
  unordered_by_dest = 0b00000010,
  /// collection of edges by source, ordered by source, can represent CSR format
  ordered_by_source = 0b00000100,
  /// collection of edges by destination, ordered by destination, can represent
  /// CSC format
  ordered_by_dest = 0b00001000,
};

It's obviously that enum's numbers are not continuous, so you need also pass value like this rather than from 0 to 3.

Overriding getValue method is key of this problem, so you'd like write enum like this:

@FFITypeAlias("GraphArchive::AdjListType")
@FFITypeRefiner("com.alibaba.graphar.types.AdjListType.get")
public enum AdjListType implements CXXEnum {
  // collection of edges by source, but unordered, can represent COO format
  unordered_by_source((byte)0b00000001),
  // collection of edges by destination, but unordered, can represent COO
  // format
  unordered_by_dest((byte)0b00000010),
  // collection of edges by source, ordered by source, can represent CSR format
  ordered_by_source((byte)0b00000100),
  // collection of edges by destination, ordered by destination, can represent
  // CSC format
  ordered_by_dest((byte)0b00001000);

  private final byte binaryNum;

  AdjListType(byte binaryNum) {
    this.binaryNum = binaryNum;
  }
    
  @Override
  public int getValue() {
    return binaryNum;
  }
}

In auto-generate Java code, method witch accept enum parameter will pass enumObject.getvalue() to native function like this:

@FFINameAlias("AddAdjList")
  @CXXValue
  public Status addAdjList(@CXXValue AdjListType adjListType, @CXXValue FileType fileType) {
    long ret$ = nativeAddAdjList0(address, com.alibaba.fastffi.CXXValueScope.allocate(com.alibaba.graphar.utils.Status_cxx_0x42f3f706.SIZE), 
                                  adjListType.getValue(), fileType.getValue()); 
      return (new com.alibaba.graphar.utils.Status_cxx_0x42f3f706(ret$));
  }

By this way, value will be passed correctly.

Related links