hnwの日記

とあるPHP拡張のCI事情

PHP Advent Calendar 2017の3日目です。公開が遅くなってしまいました、ごめんなさい。


筆者はphp-timecopというPHP拡張を5年間ほどメンテナンスしています。このPHP拡張はCで書かれているのですが、Travis CIやAppVeyorなど複数のクラウドCIサービスを組み合わせてテストを回しています。本稿では各サービスをどのように利用しているか、それぞれの使い分けや特徴などを紹介していきたいと思います。

背景

本題に入るまえに、なぜPHP拡張でCIが必要か、という話を紹介します。


C言語で書かれたプログラムはポータビリティが高いような印象を持つ人が多いかもしれませんが、実際はむしろ逆で、ポータブルに書くのが難しい言語の1つです。Cではシステムコールやライブラリ関数を直接呼び出すのが普通ですが、これらは環境によって存在しないこともありますし、細かい挙動が異なることも珍しくありません。また、コンパイラごとの方言も存在します。


こうした事情から、C言語で書かれているOSSでは環境の差を埋めるためにAutoconf/Automake/Libtoolを利用するのが定番ですが、それでも未知の環境でリンクに失敗することも珍しくありませんし、環境の差を検出するためのconfigureスクリプトがバグで停止するようなことも起こりうるので、各環境でテストしておくに越したことはありません。


さらに面倒なことに、PHP拡張ではPHP本体のC APIを呼び出したり本体で定義されている構造体を操作したりすることがあるので、PHPの特定バージョンでだけ動かない、というようなバグがありえます。


要するに、PHP拡張をメンテナンスするためには、様々なOS、コンパイラPHPバージョンの組み合わせでテストをし続ける必要があるのです。ひとことで言えば地獄のような状況 ポジティブな言いかたをすると、とてもCIしがいのある題材だと言えるでしょう。

各CIサービスの特徴と実際の使い方

php-timecopで現在利用しているCIサービスは下記の4つです。


いずれのサービスも無料プランで利用しています。筆者自身がこのPHP拡張でお金を得ているわけではないのでお金を払ってまでCIする気にはならないですし、各社さんOSSへの応援の意味も込めて公開リポジトリのCIを無料で提供していると思うので、ありがたく使わせてもらっています。


以下、個別のサービスについて紹介します。

Travis CI

言わずと知れた最大手クラウドCIサービスです。

  • LinuxUbuntu)とmacOSの2つのOS上でCIできる
  • PHP 5.4から7.2までの6バージョンがビルド済み、設定で簡単に切り替えられる(記事執筆時)
  • 無料プランでは最大5並列でジョブが走る


無料プランでもかなりの大盤振る舞いなので、OSSでCIしたい場合は最初の選択肢になるでしょう。


また、PHP拡張のCIに関していうとPHPの全マイナーバージョンの最新版が提供されているのが素晴らしい点です。PHPではマイナーバージョンアップのタイミングで内部構造の大変更が入ることが珍しくない(5.3→5.4など)ので、全マイナーバージョンでテストするのは非常に重要です。それが設定を少し書くだけで簡単に対応できるのは本当に助かります。


実際、筆者はLinux環境上で7つのPHPバージョン*1のテストを走らせています。


Travis CIではビルド済みのPHPバイナリがZTS(Zend Thread Safe)有効になっているのが特徴的です。大抵の人はPHPNTS(Non Thread Safe)で利用しているはずなので、ある意味テスト環境として不適切と言えなくもありません。一方で、ZTSでだけビルドが通らないようなバグもありうるので、個人的にはテスト環境としてありがたいと感じます。

AppVeyor

Windows環境のCIが必要ならほぼ唯一の選択肢*2と言えるでしょう。

  • Windows環境でのビルドに対応
    • Visual C++も各バージョンがインストール済み
    • chocolateyやMSYS2-pacmanがインストール済みなので追加パッケージのインストールも簡単


PHPWindows版はバージョンごとに要求するVisual C++のバージョンが異なるため、自分でCI環境を準備するのは比較的面倒です。その意味で、クラウドCIサービスが利用できるのは良いですね。


php-timecopでは現在PHP 5.4、PHP5.6、PHP 7.1の3バージョンでテストを行っています。

Wercker

最近Oracleに買収されたクラウドCIサービスです。「わーかー」と読むらしいですね。CI環境を整えた当時、Dockerコンテナの扱いが一番楽そうだったので利用することにしました。


php-timecopではCentOSと32bit Ubuntuの2環境でのテストを走らせるのに利用しています。


CentOSでのテストは、specファイルによるバイナリパッケージ作成の動作確認のために行っています。通常Linuxディストリビューションごとのテストを行う必要性はあまり無いと思いますが、パッケージングのテストや、バイナリパッケージのデプロイなどの必要性があればディストリビューションの数を増やす必要があるでしょう。


一方、32bit Ubuntuでのテストは拡張自体のテスト目的です。PHPは32bit環境と64bit環境とで整数のサイズが変わるなど言語レベルで影響があるので、32bit環境でのテストには十分意味があります。

CircleCI

クラウドCIサービスの中でも有名どころの1つです。最近大幅バージョンアップしてコンテナベースになったと聞きましたが、筆者はまだ古いバージョンを利用しています。


現状ではNTS環境でのテストの意味で走らせているだけで、あまり活用できていません。

実際にCIは有用か?

PHP拡張でのCIを回してみて個人的には非常に有用だと感じています。客観的証拠のようなものは挙げにくいのですが、メリットだと思われる内容を何点か紹介します。


たとえば、新機能を実装するときなど、大変更の際はCIのおかげで特定バージョンでのバグを未然に防げることは珍しくありません。これまでの経験としてはOSやコンパイラの差で怒られることはそれほど多くなく、PHPの特定バージョンで動かないバグが多いので、まずはTravis CIで複数PHPバージョンでのテストを走らせるのがオススメです。


また、しっかりテストできているとPull Requestを受け取ったときの安心感も大きいので、その意味でもオススメです。


PHP 5時代はZTS環境で必要な「おまじない」の付け忘れにCIで気づく、ということが頻繁にありましたが、PHP 7になっておまじないが減ったので、最近はあまりそんなことはありません。


変わったところでは、Windows上でテストが失敗すると思ったらWindowsPHPのバグ(仕様?)だった、ということがありました。(参照:「PHPのsleep関数とusleep関数の挙動を調べてみた」)

まとめ

php-timecopのCIでは複数サービスを活用しており、有用だと感じているという話を紹介しました。また、テストの軸が複数あることも紹介しました。


何個もCIサービスを使うのは若干趣味的に思われるかもしれませんが、LinuxWindowsの両OSに対応するためにTravis CIとAppVeyorの2サービス併用、くらいまでは十分実用的だと思います。


PHP拡張を公開しているけどテストしてないという方はphp-timecopのリポジトリから設定ファイルをコピーすれば簡単に導入できますので、ぜひお試しください。

*1:PHP 5.4から7.2まで6バージョン、およびmasterブランチ

*2:本来ならVSTSが本命だと思うんですが、イマイチ知名度が低い気がします…

PHPカンファレンス2017でphp-timecopをPECLに登録した話をしました

10月8日に開催されたPHPカンファレンス2017でLT発表をしました。以下が発表資料です。



PECLは登録までの敷居が高そうな印象があったので、以前は自作のPHP拡張を登録するなんて考えもしなかったのですが、やってみたら案外あっさり登録できた、という内容を紹介しました。詳細の手順については過去の記事「php-timecopをPECLに登録しました」に書きましたので、合わせてご確認ください。


また、PECLに登録すると自動的にRemi's RPM repositoryに入ること、さらに運が良ければ(?)Fedoraリポジトリにも入ることを紹介しました。これはRemiさんがPHPコアコミッターであるだけでなく、RedHat社員でもあるという事情もあると思いますが、何にせよやってみないとわからなかったことかなと思います。


余談ですが、PECLに登録してからissueが増えたという話を紹介しましたが、実は難しいissueが増えて私自身が捌き切れていない、というオチがあったりします。特に「PHP 7.2.0RC3でPHPの内部実装を変えたらphp-timecop動かなくなったからよろしくな」というissueの難易度が高く現在進行形で悩んでいるのですが、7.2.0リリースまでに解決できるよう頑張ります。

他のセッションについての感想など

今年のPHPカンファレンスも刺激をたくさんもらった集まりでした。個人的には内山さんの「OPcacheの最適化器の今」が興味深い内容でした。私もPHP 5.5の頃に似た内容の発表(「Zend OPcacheの速さの秘密を探る」)をしたことがあるのですが、当時は未実装だったconstant propagationやdead code eliminationといった最適化が実装されているというのは知りませんでした。


発表後に「われわれの普段書くコードの性能改善につながるのか?」というような質問があり、内山さんは「大抵の場合WebアプリケーションのボトルネックはDBアクセスなどになるのではないか、その意味ではOPcacheの最適化が実コードに与える影響は少ないと思われる、という回答をされていました。


大抵の現場で起きている問題を解決する特効薬にはならない、という意味では非常に正しい回答なのですが、現実のコードに対して意味のある最適化ではない、という風に捉えてしまった人もいたような気がします。しかし、特にconstant propagationは身近なコードでも有用な最適化です。たとえば下記のようなコードを書いたことがある人は多いのではないでしょうか。

<?php
$seconds_per_minute = 60;
$minutes_per_hour = 60;
$hours_per_day = 24;
$seconds_per_day = $seconds_per_minute * $minutes_per_hour * $hours_per_day


このようなコードは読みやすさの観点では意味がありますが、これまでのPHPでは性能面で僅かに不利になっていたわけです*1。これがきちんと最適化されるのであれば、安心して読みやすさ優先でコードを書けるわけですから、多くのPHPユーザーにとって嬉しい内容であるはずです。


それ以外の方々の発表も楽しかったですし、懇親会も2次会も非常に盛り上がった気がします。毎度おなじみの方も久々の方ともお会いでき、色々なお話が聞けて良かったです。php-timecop使ってます、という話も複数の方から聞けて非常に参考になりました。ありがとうございます。


最後になりますが、スタッフの皆様、今年もおつかれさまでした。これほどの規模になると運営は本当に大変だと思いますが、来年も期待しておりますのでよろしくお願いいたします。

*1:このコードを最適化するためには各行間へのジャンプ命令が存在しないことを知らないといけませんが、過去のPHPではそのような解析を行っていませんでした

PHPのmysqlndの圧縮プロトコルについてのメモ

PHP+PDO+MySQLの環境では、PHP-MySQL間の通信についてzlibを使った圧縮プロトコルを利用することができます。この機能は、DBサーバのCPU利用率に十分余裕があり、かつPHP-MySQL間のネットワーク帯域が逼迫している状況で有用です。

MySQLの圧縮プロトコルとそのマニュアル


PHP+MySQLの環境で、圧縮プロトコルは下記のようなコードで利用できます。

<?php
    $options = [
        PDO::MYSQL_ATTR_COMPRESS => true
    ];
    $db = new PDO($dsn, $user, $pass, $options);


MySQLドライバとしてmysqlndを利用している場合*1PHP 5.3.11(2012年4月リリース)以降であれば圧縮プロトコルに対応しています。このことはPHPマニュアルにも下記の通り記載があります。

PDO::MYSQL_ATTR_COMPRESS (integer)


Enable network communication compression. This is also supported when compiled against mysqlnd as of PHP 5.3.11.


https://secure.php.net/manual/en/ref.pdo-mysql.php#pdo.constants.mysql-attr-compress


ただし、この記述は2015年9月頃に追加されたものです。それまでマニュアル上では「PDO_MYSQL+mysqlndは圧縮をサポートしていない」という記述しかありませんでした(2010年頃の記述で、当時は正しかった)。


2015年8月頃に筆者が業務で関わっていた環境で圧縮プロトコルを導入したのですが、マニュアルに明記されていない機能を商用環境で使うのは気が引けたので、マニュアルにバグがあるよ!というバグレポをPHP本家に投げてみました。その甲斐あってか、しばらくしてマニュアルが修正されました。これで今後は安心して利用できるというわけです。

mysqlndの圧縮プロトコルの実装

PHP 7.1.7のソースコード上で、mysqlndの圧縮プロトコルの実装部分を探してみました。


すると、ext/mysqlnd/mysqlnd_net.cext/mysqlnd/mysqlnd_protocol_frame_codec.cに同じようなコードが見つかります。

MYSQLND_METHOD(mysqlnd_pfc, encode)(zend_uchar * compress_buffer, size_t * compress_buffer_len,
                                    const zend_uchar * const uncompressed_data, const size_t uncompressed_data_len)
{
(略)
    error = compress(compress_buffer, &tmp_complen, uncompressed_data, uncompressed_data_len);
(略)
}


このcompress()はzlibの提供する関数で、Deflateアルゴリズムを使って圧縮するものです。ちゃんと真面目な圧縮を使ってるんですね!(驚くところではない気もしますが)


コードを追っていくと、クエリやクエリ結果だけでなく通信内容全体を圧縮していることもわかります。


また、compress()を使っているので圧縮レベルを与える方法はありません。常にデフォルト値(Z_DEFAULT_COMPRESSION、おそらく6)になります。

*1:PHP 5.4.0以降の環境ではデフォルトでmysqlndを利用しているはずです

PHPのsleep関数とusleep関数の挙動を調べてみた

筆者はPHPの現在時刻を上書きするPHP拡張モジュールphp-timecopを開発しているため、PHPの時間がらみのテストを世間一般の人より多く書いていると思います。テストケース中でusleep関数を多用しているのは世界中でも筆者くらいかもしれません。

ところで、先日php-timecopのテストをWindows上で動かしたところ、 usleep(100000) が99.8msくらいで帰ってきてテストに失敗するということがありました。

筆者はsleep関数やusleep関数は指定した時間と同じかそれより長い時間スリープすると考えていたのですが、本当にそのような性質があるのでしょうか?また、sleep関数やusleep関数はどの程度の誤差があるのでしょうか?

本稿ではこうしたsleepやusleepの挙動について深掘りしてみます。

sleep関数の挙動

まずはsleep関数の挙動から調べてみましょう。LinuxmacOSWindowsの各環境で sleep(1) して帰ってくるまでの時間を1000回測定したときの結果を下記に示します。

Linuxの場合

下図がLinux環境(Debian 8.7, Kernel 3.16.0, x86_64, PHP 5.6.30)での実験結果のヒストグラムです。横軸の単位はusです。

1.0001秒あたりにピークがあるのがわかります。言い換えると約100usのズレです。

Windowsの場合

Windows環境(Windows 10, x86_64, PHP 7.1.6)での実験結果のヒストグラムです。

ピークは1.001秒あたりになっています。言い換えると約1msのズレです。

macOSの場合

macOS環境(Mac OS X 10.11.6, x86_64, PHP 7.1.6)での実験結果のヒストグラムです。

ピークは1.005秒あたりになっています。言い換えると約5msのズレです。

3環境とも、1秒より早く帰ってきたものは1件もありませんでした。

POSIXの仕様を確認する

ちなみに、PHPのsleep関数はどの環境でもOS/標準ライブラリのsleepを呼び出しています。POSIXのsleep()のmanpageには次のような記述があります。

The sleep() function shall cause the calling thread to be suspended from execution until either the number of realtime seconds specified by the argument seconds has elapsed or a signal is delivered to the calling thread and its action is to invoke a signal-catching function or to terminate the process. The suspension time may be longer than requested due to the scheduling of other activity by the system.

http://pubs.opengroup.org/onlinepubs/9699919799/functions/sleep.html

指定された時間が経過するまで実行を停止するよ、スケジューリングの都合で指定されたよりも長く停止することがあるよ、とのことですから、指定時間より早く帰ってくることはないことが保証されているわけです。

usleep関数の挙動

次にusleepの傾向を調べてみます。sleep関数と同様に3環境でusleep(1000000)して帰ってくるまでの時間を測定しました。

Linux/macOSの場合

LinuxmacOSの場合はsleep(1)のときと大差ない結果になりました。下記のヒストグラムLinuxの結果です。

以下はmacOSの結果です。

どちらの環境もsleep関数のときと同様、1秒より早く帰ってくるものは1件もありませんでした。両OSとも内部的にPOSIX準拠のusleepを呼び出しているため、sleep関数のときと似た挙動なのは当然と言えそうです。

Windowsの場合

さて、Windowsの場合は意外とも言える結果になりました。

コブが2つある分布になっており、最初のピークは1000000ns(=1秒)より僅かに前になっています。本稿の最初でWindows上のPHPではusleep関数が指定した時間より少し早く帰ってくることがあるという話を紹介しましたが、その通りの結果になっているわけです。

PHPWindows用のソースコードを見てみると、usleep関数の実現にはWindowsの「Waitable Timer Objects」が利用されています。また、これを利用すると停止時間が指定より短くなることがあるようです(たとえば下記の記事を参照)。

まとめ

  • PHPのusleep関数はWindows上では指定された時間より短い停止時間になることがある
    • Windows APIを使った独自実装をしているため
    • 手元の環境では最大1ms程度早まった(仮想環境ではさらにズレる印象)
  • 全ての環境のsleep関数・Linux/macOS上のusleep関数は指定された時間と同じかそれ以上停止する
    • 内部的に呼び出しているライブラリコールsleep・usleepの仕様
    • 指定された秒数からどれくらいズレるかはOSによって異なる

usleepという関数名なのにWindows上の挙動が他環境と異なっているのはバグとまでは言い切れませんが、ポータビリティ上は問題がありそうです。念のためPHP本体にバグレポートを出しておこうと思います。

追試用の情報

今回の実験に使ったPHPスクリプトは下記です。カーネルコンパイルオプションやその他の条件次第で結果が変わるかと思います。

php-timecopをPECLに登録しました

かれこれ5年ほどメンテしている拙作のPHP拡張「php-timecop」ですが、このたびPECLに登録しました(PECL :: Package :: timecop)。


PECLというのはPHP本体に含まれないPHP拡張を提供する公式のリポジトリです。PECLのアカウントは承認制になっており、誰でも登録できるわけではありません。イタズラやお試しでの登録は減るでしょうが、代わりに登録への精神的ハードルが上がってしまうような仕組みだと言えるでしょう。実際、PECLに登録されているパッケージ総数は365個(2017/7/8時点)と多くはありません。また、日本人と思われるPECLアカウントは筆者以外では5人でした。


本稿では、PHP拡張をPECLに登録するまでのプロセスや、実際に登録してみてわかったことなどを紹介します。

PECLに登録するメリット

さて、そのPECLですが、PEAR*1の衰退とともに徐々に存在感が薄れている印象があります。今さらPECLに登録するメリットとは何でしょうか?


今回PECLに登録してみて、メリットとして次の3点を感じました。

  • peclコマンドを利用している人はインストールが簡単になる
  • PECLに登録することでコード品質が高そうに見える
  • PHP拡張のWindowsビルドが勝手に行われる


1番目ですが、peclコマンドで拡張をインストールしている人が一定数いるようです。そういう人にとっては、下記のコマンドでPHP拡張がインストールできるのは大きなメリットでしょう。

$ pecl install timecop-beta # 正式リリース後には「-beta」が不要になります


私個人はphpizeでビルドするのが普通になってしまったのでpeclで扱えてもありがたみは感じないのですが、インストールまでの手間は確実に減るので、悪いことではないでしょう。


2番目は印象論に近い話になりますが、PECLに登録してあるパッケージの方がロングサポートが期待できたり、多くの人のチェックが入っているように見えるかと思います。実際、後述するようにアカウント取得時にレビューが入りますので、PECLに登録されている時点で一定以上の品質だと期待できるでしょう。また、PECLのサイトに掲載されることで宣伝になるような側面もあるはずです。


最後はあまり知られていない気がしますが、PECLにはWindows版DLLの自動ビルドの仕組みがあります。PECLにパッケージをリリースすると、数時間後にWindows版DLLが勝手にアップロードされます。しかも、5.5から7.1までの4バージョン、スレッドセーフ有効/無効、32bit版/64bit版の全組み合わせ16個のDLLが提供されます。自力でここまで対応するのは大変なので助かりますね。

PECLアカウント取得まで


PECLアカウントの取得は承認制だと書きましたが、割とゆるい感じで運用されています。手順は全て「PECL :: Request Account」に書いてあるのですが、改めて紹介します。

  1. メーリングリスト pecl-dev@lists.php に参加して「自己紹介」「PECLに登録したい拡張の紹介」「コードへのリンク」を書き込む
  2. 誰かがレビューしてくれるので、返事をしたりコードを修正したりする
  3. 頃合いを見て「PECL :: Request Account」のフォームを埋める


1番目はそのままです。私は自己紹介が若干適当でしたが、仕事でPHPに触るようになって何年、とか言っておけばいいんじゃないでしょうか。


2番目については、割とすぐに誰かがコードを見た上で返事をくれます。人によっては「拡張の中でfork()するのは頂けない」みたいな真面目なコメントがついたりします。私の場合はRemiさん*2がコードレビュー&動作確認をしてくれて、「PHP 7.2で動かないよ」「テストが何件か通らないよ」という指摘をもらいました。


MLでの議論を尽くしたらアカウント申請を行います。リクエストフォームの「Sponsoring users」の欄にML上でレビューしてくれた人の名前を書けば良いでしょう。ここで登録したメールアドレスなどの情報はアカウントが作成された後のプロフィールページの初期値になります(参考:「PECL :: Yoshio HANAWA」)。


ちなみに、PECLアカウント申請だけではphp.netアカウントは作成されません。必要なら別途申請する必要があります。

PECLパッケージを作るまで

PECLにアップロードするパッケージは適当なtar ballではダメで、peclコマンドが取り扱える形式でないといけません。この作成にはpeclコマンドが必要です。もし手元に見当たらない場合はpearをインストールする必要があります。


また、パッケージングには package.xml が必要です。既存のパッケージを参考に適当に書いた上で、同じディレクトリで下記のようにすればパッケージが作られます。

$ pecl package
Package timecop-1.2.8.tgz done


XMLの中身がおかしいと警告やエラーが出たりしますので、適宜修正してください。できたtgzファイルをPECLのWeb管理画面からアップロードすればリリース完了です。CIと連携するようなオシャレなAPIは無いみたいです。


既に紹介した通りPECLパッケージのリリースのたびにWindowsのビルドが走るのですが、ビルドに失敗するとDLLがアップロードされません。ビルド時のログが下記のようなURLから確認できるので、失敗していた場合はログから原因を推測する必要があります。Windowsビルドを通すために何度もリリースするのも格好悪いので、真面目にやるなら手元にビルド環境を用意した方がいいでしょう。

まとめ

そんなわけで無事PECLへのリリースができました。3年前にchobieさんに言われたときからの宿題がようやく終わって良かったなーと思っています。



手元にPHP拡張を隠し持っているみなさんもPECLにリリースしてみてはいかがでしょうか。

*1:PHPライブラリのパッケージマネージャ&リポジトリ。現在はComposerが主流

*2:Remi's RPM repositoryの運営者、かつPHPコアコミッターの一人

古いPHPでDateTime::modify(’+0 days’) すると時間がずれるバグ

表題の通りですが、PHPの特定のバージョンにおいて、生成したDateTimeオブジェクトに対して時刻の操作を行うと期待と1時間ずれてしまうことがあります。

<?php
$dt = date_create('@0');
var_dump($dt->format('c')); // string(25) "1970-01-01T00:00:00+00:00"
$dt->modify('+0 days');
var_dump($dt->format('c')); // string(25) "1970-01-01T01:00:00+00:00" (PHP 5.3.9 - 5.4.7)


上記コードの場合で言えば、Unix epoch(Unix time 0秒)のDateTimeオブジェクトを生成してmodifyメソッドで0日後を指定すると、なぜかDateTimeオブジェクトの指す時刻が1時間後になってしまうのです。


このバグの再現には、次の条件が揃う必要があります。

  • PHP 5.3.9 - 5.3.29 または 5.4.0 - 5.4.7
  • 実行環境の現在時刻が夏時間
  • DateTimeオブジェクトをUnix time指定で作成している("@12345"のように)
  • 作成したDateTimeオブジェクトに対してmodifyメソッドやsetTimestampメソッドで時刻操作する


バグの再現に夏時間が必須となると日本人である我々には関係ないようにも見えますが、たとえばTravis CI上のPHP 5.3上でテストを実行すると、今の時期は上記4条件のうち最初の2つを満たしてしまいます。実際に私はハマりました。


このバグは「PHP :: Bug #62896 :: "DateTime->modify('+0 days')" Modifies DateTime Object」で修正されており、PHP 5.4.8以降ではこのような不思議な現象は起こりません。

BuffaloのUSB無線LANアダプタの返す製造元の文字列を解読する

私の手元に「Buffalo WLI-UC-AG300N」というUSB無線LANアダプタがあるのですが、Macの「システム情報」で見ると製造元が「敇瑭步挮浯䩟」となっていることに気づきました。

バッファローとかメルコとか書いてあるなら分かりますが、少なくとも日本語ではありませんし、簡体字なり繁体字なりだとしても不自然に思えます。となると、一体何が表示されているのでしょうか?文字コード警察的な意味で興味を持ったので、調べてみました。

謎解き(1) 何が書いてあるのか

この記事の執筆時点では「敇瑭步挮浯䩟」でGoogle検索しても同じ無線LANアダプタの情報が1件見つかるだけで、そんなメーカーは地球上に存在しなさそうなことがわかります。

ネット上にも情報が無いときに頼れるのは自分の直感だけです。そこで、私は謎の漢字列をUTF-16にしてみることにしました。

上記PHPファイルをUTF-8で保存して実行すると、次の文字列を得ます。

Gemtek.com_J

なんと7bitの可読文字が現れました。UTF-16は原則2バイトで1文字を表しますが、UTF-16の6文字を無作為抽出した場合に対応する12バイト全てがASCIIの7bit可読文字になる確率は0.0007%以下です。これは偶然なわけがありません。

この文字列のGemtekというのは台湾のメーカー「Gemtek Technology Co., Ltd」のことでしょう。同社はバッファロー社の無線LAN製品のOEM仕入れ先としても有名です。

実際、下記URLに「Buffalo WLI-UC-AG300N」のOEM元がGemtek社であることが書かれています。

そんなわけで、隠された文字列は「Gemtek.com_J」であることがわかりました。

謎解き(2) なぜ文字化けが起きたのか

さて、謎の漢字列はGemtek社を表すようですが、どうすればこんな不思議な文字化けが起きるのでしょうか?

実は、下記引用部の通り、USB 2.0の文字エンコーディングはUTF-16LEだと決められています。

Unicode ECN: Released in February 2005.
This ECN specifies that strings are encoded using UTF-16LE. USB 2.0 specified Unicode, but did not specify the encoding.

https://en.wikipedia.org/wiki/USB

つまり、USBで「abc」という文字列を表したい場合、「0x61 0x00 0x62 0x00 0x63 0x00」の6バイトにする必要があるわけです。これを知らずに(もしくは後で作業しようと思って忘れて)実装してしまうと、UTF-16としては不思議な文字列になるわけです。これが今回の文字化けの真相でしょう。

まとめ

WLI-UC-AG300Nの製造元文字列「敇瑭步挮浯䩟」について調べました。

  • 「敇瑭步挮浯䩟」は本来「Gemtek.com_J」となるはずだった
    • これは台湾のメーカーGemtek社を意味する
  • USBの仕様上UTF-16LEで記述すべきところをASCIIで記述したため文字化けが起きた

イースターエッグ的なものである可能性もゼロではありませんが、受託ビジネスでそんな無駄なリスクは取らないと思います。