Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling a Clojure Function from Haskell

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?

like image 372
George Avatar asked Oct 29 '16 19:10

George


2 Answers

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.

Setup

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

C Interface to Clojure functions

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 long
    • newLong converts a C long to a Clojure long
    • invokeFn 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);
}

Haskell Interface to C functions

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

Try it!

Compiling should be just make all and running make run.

Limitations

Since this is only a proof of concept, there are a bunch of things that should be fixed:

  • proper conversion for all of Clojure's primitive types
  • tear down the JVM after you are done!
  • make sure we aren't introducing memory leaks anywhere (which we might be doing with newArray)
  • represent Clojure objects properly in Haskell
  • many more!

That said, it works!

like image 141
Alec Avatar answered Nov 09 '22 16:11

Alec


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.

like image 24
ChrisBlom Avatar answered Nov 09 '22 15:11

ChrisBlom