是否可以使用FFI或其他技巧,在GHC上从Haskell中调用Clojure函数?我希望在GHC范围内实现这一点,而不是使用Frege。我还希望将主程序保持在Haskell中(这意味着应该从Haskell调用Clojure函数,而不是反过来)。
如何做到这一点?
inline-java
,它可以很容易地调用Clojure,只需编写调用Clojure API的Java代码。但是,由于我没有运行最新版本的GHC 8.0.2(并且遇到了各种其他安装问题),我无法使用它。如果我成功使用inline-java
,我会更新这个解决方案。7
打印到stdout中。这是Clojure计算出的3
加4
。
LD_LIBRARY_PATH
。在我使用的机器上,这意味着导出export LD_LIBRARY_PATH="/usr/lib64/jvm/java/jre/lib/amd64/server/"
最后,这里有一个makefile可以使编译更加容易。您可能需要调整一些库和包含路径。
# 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
现在,我们将为我们所需的JVM和Clojure功能创建一个C接口。为此,我们将使用JNI。我选择暴露一个非常有限的接口:
create_vm
初始化带有Clojure jar类路径的新JVM(请确保您调整了此项,如果您将Clojure jar放在其他位置而不是同一个文件夹中)。load_methods
查找我们需要的Clojure方法。幸运的是,Java Clojure API非常小,因此我们几乎可以包装所有那里的函数而不会遇到太多困难。我们还需要具有将数字或字符串转换为其相应Clojure表示形式以及从其相应Clojure表示形式进行转换的函数。我只对java.lang.Long
(这是Clojure的默认整数类型)做了这个。readObj
包装clojure.java.api.Clojure.read
(使用C字符串)varObj
包装了clojure.java.api.Clojure.var
的一个参数版本(使用C字符串)varObjQualified
包装了clojure.java.api.Clojure.read
的两个参数版本(使用C字符串)longValue
将Clojure长整型转换为C长整型newLong
将C长整型转换为Clojure长整型invokeFn
分派到正确数量的clojure.lang.IFn.invoke
。在这里,我只打算将其暴露到arity 2,但没有什么能阻止您进一步进行。以下是代码:
// 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的FFI来连接我们刚刚创建的C函数。这将编译为可执行文件,使用Clojure的add函数将3
和4
相加。在这里,我失去了创建readObj
和varObj
函数的动力(主要是因为在我的例子中没有用到它们)。
-- 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
编译只需要使用make all
,运行则使用make run
。
由于这只是一个概念证明,有很多问题需要修复:
newArray
中出现)。话虽如此,它确实有效!
inline-java
的 Hackage 页面说我至少需要那个版本。 :) - Alec
.class
文件? - Alec