C# P/InvokeでSetSystemTimeAdjustmentが無効化になる原因と修正方法(GetSystemTimeAdjustment / DllImport)

C# から kernel32.dll の SetSystemTimeAdjustment を P/Invoke で呼び出し、システム時刻の進む速さ(クロックレート)を調整したいのに、GetSystemTimeAdjustment で読み返すと lpTimeAdjustmentDisabled が true になってしまう――この症状の多くは DllImport 宣言の ref/out 取り違えが原因です。正しい宣言、確認手順、運用上の注意点までまとめます。

目次

起きている症状を整理する

まずは現象を「期待」と「実際」に分けて言語化しておくと、切り分けが一気に楽になります。今回のパターンは次の組み合わせが典型です。

操作期待する結果実際の結果(よくある)
SetSystemTimeAdjustment(...) を呼ぶクロックレート(時刻の進む速さ)が指定どおりに変わる戻り値は true(成功扱い)
GetSystemTimeAdjustment(...) で読み返すlpTimeAdjustmentDisabled が false で、lpTimeAdjustment が指定値lpTimeAdjustmentDisabled が true になり、lpTimeAdjustment が 156250 など固定値に見える
権限や時刻同期サービスを確認権限不足や w32time の干渉が原因なら変化するSE_SYSTEMTIME_NAME を有効化しても、w32time を止めても挙動が変わらない

この時点で「権限」や「w32time の上書き」という筋も疑えますが、実はもっと手前――P/Invoke の宣言ミスが原因のことが多いです。

結論:Set 側に ref を付けると、別の値が渡ってしまう

原因は P/Invoke の型指定ミス(DllImport の宣言ミス)です。ポイントは次のとおりです。

  • GetSystemTimeAdjustment は「結果を受け取る」APIなので out/ref で受けるのが正しい
  • SetSystemTimeAdjustment は Win32 的には 値渡しのAPI(DWORD と BOOL を値として渡す)
  • しかし C# 側で SetSystemTimeAdjustment の引数に ref を付けると、ネイティブ側には「値」ではなく「アドレス(ポインタ)」が渡る
  • ネイティブ側は BOOL を「0 か非0」で判定するため、ポインタ値はほぼ確実に非0→結果として bTimeAdjustmentDisabled が true 扱いになりやすい

つまり「true が返っているのに、設定が反映されない」のではなく、別の引数が渡っているので、別の設定が行われている状態です。

よくある間違い(Set 側に ref/out を付けてしまう)

[DllImport("kernel32.dll")]
static extern bool SetSystemTimeAdjustment(ref uint TimeAdj, ref bool AdjDisable);

この宣言だと、C# からは「値」ではなく「変数のアドレス」を渡します。Win32 はそれを DWORD/BOOL として解釈するため、結果が破綻します。

修正例(Set 側は ref を外す)

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool SetSystemTimeAdjustment(uint dwTimeAdjustment, bool bTimeAdjustmentDisabled);

呼び出し側も ref を使わず、値をそのまま渡します。

uint newRate = 156250;     // 例:ここは狙うクロックレートに合わせて計算
bool disable = false;      // 調整を無効化しない(=有効化する)
bool ok = SetSystemTimeAdjustment(newRate, disable);

if (!ok)
{
int err = Marshal.GetLastWin32Error();
throw new Win32Exception(err);
}

Win32 シグネチャを「型」と「方向」で合わせる

P/Invoke は「型」だけでなく引数が入力(in)なのか、出力(out)なのかを合わせるのが重要です。今回のように似た名前の Get/Set がある API は、方向を取り違えやすいので要注意です。

API引数Win32 の意味C# 側の扱いよくあるミス
GetSystemTimeAdjustmentlpTimeAdjustment取得結果(DWORD を書き込む先)out uint値型にしてしまい取得できない
lpTimeIncrement取得結果(DWORD を書き込む先)out uintout を付け忘れる
lpTimeAdjustmentDisabled取得結果(BOOL を書き込む先)out boolin/ref の混在
SetSystemTimeAdjustmentdwTimeAdjustment設定する値(DWORD)uintref を付ける
bTimeAdjustmentDisabled無効化フラグ(BOOL)boolref を付ける

