きしだのはてな このページをアンテナに追加 RSSフィード

2018-04-23(月) Hyper-Vで仮想スイッチを作るとWavesのオーディオプラグインが死ぬ

Hyper-Vで仮想スイッチを作るとWavesのオーディオプラグインが死ぬ 05:38  Hyper-Vで仮想スイッチを作るとWavesのオーディオプラグインが死ぬを含むブックマーク

Hyper-Vで仮想スイッチを作るとWavesのライセンスが無効になってしまいます。


どうも、WavesのライセンスはMACアドレスと結びつけているようで、Hyper-Vで別のMacアドレスのネットワークができることで別デバイスと認識されてしまうらしい。

Waves License Center(WLC)でライセンスが無効になる ( その他趣味 ) - 音響・映像・電気設備が好き - Yahoo!ブログ


ということで、追加した仮想スイッチを一旦削除するとライセンスが復活。そこでライセンスをクラウドに退避、仮想スイッチを作り直してライセンスを結び付けることで復活しました。


恐らくVirtualBoxなどで仮想ネットワークカードを作っても同じではないかと。

気をつけよう。

あと、これを利用して、常に同じMACアドレスの仮想スイッチと結びつくようにしておけば、マシンが変わってもライセンスを引き継げるぽい。

2018-04-22(日) GraalVMでRust動かしたりレイトレをネイティブコンパイルしたり

[][]GraalVMでRust動かしたりレイトレをネイティブコンパイルしたり 02:17 GraalVMでRust動かしたりレイトレをネイティブコンパイルしたりを含むブックマーク

GraalVMが正式にリリースされました。結構話題になってますね。

GraalVMは、JavaベースJITとAoT、そしてASTエンジンTruffleの複合体です。(かな?)

GraalVM


ということで、Rust動かしたりJavaで書いたレイトレコードをネイティブコンパイルしたりしてみました。


Hyper-VUbuntuを用意する

ほんとはWindows Subsystem of Linux(WSL)でやりたかったのだけど、WSL上でJavaがちゃんと動いてくれなかったのであきらめました。

で、VirtualBox使うかなと思ったけど、Hyper-Vを無効にしないといけなくて、Hyper-Vを無効にするとDockerが動かなくなるのでやだなーと思ってたのだけど、普通にHyper-VUbuntuたちあげればいいのではーと思ってやってみました。


普通に使えますが、画面サイズ調整やフォルダ共有、クリップボード共有など、VirtualBoxのほうが使いやすいですね。

Macの場合はCE版ではなくOracle版を使うといいと思います。


RustをGraalVMで動かす

ASTエンジンTruffleというのは、プログラム言語を構文木に落として、最適化・実行する処理エンジンです。

JavaバイトコードLLVMのビットコードよりも抽象度が高く言語の構造が残っているため、最適化がやりやすいということを狙ってます。(たぶん)

で、そこでLLVMのビットコードをTruffleコードに変換するというSulongというツールもあるので、Rustも動きます。ライブラリなんかは、x86コードをLLVMビットコードに変換してTruffleコードに変換して動かすらしい。変態。


で、こんな感じでRustコードをmain.rsという名前で用意します。

fn main() {
  println!("Hello rust! fib(8) is {}", fib(8));
}

fn fib(i:i64) -> i64 {
  if i < 2 {
    i
  } else {
    fib(i - 2) + fib(i - 1)
  }
}

とりあえず普通にコンパイルして動かしてみます。

$ rustc main.rs
$ ./main
Hello rust! fib(8) is 21

では、これをLLVMのビットコードを吐きだしてGraalVMで動かしてみます。

$ rustc --emit=llvm-bc main.rs
$ ls -l main.*
-rw-rw-r-- 1 naoki naoki 6860  4月 23 01:37 main.bc
-rw-rw-r-- 1 naoki naoki  146  4月 23 01:36 main.rs
$ graalvm-1.0.0-rc1/bin/lli --lib $(rustc --print sysroot)/lib/libstd-* main.bc
Hello rust! fib(8) is 21

動きました!


Haskellでもいけるかなと思ったけど、LLVM3.5が必要っぽく、パッケージが3.7からしかないっぽく、とりあえずあきらめ。


