(2021/3初稿。2023/12にESP32の最新状況を加筆(ラズパイ64bitは未だに試してません))
これまでのあらすじ
「え、Arduino って(time_t)0 が2000/1/1 なの?しかも time_t が uint32_t だと?2038年問題対応済み?」
そういう情報をネットで見たので確かめようと思ったが…
2038年問題とは
C言語(あるいは派生形である C++ やArduino言語) において、以下の3条件を満たしている場合
1. time_t
型が 32bit 符号付き整数(int32_t等)で表現されており、
2. (time_t)0
が指す日時が UNIX EPOCH TIME(1970/01/01 00:00:00)であり、
3. time_t
が1増えると1秒増えるよう実装されている
その場合に、time_t
の最大値が 0x7fffffff (2038/01/19 03:14:07に相当)になり、その先を表現できないという問題。
具体的には 0x7fffffff に1足すと 0x80000000 になるが、これが int32_t
的にはマイナスの値になるため、いきなり過去を表現した値に化けてしまう、というような問題が起こる。
たとえば2の(time_t)0
が指す日時が2000/01/01 00:00:00 になっていれば、30年ずれるので、それだけで2038年問題は2068年問題になる。あとは1をunsigned にしたり64bitにしたりすることで未来を表現できる幅が拡がる。
Arduino の確認はどうすれば?
time_t
の型はヘッダのどこかに書いてあるはずだけど面倒だし、(time_t)0
を渡した mktime
が2000年1月1日を返すかどうかはライブラリ依存なので、ライブラリを呼ぶプログラムを書いみた方が早いでしょう。ついでにArduinoIDEで開発している esp32 や nRF51822(micro:bit や AE-TYBLE16)あたりも確認してしまいましょう。(おまけにラズパイのgcc with glibcやらでも)
確認したいこと
仕様確認という意味では4点。
-
time_t
は何ビットの整数か
sizeof(time_t)
でバイト数が解るので8倍しましょう。
本当に整数型かどうかはヘッダファイルを見るべきですが、面倒なのでfloat や double、long double に 1.1 を代入して、time_t にキャストしてみます。どれかの型であればキャスト前後で一致しますが、整数型なら 1 にされるので一致しません。 -
エポックタイム :
(time_t)0
がいつをさすか
1970/01/01 00:00:00 (UTC) が UNIX epoch ですが、
AVR Arduino では2000/01/01 00:00:00 (UTC)であるらしく、
time_t
がsigned 32bit integer であっても、これだけで2068年まで延命されますね。
mktime
でtm
構造体に変換して確認しましょう。 -
time_t
は signed か unsigned か
singed か unsigned か、は(time_t)0
と(time_t)-1
の比較で解ります。
マイナスが扱えるなら(time_t)-1
は(time_t)0
に対して過去になりますし、が未来になるなら unsigned となります。
(型としてはsignedなんだけどライブラリがunsignedに変換して処理、などとしているのは実質unsignedという結果主義で。そんな実装無いとは思いますが) -
time_t
は1秒刻みか?
・signed 32bit integer の最大値 0x7fffffff がいつを指すか
・signed 32bit integer の最大値から1を減じた 0x7ffffffe がいつを指すか
を見比べれば良いでしょうか。
さらに
・signed 32bit integer の最大値に1を加えた 0x80000000 がいつを指すか
・unsigned 32bit integer の最大値 0xffffffff (これはsigned 32bit だと-1と扱われる)がいつを指すか
・unsigned 32bit integer の最大値に1を加えた33bit値 0x100000000 を入れたらいつを指すか(内部64bitになっているかどうか)
なんてのも見てみれば、2038年問題がどうなっているか解るでしょう。
検証プログラム
#include <time.h>
char buf[1000];
void test_tm(time_t t)
{
struct tm *p;
p = gmtime(&t);
sprintf( buf, "%d/%02d/%02d %02d:%02d:%02d\n", p->tm_year + 1900, p->tm_mon +1, p->tm_mday, p->tm_hour, p->tm_min, p->tm_sec );
Serial.print(buf);
}
void setup()
{
Serial.begin(115200);
// time_t のバイト数、ビット数
sprintf( buf, "sizeof(time_t) : %d bytes (%d bits) \n", sizeof(time_t), sizeof(time_t)*8 );
Serial.print(buf);
// time_t は整数型か浮動小数点型か
float a = 1.1;
double b = 1.1;
long double c = 1.1;
if ( a == (time_t)a || b == (time_t)b || c == (time_t)c ) {
Serial.print( "floating point number type\n" );
}
else {
Serial.print( "integer type\n" );
}
Serial.print( "(time_t) 0 :" ); // epoch 次第で結果が変わる
test_tm( (time_t)0 );
Serial.print("(time_t)-1 :");// time_t の定義次第で結果が変わる
test_tm( (time_t)-1 );
Serial.print("(time_t)0x ffff ffff :"); // unsigned 32bit int の正の最大値
test_tm( (time_t)0xffffffff );
Serial.print("(time_t)0x 7fff ffff :"); // signed 32bit int の正の最大値
test_tm( (time_t)0x7fffffff );
Serial.print("(time_t)0x 7fff fffe :"); // signed 32bit int の正の最大値 -1
test_tm( (time_t)0x7ffffffe );
Serial.print("(time_t)0x 8000 0000 :"); // signed 32bit int の正の最大値 +1
test_tm( (time_t)0x80000000 );
Serial.print("(time_t)0x10000 0000 :"); // 33bit を time_t にキャストしたら?
test_tm( (time_t)0x100000000 );
}
void loop()
{
delay(10000); //なにもしない
}
Arduino (ATmega328P)での実行結果
sizeof(time_t) : 4 bytes (32 bits)
integer type
(time_t) 0 :2000/01/01 00:00:00
(time_t)-1 :2136/02/07 06:28:15
(time_t)0x ffff ffff :2136/02/07 06:28:15
(time_t)0x 7fff ffff :2068/01/19 03:14:07
(time_t)0x 7fff fffe :2068/01/19 03:14:06
(time_t)0x 8000 0000 :2068/01/19 03:14:08
(time_t)0x10000 0000 :2000/01/01 00:00:00
- time_t は 32bitで、整数です。
- エポックはたしかに 2000/1/1 00:00:00 でした。
- time_t は unsigned です。(-1がマイナスにならず未来を指しているので)
- 0x7fff ffff と 0x7xfff fffe の差が1秒なので1秒刻み
0xffff ffff の2136/02/07 06:28:15 が表現できる最大値で(-1を uint32_t にキャストするとこれになります)
その1秒後は0x1 0000 0000 のはずが、32bitに収まっておらずキャストすると 0x0000 0000 に削られてしまうので、指す時刻も 2000/1/1 00:00:00 に戻ってしまいます。
2136年問題が発生しうるのですが、自分は流石に死んでると思うので気にしないことに。
ESP32-DevkitC (Arduino-ESP32 v1.0.5) やmicro:bit (nRF51822) の実行結果
sizeof(time_t) : 4 bytes (32 bits)
integer type
(time_t) 0 :1970/01/01 00:00:00
(time_t)-1 :1969/12/31 23:59:59
(time_t)0x ffff ffff :1969/12/31 23:59:59
(time_t)0x 7fff ffff :2038/01/19 03:14:07
(time_t)0x 7fff fffe :2038/01/19 03:14:06
(time_t)0x 8000 0000 :1901/12/13 20:45:52
(time_t)0x10000 0000 :1970/01/01 00:00:00
esp32 (Arduino-ESP32 v1.0.5)も nRF51822 も全く同じなので一緒に。
-
time_t
は 32bitで、整数です。 - エポックはUNIX EPOCH です。
-
time_t
は signed です。(-1がマイナスになって過去を指しているので) - 0x7fff ffff と 0x7xfff fffe の差が1秒なので1秒刻み
2038年問題の条件が揃いきってるので、 0x7fffffff が 0x80000000 になった瞬間1901年に戻っています。
ライブラリが将来アップデートされる、なんてことはあるのでしょうか。→2023/12/1 次項加筆しました
同じArduinoIDE で同じコードをビルドしても、呼ばれるライブラリプログラムの実装が違えば結果が変わるので、走らせてみて確認するので正しかった気がします。
Arduino-ESP32 3.0.0 Alpha2 (2023/12加筆)
ArduinoIDEのボードマネージャで、esp32 by espressif Systems の v3.0.0-alpha2 が来ていました。esp32はesp-idf v5.x系からtime_tが64bit になり、それはArduino-ESP32は 3.xからidf v5系になると事前に聞いていたので、Alphaじゃなくなったら試そうかなと思っていたのですが、待ちきれずにAlpha2で試してしまいました。
ESP32-DevkitC (Arduino-ESP32 v3.0.0alpha2)の実行結果
sizeof(time_t) : 8 bytes (64 bits)
integer type
(time_t) 0 :1970/01/01 00:00:00
(time_t)-1 :1969/12/31 23:59:59
(time_t)0x ffff ffff :2106/02/07 06:28:15
(time_t)0x 7fff ffff :2038/01/19 03:14:07
(time_t)0x 7fff fffe :2038/01/19 03:14:06
(time_t)0x 8000 0000 :2038/01/19 03:14:08
(time_t)0x10000 0000 :2106/02/07 06:28:16
64bitですので、0x100000000 まできっちり未来に向かっています(0xffffffffの1秒後)
記事を書いたときからずっとこれを待っていました!
signed 64bit であれば過去のほうも相当昔まで表現できそうですね。
おまけ: raspberrypi (32bit)
ArduinoIDE 用プログラムは(必要なら sprintf
してから) Serial.print
してますが、全て printf
にすればよくて(もちろん#include <stdio.h>
もしましょう) test.c として保存、 gcc test.c でコンパイルして出来る a.out 実行すれば試せます。
プログラム
#include <time.h>
#include <stdio.h>
char buf[1000];
void test_tm(time_t t)
{
struct tm *p;
p = gmtime(&t);
printf( "%d/%02d/%02d %02d:%02d:%02d\n", p->tm_year + 1900, p->tm_mon +1, p->tm_mday, p->tm_hour, p->tm_min, p->tm_sec );
}
int main()
{
// time_t のバイト数、ビット数
printf( "sizeof(time_t) : %d bytes (%d bits) \n", sizeof(time_t), sizeof(time_t)*8 );
// time_t は整数型か浮動小数点型か
float a = 1.1;
double b = 1.1;
long double c = 1.1;
if ( a == (time_t)a || b == (time_t)b || c == (time_t)c ) {
printf( "floating point number type\n" );
}
else {
printf( "integer type\n" );
}
printf( "(time_t) 0 :" ); // epoch 次第で結果が変わる
test_tm( (time_t)0 );
printf("(time_t)-1 :");// time_t の定義次第で結果が変わる
test_tm( (time_t)-1 );
printf("(time_t)0x ffff ffff :"); // unsigned 32bit int の正の最大値
test_tm( (time_t)0xffffffff );
printf("(time_t)0x 7fff ffff :"); // signed 32bit int の正の最大値
test_tm( (time_t)0x7fffffff );
printf("(time_t)0x 7fff fffe :"); // signed 32bit int の正の最大値 -1
test_tm( (time_t)0x7ffffffe );
printf("(time_t)0x 8000 0000 :"); // signed 32bit int の正の最大値 +1
test_tm( (time_t)0x80000000 );
printf("(time_t)0x10000 0000 :"); // 33bit を time_t にキャストしたら?
test_tm( (time_t)0x100000000 );
}
実行結果
sizeof(time_t) : 4 bytes (32 bits)
integer type
(time_t) 0 :1970/01/01 00:00:00
(time_t)-1 :1969/12/31 23:59:59
(time_t)0x ffff ffff :1969/12/31 23:59:59
(time_t)0x 7fff ffff :2038/01/19 03:14:07
(time_t)0x 7fff fffe :2038/01/19 03:14:06
(time_t)0x 8000 0000 :1901/12/13 20:45:52
(time_t)0x10000 0000 :1970/01/01 00:00:00
32bit版なのでそりゃそうかっていう結果。
64bit Linux PC でやると(time_t)0x100000000 のところがちゃんと0xffffffff の1秒後になるのでラズパイも64bit版を待ちましょう。2038年まではかからないはず。
おまけ2
arduino-1.8.13\hardware\tools\avr\avr\include\time.h には
typedef uint32_t time_t;
と書いてありますね。
esp32 なら
Arduino15\packages\esp32\hardware\esp32\1.0.5\tools\sdk\include\newlib\sys\timeb.h で
typedef _TIME_T_ time_t;
となっていて、
Arduino15\packages\esp32\hardware\esp32\1.0.5\tools\sdk\include\newlib\machine\types.h で
#define _TIME_T_ long /* time() */
となっています。(sdkでそうなので、ESP-IDFで開発しても一緒でしょう)
nRF51 なら、
Arduino15\packages\sandeepmistry\tools\gcc-arm-none-eabi\5_2-2015q4\arm-none-eabi\include\sys\timeb.h
typedef _TIME_T_ time_t;
Arduino15\packages\sandeepmistry\tools\gcc-arm-none-eabi\5_2-2015q4\arm-none-eabi\include\machine\types.h
#define _TIME_T_ long /* time() */
面倒だけど一応調べました(笑)