「Get は out、Set は値渡し」という単純なルールですが、ここを間違えると今回のような不可解な挙動になります。

正しい P/Invoke 宣言(実戦向け)

実務では、失敗時の原因特定をしやすくするために SetLastError = true を付け、失敗時に Marshal.GetLastWin32Error() を拾えるようにしておくのが定石です。

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;

internal static class TimeAdjustmentNative
{
    [DllImport("kernel32.dll", SetLastError = true)]
    internal static extern bool GetSystemTimeAdjustment(
        out uint lpTimeAdjustment,
        out uint lpTimeIncrement,
        out bool lpTimeAdjustmentDisabled);

    [DllImport("kernel32.dll", SetLastError = true)]
    internal static extern bool SetSystemTimeAdjustment(
        uint dwTimeAdjustment,
        bool bTimeAdjustmentDisabled);

    internal static void ThrowLastWin32ErrorIfFalse(bool ok)
    {
        if (!ok)
        {
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }
    }
}

読み出し→設定→読み返しの最小サンプル

「本当に反映されたか」を確認するために、設定前後で値を読み出してログ化します。

uint adj, inc;
bool disabled;

TimeAdjustmentNative.ThrowLastWin32ErrorIfFalse(
    TimeAdjustmentNative.GetSystemTimeAdjustment(out adj, out inc, out disabled));

Console.WriteLine($"Before: adj={adj}, inc={inc}, disabled={disabled}");

uint newAdj = inc + 10;     // 例:ほんの少しだけ速くする(環境により効果は変わります)
bool newDisabled = false;   // 調整を有効化
TimeAdjustmentNative.ThrowLastWin32ErrorIfFalse(
    TimeAdjustmentNative.SetSystemTimeAdjustment(newAdj, newDisabled));

TimeAdjustmentNative.ThrowLastWin32ErrorIfFalse(
    TimeAdjustmentNative.GetSystemTimeAdjustment(out adj, out inc, out disabled));

Console.WriteLine($"After : adj={adj}, inc={inc}, disabled={disabled}");

ここで disabled が false になり、adj が指定値付近に変わっていれば、少なくとも P/Invoke 宣言は正しく通っています。

「156250 など固定っぽい値」になる理由

読めてくる値が 156250 のように「固定」に見えると、OS が強制的に戻しているのでは?と疑ってしまいます。しかし、今回の症状では次の説明がしっくり来ます。

  • lpTimeIncrement は「基本となる刻み幅(1 tick の長さ)」で、環境によっては一定(例:15.625ms など)
  • lpTimeAdjustmentDisabled が true のとき、調整が無効化され、結果として lpTimeAdjustment も「基準値」に寄る
  • Set 側の引数が壊れていると、意図せず bTimeAdjustmentDisabled が true になり、読み返すと「固定値に戻った」ように見える

参考として、よく見かける値を「時間」に換算するとイメージが掴めます(単位は 100ns 刻みで扱われます)。

値(100ns 単位の数)換算(おおよそ)見え方
15625015.625ms昔からよくある “15.6ms tick” の代表例
100001.0ms高頻度タイマ要求などの影響で見えることがある
50000.5ms環境によってはさらに細かくなる場合も

固定値に見えるときは、値そのものよりも、まず lpTimeAdjustmentDisabled の状態を疑うのが近道です。

Win32 型と C# 型の対応表(P/Invoke の落とし穴を避ける)

今回の事故は「ref を付けてしまった」ことが本丸ですが、合わせて Win32 型と C# 型の対応も整理しておくと、別 API でも応用できます。

