Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
64fd1ac
Sending messages between two JVMs with the help of Persistance.Pool
JaroslavTulach May 30, 2025
2e3911c
ExecuteMainClass message doesn't have a return value
JaroslavTulach May 30, 2025
5b42e1d
Exchange messages between JVMs. Send ReportResult back.
JaroslavTulach Jun 2, 2025
9dc5646
Introducing channel between two JVMs
JaroslavTulach Jun 2, 2025
4d69228
Using RequestFactorial message
JaroslavTulach Jun 2, 2025
1e753eb
Message with a result
JaroslavTulach Jun 2, 2025
d7d547b
Return BigInteger result
JaroslavTulach Jun 2, 2025
879a092
Remove runtime dependency on Lookup
JaroslavTulach Jun 2, 2025
a23cdd5
Back and forth computation of factorial
JaroslavTulach Jun 2, 2025
eeab6a9
Enabling Foreign API support
JaroslavTulach Jun 2, 2025
cf02db1
Moving Channel and Message into their own class
JaroslavTulach Jun 3, 2025
32aa5b0
Handling executeMain the old way to avoid access checks associated wi…
JaroslavTulach Jun 3, 2025
22de1c4
Removing usage of NetBeans Lookup from benchmarks
JaroslavTulach Jun 3, 2025
5e1a8e7
Removing org-openide-util-lookup from componentModulesPaths to fix JP…
JaroslavTulach Jun 3, 2025
e28e6ea
Allocate both peers of the Channel at once
JaroslavTulach Jun 3, 2025
7133830
Look a Channel peer up by its ID
JaroslavTulach Jun 3, 2025
3f16786
Add debugging symbols when ENSO_LAUNCHER=debug
JaroslavTulach Jun 3, 2025
b04963d
Multiple nested callbacks are possible
JaroslavTulach Jun 3, 2025
d11a0cf
Allow implementation of Channel messages as records
JaroslavTulach Jun 3, 2025
42d24f4
Avoid global pool reference when local one is available
JaroslavTulach Jun 3, 2025
21fb777
Load pool from a specified Class supplying it
JaroslavTulach Jun 3, 2025
bc9aab7
Public factory method to create a Channel
JaroslavTulach Jun 3, 2025
4531025
RuntimeVisualizationsTest uses HandlerFactory
JaroslavTulach Jun 4, 2025
fa46a7c
Basic handling of exceptions in the other JVM
JaroslavTulach Jun 4, 2025
0445962
Propagate exceptions back and forth among JVMs
JaroslavTulach Jun 4, 2025
93c6fd5
Allocate the method handle at Channel creation time
JaroslavTulach Jun 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 21 additions & 19 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2141,13 +2141,10 @@ lazy val `persistance` = (project in file("lib/java/persistance"))
Compile / javacOptions := ((Compile / javacOptions).value),
inConfig(Compile)(truffleRunOptionsSettings),
libraryDependencies ++= slf4jApi ++ Seq(
"org.netbeans.api" % "org-openide-util-lookup" % netbeansApiVersion,
"junit" % "junit" % junitVersion % Test,
"com.github.sbt" % "junit-interface" % junitIfVersion % Test
"junit" % "junit" % junitVersion % Test,
"com.github.sbt" % "junit-interface" % junitIfVersion % Test
),
Compile / moduleDependencies ++= slf4jApi ++ Seq(
"org.netbeans.api" % "org-openide-util-lookup" % netbeansApiVersion
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NetBeans Lookup shouldn't be used during runtime: 879a092

)
Compile / moduleDependencies ++= slf4jApi
)
.dependsOn(`persistance-dsl` % Test)

