Chapter 13: JVM Interop

Where you will cross the great divide.


No language is an island, and even compiled-to-native languages need to have some FFI (foreign function interface) to interact with C, Fortran or some other language.

There's a ton of awesome infrastructure out there that was implemented in other technologies, and there is no reason for us not to take a piece of that pie.

The beautiful thing about the inter-operation mechanism offered by Lux is that it can be used to interact with any language running on the JVM (not only Java).

Although, due to its simplicity, it's better suited for interacting with programs originally written in Java, as other languages tend to implement some tricks that make the classes they generate a little bit... funky.

So, what do you need to work with the JVM?

Basically, just 3 things:

  1. The means to consume the resources it provides (classes/methods/fields/objects).
  2. The means to provide your own resources (class definitions).
  3. The means to access its special features (such as synchronization).

Let's explore them.

By the way, the only module relevant to this chapter is lux/host.

Importing Classes, Methods & Fields

It's all done with the help of the jvm-import macro:

## Allows importing JVM classes, and using them as types.
## Their methods, fields and enum options can also be imported.
## Also, classes which get imported into a module can also be referred-to with their short names in other macros that require JVM classes.
## Examples:
(jvm-import java.lang.Object
  (new [])
  (equals [Object] boolean)
  (wait [int] #io #try void))

## Special options can also be given for the return values.
## #? means that the values will be returned inside a Maybe type. That way, null becomes #;None.
## #try means that the computation might throw an exception, and the return value will be wrapped by the Error type.
## #io means the computation has side effects, and will be wrapped by the IO type.
## These options must show up in the following order [#io #try #?] (although, each option can be used independently).
(jvm-import java.lang.String
  (new [(Array byte)])
  (#static valueOf [char] String)
  (#static valueOf #as int-valueOf [int] String))

(jvm-import #long (java.util.List e)
  (size [] int)
  (get [int] e))

(jvm-import (java.util.ArrayList a)
  ([T] toArray [(Array T)] (Array T)))

## #long makes it so the class-type that is generated is of the fully-qualified name.
## In this case, it avoids a clash between the java.util.List type, and Lux's own List type.
(jvm-import java.lang.Character$UnicodeScript
  (#enum ARABIC CYRILLIC LATIN))

## All enum options to be imported must be specified.
(jvm-import #long (lux.concurrency.promise.JvmPromise A)
  (resolve [A] boolean)
  (poll [] A)
  (wasResolved [] boolean)
  (waitOn [lux.Function] void)
  (#static [A] make [A] (JvmPromise A)))

## It should also be noted, the only types that may show up in method arguments or return values may be Java classes, arrays, primitives, void or type-parameters.
## Lux types, such as Maybe can't be named (otherwise, they'd be confused for Java classes).
## Also, the names of the imported members will look like ClassName.MemberName.
## E.g.:
(Object.new [])

(Object.equals [other-object] my-object)

(java.util.List.size [] my-list)

Character$UnicodeScript.LATIN

This will be the tool you use the most when working with the JVM.

As you have noticed, it works by creating functions (and constant values) for all the class members you need. It also creates Lux type definitions matching the classes you import, so that you may easily refer to them when you write your own types later in regular Lux code.

It must be noted that jvm-import requires that you only import methods and fields from their original declaring classes/interfaces.

What that means is that if class A declares/defines method foo, and class B extends A, to import foo, you must do it by jvm-import'ing it from A, instead of B.

Writing Classes

Normally, you'd use the class: macro:

## Allows defining JVM classes in Lux code.
## For example:
(class: #final (JvmPromise A) []

  (#private resolved boolean)
  (#private datum A)
  (#private waitingList (java.util.List lux.Function))

  (#public [] new [] []
           (exec (:= .resolved false)
             (:= .waitingList (ArrayList.new []))
             []))
  (#public [] resolve [{value A}] boolean
           (let [container (.new! [])]
             (synchronized _jvm_this
               (if .resolved
                 false
                 (exec (:= .datum value)
                   (:= .resolved true)
                   (let [sleepers .waitingList
                         sleepers-count (java.util.List.size [] sleepers)]
                     (map (lambda [idx]
                            (let [sleeper (java.util.List.get [(l2i idx)] sleepers)]
                              (Executor.execute [(@runnable (lux.Function.apply [(:! Object value)] sleeper))]
                                                executor)))
                          (i.range 0 (i.dec (i2l sleepers-count)))))
                   (:= .waitingList (null))
                   true)))))
  (#public [] poll [] A
           .datum)
  (#public [] wasResolved [] boolean
           (synchronized _jvm_this
             .resolved))
  (#public [] waitOn [{callback lux.Function}] void
           (synchronized _jvm_this
             (exec (if .resolved
                     (lux.Function.apply [(:! Object .datum)] callback)
                     (:! Object (java.util.List.add [callback] .waitingList)))
               [])))
  (#public #static [A] make [{value A}] (lux.concurrency.promise.JvmPromise A)
           (let [container (.new! [])]
             (exec (.resolve! (:! (host lux.concurrency.promise.JvmPromise [Unit]) container) [(:! Unit value)])
               container))))

## The vector corresponds to parent interfaces.
## An optional super-class can be specified before the vector. If not specified, java.lang.Object will be assumed.
## Fields and methods defined in the class can be used with special syntax.
## For example:
## .resolved, for accessing the "resolved" field.
## (:= .resolved true) for modifying it.
## (.new! []) for calling the class's constructor.
## (.resolve! container [value]) for calling the "resolve" method.

And, for anonymous classes, you'd use object:

## Allows defining anonymous classes.
## The 1st vector corresponds to parent interfaces.
## The 2nd vector corresponds to arguments to the super class constructor.
## An optional super-class can be specified before the 1st vector. If not specified, java.lang.Object will be assumed.
(object [java.lang.Runnable]
  []
  (java.lang.Runnable (run) void
                      (exec (do-something some-input)
                        [])))

Also, it's useful to know that you can always use the short name for classes in the java.lang package, even if you haven't imported them.

Special Features

  • Accessing class objects.
    ## Loads the class as a java.lang.Class object.
    (class-for java.lang.String)
    
  • Test instances.
    "Checks whether an object is an instance of a particular class.
     Caveat emptor: Can't check for polymorphism, so avoid using parameterized classes."
    (instance? String "YOLO")
    
  • Synchronizing threads.
    "Evaluates body, while holding a lock on a given object."
    (synchronized object-to-be-locked
      (exec (do-something ...)
        (do-something-else ...)
        (finish-the-computation ...)))
    
  • Calling multiple methods consecutively
    (do-to vreq
      (HttpServerRequest.setExpectMultipart [true])
      (ReadStream.handler
        [(object [(Handler Buffer)]
           []
           ((Handler A) handle [] [(buffer A)] void
             (run (do Monad<IO>
                     [_ (frp;write (Buffer.getBytes [] buffer) body)]
                     (wrap []))))
           )])
      (ReadStream.endHandler
        [[(object [(Handler Void)]
            []
            ((Handler A) handle [] [(_ A)] void
             (exec (do Monad<Async>
                     [#let [_ (run (frp;close body))]
                      response (handler (request$ vreq body))]
                     (respond! response vreq))
               []))
        )]]))
    

    do-to is inspired by Clojure's own doto macro. The difference is that, whereas Clojure's version pipes the object as the first argument to the method, Lux's pipes it at the end (which is where method functions take their object values).

The lux/host module offers much more, but you'll have to discover it yourself by heading over to the documentation for the Standard Library.

Using Lux from Java

It may be the case that you want to consume Lux code from some Java code-base (perhaps as part of some mixed-language project).

The current way to do it, is to create a Lux project that exposes classes defined with the class: macro.

Once compiled, the resulting JAR can be imported to a Java project and consumed just like any other JAR.

Here is some example code:

(;module:
  lux
  (lux (data text/format)
       host))

(class: MyAPI []
  (#public [] (new) []
           [])
  (#public #static (foo) java.lang.Object
           "LOL")
  (#public #static (bar [input long]) java.lang.String
           (%i input))
  )

Here is the example project.clj file:

(defproject lux_java_interop "0.1.0"
  :plugins [[com.github.luxlang/lein-luxc "0.5.0"]]
  :lux {:program "api"}
  :source-paths ["source"]
  )

Host platform inter-operation is a feature whose value can never be understated, for there are many important features that could never be implemented without the means provided by it.

We're actually going to explore one such feature in the next chapter, when we abandon our old notions of sequential program execution and explore the curious world of concurrency.

See you in the next chapter!

results matching ""

    No results matching ""