Win32 型意味C# での推奨補足
DWORD32bit 符号なし整数uint「時間(100ns 単位)」のようなカウンタで使われやすい
BOOL32bit の真偽(0/非0)boolP/Invoke では通常 bool で問題ない(必要なら MarshalAs を明示)
PDWORD / LPDWORDDWORD へのポインタout uint または ref uint「結果を書き込む」なら out が自然
PBOOLBOOL へのポインタout bool または ref boolGet 系 API のフラグ取得で頻出

判断に迷ったら「ネイティブ側がポインタを要求しているか?」を起点にすると安全です。ドキュメントで引数名に lp が付いている(lpTimeAdjustment など)場合は、ポインタ(=out/ref)が期待されていることが多いです。

権限:SeSystemtimePrivilege(SE_SYSTEMTIME_NAME)を有効化する

SetSystemTimeAdjustment はシステム全体の時刻の進み方に触るため、通常は権限が必要です。権限不足のときは API が失敗(false)になりやすいので、今回のように「true なのに変」とは症状が違うことが多いですが、記事としては押さえておきます。

代表的な手順は次のとおりです。

  • 管理者として起動(UAC を含む)
  • プロセストークンを開く
  • SE_SYSTEMTIME_NAME を AdjustTokenPrivileges で有効化する

最小限のコード例(エラーハンドリングは簡略化)です。

using System;
using System.ComponentModel;
using System.Runtime.InteropServices;

internal static class Privilege
{
    private const string SE_SYSTEMTIME_NAME = "SeSystemtimePrivilege";
    private const int TOKEN_ADJUST_PRIVILEGES = 0x0020;
    private const int TOKEN_QUERY = 0x0008;
    private const int SE_PRIVILEGE_ENABLED = 0x0002;

    [StructLayout(LayoutKind.Sequential)]
    private struct LUID
    {
        public uint LowPart;
        public int HighPart;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct TOKEN_PRIVILEGES
    {
        public int PrivilegeCount;
        public LUID Luid;
        public int Attributes;
    }

    [DllImport("advapi32.dll", SetLastError = true)]
    private static extern bool OpenProcessToken(IntPtr ProcessHandle, int DesiredAccess, out IntPtr TokenHandle);

    [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    private static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, out LUID lpLuid);

    [DllImport("advapi32.dll", SetLastError = true)]
    private static extern bool AdjustTokenPrivileges(IntPtr TokenHandle, bool DisableAllPrivileges,
        ref TOKEN_PRIVILEGES NewState, int BufferLength, IntPtr PreviousState, IntPtr ReturnLength);

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetCurrentProcess();

    public static void EnableSystemTimePrivilege()
    {
        if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out var token))
            throw new Win32Exception(Marshal.GetLastWin32Error());

        if (!LookupPrivilegeValue(null, SE_SYSTEMTIME_NAME, out var luid))
            throw new Win32Exception(Marshal.GetLastWin32Error());

        var tp = new TOKEN_PRIVILEGES
        {
            PrivilegeCount = 1,
            Luid = luid,
            Attributes = SE_PRIVILEGE_ENABLED
        };

        if (!AdjustTokenPrivileges(token, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero))
            throw new Win32Exception(Marshal.GetLastWin32Error());
    }
}

権限まわりは環境(ローカルポリシー、ドメインポリシー、サービス実行、タスクスケジューラ実行など)で挙動が変わるので、運用設計とセットで確認してください。

w32time(w32tm)との関係:上書きされるケースを想定しておく

Windows の時刻同期(NTP / w32time)と、SetSystemTimeAdjustment による「クロックレート調整」は別のレイヤーです。ただし実務では、次のような形で結果として上書きに見えることがあります。

  • 時刻同期サービスが、一定間隔で時計合わせを行い、時刻のズレを補正する
  • 補正の方法が「一気に合わせる(ステップ)」か「少しずつ寄せる(スルー)」かは状況により変わる
  • その結果、アプリが意図した “一定の進み方” が維持されず、観測が難しくなる

「P/Invoke を直したのに思ったほど効かない」と感じたら、まずは次の観点で観測方法を見直すのが効果的です。

