Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Use Java release option #2825

Closed
wants to merge 7 commits into from
Closed

Use Java release option #2825

wants to merge 7 commits into from

Conversation

vasilmkd
Copy link
Member

@vasilmkd vasilmkd commented Feb 16, 2022

Example 1

The easy example first, APIs that don't exist on JDK 8 (VarHandle):

package io.vasilev;

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;

public class VarHandles {
    private volatile int n;

    private static final VarHandle N;

    static {
        try {
            N = MethodHandles.lookup().findVarHandle(VarHandles.class, "n", int.class);
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e);
        }
    }
}

Compiling:

javac io/vasilev/VarHandles.java --release 8
io/vasilev/VarHandles.java:4: error: cannot find symbol
import java.lang.invoke.VarHandle;
                       ^
  symbol:   class VarHandle
  location: package java.lang.invoke
io/vasilev/VarHandles.java:9: error: cannot find symbol
    private static final VarHandle N;
                         ^
  symbol:   class VarHandle
  location: class VarHandles
io/vasilev/VarHandles.java:13: error: cannot find symbol
            N = MethodHandles.lookup().findVarHandle(VarHandles.class, "n", int.class);
                                      ^
  symbol:   method findVarHandle(Class<VarHandles>,String,Class<Integer>)
  location: class Lookup
3 errors

Example 2

Our favorite example, ByteBuffer#flip() on JDK 9+ overrides Buffer#flip() with a more specific type.

  • On JDK 8 we have:

    1. Buffer.flip() has the JVM signature java/nio/Buffer.flip:()Ljava/nio/Buffer
    2. ByteBuffer.flip() has the JVM signature java/nio/ByteBuffer.flip:()L/java/nio/Buffer
  • On JDK 9+ we have:

    1. Buffer.flip() has the same JVM signature java/nio/Buffer.flip:()Ljava/nio/Buffer
    2. ByteBuffer.flip() has the new JVM signature java/nio/ByteBuffer.flip:()L/java/nio/ByteBuffer and thus cannot be called on JDK 8.

Let's examine the output of javac --relase 8 in this case, when we have an explicit return type in the code.

package io.vasilev;

import java.nio.ByteBuffer;

public class Buffers {

    private static final ByteBuffer buffer = ByteBuffer.allocate(16);

    static {
        ByteBuffer flipped = buffer.flip();
    }
}
io/vasilev/Buffers.java:10: error: incompatible types: Buffer cannot be converted to ByteBuffer
        ByteBuffer flipped = buffer.flip();
                                        ^
1 error

Example 3

Final example, what if we do not have an explicit return type and let Java figure it out. In this case we have to examine the produced bytecode, because javac won't complain in this case, because we have a method in JDK 8 that syntactically matches the code.

package io.vasilev;

import java.nio.ByteBuffer;

public class Buffers {

    private static final ByteBuffer buffer = ByteBuffer.allocate(16);

    static {
        buffer.flip();
    }
}
  1. Let's compile with JDK 17, without the --release 8 option.
javac io/vasilev/Buffers.java  

javap -p -c io.vasilev.Buffers 

Compiled from "Buffers.java"
public class io.vasilev.Buffers {
  private static final java.nio.ByteBuffer buffer;

  public io.vasilev.Buffers();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  static {};
    Code:
       0: bipush        16
       2: invokestatic  #7                  // Method java/nio/ByteBuffer.allocate:(I)Ljava/nio/ByteBuffer;
       5: putstatic     #13                 // Field buffer:Ljava/nio/ByteBuffer;
       8: getstatic     #13                 // Field buffer:Ljava/nio/ByteBuffer;
      11: invokevirtual #19                 // Method java/nio/ByteBuffer.flip:()Ljava/nio/ByteBuffer; <------- notice the new JDK 9+ method with overloaded return type, which cannot be called on JDK 8.
      14: pop
      15: return
}
  1. Let's now compile with JDK 17 with the --release 8 option.
javac io/vasilev/Buffers.java --release 8

javap -p -c io.vasilev.Buffers

Compiled from "Buffers.java"
public class io.vasilev.Buffers {
  private static final java.nio.ByteBuffer buffer;