レイトレをネイティブコンパイルしてみる

GraalVMにはAoTコンパイラもあります。AoTというのはAhead of Timeで、事前コンパイルのことです。これを使ってJavaコードをネイティブコンパイルしてみます。

それなりに処理量があるコードということで、こないだ作ったレイトレを動かしてみます。

kishida/smallpt4j: smallpt Java port


最新版はいろいろいじってて複数ソースに分かれてたりするので、初期のものを使います。

https://github.com/kishida/smallpt4j/blob/original/src/main/java/naoki/smallpt/SmallPT.java


パッケージをデフォルトパッケージに変更しておきます。また。FastMathを使っているのでjava.lang.Math.*をstatic importしてこちらを使います。

で、普通にコンパイルして実行

$ graalvm-1.0.0-rc1/bin/javac SmallPT.java
$ graalvm-1.0.0-rc1/bin/java SmallPT

画像ができました。

f:id:nowokay:20180423023819p:image

今回は早く終わるよう、画像を荒くしたままにしてます。


ではネイティブコンパイルを。

ネイティブコンパイルにはzlib.hが必要なので、とってきます。

$ sudo apt install zlib1g-dev

そしてnative-imageでコンパイル

$ graalvm-1.0.0-rc1/bin/javac SmallPT.java 
$ graalvm-1.0.0-rc1/bin/native-image SmallPT
Build on Server(pid: 7220, port: 26681)
   classlist:     166.86 ms
       (cap):     544.93 ms
       setup:     803.27 ms
    analysis:   3,460.75 ms
error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Unsupported method java.lang.ClassLoader.getParent() is reachable: The declaring class of this element has been substituted, but this element is not present in the substitution class
To diagnose the issue, you can add the option -H:+ReportUnsupportedElementsAtRuntime. The unsupported element is then reported at run time when it is accessed the first time.

なんかエラー。

ImageIOがだめっぽい。動的にクラスロードしてるからだろうな。


ということで、ImageIOをやめてオリジナルのsmallptと同様にppmを吐き出すように変更します。

        try(BufferedWriter bw = Files.newBufferedWriter(Paths.get("image.ppm"));
            PrintWriter pw = new PrintWriter(bw)) {
            pw.printf("P3\n%d %d\n%d\n", w, h, 255);
            for (Vec v : c) {
                pw.printf("%d %d %d ", toInt(v.x), toInt(v.y), toInt(v.z));
            }
        }

ということで、全体のコードはこちらに

SmallPT4j without ImageIO


改めてJavaで動かします。

$ graalvm-1.0.0-rc1/bin/javac SmallPT.java 
$ graalvm-1.0.0-rc1/bin/java SmallPT 
JVM:1.8.0_161 sample:40
Samples:40 Type:master Time:PT24.042S

これをネイティブコンパイル

$ graalvm-1.0.0-rc1/bin/native-image SmallPT
Build on Server(pid: 7220, port: 26681)
   classlist:     167.94 ms
       (cap):     592.71 ms
       setup:     808.36 ms
  (typeflow):   2,479.49 ms
   (objects):     875.25 ms
  (features):      24.48 ms
    analysis:   3,439.52 ms
    universe:     134.33 ms
     (parse):     318.66 ms
    (inline):     550.73 ms
   (compile):   2,101.23 ms
     compile:   3,249.17 ms
       image:     466.18 ms
       write:     120.34 ms
     [total]:   8,412.47 ms
$ ls -l smallpt
-rwxrwxr-x 1 naoki naoki 5696832  4月 23 02:27 smallpt

こんどは いけました。ネイティブファイルができてますね。そして5MB。案外小さい。


実行してみます。

$ rm image.ppm 
$ ./smallpt 
JVM:null sample:40
Samples:40 Type:master Time:PT37.341S
$ ls -l image.ppm
-rw-rw-r-- 1 naoki naoki 8244655  4月 23 02:28 image.ppm

というか、遅くなっている。。。24秒だったのが37秒に。


ついでに、通常のOpenJDKで動かしてみます。

