diff --git a/build.sbt b/build.sbt index 980a8892c2d8..7b34b47b24bf 100644 --- a/build.sbt +++ b/build.sbt @@ -2130,6 +2130,8 @@ lazy val `language-server` = (project in file("engine/language-server")) (`language-server-deps-wrapper` / Compile / exportedModule).value, (`fansi-wrapper` / Compile / exportedModule).value, (`text-buffer` / Compile / exportedModule).value, + (`jvm-channel` / Compile / exportedModule).value, + (`jvm-interop` / Compile / exportedModule).value, (`runtime-suggestions` / Compile / exportedModule).value, (`runtime-parser` / Compile / exportedModule).value, (`runtime-compiler` / Compile / exportedModule).value, @@ -2414,10 +2416,13 @@ lazy val `runtime-language-epb` = "org.graalvm.sdk" % "nativeimage" % graalMavenPackagesVersion ), Compile / internalModuleDependencies := Seq( + (`jvm-channel` / Compile / exportedModule).value, + (`jvm-interop` / Compile / exportedModule).value, (`ydoc-polyfill` / Compile / exportedModule).value, (`runtime-utils` / Compile / exportedModule).value ) ) + .dependsOn(`jvm-interop` % Test) lazy val `runtime-language-arrow` = (project in file("engine/runtime-language-arrow")) @@ -2696,6 +2701,8 @@ lazy val `runtime-integration-tests` = (`connected-lock-manager` / Compile / exportedModule).value, (`library-manager` / Compile / exportedModule).value, (`persistance` / Compile / exportedModule).value, + (`jvm-channel` / Compile / exportedModule).value, + (`jvm-interop` / Compile / exportedModule).value, (`interpreter-dsl` / Compile / exportedModule).value, (`engine-common` / Compile / exportedModule).value, (`edition-updater` / Compile / exportedModule).value, @@ -2882,6 +2889,8 @@ lazy val `runtime-benchmarks` = (`library-manager` / Compile / exportedModule).value, (`persistance` / Compile / exportedModule).value, (`interpreter-dsl` / Compile / exportedModule).value, + (`jvm-channel` / Compile / exportedModule).value, + (`jvm-interop` / Compile / exportedModule).value, (`engine-common` / Compile / exportedModule).value, (`edition-updater` / Compile / exportedModule).value, (`editions` / Compile / exportedModule).value, @@ -3990,22 +3999,19 @@ lazy val `jvm-interop` = (Test / fork) := true, commands += WithDebugCommand.withDebug, libraryDependencies ++= Seq( - "org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion % "provided", - "org.graalvm.truffle" % "truffle-dsl-processor" % graalMavenPackagesVersion % "provided", - "org.netbeans.api" % "org-openide-util-lookup" % netbeansApiVersion % "provided", - "org.graalvm.sdk" % "graal-sdk" % graalMavenPackagesVersion % Test, - "junit" % "junit" % junitVersion % Test, - "com.github.sbt" % "junit-interface" % junitIfVersion % Test + "org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion % "provided", + "org.graalvm.truffle" % "truffle-dsl-processor" % graalMavenPackagesVersion % "provided", + "org.graalvm.sdk" % "graal-sdk" % graalMavenPackagesVersion % Test, + "junit" % "junit" % junitVersion % Test, + "com.github.sbt" % "junit-interface" % junitIfVersion % Test ), Compile / moduleDependencies ++= Seq( - "org.netbeans.api" % "org-openide-util-lookup" % netbeansApiVersion, - "org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion, - "org.graalvm.sdk" % "nativeimage" % graalMavenPackagesVersion, - "org.graalvm.polyglot" % "polyglot" % graalMavenPackagesVersion, - "org.graalvm.sdk" % "word" % graalMavenPackagesVersion + "org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion, + "org.graalvm.sdk" % "nativeimage" % graalMavenPackagesVersion, + "org.graalvm.polyglot" % "polyglot" % graalMavenPackagesVersion, + "org.graalvm.sdk" % "word" % graalMavenPackagesVersion ), Compile / internalModuleDependencies ++= Seq( - (`engine-common` / Compile / exportedModule).value, (`jvm-channel` / Compile / exportedModule).value, (`persistance` / Compile / exportedModule).value ) diff --git a/docs/polyglot/README.md b/docs/polyglot/README.md index eb73117571fe..abde59e9a9d3 100644 --- a/docs/polyglot/README.md +++ b/docs/polyglot/README.md @@ -8,11 +8,40 @@ order: 0 # Enso Polyglot Support -Enso supports robust polyglot interoperation with other programming languages -that are supported on its platform. This section of the design documentation -deals with +Enso supports robust interoperability with other programming languages - e.g. +Enso is a _polyglot programming_ languge. -It is broken down into the following sections: +### interoperability with Java + +Unlike many other programming languages the _system language_ of Enso (e.g. the +language that is used to do low-level operating system tasks) is **Java**. As +such a lot of attention has been dedicated to make interoperability with +**Java** as smooth as possible: + +- [**Java:**](./java.md) Detailed info about the Java polyglot bindings. + +Many `Standard` libraries are using these `polyglot java import` statements. +Custom projects and libraries are encouraged to do the same. Interoperability +with Java is a first class citizen in the Enso programming language. + +### Interoperability with Python, JavaScript & co. + +Enso greatly benefits from the +[polyglot ecosystem of GraalVM](http://graalvm.org) and easily interops with any +language from that ecosystem. Including **JavaScript**, **Python**, **R**, etc. + +- [**Python:**](./python.md) Specifics of the Python polyglot bindings. + +Interop with these _dynamic languages_ is primarily supported via +[foreign function definitions](./polyglot-bindings.md#foreign-functions). When +accessing Python, as well as other dynamic languages, the same pattern is used. +Enough include the language support in the +[distribution](../distribution/distribution.md) and the language gets +automatically exposed via `foreign` directive to Enso programs. + +## Implementation + +Additional overview is provided in following documents: - [**Polyglot Bindings:**](./polyglot-bindings.md) A document providing an overview of the mechanisms provided to work with polyglot bindings in Enso. @@ -20,9 +49,5 @@ It is broken down into the following sections: of how we can provide a modicum of type safety for the polyglot bindings in Enso. -It also provides language-specific documentation for the various supported -polyglot languages. These are as follows: - -- [**Java:**](./java.md) Information specific to the Java polyglot bindings. -- [**Python:**](./python.md) Information specific to the Python polyglot - bindings. +Implementation details are described in +[EpbLanguage javadoc](../../engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/EpbLanguage.java). diff --git a/docs/polyglot/polyglot-bindings.md b/docs/polyglot/polyglot-bindings.md index f35896a364d7..400982350121 100644 --- a/docs/polyglot/polyglot-bindings.md +++ b/docs/polyglot/polyglot-bindings.md @@ -8,98 +8,19 @@ order: 1 # Polyglot Bindings -This document deals with the specification and design for the polyglot interop -system provided in the Enso runtime. This system allows users to connect Enso to -other supported programming languages, to both provide access to a wealth of -libraries, and to integrate Enso into existing systems. - -The polyglot support in Enso is best-in class, and it supports this through two -main mechanisms: - -1. **Polyglot FFI:** The low-level polyglot support provides a fairly low-level - syntax sugar system for working with values from foreign languages. -2. **Embedded Syntax:** This system allows users to write code from other - languages directly in their `.enso` files, and to seamlessly share values - between Enso and that foreign code. - - - -- [Impedance Mismatch](#impedance-mismatch) -- [The Polyglot FFI](#the-polyglot-ffi) - - [Importing Polyglot Bindings](#importing-polyglot-bindings) - - [Using Polyglot Bindings](#using-polyglot-bindings) - - [Importing Polyglot Bindings \(Syntax\)](#importing-polyglot-bindings-syntax) - - [Using Polyglot Bindings \(Syntax\)](#using-polyglot-bindings-syntax) - - [Finding Polyglot Bindings](#finding-polyglot-bindings) -- [Embedded Syntax](#embedded-syntax) - - [Embedded Syntax Usage \(Syntax\)](#embedded-syntax-usage-syntax) - - +Enso provides [robust interoperability](./README.md) with other programming +languages. This document describes how users can connect Enso to other supported +programming languages to gain access to a wealth of libraries, as well as to +integrate Enso into existing systems. -## Impedance Mismatch - -Polyglot interoperation in Enso has a significant impedance mismatch. In -essence, this means that there is a mismatch between Enso's language semantics -and the semantics of the foreign languages that are being worked with. - -While some of this mismatch can be worked around by manually wrapping the -foreign constructs in Enso, there are still concepts that can't easily be -represented by Enso. - -> The actionables for this section are: -> -> - Expand on the impedance mismatch and how it leads to the defined semantics. - -## The Polyglot FFI - -The low-level polyglot FFI mechanism refers to a way to use polyglot objects -directly in Enso code. This can be used to underlie a library implementaion in -Enso, or to interoperate with code running in other languages. - -The mechanism provides users with the facilities to import bindings from other -languages and call them via a generic mechanism. - -### Importing Polyglot Bindings - -When importing a polyglot binding into scope in an Enso file, this introduces a -_polyglot object_ into scope. This object will have appropriate fields and/or -methods defined on it, as described by the foreign language implementation. - -> The actionables for this section are: -> -> - Expand greatly on the detail of this as the semantics of the imports become -> clearer. - -### Using Polyglot Bindings - -With a polyglot object in scope, the user is free to call methods on it -directly. These polyglot objects are inherently dynamically typed, meaning that -any operation may _fail_ at runtime. - -Enso implements a generic variadic syntax for calling polyglot functions using -vectors of arguments. In essence, this is necessary due to the significant -impedance mismatch between Enso's runtime semantics (let alone the type system) -and the runtime semantics of many of the polyglot languages. - -We went the way of the variadic call for multiple reasons: - -- It allows us to match up with a wide range of language semantics (such as - subtyping and overloading). -- It is flexible and easy to expand in the future. -- We can easily build a more Enso-feeling interface on top of it. - -By way of illustrative example, Java supports method overloading and subtyping, -two things which have no real equivalent in the Enso type system. +## `polyglot import` -> The actionables for this section are: -> -> - Expand greatly on the runtime semantics of working with polyglot bindings. -> - Determine how to make the inherent 'failability' of polyglot objects safer. +Accessing existing objects of foreign languages can be done via +`polyglot xyz import` statements. This primarily works for **Java** classes: -### Importing Polyglot Bindings (Syntax) +- [**Java:**](./java.md) Detailed info about the Java polyglot bindings. -Polyglot bindings can be imported using a polyglot import directive. This is -constructed as follows: +The _polyglot import directive_ is constructed as follows: - The `polyglot` keyword - A language identifier (e.g. `java`). @@ -113,39 +34,41 @@ For example: ```ruby polyglot java import org.example.MyClass as MyClassJava -polyglot c import struct NetworkPacket as NetworkPacketC +polyglot c import struct NetworkPacket ``` -### Using Polyglot Bindings (Syntax) - -A polyglot binding is a polyglot object that has methods and/or fields defined -on it. Due to an impedance mismatch between languages, Enso implements a -variadic syntax for calling these polyglot bindings using vectors. - -In essence, we have a primitive function as follows: +Once imported the `MyClassJava` as well as `NetworkPacket` objects behave as +`Any` Enso objects. Such objects have methods and/or fields defined on them. The +following is a valid usage of a polyglot binding: ```ruby -Polyglot.method : Polyglot.Object -> [Any] -> Any +main = + x = MyClassJava.foo [1, 2, 3] # a static method + inst = MyClassJava.new [a, b, c] # a constructor + bar = inst.method [x, y] # an instance method ``` -It works as follows: +### Using Polyglot Bindings -- It is a method called `method` defined on the `Polyglot` type. The name - `method` is, however, a stand-in for the name of the method in question. -- It takes an object instance of the polyglot object. -- It takes a vector of arguments (and is hence variadic). -- And it returns some value. +With a polyglot object in scope, the user is free to call methods on it +directly. These polyglot objects are inherently dynamically typed, meaning that +they have `Any` type. As such there is no _static type checking_ when invoking +methods on such types and potential errors are only detected during runtime and +result in a runtime _failure_ (a typical behavior of Python or JavaScript +programs). -By way of example, the following is a valid usage of a polyglot binding: +Enso implements a generic variadic syntax for calling polyglot functions using +vectors of arguments. In essence, this is necessary due to the significant +impedance mismatch between Enso's runtime semantics and the runtime semantics of +many of the polyglot languages. Such a solution: -```ruby -polyglot java import com.example.MyClass as MyClassJava +- allows Enso to match up with a wide range of language semantics + - for example Java's subtyping and overloading +- it is flexible and easy to expand in the future. +- allows building a more Enso-feeling interface on top of it. -main = - x = MyClassJava.foo [1, 2, 3] # a static method - inst = MyClassJava.new [a, b, c] # a constructor - bar = inst.metod [x, y] # a static method -``` +Thanks to the generic variadic syntax, it is possible to smoothly invoke Java +overloaded and overriden methods. ### Finding Polyglot Bindings @@ -158,32 +81,49 @@ Inside each directory is an implementation-defined structure, with the polyglot implementation for that particular language needing to specify it. Please see the language-specific documentation for details. -## Embedded Syntax +## `foreign` functions + +It is possible to define new code snippets of foreign languages directly in +`.enso` source files using _"Embedded Syntax"_. Such a handy support provides a +truly smooth user experience: -The term "Embedded Syntax" is our terminology for the ability to use foreign -language syntaxes directly from inside `.enso` files. This system builds upon -the more generic mechanisms used by the [polyglot FFI](#the-polyglot-ffi) to -provide a truly seamless user experience. +```ruby +foreign python concat x y = """ + def join(a, b): + return str(a) + str(b) + return join(x, y) -### Embedded Syntax Usage (Syntax) +main u="Hello" s=" " w="World!" = + concat (concat u s) w +``` -A polyglot block is introduced as follows: +The previous example defines an Enso function `concat` that takes two arguments +`a` and `b`. The function is implemented in Python. The Python code defines a +local function `join` and uses it to compute and return result of `concat`. Then +the `concat` function is invoked from a `main` Enso function to concatenate +typical _Hello World!_ message. -- The `foreign` keyword starts a block. -- This must be followed by a language identifier (e.g. `python`). -- After the language identifier, the remaining syntax behaves like it is an Enso - function definition until the `=`. -- After the `=`, the user may write their foreign code as a string. +- [**Python:**](./python.md) Details on Python polyglot bindings. -```ruby -foreign python concat a b = """ - def concat(a, b): - str(a) + str(b) -``` +Similar syntax can be used for `js` and other dynamic languages. Certain +languages require/have special support, but in general this mechanism is reusing +polyglot capabilities of GraalVM Truffle framework and works with any language +that implements its `InteropLibrary` and _"parse in a context"_ protocols. -In the above example, this defines a function `concat` that takes two arguments -`a` and `b`, implemented in Python. +## Impedance Mismatch -> The actionables for this section are: -> -> - Greatly flesh out the syntax for the high-level polyglot functionality. +Enso is designed as a functional programming language and as such it assumes +_mininal side effects_ when performing operation. Especially the _live +programming_ environment provided by the Enso visual editor relies on operations +being idempotent and having no side effects. Enso semantic enforces such _no +side effects_ behavior for programs written in Enso. + +This is not a typical behavior of other programming languages and certainly it +is not enforced in languages like JavaScript, Python or Java. Polyglot +interoperation in Enso has a significant impedance mismatch. In essence, this +means that there is a mismatch between Enso's language semantics and the +semantics of the foreign languages that are being worked with. + +Some of thes mismatches can be worked around by manually wrapping the foreign +constructs in Enso, however some just cannot. Care must be taken when dealing +with other languages and especially their side-effects. diff --git a/docs/polyglot/typing-polyglot-bindings.md b/docs/polyglot/typing-polyglot-bindings.md index 38e1b1fe8642..a41163b4fdea 100644 --- a/docs/polyglot/typing-polyglot-bindings.md +++ b/docs/polyglot/typing-polyglot-bindings.md @@ -8,96 +8,43 @@ order: 2 # Typing the Polyglot Bindings -The polyglot bindings inherently provide a problem for the Enso type system. -When many of the languages with which we can interoperate are highly dynamic and -flexible, or have significant mismatches between their type system and Enso's, -we can only make a best effort attempt to maintain type safety across this -boundary. - - - -- [Enso Values](#enso-values) -- [Polyglot Values](#polyglot-values) -- [Dynamic](#dynamic) - - [The Enso Boundary](#the-enso-boundary) - - +The polyglot bindings inherently provide a problem for the Enso static type +system. Many of the languages with which we can interoperate are highly dynamic +and flexible, or have significant mismatches between their type system and Enso. +The Enso static analysis just gives up when dealing with such +[polyglot bindings](./polyglot-bindings.md). ## Enso Values -The underlying nature of our runtime allows us to pass Enso values across the -polyglot boundary while ensuring that they aren't modified. This means that the -typing information known about a value `v` _before_ it is passed to a polyglot -call is valid after the polyglot call, as long as the following properties hold: - -- The polyglot call does not modify the entity passed in. -- The polyglot call returns an entity of the same type. - -However, there are sometimes cases where we _want_ to let an Enso value be used -freely by the polyglot language. To that end, we have to have some way of -distinguishing _safe_ usages of Enso values from _unsafe_ ones. In the latter -case, the value needs to be treated as `Dynamic` after its use. - -> The actionables for this section are: -> -> - Think much more on this. +Enso values are immutable which allows us to pass Enso values across the +polyglot boundary while ensuring that they aren't modified by foreign languages. +This means that the typing information known about a value `v` _before_ it is +passed to a polyglot call is valid after the polyglot call, ## Polyglot Values In the presence of a polyglot value, however, there is very little that we can determine about a value with which we are working. This means that we need to have a principled way to assert properties on a polyglot object that can then be -reflected in the Enso type system. This mechanism needs to deal with: - -- Concurrent access to polyglot objects. -- Mutation and modification of polyglot objects. -- Potentially taking _ownership_ of polyglot objects. - -> The actionables for this section are: -> -> - Reflect more on this problem and think about what principled approaches we -> could take to it. - -## Dynamic - -As Enso can seamlessly interoperate with other programming languages, we need a -principled way of handling dynamic types that we don't really know anything -about. This mechanism needs: +reflected in the Enso type system. An object coming from foreign language can +mutate and change its state anytime. -- A way to record what properties we _expect_ from the dynamic. -- A way to turn a dynamic into a well-principled type-system member without - having the dynamics pollute the whole type system. This may involve a 'trust - me' function, and potentially dynamicness-polymorphic types. -- A way to understand as much as possible about what a dynamic _does_ provide. -- A way to try and refine information about dynamics where possible. +### Array like Structures -```ruby -obj.model = - { atom : Text - , dict : Map Text Any - , info : - { doc : Text - , name : Text - , code : Text - , loc : Location - } - , arg : # used only when calling like a function - { doc : Text - , default : Maybe Any - } - } -``` +GraalVM `InteropLibrary` offers a protocol for recognizing certain types of +objects. For example one can find out whether an object `hasArrayElements`. Such +objects are then recognized as _array like structures_. -> The actionables for this section are: -> -> - Work out how to do dynamic properly, keeping in mind that in a dynamic value -> could self-modify underneath us. +Enso distinguishes between its own _array like structures_ and foreign ones. +While Enso `Vector` is immutable, _array like foreign objects_ may potentially +mutate. To address that Enso offers two types: -### The Enso Boundary +- `Vector` - guaranteed (by those who create it) to be immutable +- `Array` - an _array like structure_ which may potentially change +- both these types follow the same interface and offer the same methods -Fortunately, we can at least avoid foreign languages modifying memory owned by -the Enso interpreter. As part of the interop library, Graal lets us mark memory -as read-only. This means that the majority of data passed out (from a functional -language like Enso) is not at risk. However, if we _do_ allow data to be worked -with mutably, when control is returned to Enso it needs to be treated as a -dynamic as it may have been modified. +There is no way in Enso to modify objects of type `Array` (neither `Vector`). A +**JavaScript** or **Python** allocated `Array` can mutate during execution of +the program. Should one need to guarantee immutability, one can convert `Array` +to `Vector` with `array_like_foreign_object . to Vector` conversion. Such a +conversion creates read-only snapshot of the original _array like object_. diff --git a/engine/common/src/main/java/module-info.java b/engine/common/src/main/java/module-info.java index 1948bd645f37..d8a8258e258b 100644 --- a/engine/common/src/main/java/module-info.java +++ b/engine/common/src/main/java/module-info.java @@ -1,5 +1,4 @@ import org.enso.common.ContextLoggingConfigurator; -import org.enso.common.PolyglotSymbolResolver; module org.enso.engine.common { requires org.graalvm.nativeimage; @@ -7,6 +6,5 @@ exports org.enso.common; - uses PolyglotSymbolResolver; uses ContextLoggingConfigurator; } diff --git a/engine/common/src/main/java/org/enso/common/PolyglotSymbolResolver.java b/engine/common/src/main/java/org/enso/common/PolyglotSymbolResolver.java deleted file mode 100644 index a5430d51bfe5..000000000000 --- a/engine/common/src/main/java/org/enso/common/PolyglotSymbolResolver.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.enso.common; - -import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.ServiceLoader; - -/** - * Generic support for loading Java polyglot symbols. The resolver provides two kinds of interfaces: - * - * - * - * Those who tend to extend the capabilities of loading Java classes into Enso runtime shall - * register their own implementation visible via {@link ServiceLoader}. - * - * @see RuntimeOptions#HOST_CLASS_LOADING - */ -public abstract class PolyglotSymbolResolver { - private static final Collection ALL; - - static { - var arr = new ArrayList(); - for (var l : ServiceLoader.load(PolyglotSymbolResolver.class)) { - arr.add(l); - } - ALL = Collections.unmodifiableList(arr); - } - - /** - * Search all providers for given name. - * - * @param name dot separated name to search for - * @return non-null object representing the name - * @throws java.lang.ClassNotFoundException if no name was found - */ - public static Object loadClass(String name) throws ClassNotFoundException { - ClassNotFoundException ex = null; - for (var p : ALL) { - try { - var found = p.handleLoadClass(name); - assert found != null; - return found; - } catch (ClassNotFoundException cnfe) { - ex = cnfe; - } - } - if (ex == null) { - throw new ClassNotFoundException(name); - } else { - throw ex; - } - } - - public static void addToClassPath(URL url) { - for (var p : ALL) { - p.handleAddToClassPath(url); - } - } - - /** - * Subclasses implement this method to search for class with the provided name. - * - * @param name dot separated name to search for - * @return non-{@code null} object representing the name - * @throws java.lang.ClassNotFoundException if no name was found - */ - protected abstract Object handleLoadClass(String name) throws ClassNotFoundException; - - protected abstract void handleAddToClassPath(URL url); -} diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/example/TestException.java b/engine/runtime-integration-tests/src/test/java/org/enso/example/TestException.java new file mode 100644 index 000000000000..e3b220e2e1af --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/example/TestException.java @@ -0,0 +1,15 @@ +package org.enso.example; + +public class TestException extends Exception { + public TestException() {} + + public static void throwMe() throws Exception { + throw new TestException(); + } + + public static void throwSubtype() throws Exception { + throw new SubException(); + } + + private static final class SubException extends TestException {} +} diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/example/ToString.java b/engine/runtime-integration-tests/src/test/java/org/enso/example/ToString.java index 7655e475ada4..eaa409d4d0b7 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/example/ToString.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/example/ToString.java @@ -1,5 +1,7 @@ package org.enso.example; +import org.graalvm.polyglot.PolyglotException; + public class ToString { private ToString() {} @@ -9,8 +11,12 @@ public static interface Fooable { } public static String callFoo(Fooable f) { - long x = f.foo(); - return "Fooable.foo() = " + x; + try { + long x = f.foo(); + return "Fooable.foo() = " + x; + } catch (Throwable t) { + throw t; + } } public static String showObject(Object obj) { @@ -18,7 +24,12 @@ public static String showObject(Object obj) { } public static String callFooAndShow(Fooable f) { - long x = f.foo(); - return "{" + f.toString() + "}.foo() = " + x; + var x = f.foo(); + try { + var s = f.toString(); + return "{" + s + "}.foo() = " + x; + } catch (PolyglotException ex) { + return "Ex: " + ex.getMessage() + ", but foo() = " + x; + } } } diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/GuestJavaInteropTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/GuestJavaInteropTest.java new file mode 100644 index 000000000000..0150f9cc9faf --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/GuestJavaInteropTest.java @@ -0,0 +1,17 @@ +package org.enso.interpreter.test.interop; + +import org.enso.test.utils.ContextUtils; +import org.junit.ClassRule; + +public final class GuestJavaInteropTest extends JavaInteropTest { + @ClassRule + public static final ContextUtils ctxRule = + ContextUtils.newBuilder() + .withModifiedContext((b) -> b.option("enso.classLoading", "guest")) + .build(); + + @Override + protected final ContextUtils ctx() { + return ctxRule; + } +} diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/HostJavaInteropTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/HostJavaInteropTest.java new file mode 100644 index 000000000000..93f8cf734f27 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/HostJavaInteropTest.java @@ -0,0 +1,13 @@ +package org.enso.interpreter.test.interop; + +import org.enso.test.utils.ContextUtils; +import org.junit.ClassRule; + +public final class HostJavaInteropTest extends JavaInteropTest { + @ClassRule public static final ContextUtils ctxRule = ContextUtils.newBuilder().build(); + + @Override + protected final ContextUtils ctx() { + return ctxRule; + } +} diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/JavaInteropTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/JavaInteropTest.java index 95c3d4e8ed92..49ca84e219a6 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/JavaInteropTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/JavaInteropTest.java @@ -12,24 +12,46 @@ import org.graalvm.polyglot.PolyglotException; import org.graalvm.polyglot.Value; import org.junit.After; -import org.junit.ClassRule; import org.junit.Test; -public class JavaInteropTest { - - @ClassRule public static final ContextUtils ctxRule = ContextUtils.createDefault(); +/** + * Tests {@code polyglot java import} behavior in isolation. When there is a problem with + * interactions with the Java classes, it is best to expand this test. It is easier to debug the + * problem then having whole integration tests and moreover this suite executes the same test in + * various configurations automatically. + * + *