  public io.vasilev.Buffers();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  static {};
    Code:
       0: bipush        16
       2: invokestatic  #7                  // Method java/nio/ByteBuffer.allocate:(I)Ljava/nio/ByteBuffer;
       5: putstatic     #13                 // Field buffer:Ljava/nio/ByteBuffer;
       8: getstatic     #13                 // Field buffer:Ljava/nio/ByteBuffer;
      11: invokevirtual #19                 // Method java/nio/ByteBuffer.flip:()Ljava/nio/Buffer;  <------ notice that javac used the JDK 8 compatible method
      14: pop
      15: return
}

@vasilmkd vasilmkd marked this pull request as draft February 16, 2022 10:20
@armanbilge
Copy link
Member

Seems like fs2 could benefit from this?

@vasilmkd
Copy link
Member Author

vasilmkd commented Feb 16, 2022

Scala 3

Example 1

package io.vasilev

import java.lang.invoke.{MethodHandles, VarHandle}

object VarHandles {

  @volatile private[this] var n: Int = 0

  private[this] final val N: VarHandle = MethodHandles.lookup().findVarHandle(classOf[VarHandles.type], "n", classOf[Int])
}
scalac io/vasilev/VarHandles.scala --release 8
-- [E006] Not Found Error: io/vasilev/VarHandles.scala:9:29 --------------------
9 |  private[this] final val N: VarHandle = MethodHandles.lookup().findVarHandle(classOf[VarHandles.type], "n", classOf[Int])
  |                             ^^^^^^^^^
  |                             Not found: type VarHandle

longer explanation available when compiling with `-explain`
-- [E008] Not Found Error: io/vasilev/VarHandles.scala:9:64 --------------------
9 |  private[this] final val N: VarHandle = MethodHandles.lookup().findVarHandle(classOf[VarHandles.type], "n", classOf[Int])
  |                                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |value findVarHandle is not a member of java.lang.invoke.MethodHandles#Lookup
2 errors found

Example 2

package io.vasilev

import java.nio.ByteBuffer

object Buffers {
  private[this] final val buffer: ByteBuffer = ByteBuffer.allocate(16)

  val flipped: ByteBuffer = buffer.flip()
}
scalac io/vasilev/Buffers.scala --release 8 
-- [E007] Type Mismatch Error: io/vasilev/Buffers.scala:8:39 -------------------
8 |  val flipped: ByteBuffer = buffer.flip()
  |                            ^^^^^^^^^^^^^
  |                            Found:    java.nio.Buffer
  |                            Required: java.nio.ByteBuffer

longer explanation available when compiling with `-explain`
1 error found

Example 3

package io.vasilev

import java.nio.ByteBuffer

object Buffers {
  private[this] final val buffer: ByteBuffer = ByteBuffer.allocate(16)

  buffer.flip()
}
  1. Compiling without -release 8 on JDK 17
scalac io/vasilev/Buffers.scala
javap -p -c io.vasilev.Buffers\$
Compiled from "Buffers.scala"
public final class io.vasilev.Buffers$ implements java.io.Serializable {
  public static final io.vasilev.Buffers$ MODULE$;

  private io.vasilev.Buffers$();
    Code:
       0: aload_0
       1: invokespecial #13                 // Method java/lang/Object."<init>":()V
       4: return

  public static {};
    Code:
       0: new           #2                  // class io/vasilev/Buffers$
       3: dup
       4: invokespecial #16                 // Method "<init>":()V
       7: putstatic     #18                 // Field MODULE$:Lio/vasilev/Buffers$;
      10: bipush        16
      12: invokestatic  #24                 // Method java/nio/ByteBuffer.allocate:(I)Ljava/nio/ByteBuffer;
      15: astore_0
      16: aload_0
      17: invokevirtual #28                 // Method java/nio/ByteBuffer.flip:()Ljava/nio/ByteBuffer; <----- Notice the new JVM signature that only works on JDK 9+
      20: pop
      21: return

  private java.lang.Object writeReplace();
    Code:
       0: new           #34                 // class scala/runtime/ModuleSerializationProxy
       3: dup
       4: ldc           #2                  // class io/vasilev/Buffers$
       6: invokespecial #37                 // Method scala/runtime/ModuleSerializationProxy."<init>":(Ljava/lang/Class;)V
       9: areturn
}
  1. Compiling with -release 8 on JDK 17
scalac io/vasilev/Buffers.scala -release 8
javap -p -c io.vasilev.Buffers\$
Compiled from "Buffers.scala"
public final class io.vasilev.Buffers$ implements java.io.Serializable {
  public static final io.vasilev.Buffers$ MODULE$;

