diff --git a/examples/jvm-interop/README.md b/examples/jvm-interop/README.md new file mode 100644 index 0000000000..5a13457e91 --- /dev/null +++ b/examples/jvm-interop/README.md @@ -0,0 +1,120 @@ +# JVM interop +This is a demo for calling Roc code from Java, and some other JVM languages. + + +## Prerequisites + +The following was tested on NixOS, with `openjdk 17.0.5` and`clang 13.0.1` but should work with most recent versions of those (jdk>=10) on most modern Linux and MacOS.\ +You're welcome to test on your machine and tell me (via [Zulip](https://roc.zulipchat.com/#narrow/pm-with/583319-dank)) if you ran into any issues or limitations. + +## Goal +Our goal here is quite simple- have java take in a number -> pass it to Roc -> Roc formats a string with the number -> pass the string back to java. +This will be done with the help of [Java Native Interface](https://docs.oracle.com/javase/8/docs/technotes/guides/jni/). +We will be using C to bridge between Java and Roc. + +## Structure +As the time of writing this post, the following is the current bare bones tree of a jvm-interop: + +``` sh + . +├──  bridge.c +├──  javaSource +│ └──  Greeter.java +├──  main.roc # application main +└──  platform + ├──  host.c + └──  main.roc # main for host +``` + +bridge.c is the JNI bridge. The interesting part of it is the function `Java_javaSource_Greeter_sayHello`, this function will accept a `jint` and return a `jstring`. + +In this function, the number, encoded as bytes, will be passed to the platform.\ +The platform will then pass the number to the application which in turn will create our newly formatted string. Just so you know what to expect, the formatting function looks like this: +``` coffee +main : U64 -> Str +main = \num -> + if num == 0 then + "I need a positive number here!" + else + str = Num.toStr num + "The number was \(str), OH YEAH!!! 🤘🤘" +``` + +The Roc string, formatted with the java integer, will then be converted into a Java String the JVM could understand. + +I mentioned the C code will accept a number, but let's step back and see how we declare our native C function and pass the number to it, from Java: + +``` java +package javaSource; + +public class Greeter { + static { + System.loadLibrary("interop"); // this loads the dynamic library created from our JNI code! + } + public static native String sayHello(int num); + + public static void main(String[] args) { + System.out.println(sayHello(420)); + } +} + +``` + + +## See it in action +#### For brevity's sake we'll run the build script and ommit some of its (intentionally) verbose output: + +``` sh +[dankey@computer:~/dev/roc/examples/jvm-interop]$ ./build.sh && java javaSource.Greeter +The number was 420, OH YEAH!!! 🤘🤘 +``` +That's pretty cool!\ +Since we're talking JVM Bytecode, we can pretty much call our native function from any language that speaks JVM Bytecode. + +Note: The JNI code depends on a dynamic lib, containing our native implementation, that now resides in our working directory.\ +So in the following examples, we'll make sure that our working directory is in LD_LIBRARY_PATH.\ +I generally speaking, you'd paobably add your dynamic library to a spot that's already on your path, for convenience sake.\ +So first, we run: + +``` sh +[nix-shell:~/dev/roc/examples/jvm-interop]$ export LD_LIBRARY_PATH=$(pwd):$LD_LIBRARY_PATH +``` + +Now, let's try Kotlin! +``` kotlin +[nix-shell:~/dev/roc/examples/jvm-interop]$ LD_LIBRARY_PATH=$(pwd):$LD_LIBRARY_PATH kotlin +Welcome to Kotlin version 1.7.20 (JRE 17.0.5+8-nixos) +Type :help for help, :quit for quit +>>> import javaSource.Greeter +>>> Greeter.sayHello(69) +res1: kotlin.String = The number was 69, OH YEAH!!! 🤘🤘 +``` +And it just works, out of the box! + +Now let's do Scala + +``` scala +[nix-shell:~/dev/roc/examples/jvm-interop]$ scala +Welcome to Scala 2.13.10 (OpenJDK 64-Bit Server VM, Java 17.0.5). +Type in expressions for evaluation. Or try :help. + +scala> import javaSource.Greeter +import javaSource.Greeter + +scala> Greeter.sayHello(1337) +val res0: String = The number was 1337, OH YEAH!!! 🤘🤘 + +``` +And it also works beautifully. + +Test it out in your favorite JVM lang!\ +And again, if anything goes not according to plan, tell me in the link above and we'll figure it out. + +## Notes on building +I suggest reading the build script and uncommenting according to your setup.\ +But one note on something that may not be obvious:\ +As of the time of writing this document, `roc build --lib` generates a shared object with the suffix `.so.1.0`.\ +This `.0` suffix is unneeded in any part of the build, so we can simply rename it. +But one does depend on `libhello.so` (without `.1`), so we symlink into it. + + diff --git a/examples/jvm-interop/HelloJNI.c b/examples/jvm-interop/bridge.c similarity index 99% rename from examples/jvm-interop/HelloJNI.c rename to examples/jvm-interop/bridge.c index 0482a4151b..bc673b2812 100644 --- a/examples/jvm-interop/HelloJNI.c +++ b/examples/jvm-interop/bridge.c @@ -209,7 +209,7 @@ size_t roc_str_len(struct RocStr str) extern void roc__mainForHost_1_exposed_generic(struct RocBytes *ret, struct RocBytes *arg); -JNIEXPORT jstring JNICALL Java_javaSource_HelloJNI_sayHello +JNIEXPORT jstring JNICALL Java_javaSource_Greeter_sayHello (JNIEnv *env, jobject thisObj, jint num) { char native_string[256] = {0}; diff --git a/examples/jvm-interop/build.sh b/examples/jvm-interop/build.sh index 9a24a12378..6161adb4e7 100755 --- a/examples/jvm-interop/build.sh +++ b/examples/jvm-interop/build.sh @@ -1,6 +1,7 @@ #!/bin/sh -set -euxo +# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ +set -euxo pipefail # don't forget to validate that $JAVA_HOME is defined, the following would not work without it! # set it either globally or here @@ -8,7 +9,11 @@ set -euxo # in nixos, to set it globally, i needed to say `programs.java.enable = true;` in `/etc/nixos/configuration.nix` +# if roc is in your path, you could +# roc build --lib +# else, assuming in roc repo and that you ran `cargo run --release` ../../target/release/roc build --lib + mv libhello.so.1.0 libhello.so.1 ln -sf libhello.so.1 libhello.so @@ -21,15 +26,19 @@ export LD_LIBRARY_PATH=$(pwd):$LD_LIBRARY_PATH # but this is the way of java packaging # we could go without it with an "implicit" package, but that would ache later on, # especially with other jvm langs -javac -h . javaSource/HelloJNI.java +javac -h . javaSource/Greeter.java + +# build jni bridge clang \ -c -fPIC \ -I"$JAVA_HOME/include" \ -I"$JAVA_HOME/include/linux" \ - -o demo.o HelloJNI.c + -o bridge.o bridge.c -clang -shared -o libdemo.so demo.o -L. -lhello + +# build interop +clang -shared -o libinterop.so bridge.o -L. -lhello # then run -java javaSource.HelloJNI +# java javaSource.Greeter diff --git a/examples/jvm-interop/javaSource/HelloJNI.java b/examples/jvm-interop/javaSource/Greeter.java similarity index 75% rename from examples/jvm-interop/javaSource/HelloJNI.java rename to examples/jvm-interop/javaSource/Greeter.java index 43cf31ad1a..1706c255a3 100644 --- a/examples/jvm-interop/javaSource/HelloJNI.java +++ b/examples/jvm-interop/javaSource/Greeter.java @@ -1,8 +1,8 @@ package javaSource; -public class HelloJNI { +public class Greeter { static { - System.loadLibrary("demo"); + System.loadLibrary("interop"); } public static native String sayHello(int num);