strptime関数のフォーマット指定子%Zはglibc拡張なのでnewlibにはない

ファームウェアエンジニアの中林 (id:tomo-wait-for-it-yuki) です。 既存実装やコメントを見ると、「お?これ、なんでだろう?」と違和感に覚えることがありますよね。システムは動作しているので、そのままにしておいても特に問題は発生しません。ですが、あえてこういうところを調査してみると、新たな発見があったり、コードをより改善できたり、ブログのネタにできたりします。

本エントリでは、ファームウェア実装の中で見つけた、次のstrptime関数の使い方について、タイトルの結論に至った経緯を共有します。

    // NTP パケットをドロップしていると使えないので
    // `Mon, 27 Jul 2020 06:53:34 GMT`
    // のフォーマットをパースして、システムの時刻とする
    struct tm t;
    char *pos = strptime(buf, "%a, %d %b %Y %H:%M:%S %Z", &t);
    // GMT 無視されるが正しく取得できている

    if (!(strlen(pos) == 3 && (0 == strncmp(pos, "GMT", 3)))) {
        // エラー処理
    }

背景

まず、上記コードがどのような処理で使われているか説明します。 Nature Remo Eではスマートメーターから取得した瞬時電力量 (まさにそのとき家庭で使われている電力量) をサーバーにアップロードしています。瞬時電力量をいつ取得したかはファームウェア側でタイムスタンプを付与しています (ネットワーク断絶時のリトライ処理があるのでサーバー側でタイムスタンプ付与ができない事情があります)。正しいタイムスタンプを付与するために時刻同期が必要で、NTPを使う方法が一般的です。

Remo EでもNTPによる時刻同期を行っています。しかしながら、これだけでは不十分であることがわかっています。過去に、NTPパケットがルーターのフィルタでドロップされる環境でRemo Eのデータが集計されない、という問題が発生していました。この問題を解決するために、NTPに依存しない、Remoとサーバーとだけで完結する時刻同期方法が必要になりました。

そこで、サーバーから返ってくるHTTPレスポンスのDateヘッダーを使って、時刻同期する方法が検討されました。Remoでは1日1回Websocketの再接続を行っており、この時のHTTP Upgradeリクエストに対するレスポンスを使って時刻同期します。NTPによる定期的な時刻同期とDateヘッダーによる時刻同期を併用することで、ほとんどの問題は解決しました (実はまだ闇があるのですが、今回は省略します) 。

このときに実装されたのが、さきほどのコードです。HTTPレスポンスのDateヘッダーからMon, 27 Jul 2020 06:53:34 GMTのような時刻データを取得して、strptime関数でパースして、Remoのシステム時刻を更新します。

最近、このあたりのコードを見直す機会があり、コメントに対して「お?これ、なんでだろう?」と思うに至りました。

    // `Mon, 27 Jul 2020 06:53:34 GMT`
    char *pos = strptime(buf, "%a, %d %b %Y %H:%M:%S %Z", &t);
    // GMT 無視されるが正しく取得できている

どうも%Zのフォーマット指定子が無視されてしまうようです。

strptime

strptimeは時刻を表す文字列をパースして、時刻構造体struct tmに変換するlibc (POSIX) に含まれる関数です。Mon, 27 Jul 2020 06:53:34 GMTのような文字列をprintfで使うようなフォーマット指定子 (例: %a, %d %b %Y %H:%M:%S %Z) を使ってパースします。

man strptime

libcの関数でわからないことがあれば、とりあえずmanやろ、ということでmanします。対象が組込みシステムなので、Linux上のmanで全ての情報を得られるわけではありませんが、ターミナル上3秒でありつける情報はとりあえずで読んでおいても損はしません (日本語訳:1) 。%Zについて書かれているところを探します。

       %Z     The timezone name.

%Zタイムゾーン名、とあります。manで付近を読んでみます。欲しい情報が見つかりました。

   Glibc notes
       For reasons of symmetry, glibc tries to support for strptime() the same
       format characters as for strftime(3).  (In most cases, the  correspond‐
       ing fields are parsed, but no field in tm is changed.)  This leads to

いくつかフォーマット指定子の説明

       %Z     The timezone name.

つまり、%Zglibc (GNU C library) の拡張サポートである、ということです。Nature Remoではglibcではなく組込み用途のC libraryであるnewlibを使っています。そこでnewlibの実装で%Zがサポートされているか確認します。と言っても、SDKではlibcはバイナリ配布されているため、GitHubでミラーされている実装を覗いてみます。

     case 'Z' :
        /* Unsupported. Just ignore.  */
        break;

github.com

あ、ダメそう…。

解決策

SDK内で同じようなことをしているコードがあるはず、ということでSDK内をgrepします (ripgrepはいいぞぉ!) 。

  char *r = strptime(s.c_str(), "%a, %d %b %Y %H:%M:%S GMT", &tm);

なるほど、GMTをハードコーディングしてしまえば良い、ということのようです。こういうとき私は、ライブラリの挙動を確かめるために、ユニットテストを書きます。ターゲットハード上でcatchを使ったユニットテスト環境を構築してあるので、次のテストを追加します。これまでの理解が正しければ次のテストがpassするはずです。

#include <catch.hpp>
#include <ctime>
#include <cstring>

TEST_CASE("strptime", "[libc]")
{
    const char *date = "Mon, 27 Jul 2020 06:53:34 GMT";
    struct tm t;

    SECTION("use %Z")
    {
        // `GMT`が無視されるので3文字残っている
        char *remaining = strptime(date, "%a, %d %b %Y %H:%M:%S %Z", &t);
        REQUIRE(strlen(remaining) == 3);
    }
    SECTION("use hard coded GMT")
    {
        // GMTもパースされるので文字は残らない
        char *remaining = strptime(date, "%a, %d %b %Y %H:%M:%S GMT", &t);
        REQUIRE(strlen(remaining) == 0);
    }
}

テストコードを実際にターゲット上で実行すると無事passしました。これで今回の調査は完了です。

本番コードもすっきり、原因もわかって気持ちもすっきりしましたね!

-     char *pos = strptime(buf, "%a, %d %b %Y %H:%M:%S %Z", &t);
-      // GMT 無視されるが正しく取得できている
- 
-     if (!(strlen(pos) == 3 && (0 == strncmp(pos, "GMT", 3)))) {
-         // エラー処理
-     }
+    char *pos = strptime(buf, "%a, %d %b %Y %H:%M:%S GMT", &t);
+    if (pos == NULL) {
+        // エラー処理
+    }

おわりに

今回は放っておいても問題にならない違和感でしたが、こういう違和感の中に潜在バグがあったりもするので、違和感を覚えたらチャンスだと思って調査するのが重要だと考えています。特にlibcの実装依存は組込みではハマるポイントなので、きちんと理解を深めていきたいところです。なんと言ってもブログのネタになるのが良いです。


Natureではちょっとした違和感でも深堀りできるエンジニアを募集しています。スマートリモコンNature Remo、スマートエナジーハブNature Remo Eを一緒に開発しましょう。

herp.careers

カジュアル面談も実施していますので、興味がある方はお気軽にご応募ください!

herp.careers