( ꒪⌓꒪) ゆるよろ日記

( ゚∀゚)o彡°オパーイ!オパーイ! ( ;゚皿゚)ノシΣ フィンギィィーーッ!!!

Scala2.9から導入されたバイナリ互換性確保のためのbridgeアノテーションについて調べた

InfoQのScala2.9リリースの記事を読んでいて、Scala2.8から2.9へのバイナリ互換性確保のための方法について、こんな記述があった

One such technological solution are compiler generated forwarders, so-called bridge methods, which delegate calls from an old to a new method.

http://www.infoq.com/news/2011/05/scala-29

" compiler generated forwarders"ってなんぞ?と思って調べてみた。多分このあたりのOderskyのメールにある@bridgeアノテーションのことだと思う。


Parallel collections binary compatibility | The Scala Programming Language

Binary compatibility: status and outlook -
scala-user |
Google Groups

ってわけで、@bridgeアノテーションScalaコンパイラがどんな挙動をするか調べてみた。

Version1

まず下準備として、バイナリ互換性の確保が必要なサンプルライブラリを用意する。適当なところにlibってディレクトリを掘って、そこに以下の内容でLib.scalaを置いてコンパイルしておく。"def combine(other:Seq[String]): Unit "ってメソッドがひとつだけ用意されているシンプルなobjectだ。

lib/Lib.scala

object Lib {
  def combine(other:Seq[String]): Unit = {
    println("-- this is first version of combine(other:Seq[String]): Unit")
    println(other.mkString("/"))
  }
}


こいつをコンパイルしてクラスファイルをlibに作っておく。

$ scalac -d lib lib/Lib.scala


次に、このサンプルライブラリを利用するクライアントを作成する。clientディレクトリにこんなカンジでMainオブジェクトを作る。上で作ったLib#combineに依存するようになっている。

object Main extends App {
  val seq = Seq("アップキャスト","ダウンキャスト",
    "クロスキャスト","静的キャスト", "ドリームキャスト")
  Lib.combine(seq)
}


で、このclient/Main.scalaもコンパイルしておく。

$ scalac -cp lib -d client client/Main.scala


この時点で、ファイル/ディレクトリ構成はこうなっている。

.
├── client
│ ├── Main$.class
│ ├── Main$delayedInit$body.class
│ ├── Main.class
│ └── Main.scala
└── lib
    ├── Lib$.class
    ├── Lib.class
    └── Lib.scala


実行してみる。まぁこうなるわな。