  private io.vasilev.Buffers$();
    Code:
       0: aload_0
       1: invokespecial #13                 // Method java/lang/Object."<init>":()V
       4: return

  public static {};
    Code:
       0: new           #2                  // class io/vasilev/Buffers$
       3: dup
       4: invokespecial #16                 // Method "<init>":()V
       7: putstatic     #18                 // Field MODULE$:Lio/vasilev/Buffers$;
      10: bipush        16
      12: invokestatic  #24                 // Method java/nio/ByteBuffer.allocate:(I)Ljava/nio/ByteBuffer;
      15: astore_0
      16: aload_0
      17: invokevirtual #28                 // Method java/nio/ByteBuffer.flip:()Ljava/nio/Buffer; <------ Notice the old JVM signature that works on JDK 8
      20: pop
      21: return

  private java.lang.Object writeReplace();
    Code:
       0: new           #34                 // class scala/runtime/ModuleSerializationProxy
       3: dup
       4: ldc           #2                  // class io/vasilev/Buffers$
       6: invokespecial #37                 // Method scala/runtime/ModuleSerializationProxy."<init>":(Ljava/lang/Class;)V
       9: areturn
}

@vasilmkd
Copy link
Member Author

@mpilquist

@vasilmkd
Copy link
Member Author

And with this, I will be deleting JDK 8 from my machine.

@vasilmkd
Copy link
Member Author

scala/scala#6362

@vasilmkd vasilmkd marked this pull request as ready for review February 16, 2022 12:32
@mpilquist
Copy link
Member

mpilquist commented Feb 16, 2022

I don’t think this helps with fs2 where we use newer JDK APIs but want a JAR that’s 8 compatible. It should work for scodec though.

Copy link
Member

@djspiewak djspiewak left a comment

Choose a reason for hiding this comment

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

This is super impressive! Two concerns. First, can we sanity-check the ByteBuffer scenario on Scala 2 as well as Scala 3? Or did you already do that and I missed it? Second, I'd rather see this done as a one-off autoplugin rather than a settings val like this.

build.sbt Outdated
@@ -255,6 +255,32 @@ val jsProjects: Seq[ProjectReference] =
val undocumentedRefs =
jsProjects ++ Seq[ProjectReference](benchmarks, example.jvm, tests.jvm, tests.js)

