Call Java from Elixir

Good day,

I am trying to make use of a Java class that handles encryption and decryption. How can I call the member function of that class from a .java file in Elixir?

Thanks!

1 Like

Do you really need to use a Java class, can’t you use the built-in Erlang crypto module for encryption and decryption or one of the many Elixir libraries?

Anyway, I am not an expert but it looks like you will need to write a NIF to call your java class, by leveraging the BEAM interoperability capabilities:

http://erlang.org/doc/tutorial/introduction.html

This section informs on interoperability, that is, information exchange, between Erlang and other programming languages. The included examples mainly treat interoperability between Erlang and C.

1 Like

Ports are the way to go - search for Erlang Java ports (either here or google). When I’m back at the PC I’ll dig something up if you don’t manage to get anywhere.

But yes, if it’s standard encryption it would be less hassle and likely more reliable in deployment and operation to rewrite in Elixir.

4 Likes

http://erlang.org/doc/man/jinterface.html

3 Likes

I followed an ruby example using ports here, but couldn’t quite get the java to do the same .
Kindly do share a sample if you can.

Thank you

Ok,

Here’s an ugly PoC chunk of java code. I have extracted out the bits that are sensitive so it almost certainly won’t work as-is, but hopefully it gives you some pointers. You will need to install the Ericsson otp java package (see the import statement at the top).

There were a couple of gotchas:

  1. Marshalling data types - strings, doubles, dates etc all need to be explicitly marshalled between built-in Java datatypes and otp ready data types. I wrote quite a few helper functions to do this.
  2. The Ericsson package fails to emit a required byte to the stream of bytes sent back to the port, so the sendResponse method has the required hack to make it work.
  3. Note the warnings from @jayjun in Killing java processes started from Port.open - #10 by jayjun re: port buffer deadlocks. I haven’t handled that potential issue as it was good enough for our PoC as-is

Don’t pick on my java code - I hadn’t written any since 2005 before this.

package com.mindok;

import java.io.*;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;


import com.ericsson.otp.erlang.*;

public class Main {

    static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd' 'HH:mm");

    public static void main(String[] args) throws Exception {
        OtpErlangObject payload = null;

        while(true) {
            try {
                payload = receivePayload();
            } catch (Exception e) {
                sendResponse(toOtpString("Receive " + e.getStackTrace()));
                break;
            }

            if (payload == null) {
                sendResponse(toOtpString("No payload received - exiting"));
                break;
            }

            OtpErlangObject response = null;
            try {
                response = processPayload(payload);
            } catch (Exception e) {
                sendResponse(toOtpString("Process " + e.getMessage()));
                Object[] ste = e.getStackTrace();
                for (int i=0; i < 5; ++i) {
                    sendResponse(toOtpString("Process " + ste[i]));
                }
                break;
            }
            if (response == null) {
                sendResponse(toOtpString("No response received - exiting"));
                break;
            }

            try {
                sendResponse(response);
            } catch (Exception e) {
                sendResponse(toOtpString("Send " + e.getStackTrace()));
                break;
            }
        }

    }

    private static void sendResponse(OtpErlangObject response) throws IOException {
        OtpOutputStream otpOutputStream = new OtpOutputStream(response);
        byte[] output = otpOutputStream.toByteArray();

        System.out.write(encodeLength(output.length + 1)); // Add one for the header byte added below
        System.out.write((byte) 131); // for some reason the encoding doesn't include the very thing that identifies it as Erlang ETF
        System.out.write(output);
    }

    private static byte[] encodeLength(int length) {
        ByteBuffer bb = ByteBuffer.allocate(4);
        bb.putInt(length);
        return bb.array();
    }

    private static OtpErlangObject processPayload(OtpErlangObject payload) {
        OtpErlangTuple tuple = (OtpErlangTuple)payload;
        OtpErlangAtom command = (OtpErlangAtom)tuple.elementAt(0);
        OtpErlangObject commandParameters = tuple.elementAt(1);

        switch (command.atomValue()) {
            case "open_file" :
                return packageOutput("open_file", openFile(commandParameters));

            case "something_else" :
                // *** etc 

            default:
                return payload;
        }
    }

    private static OtpErlangObject openFile(OtpErlangObject commandParameters) {
        String outcome = "ok";
        String msg = "Successfully read ";
        String fileName = "";
        try {
            fileName = new String(((OtpErlangBinary)commandParameters).binaryValue());
            msg = "Successfully read " + fileName;
        } catch (Exception e) {
            outcome = "error";
            msg = "Error reading file: " + e.getMessage();
        }

        if (outcome != "error") {
            try {
                // ***  Do something with file that was opened
            } catch (Exception e) {
                outcome = "error";
                msg = "Error reading file " + fileName + " - " + e.getMessage();
            }
        }

        OtpErlangObject[] tupleContents = {toOtpAtom(outcome), toOtp(msg)};
        return new OtpErlangTuple(tupleContents);
    }

    private static OtpErlangObject packageOutput(String cmd, OtpErlangObject payload) {
        OtpErlangObject cmdAtom = toOtpAtom(cmd);
        OtpErlangObject[] tupleContents = {cmdAtom, payload};
        return new OtpErlangTuple(tupleContents);
    }

    private static int fromOtpInt(OtpErlangObject otpInt) {
        try {
            return ((OtpErlangLong)otpInt).intValue();
        } catch (OtpErlangRangeException e) {
            return -1;

        }
    }

    private static OtpErlangObject toOtp(Object o) {
        if (o == null) {
            return new OtpErlangAtom("nil");
        } else {
            if (o instanceof Integer) { return toOtpInt((Integer)o);}
            if (o instanceof String) { return toOtpString((String)o);}
            if (o instanceof Date) { return toOtpDate((Date)o);}
            if (o instanceof Double) { return toOtpDouble((Double)o);}
        }
        return new OtpErlangAtom("nil");
    }

    private static OtpErlangObject toOtpDouble(Double dbl) {
        return new OtpErlangDouble(dbl);
    }

    private static OtpErlangString toOtpString(String str) {
        return new OtpErlangString(str);
    }

    private static OtpErlangString toOtpDate(Date date) {
        return toOtpString(dateFormatter.format(date));
    }

    private static OtpErlangLong toOtpInt(Integer integer) {
        return new OtpErlangLong(integer);
    }

    private static OtpErlangAtom toOtpAtom(String str) {
       return new OtpErlangAtom(str);
    }


    private static OtpErlangObject receivePayload() throws Exception {
        byte[] msgLen = new byte[4];

        int readByteCount = System.in.read(msgLen);
        if (readByteCount != msgLen.length) {throw new Exception("Didn't get message length");}

        ByteBuffer bb = ByteBuffer.wrap(msgLen);
        int encodedLength = bb.getInt();

        byte[] rawPayload = new byte[encodedLength];

        readByteCount = System.in.read(rawPayload);
        if (readByteCount != encodedLength) {throw new Exception("Payload didn't match message length");}

        OtpInputStream otpInputStream = new OtpInputStream(rawPayload);
        return otpInputStream.read_any();
    }

}

3 Likes

One more thing. To open the port on the elixir side you will need something like:

    jar_location = ~S|path\to\my\java_jars\encryptor_interface.jar|
    cmd = "java -jar #{jar_location}"
    Port.open({:spawn, cmd}, [:binary, :exit_status, :use_stdio, :stderr_to_stdout, {:packet, 4}])

2 Likes