[2$ scala -cp lib:client Main
-- this is first version of combine(other:Seq[String]): Unit
アップキャスト/ダウンキャスト/クロスキャスト/静的キャスト/ドリームキャスト

ここまで下準備は終わり。

Version2 引数をSeqからTraversableへ変更する

Lib#combineの引数を、SeqからTraversableへ変更して、コンパイルする。


lib/Lib.scala

object Lib {
  def combine(other:Traversable[String]): Unit = {
    println("-- this is first version of combine(other:Traversable[String]): Unit")
    println(other.mkString("/"))
  }
}


先ほどと同様にコンパイルしてクラスファイルをlibに作っておく。

$ scalac -d lib lib/Lib.scala


で、このLib#combineに依存するMainは再コンパイルせずに、再度実行してみる。ここで、Libのバイナリ互換性は破壊されているので、Mainは実行できないはず。

$ scala -cp lib:client Main
java.lang.NoSuchMethodError: Lib$.combine(Lscala/collection/Seq;)V
        at Main$delayedInit$body.apply(Main.scala:5)
        at scala.Function0$class.apply$mcV$sp(Function0.scala:34)
        at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
  ...


予想通り、Lib#combineのシグニチャが変更になったため、NoSuchMethodErrorが発生した。

Version3 bridgeメソッドを用意する

バイナリ互換性が壊れたのは、Version2でcombine(other:Seq[String])がclassファイルから消えたためだ。そこで、最初のSeq[String]を引数に取るバージョンを復活させる。その際には、Traversableを引数にとるcombineへそのままforwardするようにしておく。このような、新しい実装へ単にforwardするようなメソッドを"bridge method"とよぶ。


このbridge methodに、@bridgeアノテーションを付けておくと後々いいことがある。

object Lib {
   
  import scala.annotation.bridge
  @bridge
  def combine(other:Seq[String]): Unit = {
    println("-- this is first version of combine(other:Seq[String]): Unit")
    combine(other:Traversable[String])
 }

  def combine(other:Traversable[String]): Unit = {
    println("-- this is second version of combine(other:Traversable[String]): Unit")
    println(other.mkString("/"))
 }
}


コイツをコンパイルして新しいLib.classを作る。

$ scalac -d lib lib/Lib.scala


Mainは再コンパイルせずに、再度実行してみる。

$ scala -cp lib:client Main
-- this is first version of combine(other:Seq[String]): Unit
-- this is second version of combine(other:Traversable[String]): Unit
アップキャスト/ダウンキャスト/クロスキャスト/静的キャスト/ドリームキャスト

こんどはちゃんと動いた。ここまでは、当たり前の話。

Version4 @bridgeアノテーションの効果

これで、シグニチャを変更してもbridge methodを用意すればバイナリ互換性は確保できることが分かったが、はっきり言って古いシグニチャのメソッドは余計なので、これから先このLibライブラリを利用するクライアントには無視させるようにしたい。実は、そのようなことを実現するために@bridgeアノテーションが存在する。


新しいVersion3のLibを利用するnewclient/NewMain.scalaを用意する。ソースファイルの内容は、さっきのMainと一緒だ。


newclient/NewMain.scala

object NewMain extends App {

  val seq = Seq("アップキャスト","ダウンキャスト",
    "クロスキャスト","静的キャスト", "ドリームキャスト")
  Lib.combine(seq)
}


これを、コンパイルして

$ scalac -cp lib -d newclient newclient/NewMain.scala


実行する。NewMainでも、combineにSeqを渡して居るので、bridge methodを経由してcombineが呼び出されるハズだが...

$ scala -cp lib:newclient NewMain 
-- this is second version of combine(other:Traversable[String]): Unit
アップキャスト/ダウンキャスト/クロスキャスト/静的キャスト/ドリームキャスト


このように、いきなりTraversableを引数に取る方のcombineが呼び出される。この仕掛けは、@bridgeアノテーションが付いたメソッドにはclassファイルの中に、このメソッドはbridge methodであるというフラグが埋め込まれるため。NewMainをコンパイルする際に、Scalaコンパイラはbridge methodのフラグが付いたメソッドを無視するようにするので、このような結果になる。 ちなみに、@bridgeアノテーションを付けなかった場合はちゃんと古いシグニチャのメソッドを経由して呼び出される。


これで、古いシグニチャのメソッドに依存するクライアントは再コンパイルなしに新しいバージョンのライブラリに移行でき、かつ新しいバージョンに依存するものはbridge methodのオーバーヘッドなしにライブラリを利用できる用になる。


TypeSafeでは、この@bridgeアノテーションを見てなんらかの処理を行う移行ツールを開発しているようだ。

Version5 奇声

っ!っあ…んくんどるすぁぁぁー! ひゃーっはーっぁーっぱぁんんっはー?んくんどるぷ? いやぁ…んんはー、 ふぇ!あふにょぱ? っ…んんっーーーー!! んんどるぽっー!!!!!おるすぁーっはぁぁん! にゅい!!!っぱぎゃっはにゅんどるぷ! モフモフモフモフ! モフモフ!!!!!!!!らめぇぇー、 い!ぃ、 あ!!!ぬぃ?にゅいっうううぅ…くんきゅわふーー!!! うっぱ!わぁ……きゅわーー? いっぱ、 おんくんっあ!きぇー! お!らぬぇ、わぁーーっ? んきゅいやっぱーーー…わぁぁぁ?ひゃっぁ? らめぇーー! きゃー…ぅ……んきゃぴーーーっきぇ。 ごるすぁっぁ! わふぁんどるすぁっはーーっー!!!!うわぁーっうううわぁんっ…きぇぁ、 らめぇぁーっあふーっはにゅいやぁっほぉおるぁあ?モフ!!! もるぽっほにょーーーっはにゅわーーっ…んきゃぴー?よぅぅ…い! おるぷぇぁ!ふぇー、ぎょぱぁ? …あんはにゃーー!にゅいっ…きぇー! ぃぱぁ…くんどるぽっ…あひぎゃう!!にょーっうわふぇ…ぅぅ……ん? ぃ?うぅうわぁぁ。はにょぱーっきゅわー! ほぉおるぁぁんはぁあふぇぁ…ん!もるすぁぁぁ…きぇぁああふにゃぴー!!いっ! ひぎゃっほぉおるぷ?ひゃぴー? あっきゅわふぅぅううっはぁぁあふぁぁーっぱ。 へぺぺ!ぅ……にゅんんどるぷぇぁ……くんどるぁ……あ? ぅぅ!!ん!へぺ! ひぃぃっーー!!!もるぁあぃ? っはぁー! おおんきぇ。 ぃぱぁぁ! おぺぺ! もるすぁー!ごるすぁんきゅわふぅ…あんきぇぁんはにゃー!! いっ…きゃぴー…よぅううっほにゅわふ、 もるぁあぁあ…ほぉおぺ!もるすぁあひぎゃあふぇー! へぺぺ… …あ!よぅ…きゅわぁ…くんどるすぁっぱぁっ!!!! モフ! ふぅ…きゅわふぬわふにゅんっ……くん!よぅ!! ぅ!うぅううふふ。ぅぅぅ……んどるぷ! ぅ! らめぇぇ!ふぁ…くんっうわー!きゃっ、 ぬわーっぱーっー!モフモフモフモフ! んきぇぁぁあ! きゅい!!ぅ!!!!ごぶぁっうわーっあ! ん!っぁー!!ごるぽっきゃぴー!!! …あっ…くん! っ…きぇぇぇぇぁーっほにゃぴー!!!!!! ひぃぃっ!はー! ぅうっあ? ぃぃっぁんっあふぅうふふふ、ひぃぱぁんはーーー、わーー。 っーー!!あひゃー… ふふぁあ…くんんどるぁ。 よぅぅううっきゃう!! よぅうぅぅ…んんくんきゃー?ぃぃっあんはにゅわぁ? っあひぎぃっはにょー!