C 言語 (Win32 API) を使って Windows アプリケーションを開発している時に、 Unix エポック (1970 年 1 月 1 日) からの経過時間を秒単位ではなくミリ秒単位で取得する必要に迫られました。
Unix エポックからの経過時間を秒単位で取得するのであれば time
関数を使うだけで済むのですが、 ミリ秒単位で取得する場合は少しコードを書く必要がありました。今回は、 Windows で Unix エポックからの経過時間をミリ秒単位で取得する方法を紹介します。
UnixエポックとUnix時間
起点のなる時刻 1970-01-01 00:00:00
(UTC) を Unix エポックと言います。
似た言葉として Unix 時間があります。Unix 時間は 「ある時刻を Unix エポックからの経過秒数として表わした数値」 です。
たとえば、 2018-02-13 19:28:30
を Unix 時間で表現すると 1518550110
になります。これは 1970-01-01 00:00:00
(UTC) から 1518550110
秒経過した時刻が 2018-02-13 19:28:30
になるということです。
Windowsの経過時間
Windows にも Unix 時間に類似した “ある時刻を起点として経過時間を数値で表わした時間表現” があります。それが FILETIME 構造体です。Windows ではこの時刻の数値表現を File Times (ファイル時間) と呼びます。
- 元々はファイルの作成時刻やアクセス時刻を記録するためのデータ型だったことから
FILETIME
という型名になっていますが、FILETIME
構造体はファイルを扱わない場面でも良く使われる時間を表すデータ型です。
Unix 時間が 1970-01-01 00:00:00
(UTC) を起点とした 「経過秒数」 であるのに対して、 Windows のファイル時間は 1601-01-01 00:00:00
(UTC) を起点とした 「100 ナノ秒単位の経過時間」 を表す数値です。
2018-02-13 19:28:30
を Windows ファイル時間で表すと 15185501100000000
になります。経過時間の単位が 100 ナノ秒単位なので桁数がずいぶんと多くなりますね。
SystemTimeToFileTime 関数を使うと SYSTEMTIME 構造体を FILETIME 構造体に変換することができます。
SYSTEMTIME 構造体は年、 月、 日、 時、 分、 秒をそれぞれ別のメンバーとして保持する構造体です。wYear
メンバーを参照すればすぐに年が分かる、 wMonth
メンバーを参照すればすぐに月が分かる、 という扱い易さがあります。しかし、 時刻を表現するメンバーが細かく分かれているので、 2 つの時刻の差を求めるのは少々面倒です。
ファイル時間は単なる数値なので引き算をすれば 2 つの時刻の差を簡単に求めることができます。(実際には上位 32 ビットと下位 32 ビットでメンバーが分かれているので 64 ビット変数に格納し直す必要はありますが…。)
Windowsファイル時間をUnix時間に変換する
Windows にも起点からの経過時間を数値で表すデータがあることが分かりました。表現精度も 100 ナノ秒単位あるので、 ミリ秒単位の経過時間に丸めることもできそうです。
それでは現在時刻を Unix エポックからの経過時間 (ミリ秒単位) で取得してみましょう。仕組みは簡単です。現在時刻と Unix エポック 1970-01-01 00:00:00
の 2 つをファイル時間で表します。現在時刻を表すファイル時間から Unix エポックを表すファイル時間を引けば、 1970-01-01 00:00:00
からの経過時間 (100 ナノ秒単位) が手に入ります。これを 10000
で割ればミリ秒単位になります。
現在時刻を1970-01-01 00:00:00からの経過時間(ミリ秒単位)で取得する関数/** 現在時刻を1970-01-01 00:00:00からの経過時間(ミリ秒単位)で返します。
*/
long long current_time_millis()
{
//Unixエポック 1970-01-01 00:00:00 を SYSTEMTIME構造体に格納します。
SYSTEMTIME unix_epoch;
unix_epoch.wYear = 1970;
unix_epoch.wMonth = 1;
unix_epoch.wDayOfWeek = 4;
unix_epoch.wDay = 1;
unix_epoch.wHour = 0;
unix_epoch.wMinute = 0;
unix_epoch.wSecond = 0;
unix_epoch.wMilliseconds = 0;
//Unixエポックを格納したSYSTEMTIME構造体をFILETIME構造体に変換します。
FILETIME unix_epoch_ft_from_1601;
SystemTimeToFileTime(&unix_epoch, &unix_epoch_ft_from_1601);
//Unixエポックを格納したFILETIME構造体を64ビット整数型に変換します。
ULARGE_INTEGER unix_epoch_from_1601;
unix_epoch_from_1601.HighPart = unix_epoch_ft_from_1601.dwHighDateTime;
unix_epoch_from_1601.LowPart = unix_epoch_ft_from_1601.dwLowDateTime;
//現在時刻をFILETIME構造体に格納します。
FILETIME current_ft_from_1601;
GetSystemTimeAsFileTime(¤t_ft_from_1601);
//現在時刻を格納したFILETIME構造体を64ビット整数型に変換します。
ULARGE_INTEGER current_from_1601;
current_from_1601.HighPart = current_ft_from_1601.dwHighDateTime;
current_from_1601.LowPart = current_ft_from_1601.dwLowDateTime;
//現在時刻ファイル時間からUnixエポックファイル時間を引きます。
//これでUnixエポックからの経過時間(100ナノ秒単位)になります。
long long diff = current_from_1601.QuadPart - unix_epoch_from_1601.QuadPart;
//10000で割って経過時間(100ナノ秒単位)を経過時間(ミリ秒単位)に丸めます。
long long t = diff / 10000;
return t;
}
Unix エポックのファイル時間を取得するのにいくつかのステップを踏んでいますが、 ファイル時間の起点 (‘1601-01-01 00:00:00’) から Unix エポック (‘1970-01-01 00:00:00’) までの経過時間というのは必ず決まった値になるので実は毎回計算する必要はありません。
上記の Unix エポックを格納した 64 ビット整数 unix_epoch_from_1601.QuadPart
は常に同じ値 116444736000000000
になります。このマジックナンバーを使えばコードは以下のように簡略化できます。
現在時刻を1970-01-01 00:00:00からの経過時間(ミリ秒単位)で取得する関数/** 現在時刻を1970-01-01 00:00:00からの経過時間(ミリ秒単位)で返します。
*/
long long current_time_millis()
{
long long unix_epoch_from_1601_int64 = 116444736000000000LL;
//現在時刻をFILETIME構造体に格納します。
FILETIME current_ft_from_1601;
GetSystemTimeAsFileTime(¤t_ft_from_1601);
//現在時刻を格納したFILETIME構造体を64ビット整数型に変換します。
ULARGE_INTEGER current_from_1601;
current_from_1601.HighPart = current_ft_from_1601.dwHighDateTime;
current_from_1601.LowPart = current_ft_from_1601.dwLowDateTime;
//現在時刻ファイル時間からUnixエポックファイル時間を引きます。
//これでUnixエポックからの経過時間(100ナノ秒単位)になります。
long long diff = current_from_1601.QuadPart - unix_epoch_from_1601_int64;
//10000で割って経過時間(100ナノ秒単位)を経過時間(ミリ秒単位)に丸めます。
long long t = diff / 10000;
return t;
}
64 ビット整数 (long long) をソースコードにリテラルとして直接記述するために 116444736000000000
の末尾に LL
を付けて 116444736000000000LL
としています。
これで、 現在時刻や任意の時刻を 1970-01-01 00:00:00
からの経過ミリ秒数に変換することができました。