ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

Kotlin で拡張関数をオーバーライドして実装を切り替えられるぞ!

背景 : コルーチンの Select 式の実装を理解するのが難しかった

コルーチンのドキュメントを読んでいて select 関数というのが出てきたのだけど、これの実装がどうなっているのかすぐにはわからなかった。

suspend fun selectFizzBuzz(fizz: ReceiveChannel<String>, buzz: ReceiveChannel<String>) {
    select<Unit> { // <Unit> means that this select expression does not produce any result 
        fizz.onReceive { value ->  // this is the first select clause
            println("fizz -> '$value'")
        }
        buzz.onReceive { value ->  // this is the second select clause
            println("buzz -> '$value'")
        }
    }
}
kotlinx.coroutines/coroutines-guide.md at master · Kotlin/kotlinx.coroutines · GitHub

fizz.onReceiveSelectClause1<E>のプロパティなのだけど、その後ろのラムダが何なのかぱっと見はわからなかったのである。 (SelectClause1<E> 型には invoke は定義されていない。)

select 関数のシグネチャは以下。 引数の関数型はレシーバ付きで、レシーバの型は SelectBuilder である。

public inline suspend fun <R> select(crossinline builder: SelectBuilder<R>.() -> Unit): R

この SelectBuilder を見ると、下記のような拡張関数が定義されていた。 これらが fizz.onReceive { /* ... */ } の実体なのであった。

public operator fun <Q> SelectClause1<Q>.invoke(block: suspend (Q) -> R)

しかも SelectBuilderインターフェイスで、拡張関数は別のクラスで実装されていた。

学び

拡張関数をオーバーライドできる

Extensions declared as members can be declared as open and overridden in subclasses.

Extensions - Kotlin Programming Language

拡張関数をメンバーとして定義できるのは知ってたのだけど、オーバーライドできるとは知らなかった!

interface StringExtensionScope {
    fun String.bar(): String
}

class StringExtensionScopeImpl : StringExtensionScope {
    override fun String.bar() = this + " bar"
}

そして実行時に実装を変更できる

拡張関数を定義した型をレシーバとするレシーバ付きの関数型を使うと、ラムダ内をスコープにして拡張関数を有効にできる! 実行時に実装を変更することも可能!

val printFooBar: StringExtensionScope.() -> Unit = {
    // StringExtensionScope で定義されている拡張関数を利用できる。
    println("foo".bar())
}

// そして実行時に拡張関数の実装を変更できる。
printFooBar(StringExtensionScopeImpl())

拡張関数といえば 「静的に解決されるものである」 という印象だったのでオーバーライドして実行時に実装を切り替えられるとは思っていなかったけど実際は切り替えることができる。 ちなみに拡張レシーバ (extension receiver) の型はやはり静的に見られるので、拡張レシーバの型に応じて動的に実装を切り替えるということはできない。

This means that the dispatch of such functions is virtual with regard to the dispatch receiver type, but static with regard to the extension receiver type.

Extensions - Kotlin Programming Language

終わり

小ネタだけど、拡張関数は常に静的に解決されるものだと思ってたのでちょっとびっくりした。

Kotlin も拡張関数と operator とレシーバ付き関数型が組み合わさってくると結構コード追いづらくなるなーと感じる。 (まあ Scala とかと比べるとまだまだ追いやすい方だとは思うけど。)

Java SE 9 の javac で過去バージョンをターゲットにするときには --release オプションを使用すると良い

JDKjavac コマンドを使って過去バージョンの Java 言語で書かれたソースコードコンパイルする際には、-source オプションや -target オプションを使用してきました。 (JDK 8 までの話。) このとき、適切なブートストラップクラスパスを設定しなければ、対象バージョンに存在しない Java API を使用していてもビルドが通ってしまう、という問題がありました。

JDK 9 の javac コマンドには --release オプションが追加されました。 今後は (基本的には) このオプションを使用するようにすると良さそうです。

  • ドキュメント : javac

Java SE 9 (Oracle JDK 9) のマイグレーションガイドより

マイグレーションガイドには以下のように書かれています。

If you use the -source and -target options with javac, then check the values that you use. In JDK 9, javac uses a "one plus three back" policy of supporting -source and -target options.

The supported -source/-target values are 9 (the default), 8, 7, and 6 (6 is deprecated, and a warning is displayed when this value is used).

In JDK 8, -source and -target values of 1.5/5 and earlier were deprecated and caused a warning to be generated. In JDK 9, those values cause an error.