The test itself is abstract class and just defines the test cases. Then there are various + * implementations: + * + *

+ * + * Those implementations setup the {@link #ctx()} and execute the test in that setup. This way we + * can guarantee consistency between various implementations of the {@code polyglot java import} + * statements. + * + *

Execute all these tests as: + * + *

+ * sbt:enso> runtime-integration-tests/testOnly *JavaInteropTest
+ * 
+ */ +public abstract class JavaInteropTest { @After public void resetOutput() { - ctxRule.resetOut(); + ctx().resetOut(); } private String[] getStdOutLines() { - return ctxRule.getOut().trim().split(System.lineSeparator()); + return ctx().getOut().trim().split(System.lineSeparator()); } private void checkPrint(String code, List expected) { - Value result = ctxRule.evalModule(code); + Value result = ctx().evalModule(code); assertTrue("should return Nothing", result.isNull()); assertArrayEquals(expected.toArray(), getStdOutLines()); } @@ -41,7 +63,7 @@ public void testClassImport() { polyglot java import org.enso.example.TestClass main = TestClass.add 1 2 """; - var result = ctxRule.evalModule(code); + var result = ctx().evalModule(code); assertEquals(3, result.asInt()); } @@ -55,7 +77,7 @@ public void testClassImportAndMethodCall() { instance = TestClass.new (x -> x * 2) instance.callFunctionAndIncrement 10 """; - var result = ctxRule.evalModule(code); + var result = ctx().evalModule(code); assertEquals(21, result.asInt()); } @@ -69,7 +91,7 @@ public void testImportStaticInnerClass() { instance = StaticInnerClass.new "my_data" instance.add 1 2 """; - var result = ctxRule.evalModule(code); + var result = ctx().evalModule(code); assertEquals(3, result.asInt()); } @@ -122,7 +144,7 @@ public void testCaseOnFunctionalInterface() { main = check """; - var check = ctxRule.evalModule(code); + var check = ctx().evalModule(code); assertEquals("'no'", check.execute("Not FnIntrfc").toString()); @@ -141,7 +163,7 @@ public void testCaseOnFunctionalInterface() { main = My_Type.Value 1 """; - var atom = ctxRule.evalModule(atomCode); + var atom = ctx().evalModule(atomCode); assertEquals( "atom is not Java interface at all " + "and it shouldn't pass the call:FnIntrfc check", "'no'", @@ -215,7 +237,7 @@ public void testImportOuterClassAndReferenceInner() { instance = TestClass.StaticInnerClass.new "my_data" instance.getData """; - var result = ctxRule.evalModule(code); + var result = ctx().evalModule(code); assertEquals("my_data", result.asString()); } @@ -246,7 +268,7 @@ public void testImportNestedInnerClass() { inner_inner_value = StaticInnerInnerClass.new inner_inner_value.mul 3 5 """; - var res = ctxRule.evalModule(code); + var res = ctx().evalModule(code); assertEquals(15, res.asInt()); } @@ -257,7 +279,7 @@ public void testImportNonExistingInnerClass() { polyglot java import org.enso.example.TestClass.StaticInnerClass.Non_Existing_Class """; try { - ctxRule.evalModule(code); + ctx().evalModule(code); fail("Should throw exception"); } catch (Exception ignored) { } @@ -270,7 +292,7 @@ public void testImportNonExistingInnerNestedClass() { polyglot java import org.enso.example.TestClass.Non_Existing_Class.Another_Non_ExistingClass """; try { - ctxRule.evalModule(code); + ctx().evalModule(code); fail("Should throw exception"); } catch (Exception ignored) { } @@ -286,7 +308,7 @@ public void testImportOuterClassAndAccessNestedInnerClass() { instance = TestClass.StaticInnerClass.StaticInnerInnerClass.new instance.mul 3 5 """; - var res = ctxRule.evalModule(code); + var res = ctx().evalModule(code); assertEquals(15, res.asInt()); } @@ -314,7 +336,7 @@ public void testToStringBehavior() { [a, b, c, d, e] """; - var res = ctxRule.evalModule(code); + var res = ctx().evalModule(code); assertTrue("It is an array", res.hasArrayElements()); assertEquals("Array with five elements", 5, res.getArraySize()); assertEquals(123, res.getArrayElement(0).asInt()); @@ -324,6 +346,85 @@ public void testToStringBehavior() { assertEquals("{(Instance 23)}.foo() = 123", res.getArrayElement(4).asString()); } + @Test + public void testToStringBehaviorSimple1() { + var code = + """ + from Standard.Base import all + + polyglot java import org.enso.example.ToString as Foo + + type My_Fooable_Implementation + Instance x + + foo : Integer + foo self = 100+self.x + + main = + fooable = My_Fooable_Implementation.Instance 23 + e = Foo.callFooAndShow fooable + e + """; + + var res = ctx().evalModule(code); + assertEquals("{(Instance 23)}.foo() = 123", res.asString()); + } + + @Test + public void throwsParsingError() { + var code = + """ + from Standard.Base import Panic + polyglot java import java.lang.Integer as Num + polyglot java import java.lang.NumberFormatException as Ex + + main = + Panic.catch Ex (Num.parseInt "NotAnInt") .payload + """; + + var res = ctx().evalModule(code); + assertTrue("Got an exception back", res.isException()); + var typeEx = res.getMetaObject(); + assertEquals("java.lang.NumberFormatException", typeEx.getMetaQualifiedName()); + try { + throw res.throwException(); + } catch (PolyglotException ex) { + assertEquals("For input string: \"NotAnInt\"", ex.getMessage()); + } + } + + @Test + public void throwsParsingErrorIndirect() { + var code = + """ + from Standard.Base import Panic + polyglot java import java.lang.Integer as Num + polyglot java import java.lang.NumberFormatException as Ex + polyglot java import org.enso.example.TestClass + + type En + Err msg + + main = + e = TestClass.newDirectExecutor + e.execute + Panic.catch Ex (Num.parseInt "NotAnInt") ex-> + Panic.throw (En.Err ex.payload.to_text) + """; + + try { + var res = ctx().evalModule(code); + fail("Expecting an exception: " + res); + } catch (PolyglotException ex) { + var exObj = ex.getGuestObject(); + var typeEx = exObj.getMetaObject(); + assertEquals("Standard.Base.Panic.Panic", typeEx.getMetaQualifiedName()); + assertEquals( + "java.lang.NumberFormatException: For input string: \"NotAnInt\"", + exObj.getMember("msg").asString()); + } + } + @Test public void testInterfaceProxyFailuresA() { var payload = evalInterfaceProxyFailures("a"); @@ -373,6 +474,54 @@ private Value evalInterfaceProxyFailures(String methodToEval) { b = Panic.catch No_Such_Method (Foo.callFoo Fooable_Unresolved.Value) (caught-> caught.payload.method_name) """; - return ctxRule.evalModule(code + "\nmain = " + methodToEval); + return ctx().evalModule(code + "\nmain = " + methodToEval); } + + @Test + public void catchCheckedExceptionValueIsReturned() { + var result = checkedException(0); + assertEquals(result.asInt(), 10); + } + + @Test + public void catchCheckedExceptionThrownInEnso() { + var result = checkedException(1); + assertEquals(result.asInt(), -1); + } + + @Test + public void catchCheckedExceptionThrownInJava() { + var result = checkedException(2); + assertEquals(result.asInt(), -1); + } + + @Test + public void catchCheckedSubExceptionThrownInJava() { + var result = checkedException(3); + assertEquals(result.asInt(), -1); + } + + private Value checkedException(int t) { + var code = + """ + polyglot java import org.enso.example.TestException + from Standard.Base import Panic + + handle_errors ~action = + Panic.catch TestException action caught_panic-> + -1 + + run t = case t of + 0 -> handle_errors 10 + 1 -> handle_errors (Panic.throw TestException.new) + 2 -> handle_errors (TestException.throwMe) + 3 -> handle_errors (TestException.throwSubtype) + + main = run + """; + var result = ctx().evalModule(code); + return result.execute(t); + } + + protected abstract ContextUtils ctx(); } diff --git a/engine/runtime-language-epb/src/main/java/module-info.java b/engine/runtime-language-epb/src/main/java/module-info.java index 17a4474d0827..e3fdea5cb594 100644 --- a/engine/runtime-language-epb/src/main/java/module-info.java +++ b/engine/runtime-language-epb/src/main/java/module-info.java @@ -4,6 +4,8 @@ requires org.graalvm.truffle; requires org.enso.runtime.utils; requires org.enso.ydoc.polyfill; + requires org.enso.jvm.channel; + requires org.enso.jvm.interop; provides com.oracle.truffle.api.provider.TruffleLanguageProvider with org.enso.interpreter.epb.EpbLanguageProvider; diff --git a/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/EpbLanguage.java b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/EpbLanguage.java index 608dc355e63a..72d09cd7c760 100644 --- a/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/EpbLanguage.java +++ b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/EpbLanguage.java @@ -4,9 +4,63 @@ import com.oracle.truffle.api.TruffleLanguage; import java.util.function.Consumer; -/** An internal language that serves as a bridge between Enso and other supported languages. */ +/** + * Enso Polyglot Bindings language is an internal language that serves as a bridge between + * Enso and other supported languages. See polyglot docs + * for a high level overview of intended behavior. Technical details are provided in this Javadoc + * and of course in this package code. + * + *

Generic foreign Support

+ * + * Each compliant enough Truffle language implementation that supports {@link + * TruffleLanguage#parse(com.oracle.truffle.api.TruffleLanguage.ParsingRequest)} with arguments + * provided via {@link ParsingRequest#getArgumentNames()} can be integrated with Enso's {@code + * foreign} code snippet concept. + * + *

foreign js Support

+ * + * Graal.js is compliant enough, but comes with a single threaded restriction. Thus it + * needs special support that creates a secondary JavaScript only context in {@link + * ForeignEvalNode#parseJs()}. + * + *

Another special support provided to {@code foreign js} code is ability to refer to Enso {@code + * self} as JavaScript {@code this}. Such a support isn't achievable via standard Truffle means and + * requires {@link JsForeignNode#doExecute} to use {@code apply} JavaScript function directly. + * + *

foreign python Support

+ * + * Graal Python had a lot of limitations in the past and as such the {@link PyForeignNode} does a + * lot of conversions to make sure Python {@code None} and Enso {@code Nothing} represent the same + * null value, that dates are properly transferred, etc. + * + *

Ideally each such limitation is reported to GraalPython guys. List of known issues: + * + *

+ * + * Once an official solution is available, workarounds may be removed. + * + *

polyglot java Support

+ * + * {@code EpbLanguage} is responsible for handling loading of Java classes via {@link + * JavaPolyglotNode}. There are three modes to load classes: + * + *
    + *
  • {@code hosted} - regular GraalVM Java interop is used to load JVM classes + *
  • {@code guest} - support SubstrateVM/HotspotVM bridge via {@code JVM} and {@code Channel} + *
  • Espresso support - experimental controlled by {@code ENSO_JAVA} environment variable + *
+ * + * Each of these mechanisms is supposed to create a {@link TruffleObject} representing the JVM class + * that Enso can then operate with. + */ @TruffleLanguage.Registration( - id = "epb", + id = EpbLanguage.ID, name = "Enso Polyglot Bridge", characterMimeTypes = {EpbLanguage.MIME}, internal = true, @@ -14,6 +68,7 @@ contextPolicy = TruffleLanguage.ContextPolicy.SHARED, services = Consumer.class) public final class EpbLanguage extends TruffleLanguage { + public static final String ID = "epb"; public static final String MIME = "application/epb"; @Override diff --git a/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/ForeignEvalNode.java b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/ForeignEvalNode.java index 1b0b0f29887d..b2defad906a0 100644 --- a/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/ForeignEvalNode.java +++ b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/ForeignEvalNode.java @@ -83,11 +83,14 @@ public Object execute(VirtualFrame frame) { var installedLanguages = context.getEnv().getPublicLanguages(); var node = switch (installedLanguages.containsKey(id) ? 1 : 0) { - case 0 -> { - var sortedLangs = new TreeSet<>(installedLanguages.keySet()); - var ex = new ForeignParsingException(id, sortedLangs, this); - yield new ExceptionForeignNode(ex); - } + case 0 -> switch (id) { + case "java" -> parseJava(); + default -> { + var sortedLangs = new TreeSet<>(installedLanguages.keySet()); + var ex = new ForeignParsingException(id, sortedLangs, this); + yield new ExceptionForeignNode(ex); + } + }; default -> { context.log( Level.FINE, @@ -111,6 +114,16 @@ yield switch (id) { } } + private ForeignFunctionCallNode parseJava() { + var code = foreignSource(langAndCode); + var context = EpbContext.get(this); + if ("hosted".equals(code)) { + return JavaPolyglotNode.createHosted(context); + } else { + return JavaPolyglotNode.create(context); + } + } + private ForeignFunctionCallNode parseJs() { var context = EpbContext.get(this); var inner = context.getInnerContext(); diff --git a/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/HostClassLoader.java b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/HostClassLoader.java new file mode 100644 index 000000000000..5a87b186f224 --- /dev/null +++ b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/HostClassLoader.java @@ -0,0 +1,232 @@ +package org.enso.interpreter.epb; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.interop.ArityException; +import com.oracle.truffle.api.interop.InteropException; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.InvalidArrayIndexException; +import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.interop.UnknownIdentifierException; +import com.oracle.truffle.api.interop.UnsupportedMessageException; +import com.oracle.truffle.api.interop.UnsupportedTypeException; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; +import java.io.File; +import java.lang.System.Logger; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.graalvm.polyglot.Context; + +/** + * Host class loader that serves as a replacement for {@link + * com.oracle.truffle.host.HostClassLoader}. Add URLs to Jar archives with {@link #add(URL)}. All + * the classes that are loaded via this class loader are first searched inside those archives. If + * not found, delegates to parent class loaders. + */ +@ExportLibrary(InteropLibrary.class) +final class HostClassLoader extends URLClassLoader implements AutoCloseable, TruffleObject { + + private final Map> loadedClasses = new ConcurrentHashMap<>(); + private static final Logger logger = System.getLogger(HostClassLoader.class.getName()); + // Classes from "org.graalvm" packages are loaded either by a class loader for the boot + // module layer, or by a specific class loader, depending on how enso is run. For example, + // if enso is run via `org.graalvm.polyglot.Context.eval` from `javac`, then the graalvm + // classes are loaded via a class loader somehow created by `javac` and not by the boot + // module layer's class loader. + private static final ClassLoader polyglotClassLoader = Context.class.getClassLoader(); + + // polyglotClassLoader will be used only iff `org.enso.runtime` module is not in the + // boot module layer. + private static final boolean isRuntimeModInBootLayer; + private Object findLibraries; + + public HostClassLoader() { + super(new URL[0]); + } + + static { + var bootModules = ModuleLayer.boot().modules(); + var hasRuntimeMod = + bootModules.stream().anyMatch(module -> module.getName().equals("org.enso.runtime")); + isRuntimeModInBootLayer = hasRuntimeMod; + } + + void add(URL u) { + logger.log(Logger.Level.DEBUG, "Adding URL '{0}' to class path", u); + addURL(u); + } + + @Override + @CompilerDirectives.TruffleBoundary + public Class loadClass(String name) throws ClassNotFoundException { + return loadClass(name, false); + } + + @Override + @CompilerDirectives.TruffleBoundary + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + logger.log(Logger.Level.TRACE, "Loading class {0}", name); + var l = loadedClasses.get(name); + if (l != null) { + logger.log(Logger.Level.TRACE, "Class {0} found in cache", name); + return l; + } + synchronized (this) { + l = loadedClasses.get(name); + if (l != null) { + logger.log(Logger.Level.TRACE, "Class {0} found in cache", name); + return l; + } + if (!isRuntimeModInBootLayer && name.startsWith("org.graalvm")) { + return polyglotClassLoader.loadClass(name); + } + if (name.startsWith("org.slf4j")) { + // Delegating to system class loader ensures that log classes are not loaded again + // and do not require special setup. In other words, it is using log configuration that + // has been setup by the runner that started the process. See #11641. + return polyglotClassLoader.loadClass(name); + } + try { + l = findClass(name); + if (resolve) { + l.getMethods(); + } + logger.log(Logger.Level.TRACE, "Class {0} found, putting in cache", name); + loadedClasses.put(name, l); + return l; + } catch (ClassNotFoundException ex) { + logger.log(Logger.Level.TRACE, "Class {0} not found, delegating to super", name); + return super.loadClass(name, resolve); + } catch (Throwable e) { + logger.log(Logger.Level.TRACE, "Failure while loading a class: " + e.getMessage(), e); + throw e; + } + } + } + + /** + * Find the library with the specified name inside the {@code polyglot/lib} directory of caller's + * project. The search inside the {@code polyglot/lib} directory hierarchy is specified by NetBeans + * JNI specification. + * + *

Note: The current implementation iterates all the {@code polyglot/lib} directories of all + * the packages. + * + * @param libname The library name. Without platform-specific suffix or prefix. + * @return Absolute path to the library if found, or null. + */ + @Override + protected String findLibrary(String libname) { + if (findLibraries != null) { + try { + var res = InteropLibrary.getUncached().execute(findLibraries, libname); + if (res instanceof String s) { + return s; + } + } catch (InteropException ex) { + logger.log(Logger.Level.WARNING, "Cannot find " + libname, ex); + } + } + logger.log(Logger.Level.WARNING, "Native library {0} not found in any package", libname); + return null; + } + + @Override + public void close() { + loadedClasses.clear(); + } + + @ExportMessage + @CompilerDirectives.TruffleBoundary + final Object invokeMember(String name, Object[] args) + throws UnknownIdentifierException, ArityException, UnsupportedTypeException { + switch (name) { + case "addPath" -> { + if (args.length != 1) { + throw ArityException.create(1, 1, args.length); + } + if (args[0] instanceof String path) { + var file = new File(path); + try { + addURL(file.toURI().toURL()); + } catch (MalformedURLException ex) { + throw UnsupportedTypeException.create(args, "Cannot convert to URL", ex); + } + } else { + throw UnsupportedTypeException.create(args); + } + } + case "findLibraries" -> { + if (args.length != 1) { + throw ArityException.create(1, 1, args.length); + } + if (InteropLibrary.getUncached().isExecutable(args[0])) { + this.findLibraries = args[0]; + } else { + throw UnsupportedTypeException.create(args); + } + } + case "close" -> { + close(); + } + default -> throw UnknownIdentifierException.create(name); + } + return this; + } + + @ExportMessage + boolean hasMembers() { + return true; + } + + @ExportMessage + boolean isMemberInvocable(String member) { + return true; + } + + @ExportMessage + boolean isMemberReadable(String member) { + return true; + } + + @ExportMessage + @CompilerDirectives.TruffleBoundary + Object readMember(String member) throws UnknownIdentifierException { + try { + var clazz = loadClass(member); + var ctx = EpbContext.get(null); + return ctx.getEnv().asHostSymbol(clazz); + } catch (ClassNotFoundException ex) { + throw UnknownIdentifierException.create(member, ex); + } + } + + @ExportMessage + Object getMembers(boolean includeInternal) throws UnsupportedMessageException { + return this; + } + + @ExportMessage + long getArraySize() { + return 0; + } + + @ExportMessage + boolean hasArrayElements() { + return true; + } + + @ExportMessage + Object readArrayElement(long index) throws InvalidArrayIndexException { + throw InvalidArrayIndexException.create(index); + } + + @ExportMessage + boolean isArrayElementReadable(long index) { + return false; + } +} diff --git a/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/JavaPolyglotNode.java b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/JavaPolyglotNode.java new file mode 100644 index 000000000000..208a2484cd15 --- /dev/null +++ b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/JavaPolyglotNode.java @@ -0,0 +1,26 @@ +package org.enso.interpreter.epb; + +import com.oracle.truffle.api.TruffleOptions; +import com.oracle.truffle.api.nodes.RootNode; +import java.io.IOException; +import java.net.URISyntaxException; +import org.enso.jvm.interop.api.OtherJvmClassLoader; + +final class JavaPolyglotNode { + static GenericForeignNode create(EpbContext context) { + try { + var isAot = TruffleOptions.AOT; + var loader = OtherJvmClassLoader.create(isAot, context.getEnv().getContext()); + var target = RootNode.createConstantNode(loader).getCallTarget(); + return new GenericForeignNode(target); + } catch (URISyntaxException | IOException ex) { + throw new IllegalStateException(ex); + } + } + + static ForeignFunctionCallNode createHosted(EpbContext context) { + var loader = new HostClassLoader(); + var target = RootNode.createConstantNode(loader).getCallTarget(); + return new GenericForeignNode(target); + } +} diff --git a/engine/runtime-language-epb/src/test/java/org/enso/interpreter/epb/EpbJvmObjectTest.java b/engine/runtime-language-epb/src/test/java/org/enso/interpreter/epb/EpbJvmObjectTest.java new file mode 100644 index 000000000000..246bd13ff43b --- /dev/null +++ b/engine/runtime-language-epb/src/test/java/org/enso/interpreter/epb/EpbJvmObjectTest.java @@ -0,0 +1,228 @@ +package org.enso.interpreter.epb; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.math.BigDecimal; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.HostAccess; +import org.graalvm.polyglot.PolyglotException; +import org.graalvm.polyglot.Value; +import org.graalvm.polyglot.proxy.ProxyExecutable; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class EpbJvmObjectTest { + private static Context ctx; + private static Value guestClassLoader; + + @BeforeClass + public static void initializeCtx() { + ctx = Context.newBuilder("js").allowHostAccess(HostAccess.ALL).build(); + var epbParse = ctx.getEngine().getInstruments().get(EpbParseInstrument.ID); + ctx.enter(); + @SuppressWarnings("unchecked") + BiFunction fn = epbParse.lookup(BiFunction.class); + guestClassLoader = ctx.asValue(fn.apply("java:0#guest", "Guest JVM")); + } + + @AfterClass + public static void closeCtx() { + guestClassLoader = null; + ctx.close(); + ctx = null; + } + + @Test + public void wrapBigDecimal() throws Exception { + var testClassValue = loadOtherJvmClass(EpbJvmObjectTest.class.getName()); + var bigReal = newBigDecimal("432.322"); + var otherValue = testClassValue.invokeMember("newBigDecimal", "432.322"); + + assertFalse("Decimal isn't array", otherValue.hasArrayElements()); + assertEquals(bigReal.toPlainString(), otherValue.invokeMember("toPlainString").asString()); + + var twiceReal = bigReal.add(bigReal); + var twiceValue = otherValue.invokeMember("add", otherValue); + assertEquals(twiceReal.toBigInteger(), twiceValue.invokeMember("toBigInteger").asBigInteger()); + + var minusValue = twiceValue.invokeMember("subtract", otherValue); + assertEquals(bigReal.toString(), minusValue.invokeMember("toString").asString()); + } + + public static BigDecimal newBigDecimal(String txt) { + return new BigDecimal(txt); + } + + public static Object[] otherJvmArrayWithPrimitives() { + var bigReal = + new Object[] { + "Ahoj", 't', (byte) 1, (short) 2, (int) 3, (long) 4, (float) 5, (double) 6, true + }; + return bigReal; + } + + @Test + public void wrapArray() throws Exception { + var testClassValue = loadOtherJvmClass(EpbJvmObjectTest.class.getName()); + var otherValue = testClassValue.invokeMember("otherJvmArrayWithPrimitives"); + + assertTrue("Array is array", otherValue.hasArrayElements()); + assertEquals("Few elements", 9, otherValue.getArraySize()); + assertEquals("Ahoj", otherValue.getArrayElement(0).asString()); + assertEquals("t", otherValue.getArrayElement(1).asString()); + assertEquals(1, otherValue.getArrayElement(2).asInt()); + assertEquals(2, otherValue.getArrayElement(3).asInt()); + assertEquals(3, otherValue.getArrayElement(4).asInt()); + assertEquals(4, otherValue.getArrayElement(5).asLong()); + assertEquals(5.0, otherValue.getArrayElement(6).asFloat(), 0.1); + assertEquals(6.0, otherValue.getArrayElement(7).asDouble(), 0.1); + assertEquals(true, otherValue.getArrayElement(8).asBoolean()); + } + + @Test + public void loadClassViaMessage() throws Exception { + var shortValue = loadOtherJvmClass("java.lang.Short"); + var value = shortValue.invokeMember("valueOf", "32531"); + assertEquals(32531, value.asInt()); + } + + @Test + public void loadTestClassViaMessage() throws Exception { + var testClassValue = loadOtherJvmClass(EpbJvmObjectTest.class.getName()); + var parsedValue = testClassValue.invokeMember("otherJvmValueOf", "32531"); + assertEquals(32531, parsedValue.asInt()); + } + + public static short otherJvmValueOf(String txt) { + return Short.parseShort(txt); + } + + @Test + public void parsingException() throws Exception { + var shortClass1 = ctx.asValue(java.lang.Short.class).getMember("static"); + try { + var value1 = shortClass1.invokeMember("valueOf", "not-a-number"); + fail("Unexpected returned value: " + value1); + } catch (PolyglotException e) { + MatcherAssert.assertThat(e.getMessage(), CoreMatchers.containsString("not-a-number")); + assertTrue("This is host exception", e.isHostException()); + assertNotNull("Host exception found", e.asHostException()); + } + var shortClass2 = loadOtherJvmClass("java.lang.Short"); + try { + var value2 = shortClass2.invokeMember("valueOf", "not-a-number"); + fail("Unexpected returned value: " + value2); + } catch (PolyglotException e) { + MatcherAssert.assertThat(e.getMessage(), CoreMatchers.containsString("not-a-number")); + assertFalse("Alas this cannot be host exception", e.isHostException()); + } + } + + @Test + public void unsupportedOperation() throws Exception { + var shortClass1 = ctx.asValue(java.lang.Short.class).getMember("static"); + try { + var value1 = shortClass1.getArrayElement(0); + fail("Unexpected returned value: " + value1); + } catch (UnsupportedOperationException e) { + MatcherAssert.assertThat( + e.getMessage(), CoreMatchers.containsString("Unsupported operation")); + } + var shortClass2 = loadOtherJvmClass("java.lang.Short"); + try { + var value2 = shortClass2.getArrayElement(0); + fail("Unexpected returned value: " + value2); + } catch (UnsupportedOperationException e) { + MatcherAssert.assertThat( + e.getMessage(), CoreMatchers.containsString("Unsupported operation")); + } + } + + private static final Object IDENTICAL = new Object(); + + public static Object otherJvmInstances(int kind) { + if (kind == 0) { + return IDENTICAL; + } else { + return new Object(); + } + } + + @Test + public void isIdenticalCheck() throws Exception { + var localClass = ctx.asValue(EpbJvmObjectTest.class).getMember("static"); + var local1 = localClass.invokeMember("otherJvmInstances", 0); + var local2 = localClass.invokeMember("otherJvmInstances", 0); + assertEquals(local1, local2); + + var otherClass = loadOtherJvmClass(EpbJvmObjectTest.class.getName()); + var other1 = otherClass.invokeMember("otherJvmInstances", 0); + var other2 = otherClass.invokeMember("otherJvmInstances", 0); + assertEquals(other1, other2); + } + + @Test + public void isNotIdenticalCheck() throws Exception { + var localClass = ctx.asValue(EpbJvmObjectTest.class).getMember("static"); + var local1 = localClass.invokeMember("otherJvmInstances", 1); + var local2 = localClass.invokeMember("otherJvmInstances", 1); + assertNotEquals(local1, local2); + + var otherClass = loadOtherJvmClass(EpbJvmObjectTest.class.getName()); + var other1 = otherClass.invokeMember("otherJvmInstances", 1); + var other2 = otherClass.invokeMember("otherJvmInstances", 1); + assertNotEquals(other1, other2); + } + + public static void callback(Consumer cb, Object value) { + cb.accept(value); + } + + @Test + public void callback() throws Exception { + class MockProxy implements ProxyExecutable { + private Value last; + + @Override + public Object execute(Value... arguments) { + assertNull("No args yet", last); + assertEquals("One arg", 1, arguments.length); + last = arguments[0]; + + var myCtx = Context.getCurrent(); + assertEquals("The right context", ctx, myCtx); + return arguments[0]; + } + + final void assertArgs(String msg, Value exp) { + assertEquals(msg, exp.asString(), this.last.asString()); + this.last = null; + } + } + var mock = new MockProxy(); + var mockValue = ctx.asValue(mock); + + var localClass = ctx.asValue(EpbJvmObjectTest.class).getMember("static"); + localClass.invokeMember("callback", mockValue, "Real"); + mock.assertArgs("Called with Real", ctx.asValue("Real")); + + var otherClass = loadOtherJvmClass(EpbJvmObjectTest.class.getName()); + otherClass.invokeMember("callback", mockValue, "RealOther"); + mock.assertArgs("Called with Real", ctx.asValue("RealOther")); + } + + private static Value loadOtherJvmClass(String name) throws Exception { + return guestClassLoader.getMember(name); + } +} diff --git a/engine/runtime-language-epb/src/test/java/org/enso/interpreter/epb/EpbParseInstrument.java b/engine/runtime-language-epb/src/test/java/org/enso/interpreter/epb/EpbParseInstrument.java new file mode 100644 index 000000000000..86b06d24453a --- /dev/null +++ b/engine/runtime-language-epb/src/test/java/org/enso/interpreter/epb/EpbParseInstrument.java @@ -0,0 +1,26 @@ +package org.enso.interpreter.epb; + +import com.oracle.truffle.api.instrumentation.TruffleInstrument; +import com.oracle.truffle.api.source.Source; +import java.io.IOException; +import java.util.function.BiFunction; + +@TruffleInstrument.Registration(id = EpbParseInstrument.ID, services = BiFunction.class) +public final class EpbParseInstrument extends TruffleInstrument { + static final String ID = "epbParseInstrument"; + + @Override + protected void onCreate(Env env) { + BiFunction fn = + (code, name) -> { + var src = Source.newBuilder(EpbLanguage.ID, code, name).build(); + try { + var target = env.parse(src); + return target.call(); + } catch (IOException ex) { + throw new IllegalStateException(ex); + } + }; + env.registerService(fn); + } +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/EnsoContext.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/EnsoContext.java index 1969ec23e7c0..b72bef9cb385 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/EnsoContext.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/EnsoContext.java @@ -11,10 +11,7 @@ import com.oracle.truffle.api.TruffleLanguage.Env; import com.oracle.truffle.api.TruffleLogger; import com.oracle.truffle.api.interop.InteropException; -import com.oracle.truffle.api.interop.InteropLibrary; import com.oracle.truffle.api.interop.TruffleObject; -import com.oracle.truffle.api.interop.UnknownIdentifierException; -import com.oracle.truffle.api.interop.UnsupportedMessageException; import com.oracle.truffle.api.io.TruffleProcessBuilder; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.profiles.ValueProfile; @@ -25,7 +22,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; -import java.net.MalformedURLException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -40,7 +36,6 @@ import java.util.function.Supplier; import java.util.logging.Level; import org.enso.common.LanguageInfo; -import org.enso.common.PolyglotSymbolResolver; import org.enso.common.RuntimeOptions; import org.enso.compiler.Compiler; import org.enso.compiler.core.EnsoParser; @@ -82,12 +77,15 @@ public final class EnsoContext { private final EnsoLanguage language; private final Env environment; - private final HostClassLoader hostClassLoader = new HostClassLoader(); private final boolean assertionsEnabled; private final boolean isPrivateCheckDisabled; private final boolean isStaticAnalysisEnabled; private final boolean isHostClassLoading; - private final boolean isResolverClassLoading; + private final boolean isGuestClassLoading; + /** + * Right now there is just a single polyglot Java system. + */ + private EnsoPolyglotJava polyglotJava; private @CompilationFinal Compiler compiler; private final PrintStream out; private final PrintStream err; @@ -152,14 +150,15 @@ public EnsoContext( this.isHostClassLoading = switch (classLoading) { case "hosted", "all" -> true; - case "service" -> false; + case "guest" -> false; case null, default -> throw new IllegalStateException(classLoading); }; - this.isResolverClassLoading = switch (classLoading) { - case "service", "all" -> true; + this.isGuestClassLoading = switch (classLoading) { + case "guest", "all" -> true; case "hosted" -> false; case null, default -> throw new IllegalStateException(classLoading); }; + this.polyglotJava = new EnsoPolyglotJava(environment, isHostClassLoading, isGuestClassLoading); } this.globalExecutionEnvironment = getOption(EnsoLanguage.EXECUTION_ENVIRONMENT); this.assertionsEnabled = shouldAssertionsBeEnabled(); @@ -228,7 +227,7 @@ public void initialize() { var preinit = environment.getOptions().get(RuntimeOptions.PREINITIALIZE_KEY); if (preinit != null && preinit.length() > 0) { - var epb = environment.getInternalLanguages().get("epb"); + var epb = findEpbLanguage(); if (epb != null) { @SuppressWarnings("unchecked") var run = (Consumer) environment.lookup(epb, Consumer.class); @@ -239,6 +238,10 @@ public void initialize() { } } + private com.oracle.truffle.api.nodes.LanguageInfo findEpbLanguage() { + return environment.getInternalLanguages().get("epb"); + } + /** Checks if the working directory is as expected and reports a warning if not. */ private void checkWorkingDirectory(Optional maybeProjectRoot) { if (maybeProjectRoot.isPresent()) { @@ -337,9 +340,8 @@ public void shutdown() { resourceManager.shutdown(); compiler.shutdown(shouldWaitForPendingSerializationJobs); packageRepository.shutdown(); - guestJava = null; topScope = null; - hostClassLoader.close(); + polyglotJava.close(); EnsoParser.freeAll(); } @@ -522,27 +524,15 @@ public Optional findModuleByExpressionId(UUID expressionId) { */ @TruffleBoundary public void addToClassPath(TruffleFile file) { - if (findGuestJava() == null) { - try { - var url = file.toUri().toURL(); - hostClassLoader.add(url); - if (isResolverClassLoading) { - PolyglotSymbolResolver.addToClassPath(url); - } - } catch (MalformedURLException ex) { - throw new IllegalStateException(ex); - } - } else { - try { var path = new File(file.toUri()).getAbsoluteFile(); if (!path.exists()) { throw new IllegalStateException("File not found " + path); } - InteropLibrary.getUncached().invokeMember(findGuestJava(), "addPath", path.getPath()); + try { + polyglotJava.addToClassPath(path); } catch (InteropException ex) { - throw new IllegalStateException(ex); + throw raiseAssertionPanic(null, "Cannot add " + file + " to classpath", ex); } - } } /** @@ -645,31 +635,16 @@ static Object lookupJavaClass( @TruffleBoundary public TruffleObject lookupJavaClass(String className) { var collectedExceptions = new ArrayList(); - - if (isHostClassLoading) { - var hostSymbol = - ClassLookup.lookupJavaClass( - className, // name to search for - this::lookupHostSymbol, // ask the classloader - collectedExceptions // put here all exceptions - ); - if (hostSymbol instanceof TruffleObject) { - return (TruffleObject) hostSymbol; - } - } - if (isResolverClassLoading) { - var javaHome = System.getProperty("java.home"); - logger.info( - () -> String.format("Class %s not found, trying to turn on JVM %s", className, javaHome)); - var hostSymbol = - ClassLookup.lookupJavaClass( - className, // name to search for - PolyglotSymbolResolver::loadClass, // pluggable polyglot searches - collectedExceptions // collect exceptions - ); - if (hostSymbol instanceof TruffleObject) { - return (TruffleObject) hostSymbol; - } + var hostSymbol = + ClassLookup.lookupJavaClass( + className, // name to search for + (fqn) -> { + return polyglotJava.loadClass(fqn); + }, // pluggable polyglot searches + collectedExceptions // collect exceptions + ); + if (hostSymbol instanceof TruffleObject) { + return (TruffleObject) hostSymbol; } var level = Level.WARNING; for (var ex : collectedExceptions) { @@ -681,56 +656,6 @@ public TruffleObject lookupJavaClass(String className) { return getBuiltins().error().makeMissingPolyglotImportError(className); } - private Object lookupHostSymbol(String fqn) - throws ClassNotFoundException, UnknownIdentifierException, UnsupportedMessageException { - try { - if (findGuestJava() == null) { - return environment.asHostSymbol(hostClassLoader.loadClass(fqn)); - } else { - return InteropLibrary.getUncached().readMember(findGuestJava(), fqn); - } - } catch (Error e) { - throw new ClassNotFoundException("Error loading " + fqn, e); - } - } - - private Object guestJava = this; - - @TruffleBoundary - private Object findGuestJava() throws IllegalStateException { - if (guestJava != this) { - return guestJava; - } - guestJava = null; - var envJava = System.getenv("ENSO_JAVA"); - if (envJava == null) { - return guestJava; - } - if ("espresso".equals(envJava)) { - var src = Source.newBuilder("java", "", "getbindings.java").build(); - try { - guestJava = environment.parsePublic(src).call(); - logger.log(Level.SEVERE, "Using experimental Espresso support!"); - } catch (Exception ex) { - if (ex.getMessage().contains("No language for id java found.")) { - logger.log( - Level.SEVERE, - "Environment variable ENSO_JAVA=" + envJava + ", but " + ex.getMessage()); - logger.log(Level.SEVERE, "Copy missing libraries to components directory"); - logger.log(Level.SEVERE, "Continuing in regular Java mode"); - } else { - var ise = new IllegalStateException(ex.getMessage()); - ise.setStackTrace(ex.getStackTrace()); - throw ise; - } - } - } else { - throw new IllegalStateException( - "Specify ENSO_JAVA=espresso to use Espresso. Was: " + envJava); - } - return guestJava; - } - /** * Finds the package the provided module belongs to. * diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/EnsoPolyglotJava.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/EnsoPolyglotJava.java new file mode 100644 index 000000000000..541f5c9a8790 --- /dev/null +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/EnsoPolyglotJava.java @@ -0,0 +1,163 @@ +package org.enso.interpreter.runtime; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.TruffleLanguage; +import com.oracle.truffle.api.interop.ArityException; +import com.oracle.truffle.api.interop.InteropException; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.interop.UnknownIdentifierException; +import com.oracle.truffle.api.interop.UnsupportedMessageException; +import com.oracle.truffle.api.interop.UnsupportedTypeException; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; +import com.oracle.truffle.api.source.Source; +import java.io.File; +import java.lang.System.Logger.Level; +import java.util.ArrayList; +import java.util.List; +import org.enso.interpreter.runtime.util.TruffleFileSystem; +import org.enso.pkg.NativeLibraryFinder; + +/** + * Handles a polyglot Java system for loading classes from a single source. Single source + * is a collection of Java modules/libraries/JARs that belong together. + */ +final class EnsoPolyglotJava { + + private static final System.Logger logger = System.getLogger(EnsoPolyglotJava.class.getName()); + private final TruffleLanguage.Env environment; + private final boolean isHostClassLoading; + private final boolean isGuestClassLoading; + + private final List pendingPath = new ArrayList<>(); + private Object polyglotJava = this; + + EnsoPolyglotJava( + TruffleLanguage.Env environment, boolean isHostClassLoading, boolean isGuestClassLoading) { + this.environment = environment; + this.isHostClassLoading = isHostClassLoading; + this.isGuestClassLoading = isGuestClassLoading; + } + + @CompilerDirectives.TruffleBoundary + private synchronized Object findPolyglotJava() throws InteropException { + if (polyglotJava != this) { + return polyglotJava; + } + polyglotJava = createPolyglotJava(); + while (!pendingPath.isEmpty()) { + addToClassPath(pendingPath.remove(0)); + } + try { + InteropLibrary.getUncached() + .invokeMember(polyglotJava, "findLibraries", new LibraryResolver()); + } catch (InteropException ex) { + logger.log(Level.WARNING, "Cannot register findLibraries", ex); + } + return polyglotJava; + } + + /** + * Modifies the classpath to use to lookup {@code polyglot java} imports. + * + * @param file the file to register + */ + @CompilerDirectives.TruffleBoundary + final synchronized void addToClassPath(File file) throws InteropException { + if (polyglotJava == this) { + pendingPath.add(file); + } else { + InteropLibrary.getUncached().invokeMember(polyglotJava, "addPath", file.toString()); + } + } + + final synchronized void close() { + if (polyglotJava instanceof TruffleObject closeJava) { + polyglotJava = null; + try { + InteropLibrary.getUncached().invokeMember(closeJava, "close"); + } catch (InteropException ex) { + logger.log(Level.WARNING, "Cannot close " + closeJava, ex); + } + } else { + polyglotJava = null; + } + } + + private Object createPolyglotJava() throws IllegalStateException { + if (isHostClassLoading) { + var src = Source.newBuilder("epb", "java:0#hosted", "").build(); + var target = environment.parseInternal(src); + return target.call(); + } + if (isGuestClassLoading) { + var envJava = System.getenv("ENSO_JAVA"); + if (envJava == null) { + logger.log(Level.ERROR, "Using experimental OtherJvm support!"); + var src = Source.newBuilder("epb", "java:0#guest", "").build(); + var target = environment.parseInternal(src); + return target.call(); + } + if ("espresso".equals(envJava)) { + var src = Source.newBuilder("java", "", "getbindings.java").build(); + try { + var java = environment.parsePublic(src).call(); + logger.log(Level.ERROR, "Using experimental Espresso support!"); + return java; + } catch (Exception ex) { + if (ex.getMessage().contains("No language for id java found.")) { + logger.log( + Level.ERROR, + "Environment variable ENSO_JAVA={0}, but {1}", + new Object[] {envJava, ex.getMessage()}); + logger.log(Level.ERROR, "Copy missing libraries to components directory"); + logger.log(Level.ERROR, "Continuing in regular Java mode"); + } else { + var ise = new IllegalStateException(ex.getMessage()); + ise.setStackTrace(ex.getStackTrace()); + throw ise; + } + } + } else { + throw new IllegalStateException( + "Specify ENSO_JAVA=espresso to use Espresso. Was: " + envJava); + } + } + return null; + } + + final TruffleObject loadClass(String fqn) + throws UnsupportedMessageException, UnknownIdentifierException, InteropException { + var raw = InteropLibrary.getUncached().readMember(findPolyglotJava(), fqn); + return (TruffleObject) raw; + } + + @ExportLibrary(InteropLibrary.class) + static final class LibraryResolver implements TruffleObject { + + @ExportMessage + @CompilerDirectives.TruffleBoundary + Object execute(Object[] args) throws ArityException, UnsupportedTypeException { + if (args.length != 1) { + throw ArityException.create(1, 1, args.length); + } + if (args[0] instanceof String libname) { + var pkgRepo = EnsoContext.get(null).getPackageRepository(); + for (var pkg : pkgRepo.getLoadedPackagesJava()) { + var libPath = + NativeLibraryFinder.findNativeLibrary(libname, pkg, TruffleFileSystem.INSTANCE); + if (libPath != null) { + return libPath; + } + } + } + throw UnsupportedTypeException.create(args); + } + + @ExportMessage + boolean isExecutable() { + return true; + } + } +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/HostClassLoader.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/HostClassLoader.java deleted file mode 100644 index 5261f5621eb6..000000000000 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/HostClassLoader.java +++ /dev/null @@ -1,125 +0,0 @@ -package org.enso.interpreter.runtime; - -import java.net.URL; -import java.net.URLClassLoader; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import org.enso.interpreter.runtime.util.TruffleFileSystem; -import org.enso.pkg.NativeLibraryFinder; -import org.graalvm.polyglot.Context; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Host class loader that serves as a replacement for {@link - * com.oracle.truffle.host.HostClassLoader}. Add URLs to Jar archives with {@link #add(URL)}. All - * the classes that are loaded via this class loader are first searched inside those archives. If - * not found, delegates to parent class loaders. - */ -final class HostClassLoader extends URLClassLoader implements AutoCloseable { - - private final Map> loadedClasses = new ConcurrentHashMap<>(); - private static final Logger logger = LoggerFactory.getLogger(HostClassLoader.class); - // Classes from "org.graalvm" packages are loaded either by a class loader for the boot - // module layer, or by a specific class loader, depending on how enso is run. For example, - // if enso is run via `org.graalvm.polyglot.Context.eval` from `javac`, then the graalvm - // classes are loaded via a class loader somehow created by `javac` and not by the boot - // module layer's class loader. - private static final ClassLoader polyglotClassLoader = Context.class.getClassLoader(); - - // polyglotClassLoader will be used only iff `org.enso.runtime` module is not in the - // boot module layer. - private static final boolean isRuntimeModInBootLayer; - - public HostClassLoader() { - super(new URL[0]); - } - - static { - var bootModules = ModuleLayer.boot().modules(); - var hasRuntimeMod = - bootModules.stream().anyMatch(module -> module.getName().equals("org.enso.runtime")); - isRuntimeModInBootLayer = hasRuntimeMod; - } - - void add(URL u) { - logger.debug("Adding URL '{}' to class path", u); - addURL(u); - } - - @Override - public Class loadClass(String name) throws ClassNotFoundException { - return loadClass(name, false); - } - - @Override - protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - logger.trace("Loading class {}", name); - var l = loadedClasses.get(name); - if (l != null) { - logger.trace("Class {} found in cache", name); - return l; - } - synchronized (this) { - l = loadedClasses.get(name); - if (l != null) { - logger.trace("Class {} found in cache", name); - return l; - } - if (!isRuntimeModInBootLayer && name.startsWith("org.graalvm")) { - return polyglotClassLoader.loadClass(name); - } - if (name.startsWith("org.slf4j")) { - // Delegating to system class loader ensures that log classes are not loaded again - // and do not require special setup. In other words, it is using log configuration that - // has been setup by the runner that started the process. See #11641. - return polyglotClassLoader.loadClass(name); - } - try { - l = findClass(name); - if (resolve) { - l.getMethods(); - } - logger.trace("Class {} found, putting in cache", name); - loadedClasses.put(name, l); - return l; - } catch (ClassNotFoundException ex) { - logger.trace("Class {} not found, delegating to super", name); - return super.loadClass(name, resolve); - } catch (Throwable e) { - logger.trace("Failure while loading a class: " + e.getMessage(), e); - throw e; - } - } - } - - /** - * Find the library with the specified name inside the {@code polyglot/lib} directory of caller's - * project. The search inside the {@code polyglot/lib} directory hierarchy is specified by NetBeans - * JNI specification. - * - *

Note: The current implementation iterates all the {@code polyglot/lib} directories of all - * the packages. - * - * @param libname The library name. Without platform-specific suffix or prefix. - * @return Absolute path to the library if found, or null. - */ - @Override - protected String findLibrary(String libname) { - var pkgRepo = EnsoContext.get(null).getPackageRepository(); - for (var pkg : pkgRepo.getLoadedPackagesJava()) { - var libPath = NativeLibraryFinder.findNativeLibrary(libname, pkg, TruffleFileSystem.INSTANCE); - if (libPath != null) { - return libPath; - } - } - logger.trace("Native library {} not found in any package", libname); - return null; - } - - @Override - public void close() { - loadedClasses.clear(); - } -} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/error/PanicException.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/error/PanicException.java index 743332a43a8e..d73cf609cd7e 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/error/PanicException.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/error/PanicException.java @@ -239,8 +239,8 @@ ExceptionType getExceptionType() { } @ExportMessage - int getExceptionExitStatus() { - return 1; + int getExceptionExitStatus() throws UnsupportedMessageException { + throw UnsupportedMessageException.create(); } @ExportMessage diff --git a/lib/java/jvm-interop/src/main/java/module-info.java b/lib/java/jvm-interop/src/main/java/module-info.java index d12a35280d64..e1045577ec6d 100644 --- a/lib/java/jvm-interop/src/main/java/module-info.java +++ b/lib/java/jvm-interop/src/main/java/module-info.java @@ -1,18 +1,11 @@ -import org.enso.common.PolyglotSymbolResolver; -import org.enso.jvm.interop.OtherJvmSymbolResolver; - module org.enso.jvm.interop { - provides PolyglotSymbolResolver with - OtherJvmSymbolResolver; - requires org.graalvm.polyglot; + requires org.enso.persistance; requires org.graalvm.truffle; requires org.enso.jvm.channel; - requires org.enso.engine.common; - opens org.enso.jvm.interop to - org.enso.jvm.channel; + exports org.enso.jvm.interop.api; - requires org.enso.persistance; - requires static org.openide.util.lookup.RELEASE180; + opens org.enso.jvm.interop.impl to + org.enso.jvm.channel; } diff --git a/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/OtherJvmException.java b/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/OtherJvmException.java deleted file mode 100644 index 61bc7939c1df..000000000000 --- a/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/OtherJvmException.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.enso.jvm.interop; - -import com.oracle.truffle.api.exception.AbstractTruffleException; - -final class OtherJvmException extends AbstractTruffleException { - OtherJvmException(String message) { - super(message); - } -} diff --git a/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/OtherJvmSymbolResolver.java b/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/OtherJvmSymbolResolver.java deleted file mode 100644 index 5d9388cb299f..000000000000 --- a/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/OtherJvmSymbolResolver.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.enso.jvm.interop; - -import java.io.File; -import java.io.IOException; -import java.net.URISyntaxException; -import java.net.URL; -import java.util.ArrayList; -import org.enso.common.HostEnsoUtils; -import org.enso.common.PolyglotSymbolResolver; -import org.enso.jvm.channel.Channel; -import org.enso.jvm.channel.JVM; - -/** Resolves symbols via interop messages to the "other" HotSpot JVM. */ -@org.openide.util.lookup.ServiceProvider(service = PolyglotSymbolResolver.class) -public final class OtherJvmSymbolResolver extends PolyglotSymbolResolver { - private Channel channel; - - private Channel getChannel() throws URISyntaxException, IOException { - if (channel == null) { - channel = initializeChannel(); - } - return channel; - } - - @Override - protected Object handleLoadClass(String name) throws ClassNotFoundException { - try { - var ch = getChannel(); - var result = ch.execute(OtherJvmResult.class, new OtherJvmMessage.LoadClass(name)); - return result.value(); - } catch (IOException | URISyntaxException ex) { - throw new ClassNotFoundException(name, ex); - } - } - - @Override - protected void handleAddToClassPath(URL url) { - try { - getChannel().execute(Void.class, new OtherJvmMessage.AddToClassPath(url.toString())); - } catch (URISyntaxException | IOException ex) { - ex.printStackTrace(); - } - } - - private Channel initializeChannel() throws IOException, URISyntaxException { - var jvm = - HostEnsoUtils.isAot() - ? // normally we run in AOT mode - initializeJvm() // then create HotSpot JVM - : // but for debugging purposes we can also - null; // emulate the connection in a single JVM - return Channel.create(jvm, OtherJvmPool.class); - } - - private JVM initializeJvm() throws IOException, URISyntaxException { - var home = System.getProperty("java.home"); - if (home == null) { - throw new IOException("No java.home specified"); - } - var javaHome = new File(home); - if (!javaHome.exists()) { - throw new IOException("JVM doesn't exists: " + javaHome); - } - var loc = getClass().getProtectionDomain().getCodeSource().getLocation(); - var component = new File(loc.toURI().resolve("..")).getAbsoluteFile(); - if (!component.getName().equals("component")) { - component = new File(component, "component"); - } - var commandAndArgs = new ArrayList(); - var assertsOn = false; - assert assertsOn = true; - if (assertsOn) { - commandAndArgs.add("-ea"); - } - commandAndArgs.add("--sun-misc-unsafe-memory-access=allow"); - commandAndArgs.add("-Dpolyglot.engine.WarnInterpreterOnly=false"); - commandAndArgs.add("-Dtruffle.UseFallbackRuntime=true"); - commandAndArgs.add("--enable-native-access=org.graalvm.truffle"); - commandAndArgs.add("--enable-native-access=org.enso.jvm.channel"); - commandAndArgs.add("--add-opens=java.base/java.nio=ALL-UNNAMED"); - if (!component.isDirectory()) { - throw new IOException("Cannot find " + component + " directory"); - } - commandAndArgs.add("--module-path=" + component.getPath()); - commandAndArgs.add("-Djdk.module.main=org.enso.jvm.interop"); - return JVM.create(javaHome, commandAndArgs.toArray(new String[0])); - } -} diff --git a/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/TruffleClassLoader.java b/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/TruffleClassLoader.java deleted file mode 100644 index 49652579d1ec..000000000000 --- a/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/TruffleClassLoader.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.enso.jvm.interop; - -import com.oracle.truffle.api.interop.InteropLibrary; -import com.oracle.truffle.api.interop.TruffleObject; -import com.oracle.truffle.api.library.ExportLibrary; -import com.oracle.truffle.api.library.ExportMessage; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.function.Supplier; -import org.graalvm.polyglot.Context; -import org.graalvm.polyglot.HostAccess; - -@ExportLibrary(value = InteropLibrary.class) -final class TruffleClassLoader extends URLClassLoader implements TruffleObject { - private Context ctx; - private Object value; - - TruffleClassLoader() { - super(new URL[0]); - } - - final synchronized void assignCtx(Context ctx) { - assert this.ctx == null; - this.ctx = ctx; - } - - private synchronized Context ctx() { - if (ctx == null) { - ctx = - Context.newBuilder() // no dynamic languages needed - .allowHostAccess(HostAccess.ALL) // all public members - .allowExperimentalOptions(true) // to survive any -Dpolyglot options - .build(); - } - return ctx; - } - - final D withCtx(Supplier action) { - ctx().enter(); - try { - return action.get(); - } finally { - ctx().leave(); - } - } - - void addToClassPath(String url) { - try { - addURL(new URI(url).toURL()); - } catch (MalformedURLException | URISyntaxException ex) { - ex.printStackTrace(); - } - } - - final TruffleObject loadClassObject(String className) throws ClassNotFoundException { - var clazz = loadClass(className); - var clazzValue1 = ctx().asValue(clazz); - var clazzValue2 = clazzValue1.getMember("static"); - ctx().asValue(this).execute(clazzValue2); - return (TruffleObject) value; - } - - @ExportMessage - final Object execute(Object[] values) { - this.value = values[0]; - return this; - } - - @ExportMessage - final boolean isExecutable() { - return true; - } -} diff --git a/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/api/OtherJvmClassLoader.java b/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/api/OtherJvmClassLoader.java new file mode 100644 index 000000000000..5bc3d3f47271 --- /dev/null +++ b/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/api/OtherJvmClassLoader.java @@ -0,0 +1,137 @@ +package org.enso.jvm.interop.api; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.TruffleContext; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.interop.UnknownIdentifierException; +import com.oracle.truffle.api.interop.UnsupportedMessageException; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import org.enso.jvm.channel.Channel; +import org.enso.jvm.channel.JVM; +import org.enso.jvm.interop.impl.OtherJvmMessage; +import org.enso.jvm.interop.impl.OtherJvmPool; +import org.enso.jvm.interop.impl.OtherJvmResult; + +/** + * Class responsible for loading Java classes from other JVM connected via a {@link + * Channel}. + */ +@ExportLibrary(InteropLibrary.class) +public final class OtherJvmClassLoader implements TruffleObject { + private final Channel channel; + + private OtherJvmClassLoader(Channel ch) { + this.channel = ch; + } + + /** + * Creates instance of the class loader. + * + * @param otherJvm normally we run in AOT mode but for debugging purposes we can also emulate the + * connection in a single JVM + * @param ctx own context to execute code in + * @return new instance of the class loader + * @throws IOException + * @throws URISyntaxException + */ + public static OtherJvmClassLoader create(boolean otherJvm, TruffleContext ctx) + throws IOException, URISyntaxException { + var jvm = otherJvm ? initializeJvm() : null; + var ch = Channel.create(jvm, OtherJvmPool.class); + var pool = ch.getConfig(); + pool.onEnterLeave(ctx::enter, ctx::leave); + return new OtherJvmClassLoader(ch); + } + + @ExportMessage + final boolean hasMembers() { + return true; + } + + @ExportMessage + boolean isMemberReadable(String member) { + return true; + } + + @ExportMessage + boolean isMemberInvocable(String member) { + return "addPath".equals(member); + } + + @ExportMessage + final Object getMembers(boolean includeInternal) { + return this; + } + + @ExportMessage + final TruffleObject readMember(String name) throws UnknownIdentifierException { + try { + return loadClass(name); + } catch (ClassNotFoundException ex) { + throw UnknownIdentifierException.create(name, ex); + } + } + + @ExportMessage + final TruffleObject invokeMember(String name, Object[] args) + throws UnknownIdentifierException, UnsupportedMessageException { + if (!"addPath".equals(name)) { + throw UnknownIdentifierException.create(name); + } else { + var path = InteropLibrary.getUncached().asString(args[0]); + addToClassPath(path); + } + return this; + } + + @CompilerDirectives.TruffleBoundary + private final TruffleObject loadClass(String name) throws ClassNotFoundException { + var result = channel.execute(OtherJvmResult.class, new OtherJvmMessage.LoadClass(name)); + return result.value(); + } + + @CompilerDirectives.TruffleBoundary + private final void addToClassPath(String path) { + channel.execute(Void.class, new OtherJvmMessage.AddToClassPath(path)); + } + + private static JVM initializeJvm() throws IOException, URISyntaxException { + var home = System.getProperty("java.home"); + if (home == null) { + throw new IOException("No java.home specified"); + } + var javaHome = new File(home); + if (!javaHome.exists()) { + throw new IOException("JVM doesn't exists: " + javaHome); + } + var loc = OtherJvmClassLoader.class.getProtectionDomain().getCodeSource().getLocation(); + var component = new File(loc.toURI().resolve("..")).getAbsoluteFile(); + if (!component.getName().equals("component")) { + component = new File(component, "component"); + } + var commandAndArgs = new ArrayList(); + var assertsOn = false; + assert assertsOn = true; + if (assertsOn) { + commandAndArgs.add("-ea"); + } + commandAndArgs.add("--sun-misc-unsafe-memory-access=allow"); + commandAndArgs.add("-Dpolyglot.engine.WarnInterpreterOnly=false"); + commandAndArgs.add("-Dtruffle.UseFallbackRuntime=true"); + commandAndArgs.add("--enable-native-access=org.graalvm.truffle"); + commandAndArgs.add("--enable-native-access=org.enso.jvm.channel"); + commandAndArgs.add("--add-opens=java.base/java.nio=ALL-UNNAMED"); + if (!component.isDirectory()) { + throw new IOException("Cannot find " + component + " directory"); + } + commandAndArgs.add("--module-path=" + component.getPath()); + commandAndArgs.add("-Djdk.module.main=org.enso.jvm.interop"); + return JVM.create(javaHome, commandAndArgs.toArray(new String[0])); + } +} diff --git a/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmException.java b/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmException.java new file mode 100644 index 000000000000..07d5f1c8b7da --- /dev/null +++ b/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmException.java @@ -0,0 +1,7 @@ +package org.enso.jvm.interop.impl; + +final class OtherJvmException extends RuntimeException { + OtherJvmException(String message) { + super(message); + } +} diff --git a/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmLoader.java b/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmLoader.java new file mode 100644 index 000000000000..b1a0f0e6449e --- /dev/null +++ b/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmLoader.java @@ -0,0 +1,55 @@ +package org.enso.jvm.interop.impl; + +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.HostAccess; + +/** Handles classloading in the "slave" JVM. */ +@ExportLibrary(value = InteropLibrary.class) +final class OtherJvmLoader extends URLClassLoader implements TruffleObject { + final Context ctx; + private Object value; + + OtherJvmLoader() { + super(new URL[0]); + ctx = + Context.newBuilder("host") // no dynamic languages needed + .allowHostAccess(HostAccess.ALL) // all public members + .allowExperimentalOptions(true) // to survive any -Dpolyglot options + .build(); + } + + void addToClassPath(String file) { + try { + addURL(new File(file).toURI().toURL()); + } catch (MalformedURLException ex) { + ex.printStackTrace(); + } + } + + final TruffleObject loadClassObject(String className) throws ClassNotFoundException { + var clazz = loadClass(className); + var clazzValue1 = ctx.asValue(clazz); + var clazzValue2 = clazzValue1.getMember("static"); + ctx.asValue(this).execute(clazzValue2); + return (TruffleObject) value; + } + + @ExportMessage + final Object execute(Object[] values) { + this.value = values[0]; + return this; + } + + @ExportMessage + final boolean isExecutable() { + return true; + } +} diff --git a/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/OtherJvmMessage.java b/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmMessage.java similarity index 65% rename from lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/OtherJvmMessage.java rename to lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmMessage.java index 255db85a4444..00d40cce5a5c 100644 --- a/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/OtherJvmMessage.java +++ b/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmMessage.java @@ -1,16 +1,23 @@ -package org.enso.jvm.interop; +package org.enso.jvm.interop.impl; +import com.oracle.truffle.api.exception.AbstractTruffleException; +import com.oracle.truffle.api.interop.ExceptionType; import com.oracle.truffle.api.interop.InteropLibrary; import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.interop.UnknownIdentifierException; import com.oracle.truffle.api.interop.UnsupportedMessageException; import com.oracle.truffle.api.library.Message; import com.oracle.truffle.api.library.ReflectionLibrary; import java.io.IOException; import java.math.BigInteger; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneId; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Function; import org.enso.jvm.channel.Channel; import org.enso.persist.Persistable; @@ -18,7 +25,7 @@ /** Sends a message to the other side with ReflectionLibrary-like arguments. */ @Persistable(id = 81901) -record OtherJvmMessage(long id, Message message, List args) +public record OtherJvmMessage(long id, Message message, List args) implements Function< Channel, OtherJvmResult> { @Persistable(id = 81908, allowInlining = false) @@ -29,7 +36,24 @@ static ReturnValue create(T value) { } @Persistable(id = 81909, allowInlining = false) - record ThrowException(int kind, String msg) + record ThrowValue(Optional msg, TruffleObject exception) + implements OtherJvmResult { + @Override + @SuppressWarnings("unchecked") + public T value() throws E { + var ex = exception(); + var msg = msg().isPresent() ? msg().get() : null; + assert InteropLibrary.getUncached().isException(ex); + if (ex instanceof AbstractTruffleException truffleEx) { + throw truffleEx; + } else { + throw new OtherJvmTruffleException(msg, (OtherJvmObject) ex); + } + } + } + + @Persistable(id = 81910, allowInlining = false) + record ThrowException(int kind, Optional msg) implements OtherJvmResult { private static final Map, Integer> kinds; @@ -37,20 +61,33 @@ record ThrowException(int kind, String msg) kinds = new LinkedHashMap<>(); kinds.put(ClassNotFoundException.class, 1); kinds.put(UnsupportedMessageException.class, 2); + kinds.put(UnknownIdentifierException.class, 3); } - static ThrowException create(E ex) { - var kind = kinds.getOrDefault(ex.getClass(), 0); - return new ThrowException<>(kind, ex.getMessage()); + @SuppressWarnings("unchecked") + static OtherJvmResult create(E ex) { + var msg = Optional.ofNullable(ex.getMessage()); + if (ex instanceof OtherJvmTruffleException truffleEx) { + var original = truffleEx.delegate; + return new ThrowValue<>(msg, original); + } else if (InteropLibrary.getUncached().isException(ex) + && ex instanceof TruffleObject truffleEx) { + return new ThrowValue<>(msg, truffleEx); + } else { + var kind = kinds.getOrDefault(ex.getClass(), 0); + return new ThrowException<>(kind, msg); + } } @Override @SuppressWarnings("unchecked") public V value() throws E { + var msg = msg().isPresent() ? msg().get() : null; switch (kind) { - case 1 -> throw (E) new ClassNotFoundException(msg()); + case 1 -> throw (E) new ClassNotFoundException(msg); case 2 -> throw (E) UnsupportedMessageException.create(); - default -> throw new OtherJvmException(msg()); + case 3 -> throw (E) UnknownIdentifierException.create(msg); + default -> throw new OtherJvmException(msg); } } } @@ -59,33 +96,32 @@ public V value() throws E { @Override public OtherJvmResult apply(Channel t) { - var res = t.getConfig().loader.withCtx(() -> handle(t)); - return res; - } - - private OtherJvmResult handle(Channel t) { + var node = ReflectionLibrary.getUncached(); + var prev = t.getConfig().enter(t.isMaster(), node); try { var receiver = t.getConfig().findObject(id); assert receiver instanceof TruffleObject; if (message == IS_IDENTICAL) { args.set(1, InteropLibrary.getUncached()); } - var res = ReflectionLibrary.getUncached().send(receiver, message, args.toArray()); + var res = node.send(receiver, message, args.toArray()); return new ReturnValue<>(res); } catch (Exception ex) { return ThrowException.create(ex); + } finally { + t.getConfig().leave(t.isMaster(), node, prev); } } @Persistable(id = 81905) - record LoadClass(String name) + public record LoadClass(String name) implements Function< Channel, OtherJvmResult> { @Override public OtherJvmResult apply(Channel t) { assert !t.isMaster() : "Class loading only works on the slave side!"; try { - var clazzRaw = t.getConfig().loader.loadClassObject(name); + var clazzRaw = t.getConfig().loadClassObject(t.isMaster(), name); return ReturnValue.create(clazzRaw); } catch (ClassNotFoundException ex) { return ThrowException.create(ex); @@ -94,10 +130,10 @@ public OtherJvmResult apply(Channel, Void> { + public record AddToClassPath(String path) implements Function, Void> { @Override public Void apply(Channel t) { - t.getConfig().loader.addToClassPath(url); + t.getConfig().addToClassPath(t.isMaster(), path); return null; } } @@ -378,4 +414,104 @@ protected BigInteger readObject(Input in) throws IOException, ClassNotFoundExcep return new BigInteger(arr); } } + + @Persistable(id = 121, clazz = ExceptionType.class) + static final class OtherMessages {} + + @Persistable(id = 122) + static final class PersistLocalDate extends Persistance { + + public PersistLocalDate() { + super(LocalDate.class, false, 122); + } + + @Override + protected void writeObject(LocalDate obj, Output out) throws IOException { + out.writeInt(obj.getYear()); + out.writeByte(obj.getMonthValue()); + out.writeByte(obj.getDayOfMonth()); + } + + @Override + protected LocalDate readObject(Input in) throws IOException, ClassNotFoundException { + var year = in.readInt(); + var month = in.readByte(); + var day = in.readByte(); + return LocalDate.of(year, month, day); + } + } + + @Persistable(id = 123) + static final class PersistLocalTime extends Persistance { + + public PersistLocalTime() { + super(LocalTime.class, false, 123); + } + + @Override + protected void writeObject(LocalTime obj, Output out) throws IOException { + out.writeByte(obj.getHour()); + out.writeByte(obj.getMinute()); + out.writeByte(obj.getSecond()); + out.writeInt(obj.getNano()); + } + + @Override + protected LocalTime readObject(Input in) throws IOException, ClassNotFoundException { + var hour = in.readByte(); + var minute = in.readByte(); + var second = in.readByte(); + var nano = in.readInt(); + return LocalTime.of(hour, minute, second, nano); + } + } + + @Persistable(id = 124) + static final class PersistZoneId extends Persistance { + + public PersistZoneId() { + super(ZoneId.class, true, 124); + } + + @Override + protected void writeObject(ZoneId obj, Output out) throws IOException { + out.writeUTF(obj.getId()); + } + + @Override + protected ZoneId readObject(Input in) throws IOException, ClassNotFoundException { + var id = in.readUTF(); + return ZoneId.of(id); + } + } + + @Persistable(id = 125) + static final class PersistOptional extends Persistance { + + public PersistOptional() { + super(Optional.class, true, 125); + } + + @Override + protected void writeObject(Optional obj, Output out) throws IOException { + if (obj.isEmpty()) { + out.writeBoolean(false); + } else { + out.writeBoolean(true); + out.writeObject(obj.get()); + } + } + + @Override + protected Optional readObject(Input in) throws IOException, ClassNotFoundException { + var is = in.readBoolean(); + if (is) { + var obj = in.readObject(); + assert obj != null; + return Optional.of(obj); + } else { + return Optional.empty(); + } + } + } } diff --git a/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/OtherJvmObject.java b/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmObject.java similarity index 83% rename from lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/OtherJvmObject.java rename to lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmObject.java index 154e7f476978..546be8d54a0c 100644 --- a/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/OtherJvmObject.java +++ b/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmObject.java @@ -1,4 +1,4 @@ -package org.enso.jvm.interop; +package org.enso.jvm.interop.impl; import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.interop.InteropLibrary; @@ -18,9 +18,15 @@ final class OtherJvmObject implements TruffleObject { /** special message */ private static final Message HAS_LANGUAGE = Message.resolve(InteropLibrary.class, "hasLanguage"); + private static final Message GET_LANGUAGE = Message.resolve(InteropLibrary.class, "getLanguage"); + private static final Message IS_IDENTICAL_OR_UNDEFINED = Message.resolve(InteropLibrary.class, "isIdenticalOrUndefined"); private static final Message IS_IDENTICAL = Message.resolve(InteropLibrary.class, "isIdentical"); + private static final Message HAS_SOURCE_LOCATION = + Message.resolve(InteropLibrary.class, "hasSourceLocation"); + private static final Message GET_SOURCE_LOCATION = + Message.resolve(InteropLibrary.class, "getSourceLocation"); private final Channel channel; private final long id; @@ -51,6 +57,9 @@ Object send(Message message, Object[] args) throws Exception { } if (message.getLibraryClass() != InteropLibrary.class || HAS_LANGUAGE == message + || GET_LANGUAGE == message + || HAS_SOURCE_LOCATION == message + || GET_SOURCE_LOCATION == message || IS_IDENTICAL_OR_UNDEFINED == message) { // we need to invoke default implementation of library // to handle the message in a proper way diff --git a/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/OtherJvmPool.java b/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmPool.java similarity index 59% rename from lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/OtherJvmPool.java rename to lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmPool.java index fb45e4051f47..ebf44e66c11b 100644 --- a/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/OtherJvmPool.java +++ b/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmPool.java @@ -1,8 +1,11 @@ -package org.enso.jvm.interop; +package org.enso.jvm.interop.impl; import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.nodes.Node; import java.util.HashMap; import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Function; import org.enso.jvm.channel.Channel; import org.enso.persist.Persistance; @@ -11,7 +14,16 @@ public final class OtherJvmPool extends Channel.Config { private final Map objectsById = new HashMap<>(); /** context to use when entering tests */ - final TruffleClassLoader loader = new TruffleClassLoader(); + private OtherJvmLoader loader; + + private Function onEnter; + private BiConsumer onLeave; + + /** Master Channel can be associated with actions on enter and on leave. */ + public final void onEnterLeave(Function onEnter, BiConsumer onLeave) { + this.onEnter = onEnter; + this.onLeave = onLeave; + } private synchronized long registerObject(TruffleObject obj) { var size = objectsById.size() + 1; @@ -58,6 +70,11 @@ public final Persistance.Pool createPool(Channel channel) { // them know it is theirs by using negative ID yield new OtherJvmObject(null, -other.id()); } + case OtherJvmTruffleException ex -> { + // unwrap the exception to object reference + // and send it back as regular OtherJvmObject + yield new OtherJvmObject(null, -ex.delegate.id()); + } case TruffleObject foreign -> { var id = registerObject(foreign); // our own truffle objects send to the other side should @@ -69,4 +86,43 @@ public final Persistance.Pool createPool(Channel channel) { }); return withReadAndWrite; } + + Object enter(boolean master, Node node) { + if (master) { + if (onEnter != null) { + return onEnter.apply(node); + } + } else { + loader(master).ctx.enter(); + } + return null; + } + + void leave(boolean master, Node node, Object prev) { + if (master) { + if (onLeave != null) { + onLeave.accept(node, prev); + } + } else { + loader(master).ctx.leave(); + } + } + + void addToClassPath(boolean master, String file) { + loader(master).addToClassPath(file); + } + + final TruffleObject loadClassObject(boolean master, String className) + throws ClassNotFoundException { + var clazz = loader(master).loadClassObject(className); + return clazz; + } + + private final synchronized OtherJvmLoader loader(boolean master) { + assert !master : "Cannot handle classloading in master, only in slave"; + if (loader == null) { + loader = new OtherJvmLoader(); + } + return loader; + } } diff --git a/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/OtherJvmResult.java b/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmResult.java similarity index 61% rename from lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/OtherJvmResult.java rename to lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmResult.java index a2836b417dd4..6ddd01836e3d 100644 --- a/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/OtherJvmResult.java +++ b/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmResult.java @@ -1,4 +1,4 @@ -package org.enso.jvm.interop; +package org.enso.jvm.interop.impl; /** * Interface describing a possible reply from the other JVM. @@ -6,8 +6,10 @@ * @param the type of result, when the operation succeeds * @param the type of exception when the operation fails */ -sealed interface OtherJvmResult // Either R or E - permits OtherJvmMessage.ReturnValue, OtherJvmMessage.ThrowException { +public sealed interface OtherJvmResult // Either R or E + permits OtherJvmMessage.ReturnValue, + OtherJvmMessage.ThrowValue, + OtherJvmMessage.ThrowException { /** * Either returns the computed result or throws exception. * diff --git a/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmTruffleException.java b/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmTruffleException.java new file mode 100644 index 000000000000..5b122cabefcc --- /dev/null +++ b/lib/java/jvm-interop/src/main/java/org/enso/jvm/interop/impl/OtherJvmTruffleException.java @@ -0,0 +1,16 @@ +package org.enso.jvm.interop.impl; + +import com.oracle.truffle.api.exception.AbstractTruffleException; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.library.ExportLibrary; + +@ExportLibrary(delegateTo = "delegate", value = InteropLibrary.class) +final class OtherJvmTruffleException extends AbstractTruffleException { + final OtherJvmObject delegate; + + OtherJvmTruffleException(String message, OtherJvmObject delegate) { + super(message); + assert delegate != null && InteropLibrary.getUncached().isException(delegate); + this.delegate = delegate; + } +} diff --git a/lib/java/jvm-interop/src/main/resources/META-INF/native-image/org/enso/jvm/interop/reflect-config.json b/lib/java/jvm-interop/src/main/resources/META-INF/native-image/org/enso/jvm/interop/reflect-config.json index 3182340809dc..f802217e27bb 100644 --- a/lib/java/jvm-interop/src/main/resources/META-INF/native-image/org/enso/jvm/interop/reflect-config.json +++ b/lib/java/jvm-interop/src/main/resources/META-INF/native-image/org/enso/jvm/interop/reflect-config.json @@ -1,6 +1,6 @@ [ { - "name":"org.enso.jvm.interop.OtherJvmPool", + "name":"org.enso.jvm.interop.impl.OtherJvmPool", "queryAllPublicMethods":true, "methods":[{"name":"","parameterTypes":[] }] } diff --git a/lib/java/jvm-interop/src/test/java/org/enso/jvm/interop/OtherJvmObjectTest.java b/lib/java/jvm-interop/src/test/java/org/enso/jvm/interop/impl/OtherJvmObjectTest.java similarity index 96% rename from lib/java/jvm-interop/src/test/java/org/enso/jvm/interop/OtherJvmObjectTest.java rename to lib/java/jvm-interop/src/test/java/org/enso/jvm/interop/impl/OtherJvmObjectTest.java index 3228eb1f9104..d555457e78eb 100644 --- a/lib/java/jvm-interop/src/test/java/org/enso/jvm/interop/OtherJvmObjectTest.java +++ b/lib/java/jvm-interop/src/test/java/org/enso/jvm/interop/impl/OtherJvmObjectTest.java @@ -1,4 +1,4 @@ -package org.enso.jvm.interop; +package org.enso.jvm.interop.impl; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; @@ -33,7 +33,16 @@ public class OtherJvmObjectTest { @BeforeClass public static void initializeChannel() { CHANNEL = Channel.create(null, OtherJvmPool.class); - CHANNEL.getConfig().loader.assignCtx(ctx.context()); + CHANNEL + .getConfig() + .onEnterLeave( + (__) -> { + ctx.context().enter(); + return null; + }, + (__, ___) -> { + ctx.context().leave(); + }); } @Test @@ -209,7 +218,7 @@ public static void callback(Consumer cb, Object value) { } @Test - public void callback() throws Exception { + public void testCallback() throws Exception { class MockProxy implements ProxyExecutable { private Value last; @@ -233,10 +242,11 @@ final void assertArgs(String msg, Value exp) { var mockValue = ctx.asValue(mock); var localClass = ctx.asValue(OtherJvmObjectTest.class).getMember("static"); + var otherClass = loadOtherJvmClass(OtherJvmObjectTest.class.getName()); + localClass.invokeMember("callback", mockValue, "Real"); mock.assertArgs("Called with Real", ctx.asValue("Real")); - var otherClass = loadOtherJvmClass(OtherJvmObjectTest.class.getName()); otherClass.invokeMember("callback", mockValue, "RealOther"); mock.assertArgs("Called with Real", ctx.asValue("RealOther")); } diff --git a/lib/java/persistance-dsl/src/main/java/org/enso/persist/impl/PersistableProcessor.java b/lib/java/persistance-dsl/src/main/java/org/enso/persist/impl/PersistableProcessor.java index 6d2d99dca8df..b5bfe1c93f70 100644 --- a/lib/java/persistance-dsl/src/main/java/org/enso/persist/impl/PersistableProcessor.java +++ b/lib/java/persistance-dsl/src/main/java/org/enso/persist/impl/PersistableProcessor.java @@ -182,13 +182,13 @@ public int compare(Object a, Object b) { .collect(Collectors.toList()); ExecutableElement cons; - Element singleton; + List singletonFields; if (constructors.isEmpty()) { - var singletonFields = + singletonFields = typeElem.getEnclosedElements().stream() .filter( e -> - e.getKind() == ElementKind.FIELD + e.getKind().isField() && e.getModifiers().contains(Modifier.STATIC) && isVisibleFrom(e, orig)) .filter(e -> tu.isSameType(e.asType(), typeElem.asType())) @@ -200,11 +200,10 @@ && isVisibleFrom(e, orig)) Kind.ERROR, "There should be exactly one constructor in " + typeElem, orig); return false; } - singleton = singletonFields.get(0); cons = null; } else { cons = (ExecutableElement) constructors.get(0); - singleton = null; + singletonFields = null; if (constructors.size() > 1) { var snd = (ExecutableElement) constructors.get(1); if (richerConstructor.compare(cons, snd) == 0) { @@ -302,11 +301,29 @@ && isVisibleFrom(e, orig)) w.append("\n"); w.append(" );\n"); } else { - w.append(" return ") - .append(typeElemName) - .append(".") - .append(singleton.getSimpleName()) - .append(";\n"); + if (singletonFields.size() == 1) { + var singleton = singletonFields.get(0); + w.append(" return ") + .append(typeElemName) + .append(".") + .append(singleton.getSimpleName()) + .append(";\n"); + } else { + w.append(" return switch (in.readByte()) {\n"); + for (var i = 0; i < singletonFields.size(); i++) { + var singleton = singletonFields.get(i); + w.append( + " case " + + i + + " -> " + + typeElemName + + "." + + singleton.getSimpleName() + + ";\n"); + } + w.append(" default -> throw new IOException();\n"); + w.append(" };\n"); + } } w.append(" }\n"); w.append(" @SuppressWarnings(\"unchecked\")\n"); @@ -350,6 +367,22 @@ && isVisibleFrom(e, orig)) .printMessage(Kind.ERROR, "Unsupported primitive type: " + v.asType().getKind()); } } + } else { + if (singletonFields.size() > 1) { + w.append(" var index = -1;\n"); + for (var i = 0; i < singletonFields.size(); i++) { + var singleton = singletonFields.get(i); + w.append( + " if (obj == " + + typeElemName + + "." + + singleton.getSimpleName() + + ") index = " + + i + + ";\n"); + } + w.append(" out.write(index);\n"); + } } w.append(" }\n"); w.append("}\n"); diff --git a/lib/java/persistance/src/test/java/org/enso/persist/PersistanceTest.java b/lib/java/persistance/src/test/java/org/enso/persist/PersistanceTest.java index 7d8ca4a05631..d99d478d8059 100644 --- a/lib/java/persistance/src/test/java/org/enso/persist/PersistanceTest.java +++ b/lib/java/persistance/src/test/java/org/enso/persist/PersistanceTest.java @@ -292,4 +292,21 @@ public void testNullReference() throws Exception { var inner1 = loaded1.y().get(LongerLoop2.class); assertSame("The reference points to null", null, inner1); } + + @Test + public void testEnumPersistance() throws Exception { + var yes = serde(Logical.class, Logical.YES, -1); + assertSame(yes, Logical.YES); + var no = serde(Logical.class, Logical.NO, -1); + assertSame(no, Logical.NO); + var maybe = serde(Logical.class, Logical.MAYBE, -1); + assertSame(maybe, Logical.MAYBE); + } + + @Persistable(id = 432442) + public enum Logical { + YES, + NO, + MAYBE; + } } diff --git a/lib/java/test-utils/src/main/java/org/enso/test/utils/ContextUtils.java b/lib/java/test-utils/src/main/java/org/enso/test/utils/ContextUtils.java index 8009cfe1fbf9..700e38b2cb74 100644 --- a/lib/java/test-utils/src/main/java/org/enso/test/utils/ContextUtils.java +++ b/lib/java/test-utils/src/main/java/org/enso/test/utils/ContextUtils.java @@ -76,9 +76,8 @@ private ContextUtils( } /** - * The created builder starts with the default context. The default context is - * roughly equivalent to the one that is created for standard command line execution via engine - * runner. + * The created builder starts with the default context. The default context is roughly + * equivalent to the one that is created for standard command line execution via engine runner. * * @param permittedLanguages List of languages that are allowed to be used in the context. If * empty, all installed languages are enabled. diff --git a/project/WithDebugCommand.scala b/project/WithDebugCommand.scala index d8257ee47e19..4a6a470997ed 100644 --- a/project/WithDebugCommand.scala +++ b/project/WithDebugCommand.scala @@ -85,7 +85,8 @@ object WithDebugCommand { Seq(DEBUG_OPTION) else Seq() val javaOpts: Seq[String] = Seq( - truffleNoBackgroundCompilationOptions, + if (debugFlags.contains(debuggerOption)) Seq() + else truffleNoBackgroundCompilationOptions, dumpGraphsOpts, showCompilationsOpts, printAssemblyOpts,