Run compiled Fortran code with Java

In the second post of my series about interfacing compiled code, I describe how to call a Fortran library function from Java.

Unlike C, which we used in a previous post to interface with Fortran, Java1 is an object oriented programming language which allows compiled code to be run on many different operating systems without the need to recompile that code for each target OS. Java being widely used in application development and ranking amongst the most popular programming languages,2 I believe it to be sensible to get some idea about interfacing compiled code with this programming language.

To this end, the remainder of my post is structured as follows: First, I discuss the prerequisites we need to take care of. In the subsequent section, I briefly present the data from which we want to obtain regression estimates. The third section describes the Java class that will call the Fortran function. Section four shows how to compile this Java class and to run the Fortran library function. In the last section, I conclude.

Requirements

First, we need our Fortran linear regression library, liblinreg.so (the file suffix may vary depending on the operating system, e.g. *.so on Linux, *.dll on Windows, *.dylib on macOS). For information about how to build the library, see this post, and for details about the implementation of the OLS algorithm in Fortran see here.

Second, we need a directory structure within which to organize the Java project. Java IDEs such as e.g. Eclipse, Intellij IDEA or NetBeans allow to automatically set up a project structure, keep track of class files, and manage dependencies (e.g. with Maven). Despite these benefits, for this blog post, I shall set up the directory structure manually,3 as defined below. The project directory java-call-fortran contains three subdirectories: classes, for the compiled Java classes; lib, for additional Java libraries; src, for Java source code. We shall place our Fortran library directly into java-call-fortran.

java-call-fortran
├── classes
├── lib
│   └── jna-5.6.0.jar
├── src
│   └── CallFortran.java
└── liblinreg.so

Third, we need a way to call our linear regression library. The Java Native Access (JNA) library provides the means by which we may interface with compiled code.4 JNA is available as a Java archive from a repository such as Maven Central Repository.5 Make sure to place the JNA archive (jna-5.6.0.jar at the time of writing this post) into the lib subdirectory.

Data and model

The data from which we will estimate the linear regression coefficients is based on the R attitude6 dataframe, exported to a space separated text file. The first row of the text file contains the variable names, the second and third the number of observations and variables respectively. The respondents’ response values are stored in the subsequent rows (see below).

rating complaints privileges learning raises critical advance
30 30 30 30 30 30 30
7 7 7 7 7 7 7
43 51 30 39 61 92 45
63 64 51 54 63 73 47
71 70 68 69 76 86 48
61 63 45 47 54 84 35
81 78 56 66 71 83 47

Given that data, we want to estimate how the variables complaints, privileges, …, advance contribute to employees’ overall department rating (rating). The model is defined as follows, with y denoting the dependent variable rating, and X the design matrix (containing 1 as well as the remaining variables)

\begin{align*} y &= X\beta + \epsilon \\ ~\epsilon &\sim N(0, \sigma^2 I) \\ \end{align*}

Java class

Some language specifics aside, the core structure of the Java class does not differ very much from the C program I wrote in my previous post.

First, declare an interface FLibrary that holds the native library function (lines 9-16). Within this interface, declare a method FLibrary.INSTANCE.linreg_ that mirrors the function linreg_ in the target library liblinreg.so. Note that the argument types need to match the native library function’s input types (see JNA type mapping).

Next, in the main method, import the attitude data from the space-separated text file attitude.txt and put it into an array of doubles (lines 19-37). This involves determining the variable names (line 23-24), getting the number of observations and variables (lines 25-26), and parsing the remaining data to store it into an array (lines 28-37).

Third, declare the inputs for FLibrary.INSTANCE.linreg_, i.e. arrays for the dependent variable y , the design matrix X , as well as arrays for coefficients and standard errors, and the variance-covariance matrix (lines 39-53)

Last, call FLibrary.INSTANCE.linreg_ (line 56) and display the results (lines 58-66). Note that linreg_ requires all arguments to be passed by reference (hence creating IntByReference objects from n and k ).7

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.ptr.IntByReference;

public class CallFortran {
    // define interface to Fortran library
    public interface FLibrary extends Library {
	FLibrary INSTANCE = Native.load("linreg", FLibrary.class);
	void linreg_(double[] y, double[] X,
		     IntByReference n, IntByReference k,
		     double[] beta, double[] vcov, double[] se);
    }