>javac -source 5 -target 5 Sample.java 
warning: [options] bootstrap class path not set in conjunction with -source 1.5 
error: Source option 1.5 is no longer supported. Use 1.6 or later. 
error: Target option 1.5 is no longer supported. Use 1.6 or later.

If possible, use the new --release flag instead of the -source and -target options. The --release N flag is conceptually a macro for:

-source N -target N -bootclasspath $PATH_TO_rt.jar_FOR_RELEASE_N

The valid arguments for the --release flag follow the same policy as for -source and -target, one plus three back.

javac can recognize and process class files of all previous JDKs, going all the way back to JDK 1.0.2 class files.

See JEP 182: Policy for Retiring javac -source and -target Options.

Java Platform, Standard Edition Oracle JDK 9 Migration Guide, Release 9

つまり、--release フラグを指定すると、-source オプションと -target オプションを指定し、さらに適切なブートクラスパスの指定も行った状態になります。 これまで、過去バージョンのブートクラスパスを設定するには、過去バージョンの Java 実行環境を取得してその中の rt.jar を指定してやる必要があったので、格段に便利になりました。

試してみた

実際に、以下のようなコードを含む Java のソースファイルをコンパイルしてみました。

public class Main {
    public static void main(String[] args) {
        String test = String.join("", ""); // Java SE 8 で Java API に導入されたメソッド。
    }
}

--release 8 を指定した場合は、問題なくコンパイルされます。

~\java-project> javac --release 8 .\Main.java

--release 7 を指定した場合は、下記のようにエラーになります。

~\java-project> javac --release 7 .\Main.java
.\Main.java:3: エラー: シンボルを見つけられません
        String test = String.join("", "");
                            ^
  シンボル:   メソッド join(String,String)
  場所: クラス String
エラー1個

Java 7 向けにビルドしたけど Java 7 の実行環境で動かすと (Java 7 にない API を使っていて) 例外が発生する、というようなミスが減りそうで最高ですね!

Gradle での使い方

Gradle 4.2.1 現在、JavaCompile タスクjavac--release オプションを指定する方法として特別なメソッドは提供されていません。 通常のコンパイラオプションを指定する方法は使用できるので、通常のコンパイラオプションの指定と同じ方法で --release オプションを指定します。

具体的には CompileOptions#compilerArgs プロパティを使用します。 このドキュメントを読むと、『For example, it is possible to pass the --release option of JDK 9』 とか 『Note that if --release is added then -target and -source are ignored.』 とか書かれています。 --release オプションにもばっちり対応されていますね。

超単純な build.gradle の例を書いておきます。

apply plugin: 'java'

tasks.withType(JavaCompile) {
    options.compilerArgs.addAll(['--release', '8'])
}

Java 9 のモジュールシステム (JPMS; JSR 376) の概要

Java SE 9 がリリースされましたね! めでたい!

さてさて、Java SE 9 の目玉といえばやはり The Java Platform Module System (JPMS; JSR 376) ですよね! Project Jigsaw の心臓部です。

この記事では JSR 376 をさらっと読んで、JPMS の全体像をまとめておきます。 (実際の使い方などはこの記事の範囲外です。) モジュールシステムの理解への取っ掛かりとして皆さんの一助となれば幸いです。

仕様 (JSR 376) 概要

The Java Platform Module System (JPMS) の目的とその手段

JSR 376 に書かれているとおりの内容を日本語にしてます。

目標

JPMS の目標は、親しみやすく、それでいてスケーラブルなモジュールシステムを定義すること。

  • 開発者がライブラリやフレームワーク、そしてアプリケーションを開発したり使用したりする際に JPMS を使用できるように、親しみやすいもの――すなわち学び易く使い易いものでなければならない。
  • Java SE プラットフォーム自身、およびその実装をモジュール化することができるように、スケーラブルでなければならない。
手段

その目標を達成するために、2 つの基本的な機能が備わっている :

  • 信頼性の高い構成の仕組み : プログラム部品間の明確な依存関係を宣言できる (脆弱で、エラーが発生しやすいクラスパスのメカニズムを置き換える)
  • 強力なカプセル化 : どの型を他の部品からアクセスできるように公開し、どの型を公開しないか、ということをプログラム部品が宣言できる

これらの機能は、新たなプログラム部品であるモジュールによって実現される。 モジュールは Java プログラミング言語の構成物として定義され、コンパイル時と実行時の両方で一様に解釈されるものである。

JPMS の全体像

JPMS の詳細は Java 言語仕様や JVM 仕様などの中に含まれています。 JSR 376 は、それらの仕様のどこに JPMS が影響しているのかを分かりやすく表現しています。

