Java和Haskell之间的通信

23

我谷歌搜索得到一些答案,称Java和Haskell之间的通信可以通过GCJNI(现在该网站已经关闭)和LambdaVM来完成。如何使用LambdaVM / GCJNI?我需要下载任何构建工具吗?由于我在线上找不到太多相关资源,请问我可以在哪里了解更多信息?

我想要开发一个应用程序,在Java和Haskell之间进行通信(在其中从Java获取输入,将其传递给Haskell进行处理,并将结果返回给Java)。这就是我想做的事情。请帮助我...


1
我不知道这是否适用于您的情况,但 JVM 上有一种非常“haskellish”的语言叫做 Frege:http://code.google.com/p/frege/。 - Landei
由于您尚未接受以下任何答案,那么您还需要知道什么呢? - Samuel Audet
4个回答

41

从C中调用Haskell似乎很容易,因此也可以通过JavaCPP轻松从Java中调用。例如,要从示例代码Safe.hs调用fibonacci_hs()函数:

{-# LANGUAGE ForeignFunctionInterface #-}

module Safe where

import Foreign.C.Types

fibonacci :: Int -> Int
fibonacci n = fibs !! n
    where fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

fibonacci_hs :: CInt -> CInt
fibonacci_hs = fromIntegral . fibonacci . fromIntegral

foreign export ccall fibonacci_hs :: CInt -> CInt

我们可以通过Java这样做:

import org.bytedeco.javacpp.*;
import org.bytedeco.javacpp.annotation.*;

@Platform(include={"<HsFFI.h>","Safe_stub.h"})
public class Safe {
    static { Loader.load(); }
    public static native void hs_init(int[] argc, @Cast("char***") @ByPtrPtr PointerPointer argv);
    public static native int fibonacci_hs(int i);
    public static void main(String[] args) {
        hs_init(null, null);
        int i = fibonacci_hs(42);
        System.out.println("Fibonacci: " + i);
    }
}

在Linux下,编译过程如下:

$ ghc -fPIC -dynamic -c -O Safe.hs
$ javac -cp javacpp.jar Safe.java
$ java -jar javacpp.jar -Dplatform.compiler=ghc -Dplatform.compiler.output="-optc-O3 -Wall Safe.o -dynamic -fPIC -shared -lstdc++ -lHSrts-ghc7.6.3 -o " -Dplatform.linkpath.prefix2="-optl -Wl,-rpath," Safe

而且该程序可以使用通常的 java 命令正常运行:

$ java -cp javacpp.jar:. Safe
Fibonacci: 267914296


编辑:我已经自由地进行了一些调用开销的微基准测试。使用以下C头文件Safe.h

inline int fibonacci_c(int n) {
    return n < 2 ? n : fibonacci_c(n - 1) + fibonacci_c(n - 2);
}
以下是Java类的代码:
import org.bytedeco.javacpp.*;
import org.bytedeco.javacpp.annotation.*;

@Platform(include={"<HsFFI.h>","Safe_stub.h", "Safe.h"})
public class Safe {
    static { Loader.load(); }
    public static native void hs_init(int[] argc, @Cast("char***") @ByPtrPtr PointerPointer argv);
    public static native int fibonacci_hs(int i);
    public static native int fibonacci_c(int n);
    public static int fibonacci(int n) {
        return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
    }
    public static void main(String[] args) {
        hs_init(null, null);

        for (int i = 0; i < 1000000; i++) {
            fibonacci_hs(0);
            fibonacci_c(0);
            fibonacci(0);
        }
        long t1 = System.nanoTime();
        for (int i = 0; i < 1000000; i++) {
            fibonacci_hs(0);
        }
        long t2 = System.nanoTime();
        for (int i = 0; i < 1000000; i++) {
            fibonacci_c(0);
        }
        long t3 = System.nanoTime();
        for (int i = 0; i < 1000000; i++) {
            fibonacci(0);
        }
        long t4 = System.nanoTime();
        System.out.println("fibonacci_hs(0): " + (t2 - t1)/1000000 + " ns");
        System.out.println("fibonacci_c(0): "  + (t3 - t2)/1000000 + " ns");
        System.out.println("fibonacci(0): "    + (t4 - t3)/1000000 + " ns");
    }
}

在使用Intel Core i7-3632QM CPU @ 2.20GHz、Fedora 20 x86_64、GCC 4.8.3、GHC 7.6.3和OpenJDK 8的情况下,这将产生以下输出:

fibonacci_hs(0): 200 ns
fibonacci_c(0): 9 ns
fibonacci(0): 2 ns

公正地说,我应该提到调用JVM也是相当昂贵的...


更新: JavaCPP最近的更改使用户现在可以通过名称从C/C++访问回调函数指针,从而轻松地从Haskell中调用JVM。例如,根据在Haskell's FFI维基页面上找到的信息,使用以下代码放置在Main.hs中:

{-# LANGUAGE ForeignFunctionInterface #-}
module Main where

import Foreign.C -- get the C types
import Foreign.Ptr (Ptr,nullPtr)

-- impure function
foreign import ccall "JavaCPP_init" c_javacpp_init :: CInt -> Ptr (Ptr CString) -> IO ()
javacpp_init :: IO ()
javacpp_init = c_javacpp_init 0 nullPtr

-- pure function
foreign import ccall "fibonacci" c_fibonacci :: CInt -> CInt
fibonacci :: Int -> Int
fibonacci i = fromIntegral (c_fibonacci (fromIntegral i))

main = do
  javacpp_init
  print $ fibonacci 42

并且定义了一个用Java编写的fibonacci函数:

import org.bytedeco.javacpp.*;
import org.bytedeco.javacpp.annotation.*;

@Platform
public class Main {
    public static class Fibonacci extends FunctionPointer {
        public @Name("fibonacci") int call(int n) {
            return n < 2 ? n : call(n - 1) + call(n - 2);
        }
    }
}

我们可以使用类似以下命令在Linux x86_64上进行构建:

$ javac -cp javacpp.jar Main.java
$ java -jar javacpp.jar Main -header
$ ghc --make Main.hs linux-x86_64/libjniMain.so

然后程序正确执行并输出以下内容:

$ ./Main
267914296

3
如果有人能提供一个相反方向的例子(Haskell -> Java),那就太好了。 - Vagif Verdi
@Vagif 顺便说一句,JavaCPP支持C回调函数(FunctionPointer),Haskell FFI也支持C函数指针(FunPtr),所以Haskell也可以通过这种方式调用Java。如果你需要类似的功能,请告诉我。 - Samuel Audet
@SamuelAudet 嗨,我正在尝试按照步骤操作,但是某种原因导致 Safe_stub.o 文件从未生成,你有什么想法吗? - Charles Durham
@SamuelAudet,嘿,我刚刚发了这篇文章https://dev59.com/5WTWa4cB1Zd3GeqPHNfc ,显然在ghc 7.2之后不再生成stub.o和stub.c文件。 - Charles Durham
@CharlesDurham 哦,这很好知道。请随意编辑我的答案,以添加一个适用于 ghc 7.2 的 java -jar javacpp.jar 编译命令,谢谢! - Samuel Audet
显示剩余13条评论

7
如果您选择Haskell服务器进程方法,您可以使用MessagePack序列化/rpc库。它具有Java和Haskell的绑定,而Haskell的绑定似乎得到了很好的维护。在Hackage上寻找msgpack和msgpack-rpc。
这里是使用MessagePack进行Java/Haskell交互的玩具示例:Java serverHaskell client(链接指向GitHub)。通信方向与您想要的相反。

4
取决于你希望它们如何进行通信。让Java和Haskell代码在同一个进程中本地运行,并通过各自的FFI在内存中交换数据是一个巨大的问题,这不仅因为你有两个GC争夺数据,还因为两个编译器都有自己关于表示各种数据类型的想法。让Haskell编译到JVM下同样困难,因为JVM目前没有任何闭包概念。当然,这些事情可以做到,但从演示版到工业工具需要付出巨大的努力。我理解您提到的工具从未超过演示版阶段。更简单,但不够优雅的解决方案是将您的Haskell程序编写为服务器进程,并通过套接字从Java发送数据。如果性能和容量不是很高,则使用JSON编码可能很简单,因为双方都有支持它的库。

问题不是缺乏JVM闭包,许多JVM语言都有闭包,Java也有内部类的技术实现,而Java 8还提出了优雅的lambda实现。问题在于1. 没有尾调用 2. 非最优的分配器/GC对函数式语言不利(Haskell代码分配比Java更多的小型短寿命对象)。尽管如此,Frege等仍然表明这是可能的。 - Philip JF
你可以通过像Kilim这样的代码操纵器来获取尾递归消除,但通常并不是非常方便。https://github.com/kilim/kilim/blob/master/docs/IFAQ.txt - misterbee

1
使用消息传递(即RPC客户端-服务器或对等)模式。
为什么?这样更安全、可扩展、灵活和易于调试。调用FFI将很脆弱且难以测试。

RPC框架/规范

  • gRPC Google的Protobufs RPC over HTTP/2的公共分支

  • msgpack-rpc不包括传输。

  • zerorpc ZeroMQ + msgpack。只有Python和Node实现。似乎也被放弃了。

  • XML-RPC 成熟。广泛互操作性,但也是XML。

  • JSON-RPC 更易于调试。不是二进制协议,尽管BSON可能会被一些库黑掉。


序列化

  • 协议缓冲区(protobufs) 比其他工具拥有更多的支持。它支持版本化/可选的数据成员,不需要重新编译(或破坏)整个系统以实现互操作。

  • msgpack 可爱、简单和高效,但它不支持向前兼容的模式更改。它纯粹是一个愚蠢的二进制编解码器。可能过于简单和低级别,无法实际使用。


传输方式

  • ZeroMQ 可能是最快的非Infiniband/FC/10 GbE、非线程安全消息传输。

  • Nanomsg 一个较新的、线程安全的、UNIX哲学重新构想的ZeroMQ分支,由其创始人之一开发。

  • HTTP/2 (gRPC使用) 其优点在于它是一个可以在数据中心内部和之间工作的标准,并且还具有TLS (gRPC本地代码使用BoringSSL,Google的“更安全”的OpenSSL分支)。

  • MQTT 当您需要向十亿设备实现推送通知并使用可读性强的协议时。


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