Background
Idris JVM backend has supported foreign function calls for some time now. For example, to invoke parseInt
method on
java.lang.Integer
class,
invokeStatic (Class "java/lang/Integer") "parseInt" (String -> JVM_IO Int) "234"
Here since the Idris compiler doesn’t know anything about Java’s Integer
class or its parseInt
method, we have to
explicitly provide the function signature. The function call also has the explicit Class
before java/lang/Integer
and the type of invocation invokeStatic
.
Since we are targeting JVM bytecode, JVM has to know whether a method call is a static method call or an interface method call or a virtual method call. It would be nice if we don’t have to worry about any of these things and just call a FFI function with a class name, method name and the arguments. This is the motivation behind this new feature along with some other nice things like null safety, construtor and method overloading resolution and exception handling.
Maybe and Either in foreign function calls
Maybe
type and Either
type can be used in foreign function calls for null safety and exception handling. Maybe
type can be used for argument types and return types. Maybe
type used in an argument position will pass null
to the target foreign function if it is Nothing
or the actual value if it is Just
. Similarly, Maybe
type
used for return type will convert null
returned from foreign function into Nothing
and the non-null value into
Just.
At the bytecode level, Maybe
wrapper doesn’t exist. It gets compiled down to
null
or the actual value.
Either
type can only be used in return types to indicate whether the foreign function can throw exceptions.
At runtime, if the foreign function throws exception, it will be captured in the “left” of type Throwable
or if the
foreign function completes normally, the result will be stored in the “right” of result type. There are functions try
and catch
to handle exceptions which we will see later in the post.
How it works
Before we look at some examples, first let’s declare some class names as we are going to use them in multiple places and we don’t want to duplicate.
stringClass: String
stringClass = "java/lang/String"
listInterface: String
listInterface = "java/util/List"
arrayListClass: String
arrayListClass = "java/util/ArrayList"
collectionInterface : String
collectionInterface = "java/util/Collection"
systemClass: String
systemClass = "java/lang/System"
comparatorClass : String
comparatorClass = "java/util/Comparator"
pointClass : String
pointClass = "java/awt/Point"
collectionsClass : String
collectionsClass = "java/util/Collections"
stringBuilderClass : String
stringBuilderClass = "java/lang/StringBuilder"
objectsClass : String
objectsClass = "java/util/Objects"
integerClass : String
integerClass = "java/lang/Integer"
And “import” some methods:
jdkimport [
(systemClass, ["getProperty", "setProperty"]),
(stringClass, ["substring", "CASE_INSENSITIVE_ORDER", "valueOf"]),
(integerClass, ["parseInt"]),
(comparatorClass, ["compare"]),
(arrayListClass, ["<init>", "add"]),
(listInterface, ["get"]),
(collectionsClass, ["max"]),
(stringBuilderClass, ["<init>", "toString"]),
(objectsClass, ["toString"]),
(pointClass, ["<init>", "x"])
]
Here jdkimport
is just an additional syntax created using Idris syntax extensions. It just calls a type provider
function written in Idris to know about these classes, methods and fields. Note that it imports fields such as
CASE_INSENSITIVE_ORDER
, x
and also constructors in the name of <init>
which is the JVM internal name for
constructors. The jdkimport
syntax launches a JVM during compilation without any classpath so it basically can import
all the JDK classes and methods.
There is also another syntax called jvmimport
that can take an additional argument, a command, which could be just the
JVM with correct classpath or could be a build tool that properly sets up the classpath from your project dependencies so
that we can “import” classes and methods from external foreign libraries.
Once the information about JVM classes and methods is collected using type provider, appropriate call site, Idris code similar to the one in the beginning of the post can be created using Idris elaborator reflection with just class name and member name from the user. As a user, we don’t have to know much about these internals, we just need to import classes and members and can use them without having to explicitly provide foreign types. Now let’s look at some examples on how we can actually make FFI calls in the new way.
Examples
1. Safe static method call
main : JVM_IO ()
main = do
exceptionOrInt <- (integerClass <.> "parseInt") "1234"
printLn $ either (const 0) id exceptionOrInt
Here the type of (integerClass <.> "parseInt")
is String -> JVM_IO (Either Throwable Int)
. Since the method can throw
exceptions, it returns an Either
. Here we return 0
in case of an exception. Later in the post, we will see a
detailed example of exception handling. As the method returns an Int
which is a primitive type in JVM, it cannot be
null and the FFI call already knows that hence the result Int
is not wrapped in a Maybe
. We don’t provide any
explicit type signature for the foreign function. If we try to pass anything other than String
for this foreign
function, it will be a compilation error!
2. Unsafe static method call
do
number <- (integerClass <.!> "parseInt") "23"
printLn number
Here we use <.!>
with an !
to indicate an unsafe method call instead of <.>
. There is also javaUnsafe
and java
if you prefer names to operators. The type of (integerClass <.!> "parseInt")
is String -> JVM_IO Int
.
Sometimes if we are sure that the foreign function would not return null or throw exceptions, we can use unsafe method
calls but as the name indicates, it would fail at runtime if null is returned or an exception is thrown.
3. Overloading resolution
We can pick which overloaded variant we want to use by passing appropriate types to the foreign function and the FFI call will automatically have corresponding types.
printLn !((stringClass <.!> "valueOf(double)") 2.5)
printLn !((stringClass <.!> "valueOf(char)") 'H')
The first function takes an Idris Double
and the second function takes Idris Char
. The types passed to the foreign
functions to resolve overloading are JVM types.
4. Safe instance method
do
s <- (stringClass <.> "substring(int)") "Foobar" 1
putStrLn !(either throw (pure . show) s)
Safe instance method calls are similar to static method calls except that the instance should be passed as the first argument. Here again, we don’t provide any explicit type signature or the type of method invocation whether it is static or instance method but it all works out automatically in a type safe way. Here also we pick a particular overloaded version.
The type of (stringClass <.> "substring(int)")
is String -> Int -> JVM_IO (Either Throwable (Maybe String))
.
Since the return type is String
and it can be null, it is in a Maybe
and the method can throw exceptions so the
overall type is in Either
.
5. Exception handling
do
propValue <- try ((systemClass <.> "getProperty(?java/lang/String)") Nothing) [
([catch IllegalArgumentExceptionClass, catch NullPointerExceptionClass], \t =>
do
printLn "property name is null or empty"
pure Nothing
),
([catchNonFatal], \t =>
do
printLn "unable to get property value"
pure Nothing
)
]
printLn propValue
This example shows how to handle exceptions with different handlers and also shows how to pass a null
to a foreign function.
If a FFI function argument type is prefixed with ?
, then the idris type would be Maybe nativeTy
and we can pass
Nothing
to pass a null
to the foreign function. We can have handlers for single exception, multiple exceptions or
for all non fatal errors similar to Scala’s
NonFatal.
6. Constructors
do
arrayList1 <- (arrayListClass <.> "<init>(int)") 10
putStrLn !(either throw toString arrayList1)
-- Unsafe constructor
arrayList2 <- arrayListClass <.!> "<init>()"
putStrLn !(toString arrayList2)
Similar to methods, constructors can be overloaded and we can select a particular overload variant by explicitly
specifying the foreign type. Constructors can also be invoked in a safe or unsafe way. As constructors cannot return
null, when invoked in a safe way, the result type will only be in Either
and not wrapped in a Maybe
.
7. Fields
do
-- static field getter
caseInsensitiveComparator <- stringClass <.#!> "CASE_INSENSITIVE_ORDER"
printLn !((comparatorClass <.!> "compare") caseInsensitiveComparator "Bar" "august")
point <- (pointClass <.!> "<init>(int,int)") 2 3
-- instance field getter
printLn !((pointClass <.#> "x") point)
-- instance field setter
(pointClass <.=> "x") point 34
printLn !((pointClass <.#> "x") point)
Similar to methods and constructors, fields can also be accessed either in a safe or unsafe way using <.#>
for safe
getter, <.=>
for safe setter, <.#!>
for unsafe getter and <.=!>
for unsafe setter. Since field access
cannot throw a exception, the return type is automatically just Maybe nativeTy
. The field types are automatically
determined without the user having to provide the foreign types of the fields.
Summary
This post demonstrated how with Idris’ powerful features FFI, type provider and elaborator reflection, we can safely and
easily access JVM foreign functions. We can access fields, methods and constructors without having to explicitly provide
foreign types and we can access them in safe way without null
getting into Idris code and handle exceptions thrown by
foreign functions. It also showed how to call overloaded methods and constructors and how Maybe
and Either
types are
used with foreign functions.
Comments