$ jdk1.8.0_171/bin/java SmallPT 
JVM:1.8.0_171 sample:40
Samples:40 Type:master Time:PT18.565S
$ jdk-10.0.1/bin/java SmallPT 
JVM:10.0.1 sample:40
Samples:40 Type:master Time:PT16.400068S

GraalVMのJVMより速いし、JDK10はさらに速い!


ということでこんな感じに。

GraalVM native-imageJDK8 JDK10
1.8.0_161null 1.8.0_17110.0.1
24.04237.34118.56516.400068

というか、Durationの精度もJDK10であがってますね。


ということで、いろいろやってみました。

Swingアプリも試してみたけど、AppContextの初期化でインプットメソッドの処理をしようとしたときに動的クラスロードしてるっぽくてそこでダメですね。

まだまだ問題はありそうだけど、もっと開発が進むと面白そう。

トラックバック - http://d.hatena.ne.jp/nowokay/20180422

2018-04-09(月) マイクロサービスフレームワークArmeriaを始める

[]マイクロサービスフレームワークArmeriaを始める 05:43 マイクロサービスフレームワークArmeriaを始めるを含むブックマーク

Armeriaのチュートリアルを書いてみる

https://line.github.io/armeria/index.html


RESTサーバーとして使う

まずは、プロジェクトを用意します。

dependencyにcom.linecorp.armeria:armeria:0.62.0を追加します。

mavenの場合、次のようなpom.xmlを用意します。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation=
           "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.mycompany</groupId>
    <artifactId>armeria_tutorial</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <dependencies>
        <dependency>
            <groupId>com.linecorp.armeria</groupId>
            <artifactId>armeria</artifactId>
            <version>0.62.0</version>
        </dependency>
    </dependencies>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>10</maven.compiler.source>
        <maven.compiler.target>10</maven.compiler.target>
    </properties>
</project>

sourceやtargetは環境に合わせて設定してください。1.8以降で動くはず。


まずは、簡単なHTTP RESTサーバーを書いてみます。

package tutorial;

import com.linecorp.armeria.common.HttpResponse;
import com.linecorp.armeria.common.HttpStatus;
import com.linecorp.armeria.common.MediaType;
import com.linecorp.armeria.server.Server;
import com.linecorp.armeria.server.ServerBuilder;
import java.util.concurrent.CompletableFuture;

public class BasicServer {
    public static void main(String[] args) {
        ServerBuilder sb = new ServerBuilder();
        sb.http(8080);
        
        sb.service("/hello", (ctx, res) -> HttpResponse.of(
                HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "Hello!!"));
        
        Server server = sb.build();
        CompletableFuture<Void> future = server.start();
        future.join();
    }
}

http://localhost:8080/helloにアクセスしてみます。

f:id:nowokay:20180410054052p:image


詳しくはこちらを

https://line.github.io/armeria/server-basics.html


アノテーションでサービスを定義する

serviceメソッドでURLとサービスを結び付けていましたが、通常のクラスにアノテーションを付けたメソッドとしてサービスを定義することもできます。

package tutorial;

import com.linecorp.armeria.common.HttpResponse;
import com.linecorp.armeria.common.HttpStatus;
import com.linecorp.armeria.common.MediaType;
import com.linecorp.armeria.server.annotation.Get;
import com.linecorp.armeria.server.annotation.Param;

public class MyService {
    @Get("/service")
    public HttpResponse service() {
        return HttpResponse.of(
                HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "My Service");
    }
    @Get("/greet/:name")
    public HttpResponse greet(@Param("name") String name) {
        return HttpResponse.of(
                HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "Hello " + name);
    }
}

BasicServerに追加します。

        sb.annotatedService(new MyService());

f:id:nowokay:20180410054049p:image


詳しくはこのあたり

https://line.github.io/armeria/server-annotated-service.html


Thriftサーバーとして使う

ThriftのIDLを書きます。

/src/main/thriftにhello.thriftファイルを置きます。

namespace java tutorial.thrift.hello

service HelloService {
  string hello(1:string name)
}

これをthriftコンパイラJavaコードを生成します。

ここからダウンロードします

https://thrift.apache.org/download


$ thrift -out src/main/java --gen java src/main/thrift/hello.thrift

こんな感じのソースが生成されます。

