Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#140 Support creating Java lambdas, export Idris functions as Java classes, constructors, fields and functions #170

Merged
merged 10 commits into from
Jan 31, 2024
1 change: 1 addition & 0 deletions .github/workflows/ci-super-linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,4 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
IGNORE_GENERATED_FILES: true
NATURAL_LANGUAGE_CONFIG_FILE: '.textlintrc.yml'
FILTER_REGEX_EXCLUDE: .*Test.java
2 changes: 1 addition & 1 deletion .github/workflows/install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ on:
env:
IDRIS2_TESTS_CG: jvm
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
PREVIOUS_VERSION: 0.6.0
PREVIOUS_VERSION: 0.6.0.4

jobs:
build:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pre-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
env:
IDRIS2_TESTS_CG: jvm
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
PREVIOUS_VERSION: 0.6.0
PREVIOUS_VERSION: 0.6.0.4

jobs:
pre-release:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ on:
env:
IDRIS2_TESTS_CG: jvm
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
PREVIOUS_VERSION: 0.6.0
PREVIOUS_VERSION: 0.6.0.4

jobs:
release:
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ endif
mkdir -p ${PREFIX}/lib/
install support/c/${IDRIS2_SUPPORT} ${PREFIX}/lib
mkdir -p ${PREFIX}/bin/${NAME}_app
install ${TARGETDIR}/${NAME}_app/* ${PREFIX}/bin/${NAME}_app
cp -rf ${TARGETDIR}/${NAME}_app ${PREFIX}/bin/

install-support:
mkdir -p ${PREFIX}/${NAME_VERSION}/support/docs
Expand Down
80 changes: 75 additions & 5 deletions docs/ffi/calling-java-from-idris.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,33 @@ Foreign specifier for constructors start with ``<init>`` which is the JVM name f
Similar to method calls, constructors specify argument types and return type. In this case, we invoke a zero-argument
constructor returning `java/util/ArrayList`.

Accessing fields
================

.. code-block:: idris

data Point : Type where [external]

-- Get an instance field
%foreign "jvm:#x(java/awt/Point int),java/awt/Point"
prim_getX : Point -> PrimIO Int

-- Set an instance field
%foreign "jvm:#=x(java/awt/Point int void),java/awt/Point"
prim_setX : Point -> Int -> PrimIO ()

-- Get a static field
%foreign "jvm:#MAX_VALUE(int),java/lang/Integer"
intMax : Int

-- Set a static field
%foreign "jvm:#=bar(java/lang/String void),io/github/mmhelloworld/helloworld/Main"
prim_setBar : String -> PrimIO ()


Field foreign specifiers start with ``#``. To mutate fields, foreign specifier starts with ``#=``. Instance field
accessors will have an additional parameter to pass the instance.

Inheritance
===========

Expand Down Expand Up @@ -133,8 +160,51 @@ Here is a detailed example showing the hierarchy between Java's ``Collection``,
printLn !(size {elemTy=String} list)
printLn !(toString list)

Here, we create an `ArrayList` instance and call `get` method from `List` and methods from `Collection` such as
`add` and `size`. We are able to pass `ArrayList` instance to the `List` and `Collection` functions because of
`Inherits` interface instances for `ArrayList`. Another note: In JVM, invoking methods on interface is different from
class methods invocation so the foreign specifiers on interface methods have `i:` prefix for the first parameter that
represents the instance that the methods are called on.
Here, we create an ``ArrayList`` instance and call ``get`` method from ``List`` and methods from ``Collection`` such as
``add`` and ``size``. We are able to pass ``ArrayList`` instance to the ``List`` and ``Collection`` functions because of
``Inherits`` interface instances for ``ArrayList``. Another note: In JVM, invoking methods on interface is different
from class methods invocation so the foreign specifiers on interface methods have ``i:`` prefix for the first parameter
that represents the instance that the methods are called on.

Class literals
================

``classLiteral`` function can be used to get Java's ``Class`` instances for JVM types.

.. code-block:: console

Main> :module Java.Lang
Imported module Java.Lang
Main> :t classLiteral
Java.Lang.classLiteral : Class ty

.. code-block:: idris

import Java.Lang

main : IO ()
main = do
printLn !(Object.toString $ classLiteral {ty=Int})
printLn !(Object.toString $ classLiteral {ty=Integer})
printLn !(Object.toString $ classLiteral {ty=String})

The above example prints:

.. code-block:: console

"int"
"class java.math.BigInteger"
"class java.lang.String"

JVM reference equality
======================
Reference equality should be avoided in Idris but it might be useful to interface with Java, for example, for overriding ``equals`` method in Idris.

.. code-block:: console

Main> :module Java.Lang
Imported module Java.Lang
Main> :exec printLn (jvmRefEq "foo" "foo")
True
Main> :exec printLn (jvmRefEq "foo" ("fo" ++ "o"))
False
3 changes: 2 additions & 1 deletion idris-jvm-assembler/pom.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>io.github.mmhelloworld</groupId>
<artifactId>idris-jvm</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,16 @@
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
Expand All @@ -29,7 +28,6 @@
import static java.nio.file.Files.newInputStream;
import static java.nio.file.Files.newOutputStream;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.synchronizedSet;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.stream.Collectors.toList;
Expand All @@ -52,18 +50,17 @@ public final class AsmGlobalState {
private final Set<String> untypedFunctions;
private final Set<String> constructors;
private final String programName;
private final Map<String, Object> fcAndDefinitionsByName;
private final Map<String, Assembler> assemblers;

public AsmGlobalState(String programName, Collection<String> trampolinePatterns) {
public <T> AsmGlobalState(String programName,
Map<String, Object> fcAndDefinitionsByName) {
this.programName = programName;
functions = new ConcurrentHashMap<>();
untypedFunctions = synchronizedSet(new HashSet<>());
constructors = synchronizedSet(new HashSet<>());
assemblers = new ConcurrentHashMap<>();
}

public AsmGlobalState(String programName) {
this(programName, emptyList());
this.fcAndDefinitionsByName = fcAndDefinitionsByName;
}

public static void copyRuntimeJar(String directory) throws IOException {
Expand All @@ -80,6 +77,31 @@ public static void copyRuntimeJar(String directory) throws IOException {
}
}

private static List<String> getJavaOptions() {
String javaOpts = getProperty("JAVA_OPTS", "-Xss8m");
return asList(javaOpts.split("\\s+"));
}

private static String getProperty(String propertyName, String defaultValue) {
String value = System.getProperty(propertyName, System.getenv(propertyName));
return value == null ? defaultValue : value;
}

private static void copy(InputStream inputStream, OutputStream outputStream) throws IOException {
byte[] buffer = new byte[BUFFER_SIZE];
while (inputStream.read(buffer) > 0) {
outputStream.write(buffer);
}
outputStream.flush();
}

private static String getRuntimeJarName() throws IOException {
ClassLoader classLoader = currentThread().getContextClassLoader();
Properties properties = new Properties();
properties.load(classLoader.getResourceAsStream("project.properties"));
return format("idris-jvm-runtime-%s.jar", properties.getProperty("project.version"));
}

public synchronized void addFunction(String name, Object value) {
functions.put(name, value);
}
Expand Down Expand Up @@ -128,7 +150,6 @@ public void classCodeEnd(String outputDirectory, String outputFile, String mainC
} else {
new File(classDirectory).mkdirs();
copyRuntimeJar(classDirectory);
Assembler.createJar(classDirectory, outputFile, mainClassNoSlash);
Assembler.createExecutable(normalizedOutputDirectory, outputFile, mainClassNoSlash);
}
}
Expand All @@ -150,41 +171,21 @@ public void interpret(String mainClass, String outputDirectory) throws IOExcepti
public void writeClass(String className, ClassWriter classWriter, String outputClassFileDir) {
File outFile = new File(outputClassFileDir, className + ".class");
new File(outFile.getParent()).mkdirs();
try (OutputStream out = new FileOutputStream(outFile)) {
try (OutputStream out = newOutputStream(outFile.toPath())) {
out.write(classWriter.toByteArray());
} catch (Exception exception) {
exception.printStackTrace();
}
}

private static List<String> getJavaOptions() {
String javaOpts = getProperty("JAVA_OPTS", "-Xss8m");
return asList(javaOpts.split("\\s+"));
}

private static String getProperty(String propertyName, String defaultValue) {
String value = System.getProperty(propertyName, System.getenv(propertyName));
return value == null ? defaultValue : value;
}

private static void copy(InputStream inputStream, OutputStream outputStream) throws IOException {
byte[] buffer = new byte[BUFFER_SIZE];
while (inputStream.read(buffer) > 0) {
outputStream.write(buffer);
}
outputStream.flush();
}

private static String getRuntimeJarName() throws IOException {
ClassLoader classLoader = currentThread().getContextClassLoader();
Properties properties = new Properties();
properties.load(classLoader.getResourceAsStream("project.properties"));
return format("idris-jvm-runtime-%s.jar", properties.getProperty("project.version"));
}

private Stream<Entry<String, ClassWriter>> getClassNameAndClassWriters() {
return assemblers.values().parallelStream()
.map(Assembler::classInitEnd)
.flatMap(assembler -> assembler.getClassWriters().entrySet().stream());
}

public Object getFcAndDefinition(String name) {
return Optional.ofNullable(fcAndDefinitionsByName.get(name))
.orElseThrow(() -> new IdrisJvmException("Unable to find function " + name));
}
}
Loading
Loading