Is it possible to call a Clojure function from Haskell (on the GHC), using the FFI or some other trick? Here I'm interested in staying within the bounds of GHC (i.e., not using Frege). I'm also interested in keeping the central program in Haskell (meaning that the Clojure function should be called from Haskell, and not vice versa).
How to do this?
Let me start by advertising inline-java
which should make it pretty easy to call Clojure by just writing the Java code that calls the Clojure API. That said, as I am not running the bleeding edge GHC 8.0.2 (and had a variety of other install issues) I haven't been able to use this. When (if) I get inline-java
running, I'll update this solution.
My solution below starts by creating a C interface to the Java methods in the Clojure API for Java via the JNI. Then, it calls that C interface using Haskell FFI support. You may need to adjust the library and include file paths depending on where your JDK and JRE are installed. If everything works right, you should see 7
printed to stdout. This is 3
plus 4
calculated by Clojure.
Download the Clojure 1.8.0 jar if you don't already have it. We'll be using the Java Clojure API. Make sure you've defined LD_LIBRARY_PATH
. On the machine I used, that means exporting
export LD_LIBRARY_PATH="/usr/lib64/jvm/java/jre/lib/amd64/server/"
Finally, here is a makefile to make compiling a bit easier. You may need to adjust some library and include paths.
# makefile
all:
gcc -O -c \
-I /usr/lib64/jvm/java/include/ \
-I /usr/lib64/jvm/java/include/linux/ \
java.c
ghc -O2 -Wall \
-L/usr/lib64/jvm/java/jre/lib/amd64/server/ \
-ljvm \
clojure.hs \
java.o
run:
./clojure
clean:
rm -f java.o
rm -f clojure clojure.o clojure.hi
Now, we will make a C interface for the JVM and Clojure functionality we need. For this, we will be using the JNI. I choose to expose a pretty limited interface:
create_vm
initializes a new JVM with the Clojure jar on the classpath (make sure you adjust this if you put your Clojure jar somewhere other than in the same folder)load_methods
looks up the Clojure methods we will need. Thankfully the Java Clojure API is pretty small, so we can wrap almost all of the functions there without to much difficulty. We also need to have functions that convert things like numbers or strings to and from their corresponding Clojure representation. I've only done this for java.lang.Long
(which is Clojure's default integral number type).
readObj
wraps clojure.java.api.Clojure.read
(with C strings)varObj
wraps the one arg version of clojure.java.api.Clojure.var
(with C strings)varObjQualified
wraps the two arg version of clojure.java.api.Clojure.read
(with C strings)longValue
converts a Clojure long to a C longnewLong
converts a C long to a Clojure longinvokeFn
dispatches to the clojure.lang.IFn.invoke
of the right arity. Here, I only bother to expose this up to arity 2, but nothing is stopping you from going further.Here is the code:
// java.c
#include <stdio.h>
#include <stdbool.h>
#include <jni.h>
// Uninitialized Java natural interface
JNIEnv *env;
JavaVM *jvm;
// JClass for Clojure
jclass clojure, ifn, longClass;
jmethodID readM, varM, varQualM, // defined on 'clojure.java.api.Clojure'
invoke[2], // defined on 'closure.lang.IFn'
longValueM, longC; // defined on 'java.lang.Long'
// Initialize the JVM with the Clojure JAR on classpath.
bool create_vm() {
// Configuration options for the JVM
JavaVMOption opts = {
.optionString = "-Djava.class.path=./clojure-1.8.0.jar",
};
JavaVMInitArgs args = {
.version = JNI_VERSION_1_6,
.nOptions = 1,
.options = &opts,
.ignoreUnrecognized = false,
};
// Make the VM
int rv = JNI_CreateJavaVM(&jvm, (void**)&env, &args);
if (rv < 0 || !env) {
printf("Unable to Launch JVM %d\n",rv);
return false;
}
return true;
}
// Lookup the classes and objects we need to interact with Clojure.
void load_methods() {
clojure = (*env)->FindClass(env, "clojure/java/api/Clojure");
readM = (*env)->GetStaticMethodID(env, clojure, "read", "(Ljava/lang/String;)Ljava/lang/Object;");
varM = (*env)->GetStaticMethodID(env, clojure, "var", "(Ljava/lang/Object;)Lclojure/lang/IFn;");
varQualM = (*env)->GetStaticMethodID(env, clojure, "var", "(Ljava/lang/Object;Ljava/lang/Object;)Lclojure/lang/IFn;");
ifn = (*env)->FindClass(env, "clojure/lang/IFn");
invoke[0] = (*env)->GetMethodID(env, ifn, "invoke", "()Ljava/lang/Object;");
invoke[1] = (*env)->GetMethodID(env, ifn, "invoke", "(Ljava/lang/Object;)Ljava/lang/Object;");
invoke[2] = (*env)->GetMethodID(env, ifn, "invoke", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
// Obviously we could keep going here. The Clojure API has 'invoke' for up to 20 arguments...
longClass = (*env)->FindClass(env, "java/lang/Long");
longValueM = (*env)->GetMethodID(env, longClass, "longValue", "()J");
longC = (*env)->GetMethodID(env, longClass, "<init>", "(J)V");
}
// call the 'invoke' function of the right arity on 'IFn'.
jobject invokeFn(jobject obj, unsigned n, jobject *args) {
return (*env)->CallObjectMethodA(env, obj, invoke[n], (jvalue*)args);
}
// 'read' static method from 'Clojure' object.
jobject readObj(const char *cStr) {
jstring str = (*env)->NewStringUTF(env, cStr);
return (*env)->CallStaticObjectMethod(env, clojure, readM, str);
}
// 'var' static method from 'Clojure' object.
jobject varObj(const char* fnCStr) {
jstring fn = (*env)->NewStringUTF(env, fnCStr);
return (*env)->CallStaticObjectMethod(env, clojure, varM, fn);
}
// qualified 'var' static method from 'Clojure' object.
jobject varObjQualified(const char* nsCStr, const char* fnCStr) {
jstring ns = (*env)->NewStringUTF(env, nsCStr);
jstring fn = (*env)->NewStringUTF(env, fnCStr);
return (*env)->CallStaticObjectMethod(env, clojure, varQualM, ns, fn);
}
Finally, we use Haskell's FFI to plug into the C functions we just made. This compiles to an executable which adds 3
and 4
using Clojure's add function. Here, I lost the motivation to make functions for readObj
and varObj
(mostly because I don't happen to need them for my example).
-- clojure.hs
{-# LANGUAGE GeneralizedNewtypeDeriving, ForeignFunctionInterface #-}
import Foreign
import Foreign.C.Types
import Foreign.C.String
-- Clojure objects are just Java objects, and jsvalue is a union with size 64
-- bits. Since we are cutting corners, we might as well just derive 'Storable'
-- from something else that has the same size - 'CLong'.
newtype ClojureObject = ClojureObject CLong deriving (Storable)
foreign import ccall "load_methods" load_methods :: IO ()
foreign import ccall "create_vm" create_vm :: IO ()
foreign import ccall "invokeFn" invokeFn :: ClojureObject -> CUInt -> Ptr ClojureObject -> IO ClojureObject
-- foreign import ccall "readObj" readObj :: CString -> IO ClojureObject
-- foreign import ccall "varObj" varObj :: CString -> IO ClojureObject
foreign import ccall "varObjQualified" varObjQualified :: CString -> CString -> IO ClojureObject
foreign import ccall "newLong" newLong :: CLong -> ClojureObject
foreign import ccall "longValue" longValue :: ClojureObject -> CLong
-- | In order for anything to work, this needs to be called first.
loadClojure :: IO ()
loadClojure = create_vm *> load_methods
-- | Make a Clojure function call
invoke :: ClojureObject -> [ClojureObject] -> IO ClojureObject
invoke fn args = do
args' <- newArray args
let n = fromIntegral (length args)
invokeFn fn n args'
-- | Make a Clojure number from a Haskell one
long :: Int64 -> ClojureObject
long l = newLong (CLong l)
-- | Make a Haskell number from a Clojure one
unLong :: ClojureObject -> Int64
unLong cl = let CLong l = longValue cl in l
-- | Look up a var in Clojure based on the namespace and name
varQual :: String -> String -> IO ClojureObject
varQual ns fn = withCString ns (\nsCStr ->
withCString fn (\fnCStr -> varObjQualified nsCStr fnCStr))
main :: IO ()
main = do
loadClojure
putStrLn "Clojure loaded"
plus <- varQual "clojure.core" "+"
out <- invoke plus [long 3, long 4]
print $ unLong out -- prints "7" on my tests
Compiling should be just make all
and running make run
.
Since this is only a proof of concept, there are a bunch of things that should be fixed:
newArray
)That said, it works!
An easy way would be to launch your Clojure process with a socket REPL or NRepl server. This enables a socket based REPL, so you could then use sockets to call your Clojure function.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With