/**
 * Autogenerated by Thrift Compiler (0.11.0)
 *
 * DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
 *  @generated
 */
package tutorial.thrift.hello;

@SuppressWarnings({"cast", "rawtypes", "serial", "unchecked", "unused"})
@javax.annotation.Generated(value = "Autogenerated by Thrift Compiler (0.11.0)", date = "2018-04-10")
public class HelloService {

  public interface Iface {

    public java.lang.String hello(java.lang.String name) throws org.apache.thrift.TException;

  }
...

実際はmavenプラグインを使ってmaven上でコンパイルできるようにしたほうがいいけど、このあたりを見てがんばる感じで。

https://stackoverflow.com/questions/18767986/how-can-i-compile-all-thrift-files-thrift-as-a-maven-phase


できたinterfaceを実装したコードを書きます。

package tutorial;

import org.apache.thrift.TException;
import tutorial.thrift.hello.HelloService;

public class HelloServiceImpl implements HelloService.Iface {

    @Override
    public String hello(String name) throws TException {
        return String.format("Hello %s!!!", name);
    }

}

ArmeriaでThriftを使うためにdependencyにarmeria-thriftを追加します。

        <dependency>
            <groupId>com.linecorp.armeria</groupId>
            <artifactId>armeria-thrift</artifactId>
            <version>0.62.0</version>
        </dependency>

そしたらserviceとして追加します。ここでは別サーバーとして追加してみましょう。

future.join()で処理がブロックしてしまうので、そこは消してまとめてjoinします。

        CompletableFuture<Void> tfuture = new ServerBuilder()
                .http(8081)
                .service("/hello", THttpService.of(new HelloServiceImpl()))
                .build()
                .start();
        
        CompletableFutures.allAsList(Arrays.asList(
                future, tfuture)).join();

詳しくはこちらを。

https://line.github.io/armeria/server-thrift.html


DocServiceを使う

しかしながら、クライアント作るまで動かせないのは不便ですね。

そこでArmeriaではDocServiceというのが用意されています。

        CompletableFuture<Void> tfuture = new ServerBuilder()
                .http(8081)
                .service("/hello", THttpService.of(new HelloServiceImpl()))
                .serviceUnder("/docs", new DocService())
                .build()
                .start();

serviceUnderにするのを忘れないように。


http://localhost:8081/docsにアクセスすると、呼び出しが行えます。

f:id:nowokay:20180410054046p:image


ただ、いきなりJSONを入力しろと言われても何を入れていいかわからないので、サンプルが欲しいところ。ということで、DocserviceBuilderを使います。

                .serviceUnder("/docs", new DocServiceBuilder()
                    .exampleRequest(new HelloService.hello_args("naoki"))
                    .build())

パラメータも用意されて、ボタンを押すと実行されました。

f:id:nowokay:20180410054042p:image


詳しくはこちらを。

https://line.github.io/armeria/server-docservice.html


Thriftクライアントとして使う

さて、ではThriftサーバーを呼びだしてみましょう。

        HelloService.Iface helloService = Clients.newClient(
                "tbinary+http://localhost:8081/hello",
                HelloService.Iface.class);
        sb.service("/thello/{name}", (ctx, res) -> HttpResponse.of(
                HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8,
                helloService.hello(ctx.pathParam("name"))));

呼び出せてる感じ。

f:id:nowokay:20180410054040p:image


詳しくはここを

https://line.github.io/armeria/client-thrift.html


HTTPクライアントとして使う

さて、最後にHTTPクライアントとしても使ってみます。

まずはHttpClientライブラリを。

        HttpClient httpClient = HttpClient.of("http://localhost:8080");
        
        CompletableFuture<Void> hfuture = new ServerBuilder()
                .http(8082)
                .service("/hello", (ctx, res) ->{
                    CompletableFuture<AggregatedHttpMessage> msg =
                            httpClient.get("/hello").aggregate();
                    return HttpResponse.of(
                            HttpStatus.OK,
                            MediaType.PLAIN_TEXT_UTF_8,
                            msg.get().content().toStringAscii());
                })
                .build()
                .start();
        
