haskell - 从 Haskell 调用 Clojure 函数

标签 haskell clojure jvm ffi

是否可以使用 FFI 或其他技巧从 Haskell(在 GHC 上)调用 Clojure 函数?在这里,我有兴趣保持在 GHC 的范围内(即不使用 Frege)。我也有兴趣将中心程序保留在 Haskell 中(这意味着 Clojure 函数应该从 Haskell 调用,反之亦然)。

如何做到这一点?

最佳答案

让我从广告开始 inline-java只需编写调用 Clojure API 的 Java 代码,就可以轻松调用 Clojure。 。也就是说,由于我没有运行前沿的 GHC 8.0.2(并且有各种其他安装问题),所以我无法使用它。当(如果)我运行 inline-java 时,我将更新此解决方案。

下面的解决方案首先通过 JNI 在 Clojure API for Java 中创建 Java 方法的 C 接口(interface)。然后,它使用 Haskell FFI 支持调用该 C 接口(interface)。您可能需要根据 JDK 和 JRE 的安装位置调整库并包含文件路径。如果一切正常,您应该会看到 7 打印到标准输出。这是 Clojure 计算出的 3 加上 4

设置

下载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 接口(interface)

现在,我们将为我们需要的 JVM 和 Clojure 功能创建一个 C 接口(interface)。为此,我们将使用 JNI 。我选择公开一个相当有限的接口(interface):

  • 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 的一个参数版本(带有 C 字符串)
    • varObjQualified 包装 clojure.java.api.Clojure.read 的两个参数版本(使用 C 字符串)
    • longValue 将 Clojure long 转换为 C long
    • newLong 将 C long 转换为 Clojure long
    • invokeFn 分派(dispatch)到正确数量的 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 函数的接口(interface)

最后,我们使用 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 - 从 Haskell 调用 Clojure 函数,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40322749/

相关文章:

haskell - 有没有办法懒惰地 `read`?

haskell - 如何从 Haskell 中的列表生成随机元素序列?

Haskell 将用户输入存储在数组中

clojure - 如何使用 Compojure 显示图像?

java - VM 设置为使用我的所有资源

java - JVM 是否仅支持 IEEE 754 所需功能的适当子集?

haskell - 无法对返回 Sing (d::Symbol) 的函数使用 ghci 推断的类型签名

apache - 可以使用 Apache Tomcat 的 Lisp 方言吗?

clojure - 在 Datomic 中查找具有特定属性的最旧和最新实体的日期?

java - 当 Java 应用程序关闭时,Java 对象会被删除吗?