The TechAscent Blog

JNA Makes Your Life Simpler

In previous posts, we have talked a lot about ways to build bridges to entities outside the JVM. Two highlights are support for unsigned contiguous containers of primitive datatypes and generalized resource management.

In this post we introduce what we believe to be a flexible, low cost means for using external libraries on the JVM. The technology is called Java Native Access and we have a small wrapper for it: tech.jna.

JNA enables wrapping C interfaces. If the library you intend to wrap has a C++ interface, then you will either need to wrap it and produce your own C interface, or use javacpp. Note that a well designed C interface makes consuming a library simple from many languages, for example cpython, Rust, Node, etc. So, if you are a C++ library writer, consider designing a C ABI to export to the rest of the world.

Bind to Things!

In this first, simplest example, we dynamically bind to the C standard library, look up the memset function, and call it with a range of datatypes. This reveals a couple very important advantages of JNA: dynamic binding and flexible type inference when calling.

user> (require '[tech.jna :as jna])
:tech.gc-resource Reference thread starting
nil

user> (jna/def-jna-fn "c" memset
        "Set byte memory to a value"
        com.sun.jna.Pointer ;;void* return value
        [data identity]     ;;Each argument has a coercer-fn. Pointers can be lots of types.
        [value int]         ;;read docs for memset
        [n-bytes jna/size-t])
#'user/memset

user> (def test-ary (float-array [1 2 3 4]))
#'user/test-ary
user> (vec test-ary)
[1.0 2.0 3.0 4.0]
user> (memset test-ary 0 (* 4 Float/BYTES))
18-11-30 22:05:58 chrisn-lt-2 INFO [tech.jna.timbre-log:8] - Library c found at [:system "c"]

#<com.sun.jna.Pointer@589c3dd3 native@0x7fbc8406dc10>
user> ;;Careful. That pointer was from a pinned array. It was unpinned when the
user> ;;function returned and thus there is no guarantee that pointer is still good.
user> (vec test-ary)
[0.0 0.0 0.0 0.0]

user> (import '[java.nio FloatBuffer])
#<Class@7768f788 java.nio.FloatBuffer>
user> (FloatBuffer/wrap test-ary)
#<java.nio.HeapFloatBuffer@3ec72d8d java.nio.HeapFloatBuffer[pos=0 lim=4 cap=4]>
user> (def test-ary-buf *1)
#'user/test-ary-buf
user> test-ary-buf
#<java.nio.HeapFloatBuffer@3ec72d8d java.nio.HeapFloatBuffer[pos=0 lim=4 cap=4]>
user> (memset test-ary-buf 1 (* 4 Float/BYTES))
#<com.sun.jna.Pointer@54b03682 native@0x7fbc84083c60>
user> (vec test-ary)
[2.3694278E-38 2.3694278E-38 2.3694278E-38 2.3694278E-38]

user> (import '[com.sun.jna Native Pointer])
#<Class@37cc5cc6 com.sun.jna.Pointer>
user> (defn float-ptr->vec
        [ptr n-floats]
        (->> (range n-floats)
             (mapv #(.getFloat ptr (* % Float/BYTES)))))
#'user/float-ptr->vec

user> (def test-ptr (-> (Native/malloc (* 4 Float/BYTES))
                        (Pointer.)))
#'user/test-ptr
user> test-ptr
#<com.sun.jna.Pointer@17facf73 native@0x7fbc84005260>
user> (float-ptr->vec test-ptr 4)
[-1.508421E-36 4.5822E-41 0.0 0.0]

user> (memset test-ptr 0 (* 4 Float/BYTES))
#<com.sun.jna.Pointer@76e15770 native@0x7fbc84005260>
user> (float-ptr->vec test-ptr 4)
[0.0 0.0 0.0 0.0]

A lot happened there. The function was dynamically found and memoized and then called with several different examples of argument types. This allows, for instance, binding to a C function while still using native java arrays, which is really nice in a lot of cases.

Struct Types

We must work with structs, both by reference and by value. JNA supports both but the easiest pathway here is to use a small bit of java in your project. See how we did this in our Clojure bindings for the libsvm library.

You can see there is a bit there, but structs are pretty straight forward. We then use those structs in the implementation.

We Can Make it Easier

The tech.datatype includes support for using native buffers based on JNA pointer types. In fact, it uses JNA to find memset and memcpy and uses those to implement some of the interfaces, thus copying blocks of data from one container to another is very likely to hit the memcpy fast path if the datatypes are the same or only differ by signed-ness and an unchecked copy was requested. This is part of the reason the datatype library performs so well.

~/dev/tech.datatype$ lein test
:tech.gc-resource Reference thread starting

lein test tech.datatype-test
Library c found at [:system "c"]
{:array-copy "Elapsed time: 85.852025 msecs"
, :buffer-copy "Elapsed time: 283.88789 msecs"
, :dtype-copy "Elapsed time: 40.332834 msecs"
, :unchecked-dtype-copy "Elapsed time: 39.580716 msecs"
, :raw-copy "Elapsed time: 39.064249 msecs"
, :generic-copy "Elapsed time: 2364.62601 msecs"
}

lein test tech.datatype.jna-test
{:array-hand-copy "Elapsed time: 85.848859 msecs"
, :ptr-ptr-copy "Elapsed time: 19.014222 msecs"
, :array-ptr-copy "Elapsed time: 7.13104 msecs"
}

lein test tech.datatype.unsigned-test

Ran 17 tests containing 189 assertions.
0 failures, 0 errors.

Copying from a native item to a native item is fast. We cannot be certain but we believe the discrepancy of time between the main tests and the JNA tests is the main tests are array-backed NIO buffers which incur a hit when the JVM ensures the array is contiguous. JNA pointers have built-in support for copying both directions between a JVM array and copying between pointer types uses memcpy.

The rules for using tech.datatype.jna are the same as our bindings to javacpp. So a quick reference back to our native pointers post may be helpful.

Serious Leverage

The tech.compute system is built specifically for this type of work. The general element-wise math components operate on NIO buffers. This means that you can use the tech.compute system on JVM arrays and native-backed types. It uses JNA to provide system blas bindings. Thus we can support blas functionality across any number of datatypes and container types, even native-backed types.

Often times, the container type you are using will be dictated by some outside requirement. We have done a lot of work to avoid boxing people into a design decision and instead provide some general purpose tools that work across many types. This allows us to use these bindings with an opencv image, for example.

Combining all this with the resource management system means that you get everything you need to use native code in your JVM projects. There are lots of great libraries that provide a C interface, especially in the realm of mathematics and cryptography. So go forth, bring great code and systems into the Clojure ecosystem. We got your back!


TechAscent: Simple libraries and clever taglines.

Contact us