Expand Down Expand Up @@ -3356,12 +3353,12 @@ lazy val `runtime-parser` =
commands += WithDebugCommand.withDebug,
fork := true,
libraryDependencies ++= Seq(
"junit" % "junit" % junitVersion % Test,
"com.github.sbt" % "junit-interface" % junitIfVersion % Test,
"org.scalatest" %% "scalatest" % scalatestVersion % Test
"junit" % "junit" % junitVersion % Test,
"com.github.sbt" % "junit-interface" % junitIfVersion % Test,
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
"org.netbeans.api" % "org-openide-util-lookup" % netbeansApiVersion % "provided"
),
Compile / moduleDependencies ++= Seq(
"org.netbeans.api" % "org-openide-util-lookup" % netbeansApiVersion
),
// Java compiler is not able to correctly find all the annotation processors, because
// one of them is on module-path. To overcome this, we explicitly list all of them here.
Expand Down Expand Up @@ -3643,9 +3640,10 @@ lazy val `runtime-instrument-common` =
"ENSO_TEST_DISABLE_IR_CACHE" -> "false"
),
libraryDependencies ++= Seq(
"junit" % "junit" % junitVersion % Test,
"com.github.sbt" % "junit-interface" % junitIfVersion % Test,
"org.scalatest" %% "scalatest" % scalatestVersion % Test
"junit" % "junit" % junitVersion % Test,
"com.github.sbt" % "junit-interface" % junitIfVersion % Test,
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
"org.netbeans.api" % "org-openide-util-lookup" % netbeansApiVersion % Test
),
javaModuleName := "org.enso.runtime.instrument.common",
Compile / moduleDependencies ++= slf4jApi ++ Seq(
Expand Down Expand Up @@ -3739,12 +3737,11 @@ lazy val `runtime-instrument-runtime-server` =
Compile / forceModuleInfoCompilation := true,
instrumentationSettings,
Compile / moduleDependencies ++= Seq(
"org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion,
"org.graalvm.polyglot" % "polyglot" % graalMavenPackagesVersion,
"org.graalvm.sdk" % "collections" % graalMavenPackagesVersion,
"org.graalvm.sdk" % "word" % graalMavenPackagesVersion,
"org.graalvm.sdk" % "nativeimage" % graalMavenPackagesVersion,
"org.netbeans.api" % "org-openide-util-lookup" % netbeansApiVersion
"org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion,
"org.graalvm.polyglot" % "polyglot" % graalMavenPackagesVersion,
"org.graalvm.sdk" % "collections" % graalMavenPackagesVersion,
"org.graalvm.sdk" % "word" % graalMavenPackagesVersion,
"org.graalvm.sdk" % "nativeimage" % graalMavenPackagesVersion
),
Compile / internalModuleDependencies := Seq(
(`runtime-instrument-common` / Compile / exportedModule).value,
Expand Down Expand Up @@ -4006,6 +4003,7 @@ lazy val `engine-runner` = project
"-H:+AddAllCharsets",
"-H:+IncludeAllLocales",
"-H:+RunReachabilityHandlersConcurrently",
"-H:+ForeignAPISupport",
"-R:-InstallSegfaultHandler",
// Workaround a problem with build-/runtime-initialization conflict
// by disabling this service provider
Expand Down Expand Up @@ -4313,6 +4311,7 @@ lazy val `os-environment` =
),
Compile / internalModuleDependencies ++= Seq(
(`engine-common` / Compile / exportedModule).value,
(`persistance` / Compile / exportedModule).value,
Copy link
Member Author

@JaroslavTulach JaroslavTulach Jun 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

os-environment now contains support for passing Messages via Channel. Messages serde is handled via persistance - hence the dependency.

(`logging-utils` / Compile / exportedModule).value,
(`logging-config` / Compile / exportedModule).value
),
Expand All @@ -4331,6 +4330,7 @@ lazy val `os-environment` =
additionalOptions = Seq(
"-ea",
"--features=org.enso.os.environment.TestCollectorFeature",
"-H:+ForeignAPISupport",
"-R:-InstallSegfaultHandler"
)
)
Expand All @@ -4352,6 +4352,8 @@ lazy val `os-environment` =
.value,
Test / fork := true
)
.dependsOn(`persistance`)
.dependsOn(`persistance-dsl` % "provided")
.dependsOn(`engine-common`)

lazy val `bench-processor` = (project in file("lib/scala/bench-processor"))
Expand Down
5 changes: 0 additions & 5 deletions distribution/engine/THIRD-PARTY/NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -556,11 +556,6 @@ The license file can be found at `licenses/APACHE2.0`.
Copyright notices related to this dependency can be found in the directory `org.netbeans.api.org-netbeans-modules-sampler-RELEASE180`.


'org-openide-util-lookup', licensed under the The Apache Software License, Version 2.0, is distributed with the engine.
The license file can be found at `licenses/APACHE2.0`.
Copyright notices related to this dependency can be found in the directory `org.netbeans.api.org-openide-util-lookup-RELEASE180`.


'reactive-streams', licensed under the CC0, is distributed with the engine.
The license file can be found at `licenses/CC0`.
Copyright notices related to this dependency can be found in the directory `org.reactivestreams.reactive-streams-1.0.3`.
Expand Down

This file was deleted.

1 change: 0 additions & 1 deletion engine/runtime-compiler/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
requires org.enso.syntax;
requires org.enso.scala.wrapper;

requires org.openide.util.lookup.RELEASE180;
requires org.slf4j;

exports org.enso.compiler;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
requires org.enso.engine.common;
requires org.enso.distribution;
requires org.enso.lockmanager;
requires org.openide.util.lookup.RELEASE180;
requires org.graalvm.polyglot;
requires org.graalvm.truffle;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.net.URI;
import java.util.Arrays;
import java.util.Optional;
import java.util.ServiceLoader;
import org.enso.distribution.locking.LockManager;
import org.enso.interpreter.instrument.Handler;
import org.enso.interpreter.instrument.HandlerFactory;
Expand All @@ -25,7 +26,6 @@
import org.graalvm.options.OptionDescriptors;
import org.graalvm.polyglot.io.MessageEndpoint;
import org.graalvm.polyglot.io.MessageTransport;
import org.openide.util.Lookup;

/**
* An instrument exposing a server for other services to connect to, in order to control the current
Expand Down Expand Up @@ -112,8 +112,9 @@ protected void onCreate(Env env) {
if (TruffleOptions.AOT) {
this.handler = HandlerFactoryImpl.create();
} else {
var loadedHandler = Lookup.getDefault().lookup(HandlerFactory.class);
this.handler = loadedHandler != null ? loadedHandler.create() : HandlerFactoryImpl.create();
var loadedHandler = ServiceLoader.load(HandlerFactory.class).findFirst();
this.handler =
loadedHandler.isPresent() ? loadedHandler.get().create() : HandlerFactoryImpl.create();
}

try {
Expand Down
1 change: 1 addition & 0 deletions lib/java/os-environment/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Image.
*/
module org.enso.os.environment {
requires org.enso.persistance;
requires org.enso.engine.common;
requires org.graalvm.nativeimage;
requires org.slf4j;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package org.enso.os.environment.jni;

import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import org.enso.persist.Persistance;
import org.graalvm.nativeimage.CurrentIsolate;
import org.graalvm.nativeimage.ImageInfo;
import org.graalvm.nativeimage.StackValue;
import org.graalvm.nativeimage.c.type.CTypeConversion;

/** Channel connects two {@link JVM} instances. */
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since e28e6ea the channels are allocated in pairs. There is always an SubstrateVM side and HotSpot JVM side of the channel. Right now these two sides are going to have the same ID stored in ID_TO_CHANNEL map.

public final class Channel {

/** persistance pool associated with this channel object */
private final Persistance.Pool pool;

private final JNI.JNIEnv env;
private final long isolate;
private final long callbackFn;

/* private */
Channel(Persistance.Pool pool, JNI.JNIEnv env) {
this.pool = pool;
this.env = env;
this.isolate = -1;
this.callbackFn = -1;
}

/* private */
Channel(Persistance.Pool pool, long isolate, long callbackFn) {
if (ImageInfo.inImageCode()) {
throw new IllegalStateException("Only usable in HotSpot");
}
this.pool = pool;
this.env = null;
this.isolate = isolate;
this.callbackFn = callbackFn;
}

/**
* <em>Executes a message</em> in the other JVM. The message is any subclass of {@link Message}
* registered for persistance via {@link Persistable @Persistable} annotation into the {@link
* Persistance.Pool pool associated with this JVM}. The result (which is of type {@code R}) also
* has to be registered for serde.
*
* @param msg the message that gets serialized, transfered into the other JVM, deserialized on the
* other side and {@link Message#evaluate() evaluated} there
* @param <R> the type of result we expect the message to return
* @return the value gets computed via {@link Message#evaluate()} in the other JVM and then it
* gets serialized and transfered back to us. Deserialized and the value is then returned from
* this method
*/
public final <R> R execute(Message<R> msg) {
if (this.isolate == -1) {
return JVM.executeImpl(pool, msg, memory -> toHotSpotMessage(env, memory));
} else {
java.lang.foreign.MemorySegment fnCallbackAddress = MemorySegment.ofAddress(callbackFn);
java.lang.foreign.FunctionDescriptor fnDescriptor =
FunctionDescriptor.of(
ValueLayout.JAVA_LONG,
ValueLayout.ADDRESS,
ValueLayout.ADDRESS,
ValueLayout.JAVA_LONG);
java.lang.invoke.MethodHandle fnHandle =
Linker.nativeLinker().downcallHandle(fnCallbackAddress, fnDescriptor);
return JVM.executeImpl(
pool,
msg,
seg -> {
Object res = -1L;
try {
java.lang.foreign.MemorySegment isoRef = MemorySegment.ofAddress(isolate);
res = fnHandle.invoke(isoRef, seg, seg.byteSize());
} catch (Throwable ex) {
ex.printStackTrace();
}
return (long) res;
});
}
}

private static long toHotSpotMessage(JNI.JNIEnv e, MemorySegment segment) {
java.lang.String classNameWithSlashes = "org/enso/os/environment/jni/JVMPeer";
java.lang.String methodName = "handle";
try (org.graalvm.nativeimage.c.type.CTypeConversion.CCharPointerHolder classInC =
CTypeConversion.toCString(classNameWithSlashes);
org.graalvm.nativeimage.c.type.CTypeConversion.CCharPointerHolder methodInC =
CTypeConversion.toCString(methodName);
org.graalvm.nativeimage.c.type.CTypeConversion.CCharPointerHolder signatureInC =
CTypeConversion.toCString("(JJJJ)J")) {
org.enso.os.environment.jni.JNINativeInterface fn = e.getFunctions();
org.enso.os.environment.jni.JNI.JClass clazz = fn.getFindClass().call(e, classInC.get());
assert clazz.isNonNull() : "Class not found " + classNameWithSlashes;
org.enso.os.environment.jni.JNI.JMethodID method =
fn.getGetStaticMethodID().call(e, clazz, methodInC.get(), signatureInC.get());
assert method.isNonNull() : "method not found in " + classNameWithSlashes;
long address = segment.address();
assert address > 0 : "We need an address";
org.enso.os.environment.jni.JNI.JValue arg = StackValue.get(4, JNI.JValue.class);
arg.addressOf(0).setLong(CurrentIsolate.getCurrentThread().rawValue());
arg.addressOf(1).setLong(JVM.CALLBACK_FN.getFunctionPointer().rawValue());
arg.addressOf(2).setLong(address);
arg.addressOf(3).setLong(segment.byteSize());
long replySize = fn.getCallStaticLongMethodA().call(e, clazz, method, arg);
return replySize;
}
}

/**
* Subclasses of message denote a computational task to be performed in the "other {@link JVM}".
*
* @param <R> type of the return value
*/
public abstract static class Message<R> {

final Class<R> replyType;

/**
* Constructor for subclasses. Use it as {@code super(Integer.class)} to specify the reply type
* of the exception which is then returned from the {@link #evaluate} method.
*
* @param replyType the type of the reply
*/
protected Message(Class<R> replyType) {
this.replyType = replyType;
}

/**
* Handles evaluation of the exception. Use {@link Channel#execute} to pass this messages to the
* other {@link JVM}. After all the serde and transfer to the other {@link JVM} this method is
* executed to perform its operation. Then the result is passed back via serde again and
* returned from the {@link Channel#execute} method.
*
* @param channel allows sending messages to the other JVM
* @return the result of the evaluation or {@code null}
* @throws Throwable the computation may yield exceptions or errors which are then transferred
* back to the callee JVM
*/
protected abstract R evaluate(Channel channel) throws Throwable;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
package org.enso.os.environment.jni;

import java.io.File;
import java.io.IOException;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.function.Function;
import org.enso.common.Platform;
import org.enso.persist.Persistance;
import org.graalvm.nativeimage.IsolateThread;
import org.graalvm.nativeimage.StackValue;
import org.graalvm.nativeimage.UnmanagedMemory;
import org.graalvm.nativeimage.c.function.CEntryPoint;
import org.graalvm.nativeimage.c.function.CEntryPointLiteral;
import org.graalvm.nativeimage.c.function.CFunctionPointer;
import org.graalvm.nativeimage.c.struct.SizeOf;
import org.graalvm.nativeimage.c.type.CCharPointer;
import org.graalvm.nativeimage.c.type.CTypeConversion;
import org.graalvm.word.WordFactory;

Expand Down Expand Up @@ -54,13 +64,47 @@ public static JVM create(File javaHome, String... options) {
return new JVM(createJvmFn, jvmArgs.toArray(new String[0]));
}

static <R> R executeImpl(
Persistance.Pool pool, Channel.Message<R> msg, Function<MemorySegment, Long> send) {
try (var arena = Arena.ofConfined()) {
var bytes = pool.write(msg, null);
var memory = arena.allocate(Math.max(bytes.length, 4096));
memory.copyFrom(MemorySegment.ofArray(bytes));
long len = send.apply(memory);
assert len >= 0;
var reply = memory.asByteBuffer();
reply.position(0);
reply.limit((int) len);
var result = pool.read(reply, null);
return result.get(msg.replyType);
} catch (IOException ex) {
throw new IllegalStateException(ex);
}
}

@CEntryPoint
private static long acceptRequestFromHotSpotJvm(
IsolateThread threadId, CCharPointer data, long size) throws Throwable {
// TBD: recursive calls will need to find a proper channel
var len = JVMPeer.handleWithChannel(null, data.rawValue(), size);
return len;
}

static final CEntryPointLiteral<CFunctionPointer> CALLBACK_FN =
CEntryPointLiteral.create(
JVM.class,
"acceptRequestFromHotSpotJvm",
IsolateThread.class,
CCharPointer.class,
long.class);

/**
* Executes main method of provided class
*
* @param classNameWithSlashes class (with `/` as separators) to search main method in
* @param args arguments to pass to the main method
*/
public void executeMain(String classNameWithSlashes, String... args) {
public final void executeMain(String classNameWithSlashes, String... args) {
var e = env();
try (var className = CTypeConversion.toCString(classNameWithSlashes);
var mainName = CTypeConversion.toCString("main");
Expand Down
Loading
Loading