        CompletableFutures.allAsList(Arrays.asList(
                future, tfuture, hfuture)).join();

Retrofitを使う

実際は、HTTP呼び出しは結構多いのでRetrofitでメソッドに結び付けたい。

ということで、armeria-retrofit2をdependencyに加えます。また、Retrofitで使うconverterやadapterも追加しておきます。

        <dependency>
            <groupId>com.linecorp.armeria</groupId>
            <artifactId>armeria-retrofit2</artifactId>
            <version>0.62.0</version>
        </dependency>
        <dependency>
            <groupId>com.squareup.retrofit2</groupId>
            <artifactId>converter-scalars</artifactId>
            <version>2.4.0</version>
        </dependency>
        <dependency>
            <groupId>com.squareup.retrofit2</groupId>
            <artifactId>adapter-java8</artifactId>
            <version>2.4.0</version>
        </dependency>

HTTP呼び出しをメソッドとして定義します。

package tutorial;

import java.util.concurrent.CompletableFuture;
import retrofit2.http.GET;
import retrofit2.http.Path;

public interface MyHttp {
    @GET("/hello")
    CompletableFuture<String> hello();
    
    @GET("/thello/{name}")
    CompletableFuture<String> hello(@Path("name") String name);
}

このメソッドを使ってHTTP呼び出し。

        Retrofit retrofit = new ArmeriaRetrofitBuilder()
                .baseUrl("http://localhost:8080/")
                .addConverterFactory(ScalarsConverterFactory.create())
                .addCallAdapterFactory(Java8CallAdapterFactory.create())
                .build();
        MyHttp myHttp = retrofit.create(MyHttp.class);
        
        CompletableFuture<Void> hfuture = new ServerBuilder()
                .http(8082)
                .service("/hello", (ctx, res) ->HttpResponse.of(
                        HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8,
                        myHttp.hello().get()))
                .service("/hello/{name}", (ctx, res) -> HttpResponse.of(
                        HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8,
                        myHttp.hello(ctx.pathParam("name")).get()))
                .build()
                .start();

2018-03-26(月) Java10のvarをどう使うか

[][]Java10のvarをどう使うか 18:42 Java10のvarをどう使うかを含むブックマーク

Java 10でvarが追加されました。が、いろいろ使い方は悩ましい気がします。

いろいろ議論をしたので、そこで考えたことをまとめておきます。

JShellでは読むことを考えなくていいのでガンガン使いましょう。

あと、OpenJDKのStyle Guidelinesも見ておくとよさげ。

Style Guidelines for Local Variable Type Inference in Java


ローカルメソッドの定義にvarを使う

通常の変数定義でのvarの使い分けは長くなりそうなので、最初にこれだけ書いておきます。

いままで、メソッド内で使えるローカルメソッドを使おうと思うと、Functional Interfaceを使うくらいしかありませんでした。

Function<Integer, Integer> twice = x -> x * 2;
println(twice.apply(4));

しかし、通常のメソッド定義とは構文が違いすぎたり、思うようなthrowsがなかったり、プリミティブ型が使えなかったり、呼び出しで処理名とは別に余計なメソッド呼び出しだったりと、あまりいいものとは言えませんでした。


varでローカル変数で型推論をしてくれるようになると、匿名クラスを型として匿名のまま使えるようになり、それを使ってローカルメソッドを定義できるようになります。

var m = new Object() {
  int twice(int x) {
    return x * 2;
  }
};
println(m.twice(4));

ローカル変数でのvarの主な使い分け

varの使い分けとして、大きく5通りあると思います。

