Skip to content

Latest commit

 

History

History
231 lines (174 loc) · 7.12 KB

libsci.md

File metadata and controls

231 lines (174 loc) · 7.12 KB

Libsci

To use sci as a shared library from e.g. C++, follow along with this tutorial. We illustrate what is happening when you run the script libsci/compile-libsci and libsci/compile-cpp.

Prerequisites

If you want to run this script yourself, prepare as follows:

  • Download GraalVM and set GRAALVM_HOME. We used GraalVM CE java8 19.3.1.
  • Install lein. This is used for compiling Clojure code.
  • You should have g++ available to compile C++ code.

Walkthrough

In libsci/src we have the following Clojure file:

(ns sci.impl.libsci
  (:require [cheshire.core :as cheshire]
            [sci.core :as sci])
  (:gen-class
   :methods [^{:static true} [evalString [String] String]]))

(defn -evalString [s]
  (sci/binding [sci/out *out*] ;; this enables println etc.
    (str (sci/eval-string
          s
          ;; this brings cheshire.core into sci
          {:namespaces {'cheshire.core {'generate-string cheshire/generate-string}}}))))

This file is compiled into a Java class with one static method, evalString. This will be our API for the native library. To make this library more interesting, we enable println by providing a value for *out* in the interpreter. Also we make the cheshire library available, just to show that you can bring in your own Clojure functions.

Now let's have a look at the bridging class between Java and C++:

package sci.impl;

import org.graalvm.nativeimage.c.function.CEntryPoint;
import org.graalvm.nativeimage.c.type.CCharPointer;
import org.graalvm.nativeimage.c.type.CTypeConversion;
import com.oracle.svm.core.c.CConst;

public final class LibSci {
    @CEntryPoint(name = "eval_string")
    public static @CConst CCharPointer evalString(@CEntryPoint.IsolateThreadContext long isolateId, @CConst CCharPointer s) {
        String expr = CTypeConversion.toJavaString(s);
        String result = sci.impl.libsci.evalString(expr);
        CTypeConversion.CCharPointerHolder holder = CTypeConversion.toCString(result);
        CCharPointer value = holder.get();
        return value;
    }
}

Here we wrap the static method evalString into a native library function that is given the name eval_string. We use GraalVM's API to convert between Java and C types.

The Clojure and Java code is compiled into .class files. Next, we compile those .class files into a shared library using native-image:

$ $GRAALVM_HOME/bin/native-image \
  -jar $SCI_JAR \
  -cp libsci/src \
  -H:Name=libsci \
  --shared \
  ...

This begets the files graal_isolate_dynamic.h, graal_isolate.h, libsci.h, libsci.dylib (on linux libsci.so) and libsci_dynamic.h. We move all these files to libsci/target.

Let's use the library from a C++ program now. Here's the code:

#include <iostream>
#include <libsci.h>

int main(int argc, char* argv[]) {
  graal_isolate_t *isolate = NULL;
  graal_isolatethread_t *thread = NULL;

  if (graal_create_isolate(NULL, &isolate, &thread) != 0) {
    fprintf(stderr, "initialization error\n");
    return 1;
  }

  char *result = eval_string((long)thread, &argv[1][0]);
  std::cout << result << std::endl;
  return 0;
}

This code gets the first command line argument and feeds it into libsci's function eval_string. We compile this code as follows:

$ g++ libsci/src/from_cpp.cpp -L libsci/target -I libsci/target -lsci -o libsci/target/from_cpp

To run, we first have to set an environment variable to locate the shared libary:

$ export DYLD_LIBRARY_PATH=libsci/target

On linux this environment variable is called LD_LIBRARY_PATH.

Now, let's run it.

$ time libsci/target/from_cpp "
(println :foo)
(require '[cheshire.core :as cheshire])
(cheshire/generate-string {:a 1})"

:foo
{"a":1}
libsci/target/from_cpp   0.01s user 0.01s system 64% cpu 0.026 total

It worked. First we printed a keyword from within the interpreter. Then we returned a Clojure hash-map that was converted into JSON by cheshire. And then we printed the JSON string from the C++ program.

Using libsci from Rust

To use libsci from a Rust program, we use the same shared lib generated in the previous section (produced by running libsci/compile-libsci). Here we describe what happens when you run libsci/compile-rust.

To build Rust language bindings to libsci, we use bindgen which need a build.rs file.

This file is located in libsci/from-rust/build.rs.

extern crate bindgen;

use std::env;
use std::path::PathBuf;

fn main() {
    let path = env::var("LIBSCI_PATH").unwrap();

    println!("cargo:rustc-link-lib=sci");

    println!("cargo:rustc-link-search={path}", path = path);

    let bindings = bindgen::Builder::default()
        .header(format!("{path}/libsci.h", path = path))
        .clang_arg(format!("-I{path}", path = path))
        .generate()
        .expect("Unable to generate bindings");

    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings!");
}

Learn more about build.rs files here.

Secondly we write a main program that uses these bindings to call libsci. This code is located in libsci/from-rust/src/main.rs.

#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]

use std::ffi::{CStr, CString};
use std::str::Utf8Error;
use std::{env, ptr};

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

fn eval(expr: String) -> Result<&'static str, Utf8Error> {
    unsafe {
        let mut isolate: *mut graal_isolate_t = ptr::null_mut();
        let mut thread: *mut graal_isolatethread_t = ptr::null_mut();

        graal_create_isolate(ptr::null_mut(), &mut isolate, &mut thread);

        let result = eval_string(
            thread as i64,
            CString::new(expr).expect("CString::new failed").as_ptr(),
        );

        CStr::from_ptr(result).to_str()
    }
}

fn main() {
    let args: Vec<String> = env::args().collect();
    let result = eval(args[1].to_owned());

    match result {
        Ok(output) => println!("{}", output),
        Err(_) => println!("Failed."),
    };
}

After running libsci/compile-rust and exporting DYLD_LIBRARY_PATH (LD_LIBRARY_PATH on linux) to libsci/target, you should be able to run as follows:

$ libsci/target/from-rust "(require '[cheshire.core :as json]) (json/generate-string (range 10))"
[0,1,2,3,4,5,6,7,8,9]

References