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# 側の扱い | よくあるミス |
|---|---|---|---|---|
GetSystemTimeAdjustment | lpTimeAdjustment | 取得結果(DWORD を書き込む先) | out uint | 値型にしてしまい取得できない |
lpTimeIncrement | 取得結果(DWORD を書き込む先) | out uint | out を付け忘れる | |
lpTimeAdjustmentDisabled | 取得結果(BOOL を書き込む先) | out bool | in/ref の混在 | |
SetSystemTimeAdjustment | dwTimeAdjustment | 設定する値(DWORD) | uint | ref を付ける |
bTimeAdjustmentDisabled | 無効化フラグ(BOOL) | bool | ref を付ける |
「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 単位の数) | 換算(おおよそ) | 見え方 |
|---|---|---|
| 156250 | 15.625ms | 昔からよくある “15.6ms tick” の代表例 |
| 10000 | 1.0ms | 高頻度タイマ要求などの影響で見えることがある |
| 5000 | 0.5ms | 環境によってはさらに細かくなる場合も |
固定値に見えるときは、値そのものよりも、まず lpTimeAdjustmentDisabled の状態を疑うのが近道です。
Win32 型と C# 型の対応表(P/Invoke の落とし穴を避ける)
今回の事故は「ref を付けてしまった」ことが本丸ですが、合わせて Win32 型と C# 型の対応も整理しておくと、別 API でも応用できます。
| Win32 型 | 意味 | C# での推奨 | 補足 |
|---|---|---|---|
| DWORD | 32bit 符号なし整数 | uint | 「時間(100ns 単位)」のようなカウンタで使われやすい |
| BOOL | 32bit の真偽(0/非0) | bool | P/Invoke では通常 bool で問題ない(必要なら MarshalAs を明示) |
| PDWORD / LPDWORD | DWORD へのポインタ | out uint または ref uint | 「結果を書き込む」なら out が自然 |
| PBOOL | BOOL へのポインタ | out bool または ref bool | Get 系 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 の宣言を正し、設定前後の値をログで比較して「本当に意図どおりの引数が渡っているか」を確認するところから始めてください。

コメント