JInterface is a Java library that ships with the Erlang standard release. It can be used to allow Java processes to communicate with Erlang processes, or for Java to Java interprocess communication, or for Java to C interprocess communication. Today we're going to use it to communicate from a Java process to an Elixir process. Let's get started.

Project

Starting a new project (not in the video)

We'll kick off a new project for this:

mix new jinterface_playground --sup
cd jinterface_playground

Next we're adding a requirement on ExActor:

vim mix.exs
defp deps do
  [
    {:exactor, "2.0.1"}
  ]
end
mix deps.get
vim lib/calculator_server.ex

Here's the calculator server from the wx calc episodes, with a slight modification for changes to ExActor

defmodule CalculatorServer do
  use ExActor.GenServer

  defstart start_link, do: initial_state(%{display: "0", numbers: [], value: 0, current_operation: :noop})

  defcall get_display, state: state, do: reply(state.display)

  defcast number(num), state: state do
    new_numbers = state.numbers ++ [num]
    new_state(%{state | numbers: new_numbers, display: Enum.join(new_numbers)})
  end

  defcast add, state: state do
    update_state(state, :addition) |> new_state
  end

  defcast subtract, state: state do
    update_state(state, :subtraction) |> new_state
  end

  defcast multiply, state: state do
    update_state(state, :multiplication) |> new_state
  end

  defcast divide, state: state do
    update_state(state, :division) |> new_state
  end

  defcast equals, state: state do
    update_state(state, :noop) |> new_state
  end

  def update_state(state, new_operation) do
    entered_number = get_entered_number(state.numbers)
    new_value = get_new_value(state.value, entered_number, state.current_operation)
    new_display = "#{new_value}"
    %{state | numbers: [], display: new_display, value: new_value, current_operation: new_operation}
  end

  def get_entered_number(numbers) do
    {entered_number, _rest} = numbers |> Enum.join |> Integer.parse
    entered_number
  end

  def get_new_value(current_value, number, operation) do
    case operation do
      :noop     -> number
      :addition -> current_value + number
      :subtraction -> current_value - number
      :multiplication -> current_value * number
      :division -> current_value / number
    end
  end
end

Finally, we'll start one of these and register it when our app starts. This is not supervised, and it's not how you ought to do this, but we're just playing a bit here right now. Open up lib/jinterface_playground.ex

    {:ok, pid} = CalculatorServer.start_link
    Process.register(pid, :calc)

With existing project (in the video)

I've already started a project. I'm depending on ExActor in the project, but other than that there are no real dependencies since JInterface is in the standard release. We're going to reuse the calculator server from WxCalc, and then we'll use it from Java to show how JInterface works. First let's see the GenServer in action:

iex -S mix
iex(1)> CalculatorServer.number(:calc, 4)
:ok
iex(2)> CalculatorServer.add(:calc)
:ok
iex(3)> CalculatorServer.number(:calc, 4)
:ok
iex(4)> CalculatorServer.equals(:calc)
:ok
iex(5)> CalculatorServer.get_display(:calc)
"8"

So here we can see that we've got a globally registered process for our calculator server. This is nothing new. Now we'll start a new Java project. God help us.

mvn -B archetype:generate \
  -DarchetypeGroupId=org.apache.maven.archetypes \
  -DgroupId=com.elixirsips.jinterface \
  -DartifactId=jinterface-java

cd jinterface-java

Now we'll add jinterface to the java project via the mvn repository. Open up pom.xml:

    <dependency>
      <groupId>org.erlang.otp</groupId>
      <artifactId>jinterface</artifactId>
      <version>1.5.6</version>
    </dependency>

We'll download the dependency when we compile the project. Get it:

mvn compile

Alright, so now we have JInterface in place. We're going to build an executable JAR for this, for ease. This means we need to pull in the "Apache Maven Shade" plugin. Open up the pom again, and I'm going to paste this in because XML of this nature was never meant to be written by hand, geez.

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
  ...
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>2.3</version>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
            <configuration>
              <transformers>
                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                  <mainClass>com.elixirsips.jinterface.App</mainClass>
                </transformer>
              </transformers>
            </configuration>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

So here all we're saying is that our main entry point for the application is our App class. The maven archetype provided that class for us. Let's look at it, then see how our build process is doing.

vim src/main/java/com/elixirsips/jinterface/App.java
package com.elixirsips.jinterface;

/**
 * Hello world!
 *
 */
public class App
{
    public static void main( String[] args )
    {
        System.out.println( "Hello World!" );
    }
}

OK, so if we package and try to run the app, we should print to standard out. Let's try it:

mvn package
java -jar target/jinterface-java-1.0-SNAPSHOT.jar
# Hello World!