  1. varを使わない
  2. 右辺に型が書いてあるときだけvarを使う
  3. 右辺に型が書いてあるときプラスαでvarを使う
  4. 明示する型を決めてそれ以外はvarを使う
  5. ローカル変数は原則すべてvar

プラスαするよりは明示する型を決めることのほうが多い気がしますね。


右辺に型が書いてあるときだけvarを使う

ここで、varを部分的に使う場合にも、右辺に型が書いてある場合には使えるということになると思います。

ここで、右辺に型が書いてある場合というのは、大きく3つあります。

List<String> strs = new ArrayList<>(); // new
List<String> params = (List<String>) attr.get("params"); // キャスト
List<String> areas = List.of("fukuoka", "tokyo", "kyoto"); // ファクトリ

newやキャストでは必ず右辺に型が書かれます。ファクトリやビルダーも多くの場合は目的の型のクラスにstaticメソッドとして用意されていることが多いと思います。

そのような場合は、右辺を見れば型がわかり、左辺に型を書くと同じ表記が重複することから、このような場合はvarを使ったほうがいいと思います。

var strs = new ArrayList<String>(); // new
var params = (List<String>) attr.get("params"); // キャスト
var areas = List.of("fukuoka", "tokyo", "kyoto"); // ファクトリ

このように書いても、変数の型をコードから読み取ることができます。


なぜvarを使いたくない場合があるか

読む人数のほうが書く人数よりも多いため、書きやすさよりも読みやすさを優先したい場合が多いです。

IDEで見えるという話もありますが、コードを読むのはGithubなどWeb上が多いので、IDE前提でコードを書きにくいというのもあります。

変数名やメソッド名を型がわかりやすいものにするという主張は、それなら型を書いた方が確実なのでは、という気がします

Javaの場合はIDEで補完できるので、書く手間というのはあまりないですね。


varを使わないほうがいい型

ラッパークラスや数値のプリミティブ型は、型を把握しておいたほうがいいと思います。特にラッパークラスの場合は、不用意な演算でぬるぽが出るので、ラッパークラスであることを明示しておいたほうがいいと思います。


次のようなコードでは、演算が行なわれていることからsizeは数値の型であることがわかります。ただ、もしここで返ってくる型がラッパークラスであれば、掛け算の前にnullチェックが必要です。

var size = getFloorSize();
print(size * 100);

getFloorSize()がnullを返さないのであれば、ラッパークラスではなくプリミティブを返すべきだし、Java8以降であればOptionalを返しましょう、ということにはなりますが。

数値の場合は精度も明示されていたほうがいいと思います。


あと、Optionalもそうですが、最近は値がそのまま返って来ずに、なんらかのクラスで包まれて返ってくることも多くあります。このような場合も型は明示したほうがいいように思います。


次のようなコードがあるとき、果たしてこれはいいでしょうか。

var size = getFloorSize();
print(size.get() * 100);

たとえばこれがOptionalであれば、不用意にget()を呼び出してはいけません。

OptionalInt size = getFloorSize();
print(size.get() * 100);

Java10からはget()を使わずorElseThrow()を使った方がいいかもしれません。


Supplierであれば、get()を呼び出しても構わないと思います。

Supplier<Integer> size = getFloorSize();
print(size.get() * 100);

CompletableFutureだったら、ちょっと考えてしまいますね。

CompletableFuture<Integer> size = getFloorSize();
print(size.get() * 100);

これらをメソッド名や変数名で表すというのも、あまり考えたくないというか、そのまま型を書いたほうが確実で余分なルールを作らなくてすみます。

ということで、値を包むような型に関しては、varではなく型を明示したほうがいいように思います。


ラムダのパラメータでの型推論はいいのになぜvarがだめなの?

という話もありましたが、実際はラムダもあまりよくないことが多くありました。

そこからvarはあまり使わないほうがいいのではということにつながっています。

CompletableFutureやOptionalの場合、メソッドチェーンも避けたい場合があります。


ラムダで、streamの場合は、ある程度は型が推測できていたというのもあります。

List<Member> members = getMembers();
List<Integer> scores = members.stream()
  .map(m -> m.getScore())
  .collect(toList());

また、この場合はメソッド参照を使えば型は明示的になりますね。


結局のところ統一されていればいいのでは

まあ結局のところ、コードの性質やチームの性質で力点は変わると思うので、ソースコードに関わる人で合意を取って、統一するというのが大事ではないかと思います。

2018-03-22(木) Java 10のコンパイラバグを見つけた

[][]Java 10のコンパイラバグを見つけた 05:15 Java 10のコンパイラバグを見つけたを含むブックマーク

予定通りにJava10が出ましたね!Javaが予定通りにリリースされることが珍しすぎたのか、関東では雪になっていたようです。


Java10の変更点についてはこちらにまとめています。

Java 10新機能まとめ - Qiita

Java10のJEP以外の変更まとめ - Qiita


ところで、varが導入されたのでいろいろ試してたらコンパイラが落ちました。

次のようなコードを-g付でコンパイルすると落ちます。

import java.util.List;
public class Main {
  public static void main(String... args) {
    var m = List.of("a", 1);
    System.out.println(m);
  }
}

このように、正式リリースであるbuild 46でコンパイルするとこうなります。

src$ javac Main.java -g
コンパイラで例外が発生しました(10)。Bug Database (http://bugs.java.com)で重複がないかをご確認のうえ、Java bugレポート・ページ(http://bugreport.java.com)でJavaコンパイラに対するbugの登録をお願いいたします。レポートには、そのプログラムと下記の診断内容を含めてく ださい。ご協力ありがとうございます。
java.lang.AssertionError: Unexpected intersection type: java.lang.Object&java.io.Serializable&java.lang.Comparable<? extends java.lang.Object&java.io.Serializable&java.lang.Comparable<?>>
        at jdk.compiler/com.sun.tools.javac.jvm.ClassWriter.enterInner(ClassWriter.java:1043)
        at jdk.compiler/com.sun.tools.javac.jvm.ClassWriter$CWSignatureGenerator.classReference(ClassWriter.java:312)

最初、コンパイルを遅くするパターンあるかなと思って、そうするとList.of("a",1)をネストするのがよさそうだなとやってみると、NetBeansから実行させたときに落ちたのです。

javacやjshellでは通ったので、NetBeansのバグかなーと思ってたのだけど、ちょうどそこにIntelliJ IDEAをデモしてる@yusukeがいたので試してもらうと、こっちでも落ちました。Scala Matsuriだったので、そこにkmizuもやってきて、いろいろ話してたら、まあどうもComparatorがまずそうね、と。


なので、こういうのは大丈夫です。

var s = List.of("a");
var t = List.of("a", 1, List.of());
var u = List.of("a", 1, Optional.empty());

これ、それぞれどうなるか考えるのも楽しそう。


List.of("a",1)は なかなかやらないと思うけど、こういうのやりますよね。

var props = Map.of(
  "name", "naoki",
  "level", 3);

例外が出るコード周辺はこんな感じ

/**********************************************************************
 * Writing Objects
 **********************************************************************/
    /** Enter an inner class into the `innerClasses' set/queue.
     */
    void enterInner(ClassSymbol c) {
        if (c.type.isCompound()) {
            throw new AssertionError("Unexpected intersection type: " + c.type);
        }

http://hg.openjdk.java.net/jdk/jdk10/file/b09e56145e11/src/jdk.compiler/share/classes/com/sun/tools/javac/jvm/ClassWriter.java#l1041

なのでクラスファイルを出力するときにうっかりintersectionが消されずにやってきてるのかなーと思ったのだけど、bitterfoxさんがそこではクラスファイル出してませんよ、でもなんか型情報出そうとしてますね、と教えてくれたので、クラスファイルではなく型情報がつくと言えばデバッグ情報だと思って、-gをつけてみたらみごとに再現しました。


んで、久保田さんに報告したらと勧められて、MLに投げてみました。そのあたりの流れは、この前後のツイートで。


ということで、MLに投げた。ドキドキ。

bug: compiler crashes with `var s=List.of(”a”,1)`


で、bitterfoxさんがパッチを投げて、ライセンスヘッダなど修正をして、取り込まれる模様けど、もっと根本から解決する必要があるらしく、別のコードで対策するらしい(3/25修正)。

[JDK-8199910] Compiler crashes with -g option and variables of intersection type inferred by `var` - Java Bug System

RFR 8199910: Compiler crashes with -g option and variables of intersection type inferred by `var`


ツイートは、こっちにもまとめています。

Java10コンパイラバグ


一度はJavaコンパイラを落としてみたいと思っていたのだけど、簡単なコードで落とせてびっくり。まあ、List.of("a",1,List.of())では問題ないとか、-gがついてなければ問題ないとか、あらかじめ見つけるのも難しそう。


Java10 たのしー

トラックバック - http://d.hatena.ne.jp/nowokay/20180322