从Haskell调用Clojure函数

16

是否可以使用FFI或其他技巧,在GHC上从Haskell中调用Clojure函数?我希望在GHC范围内实现这一点,而不是使用Frege。我还希望将主程序保持在Haskell中(这意味着应该从Haskell调用Clojure函数,而不是反过来)。

如何做到这一点?


2
谷歌搜索“haskell inline-java”。这是最近的工作,我对它一无所知。 - jberryman
5
潜在的投票者:如果这个问题因为是一个推荐问题而被关闭,那么我们也可以基于所有这些问题都要求我们推荐做事情的方法的理由关闭 Stack Overflow 中每一个非概念性问题。 - duplode
3
我同意这是一个非常有价值的问题,不应该被关闭。 - dkasak
1
你最好的选择是遵循这条路径:Haskell -> C (jni) -> Java -> Clojure -> Java -> C -> Haskell。你的Clojure代码是否已经编译成了.class文件? - Alec
2
@Marcs 我一直在尝试这个,现在我已经有了一些Clojure的C接口。我希望下一步能够将它与Haskell进行接口调用...也许明天吧。 - Alec
显示剩余4条评论
2个回答

3
让我先介绍一下inline-java,它可以很容易地调用Clojure,只需编写调用Clojure API的Java代码。但是,由于我没有运行最新版本的GHC 8.0.2(并且遇到了各种其他安装问题),我无法使用它。如果我成功使用inline-java,我会更新这个解决方案。
我的解决方案首先通过JNI创建了一个C接口,用于调用Clojure API的Java方法。然后,它使用Haskell FFI支持调用该C接口。您可能需要根据JDK和JRE的安装位置调整库和包含文件路径。如果一切正常,应该看到7打印到stdout中。这是Clojure计算出的34

设置

如果您尚未下载,请下载Clojure 1.8.0 jar。我们将使用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表示形式以及从其相应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与C函数接口

最后,我们使用Haskell的FFI来连接我们刚刚创建的C函数。这将编译为可执行文件,使用Clojure的add函数将34相加。在这里,我失去了创建readObjvarObj函数的动力(主要是因为在我的例子中没有用到它们)。

-- 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对象。
  • 还有更多!

话虽如此,它确实有效!


不错。我认为在 Haskell 文档中包含这个例子会很好。 - Marcs
非常好的答案。它展示了从Haskell调用Clojure可能会有多么繁琐。 - George
如果你能演示如何使用inline-java方法,我将再奖励50美元。 - George
@George 当然可以,但我会等到 GHC 8.0.2 的正式发布(已经过期了),因为 inline-java 的 Hackage 页面说我至少需要那个版本。 :) - Alec
@Alec:看起来GHC 8.0.2今天发布了。只是想让你知道这个优惠仍然存在 :) - George
@George 哈哈,我可能要等到明年一月才能开始做这件事 - 我目前不在国内,而且Wi-Fi信号很弱。 - Alec

2
一个简单的方法是使用socket REPLNRepl服务器启动您的Clojure进程。 这将启用基于套接字的REPL,因此您可以使用套接字调用您的Clojure函数。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接