観点確認のしかた(例)意図しない影響
時刻同期が動いているかw32tm /query /status で状態を確認同期タイミングで時計が寄せられ、調整効果が見えにくい
観測対象が「壁時計」になっていないか測定には QueryPerformanceCounter などの単調増加カウンタも併用システム時刻は補正されるため、短時間の評価が歪む
仮想環境/同期機構Hyper-V / VMware などの時刻同期機能の有無を確認ホスト側の同期でゲストが補正される

なお「時刻同期を止めればいい」と単純に言い切れないのが実務です。ドメイン環境では Kerberos、証明書検証、ログ相関など、時刻の整合性が前提の仕組みが多いため、止める/止めないは要件と影響範囲を整理したうえで判断してください。

実務で役立つ:安全に試すための手順

SetSystemTimeAdjustment はシステム全体に影響するため、いきなり本番で触るのは避け、段階を踏んで検証するのが現実的です。以下は「事故を起こしにくい」順序です。

  • 検証環境(仮想マシンなど)でまず動作確認する
  • 設定前の GetSystemTimeAdjustment をログに残す(後で戻せるように)
  • 変更量は 最小から始める(例:inc + 1 など)
  • 設定後に GetSystemTimeAdjustment を再取得し、disabled=false を確認する
  • 数分〜数十分の観測で、ズレが期待方向に出るかを確認する
  • 終了時は必ず元の値に戻す(手順をコード化する)

「戻す」を忘れると、次の担当者が原因不明の時刻ズレに悩むことになります。復旧を想定したコード(例:finally で復元)を最初から組み込みましょう。

トラブルシューティング:症状別チェックリスト

最後に、現場でよく出る「似た症状」をまとめます。原因が複合することもあるので、上から順に潰すのが安全です。

症状ありがちな原因チェック対処
Set が true なのに disabled が true になるP/Invoke の ref/out ミスSet 側の宣言に ref/out が付いていないかSet は値渡しに修正(ref を外す)
Set が false で失敗する権限不足、UAC、ポリシーMarshal.GetLastWin32Error() を確認SeSystemtimePrivilege を有効化、管理者実行
値は反映されるが、しばらくすると元に戻るw32time や仮想環境の時刻同期の介入同期状態・同期タイミングを確認要件に応じて同期設計を見直す
調整してもズレが観測できない観測方法が不適切、変更量が小さすぎる観測時間と指標を見直す単調増加カウンタ併用、変更量を段階的に増やす

この API を使う前に知っておきたい注意点

SetSystemTimeAdjustment は「OS 全体の時計の進み方」に影響します。便利な反面、次のような副作用を招く可能性があります。

  • ログの時刻がズレ、障害解析が難しくなる
  • 証明書の有効期限やトークン期限など、時刻依存の機構に影響する
  • ドメイン環境では認証・ポリシー適用に影響することがある
  • 複数台での時刻整合性が前提のシステムでは、全体設計としての整合が崩れる

もし目的が「アプリ内だけで時間を速く/遅く扱いたい」なら、システム時刻をいじらず、アプリ側で独自のタイムソース(基準時計+倍率)を持つ設計のほうが安全な場合もあります。逆に「どうしても OS 全体で必要」な要件なら、影響範囲と復旧手順まで含めて運用設計を行いましょう。

まとめ

SetSystemTimeAdjustment が成功(true)を返すのに、GetSystemTimeAdjustment で lpTimeAdjustmentDisabled が true になってしまう問題は、Set 側の P/Invoke 宣言に ref を付けてしまったことが原因のケースが非常に多いです。

  • Get は out/ref で受ける(ポインタが必要)
  • Set は値渡し(ref を付けない)
  • 156250 などの値は「基準の刻み幅」に見えている可能性がある
  • 権限や時刻同期サービスの影響も、再現条件としては押さえておく

まずは DllImport の宣言を正し、設定前後の値をログで比較して「本当に意図どおりの引数が渡っているか」を確認するところから始めてください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次