lazy val releaseSettings = Seq(
Copy link
Member

Choose a reason for hiding this comment

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

This can be achieved more easily by tossing a one-off autoplugin into project/

build.sbt Outdated
.jvmSettings(
javacOptions ++= Seq("-source", "1.8", "-target", "1.8")
javacOptions ++= {
Copy link
Member

Choose a reason for hiding this comment

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

IMO we should add these to the aforementioned autoplugin and apply them to all projects just so we don't accidentally forget to add them in the future.

@vasilmkd
Copy link
Member Author

@djspiewak Please read the first post carefully. ByteBuffer is covered.

@djspiewak
Copy link
Member

@vasilmkd I was mostly concerned about Scala 2, since the OP output looks more like Scala 3's error messages and they have two different backends. If both have been sanity checked though I'm very strongly 👍

@vasilmkd
Copy link
Member Author

I'll post Scala 2 too.

@vasilmkd
Copy link
Member Author

Scala 2

Example 1

package io.vasilev

import java.lang.invoke.{ MethodHandles, VarHandle }

object VarHandles {

  @volatile private[this] var n: Int = 0

  private[this] final val N: VarHandle = MethodHandles.lookup().findVarHandle(classOf[VarHandles.type], "n", classOf[Int])
}
scalac io/vasilev/VarHandles.scala --release 8 
io/vasilev/VarHandles.scala:3: error: object VarHandle is not a member of package invoke
import java.lang.invoke.{ MethodHandles, VarHandle }
       ^
io/vasilev/VarHandles.scala:9: error: not found: type VarHandle
  private[this] final val N: VarHandle = MethodHandles.lookup().findVarHandle(classOf[VarHandles.type], "n", classOf[Int])
                             ^
io/vasilev/VarHandles.scala:9: error: value findVarHandle is not a member of java.lang.invoke.MethodHandles.Lookup
  private[this] final val N: VarHandle = MethodHandles.lookup().findVarHandle(classOf[VarHandles.type], "n", classOf[Int])
                                                                ^
3 errors

Example 2

package io.vasilev

import java.nio.ByteBuffer

object Buffers {
  private[this] final val buffer: ByteBuffer = ByteBuffer.allocate(16)

  val flipped: ByteBuffer = buffer.flip()
}
scalac io/vasilev/Buffers.scala --release 8
io/vasilev/Buffers.scala:8: error: type mismatch;
 found   : java.nio.Buffer
 required: java.nio.ByteBuffer
  val flipped: ByteBuffer = buffer.flip()
                                       ^
1 error

Example 3

package io.vasilev

import java.nio.ByteBuffer

object Buffers {
  private[this] final val buffer: ByteBuffer = ByteBuffer.allocate(16)

  buffer.flip()
}
  1. Compiling without -release 8 on JDK 17
scalac io/vasilev/Buffers.scala
javap -p -c io.vasilev.Buffers\$
Compiled from "Buffers.scala"
public final class io.vasilev.Buffers$ {
  public static final io.vasilev.Buffers$ MODULE$;

  private static final java.nio.ByteBuffer buffer;

  public static {};
    Code:
       0: new           #2                  // class io/vasilev/Buffers$
       3: dup
       4: invokespecial #14                 // Method "<init>":()V
       7: putstatic     #16                 // Field MODULE$:Lio/vasilev/Buffers$;
      10: bipush        16
      12: invokestatic  #22                 // Method java/nio/ByteBuffer.allocate:(I)Ljava/nio/ByteBuffer;
      15: putstatic     #24                 // Field buffer:Ljava/nio/ByteBuffer;
      18: getstatic     #24                 // Field buffer:Ljava/nio/ByteBuffer;
      21: invokevirtual #28                 // Method java/nio/ByteBuffer.flip:()Ljava/nio/ByteBuffer; <------- notice the new JDK 9+ method with overloaded return type, which cannot be called on JDK 8.
      24: pop
      25: return

  private io.vasilev.Buffers$();
    Code:
       0: aload_0
       1: invokespecial #29                 // Method java/lang/Object."<init>":()V
       4: return
}
  1. Compiling with -release 8 on JDK 17
scalac io/vasilev/Buffers.scala -release 8
javap -p -c io.vasilev.Buffers\$
Compiled from "Buffers.scala"
public final class io.vasilev.Buffers$ {
  public static final io.vasilev.Buffers$ MODULE$;

  private static final java.nio.ByteBuffer buffer;

  public static {};
    Code:
       0: new           #2                  // class io/vasilev/Buffers$
       3: dup
       4: invokespecial #14                 // Method "<init>":()V
       7: putstatic     #16                 // Field MODULE$:Lio/vasilev/Buffers$;
      10: bipush        16
      12: invokestatic  #22                 // Method java/nio/ByteBuffer.allocate:(I)Ljava/nio/ByteBuffer;
      15: putstatic     #24                 // Field buffer:Ljava/nio/ByteBuffer;
      18: getstatic     #24                 // Field buffer:Ljava/nio/ByteBuffer;
      21: invokevirtual #28                 // Method java/nio/ByteBuffer.flip:()Ljava/nio/Buffer; <------ notice that scalac used the JDK 8 compatible method
      24: pop
      25: return

  private io.vasilev.Buffers$();
    Code:
       0: aload_0
       1: invokespecial #29                 // Method java/lang/Object."<init>":()V
       4: return
}

@djspiewak
Copy link
Member

Honestly outstanding! I think this means I can delete Java 8

@armanbilge
Copy link
Member

Second, I'd rather see this done as a one-off autoplugin rather than a settings val like this.

cough sbt-typelevel cough 😝

@djspiewak
Copy link
Member

cough sbt-typelevel cough 😝

:-D It's on my "to peruse" list. Still digging out from under everything else for the time being though.

// The release flag controls what JVM platform APIs are called. We only want JDK 8 APIs
// in order to maintain compability with JDK 8.
val releaseFlag =
if (version.startsWith("1.8"))
Copy link
Member Author

Choose a reason for hiding this comment

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

Let's factor this out as it is repeated:
val isJdk8: Boolean = System.getProperty("java.version").startsWith("1.8")

@vasilmkd
Copy link
Member Author

vasilmkd commented Mar 9, 2022

Superseded by #2857.

@vasilmkd vasilmkd closed this Mar 9, 2022
@vasilmkd vasilmkd deleted the java-release branch March 9, 2022 18:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants