scala3
scala3 copied to clipboard
Runtime code implementing lazy val should not use sun.misc.Unsafe on Java 9+
Currently, any usage of a lazy val requires getitng an instance of sun.misc.Unsafe at runtime: https://github.com/lampepfl/dotty/blob/43e4bfa5598e9cdbebb1dc56ed25a319d5aa8fbe/library/src/dotty/runtime/LazyVals.scala#L7 which is problematic for various reasons (e.g. usage of a security manager, using Graal Native (https://github.com/lampepfl/dotty/issues/13985)).
On Java 8, there's no good alternative, but on Java 9+ we should be able to replace that using VarHandle.
~~We should be able to use the same trick used in scala.runtime.Statics to check once at runtime if VarHandle is available to stay compatible with Java 8: https://github.com/scala/scala/blob/a8a726118d06c90b5506f907b1524457c0d401a7/src/library/scala/runtime/Statics.java#L158-L173~~ (EDIT: actually I don't think this is good enough: using VarHandle would require adding static fields in any class that has a lazy val, so we need to know at compile-time the version of Java we support (via -release/-Xtarget)
Here's some prototype target code @smarter paired with me to write:
/*
* Decompiled with CFR 0.152.
*
* Could not load the following classes:
* scala.runtime.LazyVals$
*/
import scala.runtime.LazyVals$;
import java.lang.invoke.VarHandle;
import java.lang.invoke.MethodHandles;
/*
* Illegal identifiers - consider using --renameillegalidents true
*/
public class Decompiled {
// public static final long OFFSET$0 = LazyVals$.MODULE$.getOffsetStatic(LazyVal.package.Foo.1.class.getDeclaredField("bitmap$1"));
// get a varhandle instead of an offset
private static final VarHandle handle;
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
handle = l.findVarHandle(Decompiled.class, "bitmap$1", long.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
public long bitmap$1;
public int value$lzy1;
static final long LAZY_VAL_MASK = 3;
static final long BITS_PER_LAZY_VAL = 2;
public long casHelper(long current, long newState, int ord) {
long mask = ~(LAZY_VAL_MASK << ord * BITS_PER_LAZY_VAL);
return (current & mask) | (newState << (ord * BITS_PER_LAZY_VAL));
}
private boolean CAS(Object t, long current, long newState, int ord) {
return handle.compareAndSet(t, current, casHelper(current, newState, ord));
}
public static void setFlag(Object t, int v, int ord) {
boolean retry = true;
while (retry) {
long cur = handle.getVolatile(t);
if (LazyVals$.MODULE$.STATE(cur, ord) == 1)
retry = !CAS(t, cur, v, ord);
else {
// cur == 2, somebody is waiting on monitor
if (CAS(t, cur, v, ord)) {
Class<?> clazz = LazyVals$.class;
var privateMethod = clazz.getDeclaredMethod("getMonitor", Object.class, int.class);
privateMethod.setAccessible(true);
Object monitor = privateMethod.invokew(LazyVals$.MODULE$, t, ord);
synchronized (monitor) {
monitor.notifyAll();
}
retry = false;
}
}
}
}
public static void wait4Notification(Object t, long cur, int ord) {
boolean retry = true;
while (retry) {
long current = handle.getVolatile(t);
int state = LazyVals$.MODULE$.STATE(current, ord);
if (state == 1)
CAS(t, current, 2, ord);
else if (state == 2) {
Class<?> clazz = LazyVals$.class;
var privateMethod = clazz.getDeclaredMethod("getMonitor", Object.class, int.class);
privateMethod.setAccessible(true);
Object monitor = privateMethod.invokew(LazyVals$.MODULE$, t, ord);
synchronized (monitor) {
if (LazyVal$.MODULE$.STATE(handle.getVolatile(t), ord) == 2) // make sure notification did not happen yet.
monitor.wait();
}
} else {
retry = false;
}
}
}
public int value() {
long l;
long l2;
while ((l2 = LazyVals$.MODULE$.STATE(l = handle.getVolatile(this), 0)) != 3L) {
if (l2 == 0L) {
if (!handle.compareAndSet(this, l, casHelper(l, 1, 0))) continue;
try {
int n;
this.value$lzy1 = n = 13;
handle.
setFlag((Object)this, 3, 0);
return n;
}
catch (Throwable throwable) {
setFlag((Object)this, 0, 0);
throw throwable;
}
}
wait4Notification((Object)this, l, 0);
}
return this.value$lzy1;
}
}
This occurs for me in another context: using jlink
to minimize the JVM foot print for a command line tool. My tool only uses the java.base module, so I want to eliminate 600MB of JDK 21 down to a 95MB JDK to deliver with my application. Since my use of threading exceeds the threading capabilities of Scala Native, I can't use that. The same problem occurs with GraalVM (see #13985). So, I'm blocked from minimizing my tool's footprint. :(
This occurs with Scala 3.3.3.
Also: in response to this from the issue description:
actually I don't think this is good enough: using VarHandle would require adding static fields in any class that has a lazy val, so we need to know at compile-time the version of Java we support (via -release/-Xtarget)
I'd be happy if Scala declared a minimum JVM version to the version that supports adding static fields in any class. Didn't the JVM just add its own bytecode manipulation API? I'm okay with JDK 21 as the minimum for Scala, and I'm sure others are not.
Noting that also comes up when using the new given
keyword. implicit val
and implicit def
do not invoke LazyVals and Unsafe. given
does invoke LazyVals and Unsafe.
OOC, why is this not a problem with lazy vals in 2.12 and 2.13? Is there something fundamentally different about lazy vals in 3.x?
@alexklibisz yes, see https://github.com/scala/scala3/pull/15296 (and linked history stretching at least as far back as https://github.com/scala/scala3/pull/6979)
Note: JEP-471 deprecates memory access methods in sun.misc.Unsafe
for removal.
https://openjdk.org/jeps/471
(thanks to @smarter for raising this)
so I guess you can use some static methodhandles that resolve to the right thing at runtime?