从 Haskell 调用 Clojure 函数
Calling a Clojure Function from Haskell
是否可以使用 FFI 或其他技巧从 Haskell(在 GHC 上)调用 Clojure 函数?在这里,我有兴趣留在 GHC 的范围内(即不使用弗雷格)。我也有兴趣将中央程序保留在 Haskell 中(这意味着应该从 Haskell 调用 Clojure 函数,反之亦然)。
如何操作?
一种简单的方法是使用 socket REPL or NRepl server 启动 Clojure 进程。
这会启用基于套接字的 REPL,因此您可以使用套接字调用 Clojure 函数。
让我从广告开始inline-java
which should make it pretty easy to call Clojure by just writing the Java code that calls the Clojure API。也就是说,因为我不是 运行 最先进的 GHC 8.0.2(并且有各种其他安装问题),所以我无法使用它。当(如果)我得到 inline-java
运行 时,我将更新此解决方案。
我下面的解决方案首先通过 JNI 在 Clojure API 中为 Java 的 Java 方法创建一个 C 接口。然后,它使用 Haskell FFI 支持调用该 C 接口。您可能需要根据 JDK 和 JRE 的安装位置调整库并包含文件路径。如果一切正常,您应该会看到 7
打印到标准输出。这是 3
加上 Clojure 计算的 4
。
设置
下载 Clojure 1.8.0 jar if you don't already have it. We'll be using the Java Clojure API。确保您已定义 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
Clojure 函数的 C 接口
现在,我们将为所需的 JVM 和 Clojure 功能创建一个 C 接口。为此,我们将使用 JNI。我选择公开一个非常有限的接口:
create_vm
使用类路径上的 Clojure jar 初始化一个新的 JVM(如果将 Clojure jar 放在同一个文件夹以外的其他地方,请确保进行调整)
load_methods
查找我们需要的 Clojure 方法。值得庆幸的是 Java Clojure API 非常小,所以我们可以毫不费力地把几乎所有的函数都放在那里。我们还需要具有将数字或字符串之类的东西与相应的 Clojure 表示形式相互转换的函数。我只对 java.lang.Long
(这是 Clojure 的默认整数类型)进行了此操作。
readObj
换行 clojure.java.api.Clojure.read
(使用 C 字符串)
varObj
包装 clojure.java.api.Clojure.var
的一个 arg 版本(使用 C 字符串)
varObjQualified
包装 clojure.java.api.Clojure.read
的两个 arg 版本(使用 C 字符串)
longValue
将 Clojure long 转换为 C long
newLong
将 C long 转换为 Clojure long
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 C 函数接口
最后,我们使用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
.
限制
由于这只是一个概念验证,因此有很多问题需要修复:
- 所有 Clojure 原始类型的正确转换
- 完成后拆除 JVM!
- 确保我们不会在任何地方引入内存泄漏(我们可能会在
newArray
中这样做)
- 在 Haskell
中正确表示 Clojure 对象
- 还有很多!
也就是说,它有效!
是否可以使用 FFI 或其他技巧从 Haskell(在 GHC 上)调用 Clojure 函数?在这里,我有兴趣留在 GHC 的范围内(即不使用弗雷格)。我也有兴趣将中央程序保留在 Haskell 中(这意味着应该从 Haskell 调用 Clojure 函数,反之亦然)。
如何操作?
一种简单的方法是使用 socket REPL or NRepl server 启动 Clojure 进程。 这会启用基于套接字的 REPL,因此您可以使用套接字调用 Clojure 函数。
让我从广告开始inline-java
which should make it pretty easy to call Clojure by just writing the Java code that calls the Clojure API。也就是说,因为我不是 运行 最先进的 GHC 8.0.2(并且有各种其他安装问题),所以我无法使用它。当(如果)我得到 inline-java
运行 时,我将更新此解决方案。
我下面的解决方案首先通过 JNI 在 Clojure API 中为 Java 的 Java 方法创建一个 C 接口。然后,它使用 Haskell FFI 支持调用该 C 接口。您可能需要根据 JDK 和 JRE 的安装位置调整库并包含文件路径。如果一切正常,您应该会看到 7
打印到标准输出。这是 3
加上 Clojure 计算的 4
。
设置
下载 Clojure 1.8.0 jar if you don't already have it. We'll be using the Java Clojure API。确保您已定义 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
Clojure 函数的 C 接口
现在,我们将为所需的 JVM 和 Clojure 功能创建一个 C 接口。为此,我们将使用 JNI。我选择公开一个非常有限的接口:
create_vm
使用类路径上的 Clojure jar 初始化一个新的 JVM(如果将 Clojure jar 放在同一个文件夹以外的其他地方,请确保进行调整)load_methods
查找我们需要的 Clojure 方法。值得庆幸的是 Java Clojure API 非常小,所以我们可以毫不费力地把几乎所有的函数都放在那里。我们还需要具有将数字或字符串之类的东西与相应的 Clojure 表示形式相互转换的函数。我只对java.lang.Long
(这是 Clojure 的默认整数类型)进行了此操作。readObj
换行clojure.java.api.Clojure.read
(使用 C 字符串)varObj
包装clojure.java.api.Clojure.var
的一个 arg 版本(使用 C 字符串)varObjQualified
包装clojure.java.api.Clojure.read
的两个 arg 版本(使用 C 字符串)longValue
将 Clojure long 转换为 C longnewLong
将 C long 转换为 Clojure longinvokeFn
调度到右元数的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 C 函数接口
最后,我们使用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
.
限制
由于这只是一个概念验证,因此有很多问题需要修复:
- 所有 Clojure 原始类型的正确转换
- 完成后拆除 JVM!
- 确保我们不会在任何地方引入内存泄漏(我们可能会在
newArray
中这样做) - 在 Haskell 中正确表示 Clojure 对象
- 还有很多!
也就是说,它有效!