Scala: 二重にネストされた内部クラスが存在すると、sbt-jmh によるコード生成が失敗する
環境
- Scala: 2.11.2, 2.11.6
- sbt-jmh: 0.1.10, 0.1.14
事象
コードベース内に二重にネストされた内部クラス/オブジェクトが存在している時に sbt-jmh を使うと
以下のようなエラーが発生する。
Annotation generator had thrown the exception. java.lang.InternalError: Malformed class name at java.lang.Class.getSimpleName(Class.java:1195) at java.lang.Class.getCanonicalName(Class.java:1238) at org.openjdk.jmh.generators.reflection.RFClassInfo.getQualifiedName(RFClassInfo.java:67) at org.openjdk.jmh.generators.core.BenchmarkGenerator.buildAnnotatedSet(BenchmarkGenerator.java:244) at org.openjdk.jmh.generators.core.BenchmarkGenerator.generate(BenchmarkGenerator.java:110) at org.openjdk.jmh.generators.bytecode.JmhBytecodeGenerator.main(JmhBytecodeGenerator.java:100) at pl.project13.scala.sbt.SbtJmh$.generateBenchmarkJavaSources(SbtJmh.scala:85) at pl.project13.scala.sbt.SbtJmh$$anonfun$jmhSettings$6.apply(SbtJmh.scala:28) at pl.project13.scala.sbt.SbtJmh$$anonfun$jmhSettings$6.apply(SbtJmh.scala:28) at scala.Function1$$anonfun$compose$1.apply(Function1.scala:47) at sbt.$tilde$greater$$anonfun$$u2219$1.apply(TypeFunctions.scala:40) at sbt.std.Transform$$anon$4.work(System.scala:63) at sbt.Execute$$anonfun$submit$1$$anonfun$apply$1.apply(Execute.scala:226) at sbt.Execute$$anonfun$submit$1$$anonfun$apply$1.apply(Execute.scala:226) at sbt.ErrorHandling$.wideConvert(ErrorHandling.scala:17) at sbt.Execute.work(Execute.scala:235) at sbt.Execute$$anonfun$submit$1.apply(Execute.scala:226) at sbt.Execute$$anonfun$submit$1.apply(Execute.scala:226) at sbt.ConcurrentRestrictions$$anon$4$$anonfun$1.apply(ConcurrentRestrictions.scala:159) at sbt.CompletionService$$anon$2.call(CompletionService.scala:28) at java.util.concurrent.FutureTask.run(FutureTask.java:262) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471) at java.util.concurrent.FutureTask.run(FutureTask.java:262) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:745)
ベンチマークに入る前の sbt run の初期化処理で sbt 自体が落ちてしまうため、ヘルプ (-h) やベンチマークの一覧 (-l) を見ることもできない。
原因
Scala コンパイラ本体の問題 [SI-2034] に起因する。
sbt-jmh は、Java のライブラリである jmh を利用するため、sbt run のタイミングでリフレクションを使って Scala コードから Java のコードを生成している。
org.openjdk.jmh.generators.bytecode.JmhBytecodeGenerator の処理の中で、全てのクラス名が java.lang.Class.getCanonicalName によって調べ上げられるのだが、これが上記のバグにより二重ネストされた内部クラスに対して正しく動作せず、java.lang.InternalError: Malformed class name が引き起こされてしまう。
回避策
この現象は Javaライブラリである jmh の処理で起こるので、sbt-jmh 側の修正で対応するのはかなり難しそうだ。
1. 内部クラスの二重ネストを避ける
これから作るものに関しては、極力ネストは避けたほうがいいのだろう。
2. 無名クラスに置き換える
以下のような変換を行えば、とりあえずは対応前とほぼ同じ挙動となるはずだ。
- 対応前
object A { object B { object C { val x = 123 } } }
- 対応後
object A { object B { val C = new { val x = 123 } } }
しかし、例えばこの場合に A.B.C.x と呼び出すと、以下のようなコンパイル時警告が出てしまう。
(scalac に -feature オプションを付けると警告の内容を見られる)
[warn] /path/to/example-sbt-jmh/src/main/scala/com/github/mogproject/example.scala:9: reflective access of structural type member value x should be enabled [warn] by making the implicit value scala.language.reflectiveCalls visible. [warn] This can be achieved by adding the import clause 'import scala.language.reflectiveCalls' [warn] or by setting the compiler option -language:reflectiveCalls. [warn] See the Scala docs for value scala.language.reflectiveCalls for a discussion [warn] why the feature should be explicitly enabled. [warn] val x = A.B.C.x [warn] ^
この警告を回避するには、以下いずれかの対応が必要となる。
- A.B.C.x を呼び出す箇所全てに import scala.language.reflectiveCalls を追加する
- scalac のオプションに -language:reflectiveCalls を追加する