inline-java: Call any JVM function from Haskell
The Haskell standard includes a native foreign function interface
(FFI). Using it can be a bit involved and only C support is
implemented in GHC. inline-java
lets you call any JVM function
directly, from Haskell, without the need to write your own foreign
import declarations using the FFI. In the style of inline-c
for
C and inline-r
for calling R, inline-java
lets you name any
function to call inline in your code. It is implemented on top of the
jni and jvm packages using a GHC Core plugin
to orchestrate compilation and loading of the inlined Java snippets.
Example
Graphical Hello World using Java Swing:
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Data.Text (Text)
import Language.Java
import Language.Java.Inline
main :: IO ()
main = withJVM [] $ do
message <- reflect ("Hello World!" :: Text)
[java| {
javax.swing.JOptionPane.showMessageDialog(null, $message);
} |]
Building it
Requirements:
- the Stack build tool;
- either, the Nix package manager,
- or, OpenJDK installed from your distro.
To build:
$ stack build
You can optionally get Stack to download a JDK in a local sandbox
(using Nix) for good build results reproducibility. This is
the recommended way to build inline-java. Alternatively, you'll need
it installed through your OS distribution's package manager for the
next steps (and you'll need to tell Stack how to find the JVM header
files and shared libraries).
To use Nix, set the following in your ~/.stack/config.yaml
(or pass
--nix
to all Stack commands, see the Stack manual for
more):
nix:
enable: true
Building the safe interface
There is an experimental interface which catches
common memory management mistakes at compile time. This interface
currently needs a fork of GHC which supports the
LinearTypes language extension. Both the GHC
fork and the safe interface can be built with:
$ stack --nix --stack-yaml stack-linear.yaml build inline-java
For examples of how to use the safe interface you can check
the directory server or the
wizzardo-http benchmark.
Further reading
Check the tutorial on how to use inline-java
.
If you want to know more about how it is implemented, look at
our post on the plugin implementation.
There is also a post which gives an overview of the
safe interface.
Debugging
The generated java output can be dumped to stderr by passing to GHC
-fplugin-opt=Language.Java.Inline.Plugin:dump-java
If -ddump-to-file
is in effect (as when using stack
), the java code
is dumped to <module>.dump-java
instead.
Troubleshooting
Build-time error package or class Blah does not exist
inline-java
is going to invoke the javac
compiler, and any classes
used in java
quotations need to be reachable via the CLASSPATH
environment variable. For instance,
CLASSPATH=/path/to/my.jar:/some/other/path ghc --make program.hs
Run-time error ThreadNotAttached
Haskell threads need to be attached to the JVM before making JNI calls.
Foreign.JNI.withJVM
attaches the calling thread, and other threads
can be attached with Foreign.JNI.runInAttachedThread
. When the JVM
calls into Haskell, the thread is already attached.
Run-time error ThreadNotBound
JNI calls need to be done from bound threads. The thread invoking the
main
function of a program is bound. Threads created with forkOS
are bound. In other threads, Control.Concurrent.runInBoundThread
can be used to run a computation in a bound thread.
Run-time error java.lang.NoClassDefFoundError
Classes might not be found at runtime if they are not in a folder or
jar listed in the parameter -Djava.class.path=<classpath>
passed
to withJVM
.
withJVM ["-Djava.class.path=/path/to/my.jar:/some/other/path"] $ do
...
Additionally, classes might not be found if a thread other than the one
calling main
is trying to use them. One solution is to have the thread
calling main
load all the classes in advance. Then the classes will
be available in the JVM for other threads that need them.
Calling Language.Java.Inline.loadJavaWrappers
will have the effect of
loading all classes needed for java
quotations, which will suffice in
many cases.
Another option is to set the context class loader of other threads,
so they earn the ability to load classes on their own. This might
work when the thread was attached to the JVM via the JNI, and
the context class loader is just null
.
loader <- [java| Thread.currentThread().getContextClassLoader() |]
...
forkOS $ runInAttachedThread $ do
[java| { Thread.currentThread().setContextClassLoader($loader); } |]
...
Run-time error JVMException
Any java exception that goes from Java to Haskell will be wrapped
as a value of type JVMException
with a reference to the Java object
representing the exception. The message and the stack trace of the
exception can be retrieved from the exception object with more JNI
calls, e.g.
\(JVMException e) -> [java| { $e.printStackTrace(); } |]
or with JNI.Foreign.showException
.
License
Copyright (c) 2015-2016 EURL Tweag.
All rights reserved.
inline-java is free software, and may be redistributed under the terms
specified in the LICENSE file.
inline-java is maintained by Tweag I/O.
Have questions? Need help? Tweet at
@tweagio.