多くのアプリケーションエンジニアが気にするのは、特に Java 言語仕様や Java API 仕様、JAR ファイル仕様といったところだと思います。 ここら辺の内容は後で少し触れます。

JSR 376 の一部ではありませんが、次のような文書も紹介されています。

また、今回のリリースに含めなかった機能等についての説明や、変更履歴についても JSR 376 には書かれています。

Java 言語仕様における JPMS

Java 言語仕様の中の JPMS に関わる部分をさらっと読んだのでメモ程度にまとめておきます。

  • 7.7 節 Module Declarations より
    • モジュール宣言は、新しい名前付きモジュール (named module) を記述する。
    • 名前付きモジュールは、他のモジュールへの依存や、他のモジュールに公開するものを記述する。
    • モジュール宣言により、モジュール名が導入される。 モジュール名は、他のモジュールとの関係を記述するのに使用される。
      • モジュール名は、1 個以上の Java 識別子をドットで連結したもの。
    • モジュールにはノーマルモジュール (normal module) とオープンモジュール (open module) の 2 種類がある。
      • ノーマルモジュールは、コンパイル時にも実行時にも export されたものしか外部コードからは扱えない。
      • オープンモジュールは、コンパイル時には export されたものしか外部コードからは扱えないが、実行時には export されていない型も外部コードから扱える。
      • リフレクションでの操作時に結構影響するっぽい。
    • モジュール宣言では、java.util.ServiceLoader によるサービスの提供あるいは利用の宣言も可能。
    • 名前付きモジュールに関連付けられていないクラスなどは、無名モジュール (unnamed module) に関連付けられる。
      • Java SE Platform は、必ず 1 つは無名モジュールをサポートしなければならない。 2 個以上サポートしても良い。
      • 無名モジュールは互換性のために存在するぽい。 (『Unnamed modules are provided by the Java SE Platform in recognition of the fact that programs developed prior to Java SE 9 could not declare named modules.』)
  • 6.1 節 より
    • モジュール名は、モジュールが export する本質的なパッケージ名に合わせるのが良い。 それが難しい場合は、著者が持つドメイン名を逆さに並べたもので始めると良い。

JAR ファイル仕様における JPMS

こちらもメモ程度に。

  • クラスパス上ではなくモジュールパス上に配置された JAR ファイルはモジュールである。
  • トップレベルに module-info.class ファイルを持つ JAR ファイルは、モジュール式 JAR ファイルである。
  • そうでない JAR ファイルは、自然発生的なモジュール (automatic module) とみなされる。
    • その場合のモジュール名は JAR ファイル名から決められ、export されるパッケージは .class ファイルから決められる。

さあ、始めましょう

WebDriver によるスクリプト実行の現状 (geckodriver と ChromeDriver)

WebDriver とは、Web ブラウザを外部から操作するための標準化された API です。 詳細は先日書きましたのでご参照ください!

今回は、WebDriver のコマンドで JS スクリプトを実行させる方法の説明です。 W3C WebDriver API を見ながらどういう API になっているのか説明します。 また、geckodriver および ChromeDriver での現在の実装状況についても書いています。

これらは 2017 年 5 月 10 日現在の情報ですので、最新の情報は最新の W3C 勧告や Driver 実装を見てください。

スクリプトを実行させる API

W3C WebDriver での仕様

W3C WebDriver 勧告では、スクリプトを実行するための下記の 2 つのコマンドが定義されています。

前者は同期的なスクリプト実行をサポートし、後者は非同期的なスクリプト実行をサポートする、という風に思ってしまうところですが、なんとどちらも非同期のスクリプト実行をサポートしています! (な、なんだってー

どちらも渡されたスクリプト (リクエストボディに含まれる script プロパティの値) を関数本体 (FunctionBody) として扱って promise-calling の形で実行するのですが、前者の方は関数の返り値をレスポンスに使うのに対して、後者の方は Promiseresolve を引数リストに追加したうえでスクリプトを呼び出し、関数の返り値を無視する (つまり、引数リストに追加された resolve を関数内で呼ぶことで結果を渡す) という違いがあります。 *1

前者の方は以下のようなスクリプトを受け付けるわけですね。

var waitTime = arguments[0] || 2000;
var p =
  new Promise(function (resolve, reject) {
    setTimeout(function () { resolve() }, waitTime);
  }).then(function () {
    return "Hello!";
  });
return p;

後者で同様の処理を実行させるには、以下のようにする必要があります。

// 引数リストの最後に追加された resolve 関数を受け取る。
var callback = arguments[arguments.length - 1];
var waitTime = arguments[0] || 2000;
var p =
  new Promise(function (resolve, reject) {
    setTimeout(function () { resolve() }, waitTime);
  }).then(function () {
    return "Hello!";
  });
// 引数リストの最後に追加された resolve 関数を呼ぶことで結果を返す。
callback(p);

ちなみに関数に渡される引数は、 リクエストボディの args プロパティで指定できます。 つまり、リクエストボディは以下の形式です。

{
  "script": "return 100 + arguments[0];",
  "args": [200]
}

JSON wire protocol での仕様

さて、どちらも非同期なスクリプト実行を扱えるのであれば、なぜ W3C WebDriver には 2 つのスクリプト実行コマンドが用意されているのでしょうか? おそらく、W3C 勧告よりも古い仕様である SeleniumJSON wire protocol から引き継いだものだと思われます。 JSON wire protocol でも 2 つのスクリプト実行コマンドが用意されていました。

これら 2 つは、(W3C 勧告の 2 つとは違って) 前者が同期的なスクリプト実行用、後者が非同期的なスクリプト実行用と、明確に役割が分かれています。 リクエストボディの型や、非同期実行での結果の返し方 (引数リストの最後に追加されたコールバック関数に値を渡す) などは W3C 勧告のコマンドと同じです。 ただし、これらは Promise を扱えません。

JSON wire protocol でスクリプトをそのまま W3C 勧告の仕様にあった Driver 実装でも使えるように、W3C 勧告の方でも 2 種類のコマンドが定義されたのだろうと思われます。

ChromeDriver で非同期スクリプトを実行すると

普通に ChromeDriver で非同期スクリプトを実行させようとすると、以下のメッセージが返ってくることがあります。

"value": { "message": "asynchronous script timeout: result was not received in 0 seconds..." }

これは、スクリプトタイムアウトが 0 s に設定されているためです。 タイムアウト時間を設定することができるので、先にタイムアウト時間を設定する必要があります。 POST /session/{session_id}/timeouts というエンドポイントに、以下のようなリクエストボディで HTTP リクエストを発行しましょう。

{
  "type": "script",
  "ms": 2000
}

ちなみにこれも W3C 勧告とは違う JSON wire protocol に沿ったものになっています。

geckodriver と ChromeDriver の現状

上記 Driver のそれぞれの実装状況を見てみました。

geckodriver の実装状況

  • W3C 勧告のコマンド (Execute Script コマンドと Execute Async Script コマンド) は実装されている。 Promise も扱える。
  • JSON wire protocol で定義されていたエンドポイントはバージョン 0.16.0 で削除済み!

ChromeDriver の実装状況

  • W3C 勧告のコマンド (Execute Script コマンドと Execute Async Script コマンド) は実装されていない。
  • JSON wire protocol で定義されていたエンドポイントは使用可能。 (Promise は扱えない。)

どちらでも動かすために

WebDriverIO の実装を見ると、古い仕様のエンドポイントでコマンドを発行してみて、エラーになったら新しい仕様でコマンドを発行しなおす、ということをやっていました。

厳しい世界ですね……。

*1:W3C 勧告を読んだ感じ、なんか remote end steps の文書がおかしい感じがしますね。 まあ意図は掴めるんだけども。

WebDriver について私が知っていること (2017 年版)

もともと Selenium 2.0 で導入された WebDriver ですが、単に 「WebDriver」 と言ってもいろいろなものを指すことがあり *1、WebDriver を初めて使おうとする人にはややこしい状況だと感じています。 (私は数か月前に WebDriver を使おうとして混乱しました。)

というわけなので、私が調べた 「WebDriver とは何か」 を簡単にまとめておきます。

WebDriver とは何か

WebDriver とは、ユーザーエージェント (web ブラウザ) を外部のソフトウェアから操作したり情報を取得したりできるようにするためのものです。 テストの自動実行などに便利ですね。

W3C WebDriver と Selenium WebDriver

冒頭で述べた通り、Selenium 2.0 の目玉機能として導入された WebDriver なのですが、現在は W3C が発行する WebDriver 勧告も存在します。 W3C WebDriver は Selenium WebDriver をもとに策定されていて、概念的には基本的には同じなのですが、言葉の定義が違っていてややこしいので分けて説明します。


W3C WebDriver
  • ドキュメント : WebDriver (W3C)
  • ユーザーエージェントの内部状態の取得や操作を可能にする遠隔操作のためのインターフェイス
  • HTTP 準拠の wire プロトコル となっている。
  • WebDriver のクライアント側を Local End といい、ユーザーエージェント側を Remote End という。
    • Remote End は 2 種類のノードに分けられて、他の Remote End のノードのプロキシとして働く Intermediary node と、ユーザーエージェント (または類似のプログラム) によって提供される Endpoint node がある。 ( https://w3c.github.io/webdriver/webdriver-spec.html#nodes )
  • WebDriver 仕様では、リモートエンドがどのように振る舞うべきかも書かれている。
Selemium WebDriver

各種 Driver 実装

初期には Selenium が独自に Driver を実装していたようなのですが、現在では各ブラウザベンダーがそれぞれ Driver を実装しているという状況です。

仕様と実装について

W3C WebDriver の仕様は最近もいろいろ変化していて、Driver 実装と W3C WebDriver のドキュメントがしばしば異なっていることがあります。 Obsolete ではあるのですが、SeleniumJSON wire protocol のドキュメントの方が実際の Driver の実装にあっていることもあるので、そちらもあわせて参考にするといいでしょう。

また、WebDriverIO の API ドキュメント も参考になります。 各コマンドのドキュメントページに 「View Source」 のリンクがあるので、どういう実装になっているのか見てみるといいでしょう。

とりあえず使ってみる (スクリーンショットを撮る例)

とりあえず WebDriver を使ってみるには、ブラウザとその Driver、それから HTTP を発行できるコマンドさえ使えれば大丈夫です。

ここでは WebDriver API (HTTP プロトコル) を使い、Firefox でスクショを撮ってみましょう。

Driver (geckodriver) の準備

Firefox のバイナリは既に存在するとします。 Firefox 用の Driver である geckodriver は GitHub 上でリリースされていますので、ダウンロードしてきます。

下記のようなコマンドで geckodriver を起動します。 (Linux での例です。 各自環境に合わせていい感じに実行してください。)

# geckodriver 起動
/opt/geckodriver/geckodriver --host 0.0.0.0 --port 9516 --binary /usr/bin/firefox

WebDriver コマンドの発行

あとは WebDriver のコマンドを発行していくだけです。 以下は PowerShell の例ですが、各自環境でいい感じに HTTP リクエストを発行してください。

# セッション開始 (レスポンス中の sessionId の値を後続のコマンドで使用する)
$session = Invoke-RestMethod -Method POST -Uri http://localhost:9516/session -Body $(ConvertTo-Json(@{}))

# ページ遷移
Invoke-RestMethod -Method POST -Uri http://localhost:9516/session/$([System.Web.HttpUtility]::UrlEncode($session.value.sessionId))/url -Body $(ConvertTo-Json(@{ url = "http://vividcode.hatenablog.com/" }))

# スクリーンショット取得
$screenshot = Invoke-RestMethod -Method GET -Uri http://localhost:9516/session/$([System.Web.HttpUtility]::UrlEncode($session.value.sessionId))/screenshot

# Base64 デコードしてファイルに出力
[System.IO.File]::WriteAllBytes($(Join-Path $(Convert-Path .) test.png), $([System.Convert]::FromBase64String($screenshot.value)))

# セッション終了
Invoke-RestMethod -Method DELETE -Uri http://localhost:9516/session/$([System.Web.HttpUtility]::UrlEncode($session.value.sessionId))

スクリーンショット{"value":"xxxx"} のような JSON で返ってきます。 value の値は Base64 エンコードされた画像の値なので、Base64 でデコードしてやってファイルに書き出すとスクリーンショットを見ることができます。 上の例ではコマンドで変換してファイル出力しちゃってますが、コマンドでの変換が難しい環境の場合は、http://www.convertstring.com/ja/EncodeDecode/Base64Decode のような web 上の Base64 デコーダを使ってデコードするのが楽でしょう。

今回の例では、以下のような感じでスクショを撮ることができました。

f:id:nobuoka:20170507214630p:plain

Remote End を Docker 環境で用意する

自動テストで使用する場合など、ブラウザ自体や Driver を含んだ環境を手軽に用意できると便利ですよね。

FirefoxChrome については、wakaba さんが公開している Docker イメージがあります。

Docker 環境さえあれば docker run -it --rm -p 9516:9516 -t quay.io/wakaba/firefoxdriver:stable という感じで Firefox と geckodriver の環境を手に入れられるので便利です。 これらのイメージは Selenium に依存していません。

Selenium が公開している Docker イメージもあります。

こちらは Selenium Server が入っているので、Selenium Server 経由で使いたい場合はこちらを使うといいでしょう。

*1:クライアントライブラリだったり Driver だったりプロトコルだったり