Вызов функции Clojure из Haskell

Можно ли вызвать функцию Clojure из Haskell (на GHC), используя FFI или какой-нибудь другой трюк? Здесь я заинтересован в том, чтобы оставаться в рамках GHC (т.е. Не использовать Frege). Я также заинтересован в сохранении центральной программы в Haskell (что означает, что функция Clojure должна вызываться из Haskell, а не наоборот).

Как это сделать?

Ответ 1

Позвольте мне начать с рекламы inline-java, из-за которой довольно легко вызвать Clojure, просто написав код Java, который вызывает Clojure API. Тем не менее, поскольку я не запускаю кровоточащий край GHC 8.0.2 (и имел множество других проблем с установкой), я не смог использовать это. Когда (if) я запускаю inline-java, я обновлю это решение.

Мое решение ниже начинается с создания интерфейса C для методов Java в API Clojure для Java через JNI. Затем он вызывает этот интерфейс C с использованием поддержки Haskell FFI. Возможно, вам придется настроить библиотеку и включить пути к файлам в зависимости от того, где установлены JDK и JRE. Если все работает правильно, вы должны увидеть 7, напечатанный на stdout. Это 3 плюс 4, вычисленное по Clojure.

Настройка

Загрузите 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

C Интерфейс для функций Clojure

Теперь мы создадим интерфейс C для функций JVM и Clojure, которые нам нужны. Для этого мы будем использовать JNI. Я предпочитаю выставлять довольно ограниченный интерфейс:

  • create_vm инициализирует новую JVM с банком Clojure в пути к классам (убедитесь, что вы отредактируете это, если вы помещаете свой баннер Clojure где-то, кроме той же папки)
  • load_methods ищет методы Clojure, которые нам понадобятся. К счастью, Java Clojure API довольно мал, поэтому мы можем без проблем перекрыть почти все функции. Нам также необходимо иметь функции, которые преобразуют такие вещи, как числа или строки, в и из их соответствующего представления Clojure. Я сделал это только для java.lang.Long (который является Clojure типом целочисленного числа по умолчанию).
    • readObj wraps clojure.java.api.Clojure.read (со строками C)
    • varObj обертывает одну версию arg clojure.java.api.Clojure.var (со строками C)
    • varObjQualified обертывает две версии arg clojure.java.api.Clojure.read (со строками C)
    • longValue преобразует a Clojure длинный в C long
    • newLong преобразует длину C в длину до 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-функциям, которые мы только что создали. Это компилируется в исполняемый файл, который добавляет 3 и 4 с помощью функции добавления Clojure add. Здесь я потерял мотивацию для выполнения функций для 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)
  • представляют объекты Clojure правильно в Haskell
  • еще много!

Тем не менее, это работает!

Ответ 2

Простым способом будет запуск вашего процесса Clojure с помощью socket REPL или Сервер NRepl. Это позволяет использовать REPL на основе сокетов, поэтому вы можете использовать сокеты для вызова функции Clojure.