typelead/eta

how to export nullable data type to java?

clojurians-org opened this issue · 11 comments

i export an eta object to java for implementing interface.
but the interface result object can be null on some situation.
i didn't find any eta code for doing this, so what i should do for this case?

@clojurians-org Have you tried using Maybe [object] as your return type? Nothing should get marshalled to null.

thanks, i'm trying do it.
but another issue occur, i don't know to build the object for export.

data HTest = HTest @my.HTest deriving Class
foreign export java mkHTest :: Int -> Java a (Maybe HTest)
mkHTest 0 = return Nothing
mkHTest _ = return $ Just HTest
    Expected type: Int -> Java a (Maybe HTest)
      Actual type: Int
                   -> Java
                        a (Maybe (ghc-prim-0.4.0.0:GHC.Prim.Object# HTest -> HTest))

Hi! I think you have to create an instance of HTest instead, somethink like:

foreign import java unsafe "@new" newHTest  :: Java a HTest
mkHTest _ = return $ Just newHTest

it seems it didn't generate the corresponding class file:

the code is very simple:

data HTest = HTest @my.HTest deriving Class
foreign import java unsafe "@new" newHTest  :: Java a HTest
foreign export java mkHTest :: Int -> Java a (Maybe HTest)
mkHTest 0 = return Nothing
mkHTest _ = Just <$> newHTest

unzip jar, and find it.

[op@my-200 tmp]$ jar -tf callJava.jar | grep HTest.class
main/main/datacons/HTest.class
main/main/tycons/HTest.class
main/Main$$fe_mkHTest.class
main/Main$DHTest.class
main/Main$mkHTest.class
main/Main$newHTest.class

[op@my-200 tmp]$ javap ./main/main/datacons/HTest.class
Compiled from "Main.hs"
public final class main.main.datacons.HTest extends main.main.tycons.HTest {
  public my.HTest x1;
  public main.main.datacons.HTest(my.HTest);
  public int getTag();
}
[op@my-200 tmp]$ javap ./main/main/tycons/HTest.class
Compiled from "Main.hs"
public abstract class main.main.tycons.HTest extends eta.runtime.stg.DataCon {
  public main.main.tycons.HTest();
}

Oh, yeah, you can't use a free type variable in exports if they are not static so i think that:

-- To make eta create the class you have to replace `a` with `HTest`
foreign export java mkHTest :: Int -> Java HTest (Maybe HTest)

... should work.

However in the java world it is not frequent to use a instance method to generate an instance of the same class but a static factory method so this one maybe would be more idiomatic:

data HTest = HTest @my.HTest deriving Class

-- As all classes have an empty constructor by default you can import it.
foreign import java unsafe "@new" newHTest  :: Java a HTest

-- This export actually creates the class my.HTest
foreign export java "@static my.HTest.mkHTest" mkHTest :: Int -> Java a (Maybe HTest)
mkHTest 0 = return Nothing
mkHTest _ = Just <$> newHTest

Note that you can use Java a (Maybe HTest) in this case cause the export has an @static annotation.
It is a little bit tricky but i hope the info in the user guide could help you.

There is plans to make exports automatic to avoid all this: #690

it seems the maybe type didn't handle null value well.
this is the information from my side

larrys-MBP:javaCall larluo$ cat src/Main.hs
module Main where

import GHC.Base(Class)
import Java (Java)
  
main :: IO ()
main = putStrLn "Hello, Eta!"

data HTest = HTest @my.HTest deriving Class
foreign import java unsafe "@new" newHTest  :: Java a HTest

foreign export java "@static my.HTest.mkHTest" mkHTest :: Int -> Java a (Maybe HTest)
mkHTest 0 = return Nothing
mkHTest _ = Just <$> newHTest

foreign export java "@static my.HTest.mkHTest2" mkHTest2 :: Int -> Java a HTest
mkHTest2 _ = newHTest
larrys-MBP:tmp larluo$ javap my/HTest.class 
public class my.HTest {
  public my.HTest();
  public static my.HTest mkHTest2(int);
  public static eta.runtime.stg.Closure<my.HTest> mkHTest(int);
}
wget https://download.clojure.org/install/clojure-tools-1.10.0.442.tar.gz
larrys-MBP:javaCall larluo$ java -cp clojure-tools-1.10.0.442.jar:./dist/build/eta-0.8.6.5/javaCall-0.1.0.0/x/javaCall/build/javaCall/javaCall.jar clojure.main
Clojure 1.10.0
user=> (import '[my HTest])
my.HTest
user=> (HTest/mkHTest2 1)
#object[my.HTest 0x45f24169 "my.HTest@45f24169"]
user=> (HTest/mkHTest 0)
Execution error (NoSuchFieldError) at my.HTest/mkHTest (REPL:-1).
x1
user=>  (HTest/mkHTest 1)
Execution error (ClassCastException) at my.HTest/mkHTest (REPL:-1).
base.ghc.maybe.datacons.Just cannot be cast to base.ghc.maybe.datacons.Nothing

@jneira I remember we allowed arbitrary closures in the foreign export mechanism a while back. I wonder if we need to fix that to handle Maybe's properly.

I am just analyzing this case.
For a very simple file:

data HTest = HTest @my.HTest deriving Class
foreign import java unsafe "@new" newHTest  :: Java a HTest

foreign export java "@static my.HTest.mkHTest4" mkHTest4 :: Int -> Java a (Maybe HTest)
mkHTest4 _ = return Nothing

I am wondering why the final cast in method bytecode is to Nothing (not to Maybe).
So far I got that
typeDataConClass is DsForeign.hs
for Type argument Maybe HTest returns a class name:
base/ghc/maybe/datacons/Nothing
Which seems odd, and is (maybe :-) ) one of the problems.

I am going further.

Hi!
I've created somehow naive but working solution.
I catch explicitly Maybe result types in export FFI and make check for Notihng.INSTANCE - in case of eq there is null returned, otherwise code as before.
After some code cleanup I could potentially make PR.
I am however, awaiting for hints / issues.
(I've used the example to relearn again eta/ghc compiler - very likely some fragments are reinventing the wheel. Also I am not even sure if the whole approach is sensible).

Fix works with such code:

data HTest = HTest @my.HTest deriving Class
foreign import java unsafe "@new" newHTest  :: Java a HTest

foreign export java "@static my.HTest.mkHTest" mkHTest :: Int -> Java a (Maybe HTest)
mkHTest 0 = return $  Nothing
mkHTest _ = Just <$> newHTest
        Closure obj = HTest.mkHTest(1);
        System.out.println(obj.getClass());
        System.out.println(obj);

        Closure obj2 = HTest.mkHTest(0);
        System.out.println(obj2);

@jarekratajski Just took a look at your fork and it looks good to me. Feel free to send a PR.

Make sure to add a couple tests. The testing framework was upgraded and it now supports full etlas projects so you can make a project that uses both Eta/Java to make sure this is working properly.

I have realised that this result as Closure<X> in exported Java is probably not something anyone would want - I am trying to reduce it to be exactly X (return type in case of Java a (Maybe X). It will take some time - but I am getting there (slowly).