    public static void main(String[] args) throws IOException {
	// import data
	BufferedReader f = new BufferedReader(new FileReader("attitude.txt"));

	// variable names, number of observations, number of variables
	String[] varnames = f.readLine().split(" ");
	varnames[0] = "intercept";
	int n = Integer.parseInt(f.readLine().split(" ")[0]);
	int k = Integer.parseInt(f.readLine().split(" ")[0]);

	// remaining data
	String[] d;
	double[] data = new double[n * k];
	for (int z = 0; z < n; z++) {
	    d = f.readLine().split(" ");
	    for (int s = 0; s < k; s++) {
		data[z * k + s] = Integer.parseInt(d[s]);
	    }
	}
	f.close();

	// prepare inputs for linreg_()
	double [] y = new double[n];
	double [] X = new double[n * k];
	double [] beta = new double[k];
	double [] vcov = new double[k * k];
	double [] se = new double[k];

	// fill arrays in column-major layout for Fortran
	for (int z = 0; z < n; z++) {
	    y[z] = data[z * k];
	    X[z] = 1;
	    for (int s = 1; s < k; s++) {
		X[s * n + z] = data[z * k + s];
	    }
	}

	// call function
	FLibrary.INSTANCE.linreg_(y, X, (new IntByReference(n)), (new IntByReference(k)), beta, vcov, se);

	// display results
	System.out.println("\nResults:");
	System.out.println("=======================================");
	System.out.println("           Estimate  Std. Err  z-Value ");
	System.out.println("=======================================");
	for (int i = 0; i < k; i++) {
	    System.out.printf("%-10s   %6.3f    %6.3f   %6.3f\n", varnames[i], beta[i], se[i], beta[i]/se[i]);
	}
	System.out.println("=======================================");
    }
}

Compilation and results

With the directory structure, the shared library, and the source code all set, we are now ready to compile an executable Java class.8 To ensure that the JNA library files (located inside lib) are found at compilation time, set their path with -classpath accordingly. With -d, tell the Java compiler to output the resulting class files into the classes directory.

javac -classpath lib/* src/*.java -d classes

To call the resulting CallFortran.class, invoke the Java interpreter from the command line. The -Djna.library.path=. option tells Java to look for the shared library in the current directory.

java -classpath classes:lib/jna-5.6.0.jar -Djna.library.path=. Callfortran

Running the Java class file yields the regression estimates displayed below. Given that the results are identical to those produced by the C program, please refer to this post (subsection Compilation and results) for the interpretation.

Results:
=======================================
	   Estimate  Std. Err  z-Value
=======================================
intercept    10,787    11,589    0,931
complaints    0,613     0,161    3,809
privileges   -0,073     0,136   -0,538
learning      0,320     0,169    1,901
raises        0,082     0,221    0,369
critical      0,038     0,147    0,261
advance      -0,217     0,178   -1,218
=======================================

Concluding remarks

In this (second) post about interfacing compiled libraries, I showed how to call our Fortran linear regression function from Java using the JNA library. Compared to C, importing space delimited text data into Java requires fewer lines of code, furthermore, there is no need to manually allocate and free memory. Despite these language specific differences, the general code structure remains the same: (1) Providing an interface to the foreign function, (2) importing the data, (3) setting up the data which shall be passed to the foreign function, (4) calling linreg_, and (5) displaying the results.

Note: For the code used in this post, see https://git.staudtlex.de/blog/call-fortran-from-java.


  1. For a more thorough description of the Java programming language, see e.g. this Wikipedia entry↩︎

  2. According to the TIOBE index (2020), GitHub (2020), or IEEE Spectrum (2019), Java ranks either second or third. ↩︎

  3. Delectationis causa ;) ↩︎

  4. Java ships with the JNI library to interface with C code. JNI however does not seem to easily allow dynamic loading of shared libraries at runtime. ↩︎

  5. See here to download version 5.6.0 of the Java Native Access library, and here for its documentation. ↩︎

  6. The data consist of aggregated responses of clerical employees of a large financial organization. Each observation corresponds to the responses of approximately 35 employees of each randomly selected department. For further information, see the R documentation ↩︎

  7. Arrays are passed by reference automatically, see the JNA documentation↩︎

  8. Please note that this will not create an self-contained jar file. Creating (executable) jar files may or may not be a topic for a future blog post. ↩︎