Alright, so we have a CLI java app ready for us to do stuff in it. That's a good start. Now the way JInterface works is to act like an Erlang node communicating on the network. So what we're going to want to do is conceptually simple:

  • Create an OtpNode with a name
  • Create a mailbox on that node
  • Construct a message to send to our calc server
  • Send the message via that mailbox

We'll start out by creating the OtpNode. This is easy enough:

package com.elixirsips.jinterface;

// I don't want to deal with imports, so we'll import everything...
import com.ericsson.otp.erlang.*;

/**
 * Hello world!
 *
 */
public class App
{
  public static void main( String[] args )
  {
    // Let's write out our pseudocode for now
    // - Create an OtpNode with a name and a cookie
    OtpNode node = new OtpNode("javaland", "test");
    // - Create a mailbox on that node
    // - Construct a message to send to our calc server
    // - Send the message via that mailbox
    System.out.println( "Hello World!" );
  }
}

That's easy enough. Turns out JInterface is pretty nice. Next, we'll create the mailbox:

    // - Create a mailbox on that node
    OtpMbox mbox = node.createMbox("javambox");

Next we need to construct a message to send. Now, here's where some relatively-arcane knowledge comes in. Let's pop open an iex session again and I'll show you what you're actually sending when you use GenServer.cast:


iex(4)> message = {:"$gen_cast", {:number, 3}}
{:"$gen_cast", {:number, 3}}
iex(5)> send(:calc, message)
{:"$gen_cast", {:number, 3}}
iex(6)> CalculatorServer.get_display(:calc)
"3"
iex(7)> send(:calc, message)
{:"$gen_cast", {:number, 3}}
iex(8)> CalculatorServer.get_display(:calc)
"33"
iex(9)> send(:calc, message)
{:"$gen_cast", {:number, 3}}
iex(10)> CalculatorServer.get_display(:calc)
"333"

So here you can see that we're sending the actual erlang message that a gen cast consists of behind the scenes. Next we're going to have our JInterface program do this instead of doing it from Elixir:

    // - Construct a message to send to our calc server
    OtpErlangObject[] msg = new OtpErlangObject[2];
    msg[0] = new OtpErlangAtom("$gen_cast");
    OtpErlangObject[] number = new OtpErlangObject[2];
    number[0] = new OtpErlangAtom("number");
    number[1] = new OtpErlangInt(3);
    msg[1] = new OtpErlangTuple(number);

This is pretty verbose, but hey, types. Anyway, this is the message we want to send. All that's left now is to send it:

    // - Send the message via that mailbox
    mbox.send("calc", "elixirnode", new OtpErlangTuple(msg));

So here we're specifying the process name we're sending to, as well as the remote node name for that process. We will have to start our elixir node with that name. Let's do that:

iex --sname elixirnode --cookie test -S mix

OK, so now we should be in business. Let's see what happens when we run our java program while the elixir program is running:

mvn package

And of course it doesn't compile, because we haven't noted that this can throw an IOException because this is a network operation. So let's add that:

package com.elixirsips.jinterface;

// I don't want to deal with imports, so we'll import everything...
import com.ericsson.otp.erlang.*;
import java.io.IOException;

/**
 * Hello world!
 *
 */
public class App
{
  public static void main( String[] args ) throws IOException
  {
    // Let's write out our pseudocode for now
    // - Create an OtpNode with a name
    OtpNode node = new OtpNode("javaland");
    // - Create a mailbox on that node
    OtpMbox mbox = node.createMbox("javambox");
    // - Construct a message to send to our calc server
    OtpErlangObject[] msg = new OtpErlangObject[2];
    msg[0] = new OtpErlangAtom("$gen_cast");
    OtpErlangObject[] number = new OtpErlangObject[2];
    number[0] = new OtpErlangAtom("number");
    number[1] = new OtpErlangInt(3);
    msg[1] = new OtpErlangTuple(number);
    // - Send the message via that mailbox
    mbox.send("calc", "elixirnode", new OtpErlangTuple(msg));
    System.out.println( "Hello World!" );
  }
}

Now we can package it up:

mvn package

And run it:

java -jar target/jinterface-java-1.0-SNAPSHOT.jar

And if we check the display from the elixir side, we can see that the cast from java came through:

CalculatorServer.get_display(:calc)

We'll do that a few more times for kicks.

Summary

In today's episode, we walked through writing a Java application that can interface with a running Elixir system. I think this is pretty great, and this was the cornerstone of the talk I gave at ElixirConf and OSCon last year where we did some robotics stuff with Elixir. I've linked to it in the show notes if you haven't seen it. See you soon!

Resources