C++/WinAPIでVHD/VHDXをdiskpart相当に自動化する方法:VDSでGPT初期化・EFI/MSR/Primary作成・フォーマット・ドライブレター割り当て

VHD/VHDX を C++/WinAPI だけで diskpart 相当に自動セットアップしたい――。CreateVirtualDisk で作成・アタッチまではできても、GPT 初期化、EFI/MSR/Primary の作成、FAT32/NTFS フォーマット、ドライブレター割り当てで詰まりがちです。本記事では VDS(Virtual Disk Service)を軸に、実務で動く組み方と落とし穴を整理します。

目次

よくある壁:Virtual Disk API だけで「diskpart 一連の流れ」を再現するのが難しい理由

Virtual Disk API(CreateVirtualDisk / OpenVirtualDisk / AttachVirtualDisk など)は、あくまで「VHD/VHDX ファイルを仮想ディスクとして OS に見せる」ための API です。ここまでできると一見ゴールに近そうに見えますが、diskpart がやっている作業はその先にあります。

  • ディスクの初期化(GPT 変換・ディスク ID 付与・プロパティ更新)
  • EFI/MSR/Primary のパーティション設計(サイズ・アライン・GPT 種別 GUID)
  • 各パーティションのファイルシステム作成(FAT32/NTFS)
  • ドライブレター/マウントポイント付与(アクセスパスの追加・削除)

これらを「DeviceIoControl を手探りで全部叩いて組む」ことも不可能ではありませんが、IOCTL の粒度が細かく、状態遷移(リスキャン・プロパティ更新・ボリューム生成待ち)も絡むため、diskpart 同等の安定性を出すのはかなり骨が折れます。

結論:diskpart 相当を狙うなら VDS(または Storage Management API)を軸にする

diskpart は内部的に VDS(Virtual Disk Service)の COM インターフェースを呼び出している、という整理をしておくと道筋が一気に明確になります。「VHDX 作成は Virtual Disk API でできるが、パーティション作成・フォーマット・アクセスパス付与までを確実に自動化したい」なら、VDS(あるいは Windows Storage Management API)を使うのが現実的です。

方式狙える範囲メリットデメリットおすすめ度
Virtual Disk API + DeviceIoControl作成/アタッチ/ディスク IOCTL依存が少ない、低レイヤ制御が可能パーティション/フォーマット/マウントが難しく、実装量が爆発しやすい中〜低(学習用途向け)
VDS(COM)作成/アタッチ/パーティション/フォーマット/アクセスパスdiskpart 近い抽象度で操作でき、手順をそのままコードに落としやすいCOM が煩雑、環境差の吸収が必要高(実務で組みやすい)
Windows Storage Management API(WMI/MI)ディスク/パーティション/ボリューム全般(新しめ)PowerShell の裏側に近く、設計思想がモダンWMI/MI の取り回しが必要、サンプル追従が前提中〜高(新規開発で検討)

この記事では、質問で詰まりやすかったポイント(GPT 初期化、EFI/MSR/Primary 作成、フォーマット、ドライブレター割り当て、EFI ボリューム GUID パス特定)を「VDS を軸に組む」前提で、手順と実装パターンをまとめます。

diskpart の操作を API に落とす対応表

diskpart のイメージやっていることC++ での現実解要注意点
create vdiskVHD/VHDX ファイル生成VDS CreateVDisk または CreateVirtualDiskVersion_2 の 0x57 はパラメータ初期化ミス/非対応が混ざりやすい
attach vdiskOS にディスクとして認識させるVDS Attach または AttachVirtualDisk権限/リスキャン/デバイス出現待ちが必要になることがある
convert gptGPT 初期化IOCTL_DISK_CREATE_DISK(または VDS 経由の初期化)初期化後に UpdateProperties/Refresh が必要な場面がある
create partition efi/msr/primaryEFI/MSR/基本データ領域の作成VDS で Free Extent を見て GPT パーティション作成アライン・サイズ・GUID の組み合わせを誤ると後工程が壊れる
format fs=fat32/ntfsファイルシステム作成VDS の Format(IVdsVolumeMF など)EFI は FAT32、MSR はフォーマットしない
assign letter=Xドライブレター付与IVdsVolumeMF::AddAccessPath / SetVolumeMountPointWSetVolumeMountPointW は “Volume 名” の取り方を誤ると 0x57

VDS で組む全体フロー

「diskpart 相当」を目指すなら、処理の順番と “状態の見え方” を先に固定しておくと安定します。代表的な流れは次の通りです。

  • COM 初期化 → VDS サービス接続(IVdsServiceLoader → LoadService)
  • Microsoft Virtual Disk Provider を取得(IVdsVdProvider)
  • VHDX 作成(CreateVDisk)→ アタッチ(Attach)
  • 物理ディスクとしてのパス/番号を確定(例:PhysicalDrive 番号)
  • GPT 初期化(IOCTL_DISK_CREATE_DISK)+プロパティ更新
  • Free Extent を取得 → EFI/MSR/Primary を作成(GPT の PartitionType GUID を指定)
  • EFI を FAT32、Primary を NTFS でフォーマット(VDS の Format)
  • 必要に応じてアクセスパス追加(ドライブレター割当、またはフォルダマウント)

この順番を守るだけでも「フォーマットしたのにドライブが見えない」「マウントポイント付与が 0x57 で失敗する」といった事故が減ります。

サンプル実装の骨格:VDS 接続〜VHDX 作成/アタッチ

以下は「動く形に持っていくための骨格」です。実務では、例外処理、ログ、リトライ(デバイス出現待ち)、COM 解放を丁寧に追加してください。なお、ディスク操作は破壊的になり得るため、必ずテスト用の VHD/VHDX と隔離環境で検証します。

<!-- C++コードはHTMLに貼るため < > をエスケープしています -->
#include &lt;windows.h&gt;
#include &lt;vds.h&gt;
#include &lt;virtdisk.h&gt;
#include &lt;initguid.h&gt;
#include &lt;comdef.h&gt;
#include &lt;string&gt;
#include &lt;vector&gt;

#pragma comment(lib, "vdsuuid.lib")
#pragma comment(lib, "virtdisk.lib")

static void ThrowIfFailed(HRESULT hr, const char* msg)
{
    if (FAILED(hr)) {
        _com_error err(hr);
        // 実務では msg と err.ErrorMessage() をログへ
        throw std::runtime_error(msg);
    }
}

static void ThrowIfWin32(BOOL ok, const char* msg)
{
    if (!ok) {
        // GetLastError() をログへ
        throw std::runtime_error(msg);
    }
}

// VDSサービスへ接続し、IVdsService を得る
static CComPtr<IVdsService> ConnectVdsService()
{
    ThrowIfFailed(CoInitializeEx(nullptr, COINIT_MULTITHREADED), "CoInitializeEx failed");

    CComPtr<IVdsServiceLoader> loader;
    ThrowIfFailed(CoCreateInstance(CLSID_VdsLoader, nullptr, CLSCTX_LOCAL_SERVER,
                                   IID_PPV_ARGS(&loader)),
                  "CoCreateInstance(CLSID_VdsLoader) failed");

    CComPtr<IVdsService> service;
    ThrowIfFailed(loader->LoadService(nullptr, &service), "LoadService failed");

    // サービスが準備できるまで待つ
    ThrowIfFailed(service->WaitForServiceReady(), "WaitForServiceReady failed");
    return service;
}

// Virtual Disk Provider(IVdsVdProvider)を探す
static CComPtr<IVdsVdProvider> GetVirtualDiskProvider(IVdsService* service)
{
    CComPtr<IEnumVdsObject> enumProviders;
    ThrowIfFailed(service->QueryProviders(VDS_QUERY_VIRTUALDISK_PROVIDERS, &enumProviders),
                  "QueryProviders failed");

    while (true) {
        ULONG fetched = 0;
        CComPtr<IUnknown> unk;
        HRESULT hr = enumProviders->Next(1, &unk, &fetched);
        if (hr == S_FALSE || fetched == 0) break;
        ThrowIfFailed(hr, "Enum providers failed");

        CComPtr<IVdsProvider> provider;
        ThrowIfFailed(unk->QueryInterface(IID_PPV_ARGS(&provider)), "QI IVdsProvider failed");

        VDS_PROVIDER_PROP prop{};
        ThrowIfFailed(provider->GetProperties(&prop), "GetProperties failed");

        // VirtualDisk Provider を取得
        CComPtr<IVdsVdProvider> vdProvider;
        if (SUCCEEDED(provider->QueryInterface(IID_PPV_ARGS(&vdProvider)))) {
            CoTaskMemFree(prop.pwszName);
            CoTaskMemFree(prop.pwszVersion);
            return vdProvider;
        }

        CoTaskMemFree(prop.pwszName);
        CoTaskMemFree(prop.pwszVersion);
    }

    throw std::runtime_error("Virtual Disk Provider not found");
}

// VHDX を作成してアタッチ(概要)
static void CreateAndAttachVhdx(IVdsVdProvider* vdProvider,
                               const std::wstring& vhdxPath,
                               ULONGLONG sizeBytes)
{
    // 実際の CreateVDisk は非同期(IVdsAsync)で返ることが多い
    // ここでは骨格のみ。実務では CreateVDisk の引数(動的/固定、ブロックサイズ等)を整理する。

    // 例:vdProvider->CreateVDisk(..., &async, &vdisk);
    // async->Wait(&hrResult, &pPercent);
    // vdisk->Attach(...);

    // 以降、GPT 初期化とパーティション作成へ進む
}

ポイントは「VDS サービス接続 → Virtual Disk Provider 取得」という入口を固めることです。ここまで到達できると、以後の “diskpart 相当” の操作(パーティション、フォーマット、アクセスパス)は VDS のオブジェクトモデル(Disk/Volume)で辿れるようになります。

GPT 初期化:IOCTL_DISK_CREATE_DISK で「convert gpt」を置き換える

アタッチした直後の仮想ディスクは「未初期化のディスク」として見えることがあります。この状態で GPT を作るには、Win32 のディスク IOCTL を使うのが分かりやすいパターンです(質問でも例として IOCTL_DISK_CREATE_DISK が挙がっていました)。

  • 仮想ディスクの “物理ディスクパス” を確定する(例:\\.\PhysicalDriveN)
  • PhysicalDrive を CreateFile で開く
  • IOCTL_DISK_CREATE_DISK で GPT 初期化
  • IOCTL_DISK_UPDATE_PROPERTIES で OS に再認識させる
<!-- 角括弧をエスケープしています -->
static void InitializeGptOnPhysicalDrive(const std::wstring& physicalDrivePath)
{
    HANDLE h = CreateFileW(physicalDrivePath.c_str(),
                           GENERIC_READ | GENERIC_WRITE,
                           FILE_SHARE_READ | FILE_SHARE_WRITE,
                           nullptr, OPEN_EXISTING, 0, nullptr);
    if (h == INVALID_HANDLE_VALUE) {
        throw std::runtime_error("CreateFileW(PhysicalDrive) failed");
    }

    CREATE_DISK cd{};
    cd.PartitionStyle = PARTITION_STYLE_GPT;
    // ここで DiskId を生成して埋める(CoCreateGuid など)
    ThrowIfFailed(CoCreateGuid(&cd.Gpt.DiskId), "CoCreateGuid failed");

    DWORD bytes = 0;
    BOOL ok = DeviceIoControl(h, IOCTL_DISK_CREATE_DISK,
                              &cd, sizeof(cd),
                              nullptr, 0, &bytes, nullptr);
    ThrowIfWin32(ok, "IOCTL_DISK_CREATE_DISK failed");

    // 変更を反映
    ok = DeviceIoControl(h, IOCTL_DISK_UPDATE_PROPERTIES,
                         nullptr, 0, nullptr, 0, &bytes, nullptr);
    ThrowIfWin32(ok, "IOCTL_DISK_UPDATE_PROPERTIES failed");

    CloseHandle(h);
}

GPT 初期化直後は、VDS 側のオブジェクトやボリューム一覧がまだ追従していないことがあります。実務では、VDS の Refresh / Reenumerate 相当の操作や、数百ミリ秒〜数秒のリトライを入れて「ディスクが VDS から見える状態」になるのを待つのがコツです。

EFI/MSR/Primary の作成方針:まず “要件” を固定する

EFI/MSR/Primary を切る理由は「UEFI ブート可能な GPT ディスクにしたい」かどうかで変わります。データ用 VHDX なら、EFI/MSR を作らず Basic Data だけで十分なケースも多いです。一方で “diskpart 相当” を忠実に再現するなら、次のように設計すると安全です。

パーティションPartitionType GUID推奨サイズの目安フォーマット備考
EFI System PartitionPARTITION_SYSTEM_GUID100〜260MB 程度FAT32bcdboot の配置先。ドライブレターを付けずに運用することも多い
Microsoft Reserved (MSR)PARTITION_MSFT_RESERVED_GUID16MB(一般的)なしフォーマットしない。アクセスパスも付けない
Basic DataPARTITION_BASIC_DATA_GUID残り全部NTFS実データ領域。ドライブレター割当対象

ここで重要なのが「Free Extent(空き領域)を見てオフセットとサイズを決める」ことです。先頭から固定オフセットで切るより、VDS が見ている空き領域に沿って切る方が整合性が取りやすく、後工程(フォーマット、アクセスパス付与)も安定します。

VDS で Free Extent を取得して GPT パーティションを作る考え方

  • 対象ディスク(IVdsDisk)を特定し、IVdsAdvancedDisk を得る
  • QueryFreeExtents で空き領域配列を取得
  • 先頭の free extent から EFI → MSR → Basic Data の順で CreatePartition(GPT 情報に PartitionType GUID を入れる)
  • 作成のたびに非同期完了を待ち、VDS を Refresh してボリューム生成を促す

実装の肝は「ディスクを VDS オブジェクトとして掴む」と「PartitionType GUID を正しく指定する」です。ここが固まると、diskpart の create partition をそのまま置き換えられます。

フォーマットとドライブレター割り当て:SetVolumeMountPointW で悩む前に VDS の AddAccessPath を使う

質問で典型的に詰まるのが、SetVolumeMountPointW が 0x57(ERROR_INVALID_PARAMETER)になるパターンです。これは「渡している “ボリューム名” が正しい形式ではない」か「末尾のバックスラッシュがない」など、前提条件を外しやすい API です。

VDS を使っているなら、ドライブレター付与は IVdsVolumeMF::AddAccessPath(または相当インターフェース)で完結させる方が安全です。VDS はボリュームをオブジェクトとして扱えるので、誤ったボリューム名を手作業で組み立てる必要が減ります。

やりたいことおすすめ API理由補足
EFI を FAT32 でフォーマットVDS の Format(IVdsVolumeMF 等)ディスク/ボリューム状態を VDS が吸収しやすいEFI は小さいので FAT32 制約に引っかかりにくい
データ領域を NTFS でフォーマットVDS の Formatクラスタサイズやクイック有無も指定しやすいフォーマット完了後に Refresh を入れると安定しやすい
ドライブレターを付けるIVdsVolumeMF::AddAccessPathSetVolumeMountPointW より前提が明確で、失敗原因を潰しやすい例:L”X:\\” を追加(末尾 \\ を忘れない)

一方で、どうしても Win32 の SetVolumeMountPointW を使いたい場面(VDS を避ける、または既存実装に合わせる)もあります。その場合は次のチェックリストを必ず通します。

SetVolumeMountPointW が 0x57 になるチェックリスト

チェック項目正しい例間違いやすい例結果
マウント先は “ルート” かX:\X: や X:\temp0x57 になりやすい
ボリューム名は Volume GUID 形式か\\?\Volume{GUID}\\\.\PhysicalDriveN / \\?\GLOBALROOT\Device\…0x57 になりやすい
ボリューム名の末尾に “\” があるか\\?\Volume{GUID}\\\?\Volume{GUID}0x57 になりやすい
対象ボリュームはフォーマット済みかNTFS/FAT32 作成後に付与未フォーマットに付与失敗や不安定要因になる
Volume GUID の取得手順が正しいかFindFirstVolume/FindNextVolumeVDS 内部 ID を流用0x57 や誤マウントの原因

とくに重要なのは「VDS の構造体で出てくる volumeid(GUID)を、\\?\Volume{GUID}\ と混同しない」ことです。前者は VDS 内部でオブジェクトを識別する ID であり、後者(Volume GUID パス)とは別物として扱うのが安全です。

CreateVirtualDisk で CREATE_VIRTUAL_DISK_VERSION_2 が 0x57 になるときの現実的な対処

CreateVirtualDisk が ERROR_INVALID_PARAMETER(0x57)で落ちる場合、原因はひとつではありません。典型的には次のような “初期化漏れ” や “組み合わせ不一致” が疑われます。

  • CREATE_VIRTUAL_DISK_PARAMETERS をゼロ初期化していない
  • Version を Version_2 にしたのに、Union の Version2 構造体に必要な値が入っていない
  • SectorSize / BlockSize / Flags の組み合わせが提供プロバイダや OS に合っていない
  • 親ディスク/ソース指定のパスが想定と違う(相対パスや UNC など)

ただし、QA の流れとしては「Version_2 の 0x57 の決定打説明が出ないまま、VDS 方式で一式を組むことで回避できた」という着地点でした。実務でも、“作成だけ Virtual Disk API、残りは VDS”のように責務分担してしまうのが、原因追跡に時間を溶かしにくい選択です。

EFI システムパーティションの Volume GUID パスを “ドライブレター無し” で特定する

bcdboot などに EFI を渡したいのに、ドライブレターを付けたくない(または一時的にしか付けたくない)場面があります。このとき必要になるのが \\?\Volume{…}\ という Volume GUID パスです。

重要な整理:VDS の volumeid と Windows の Volume GUID は別物

スレ内で混乱が起きやすかった点として、VDS が返す “volumeid” が GUID_NULL になったり、期待した形式でない、という話がありました。ここで覚えておきたいのは次の整理です。

名称見た目用途取得方法の現実解
VDS 内部 ID(オブジェクト ID)GUID(構造体内の id)VDS が COM オブジェクトを識別するためVDS の GetProperties など
Volume GUID パス\\?\Volume{GUID}\SetVolumeMountPointW、FindFirstVolume、ボリューム操作FindFirstVolume/FindNextVolume で列挙して特定

「Volume GUID パスが欲しい」なら、VDS の ID にこだわらず、Win32 のボリューム列挙から入るのが堅いです。

現実解:ボリューム列挙+PARTITION_SYSTEM_GUID 判定で EFI を拾う

手順はシンプルです。

  • FindFirstVolume / FindNextVolume で Volume GUID パスを列挙する
  • 各ボリュームを CreateFile で開いて IOCTL_DISK_GET_PARTITION_INFO_EX を取得する
  • GPT の PartitionType が PARTITION_SYSTEM_GUID(EFI)なら、その Volume GUID パスが EFI パーティション
<!-- 角括弧をエスケープしています -->
#include &lt;windows.h&gt;
#include &lt;winioctl.h&gt;
#include &lt;string&gt;

static std::wstring TrimTrailingBackslashForCreateFile(const std::wstring& volumeGuidPath)
{
    // FindFirstVolume は末尾 '\' 付き(例:\\?\Volume{...}\)
    // CreateFile で開くときは末尾 '\' を外すのが定石
    if (!volumeGuidPath.empty() && volumeGuidPath.back() == L'\\') {
        return volumeGuidPath.substr(0, volumeGuidPath.size() - 1);
    }
    return volumeGuidPath;
}

static bool IsEfiSystemPartitionVolume(const std::wstring& volumeGuidPath)
{
    std::wstring pathForOpen = TrimTrailingBackslashForCreateFile(volumeGuidPath);

    HANDLE h = CreateFileW(pathForOpen.c_str(),
                           GENERIC_READ,
                           FILE_SHARE_READ | FILE_SHARE_WRITE,
                           nullptr, OPEN_EXISTING, 0, nullptr);
    if (h == INVALID_HANDLE_VALUE) return false;

    PARTITION_INFORMATION_EX pie{};
    DWORD bytes = 0;
    BOOL ok = DeviceIoControl(h, IOCTL_DISK_GET_PARTITION_INFO_EX,
                              nullptr, 0,
                              &pie, sizeof(pie),
                              &bytes, nullptr);
    CloseHandle(h);
    if (!ok) return false;

    if (pie.PartitionStyle != PARTITION_STYLE_GPT) return false;

    // EFI = PARTITION_SYSTEM_GUID
    return IsEqualGUID(pie.Gpt.PartitionType, PARTITION_SYSTEM_GUID) ? true : false;
}

static std::wstring FindEfiVolumeGuidPath()
{
    wchar_t volumeName[MAX_PATH] = {};
    HANDLE hFind = FindFirstVolumeW(volumeName, MAX_PATH);
    if (hFind == INVALID_HANDLE_VALUE) return L"";

    std::wstring result;

    while (true) {
        std::wstring v = volumeName; // 例:\\?\Volume{GUID}\
        if (IsEfiSystemPartitionVolume(v)) {
            result = v;
            break;
        }

        BOOL ok = FindNextVolumeW(hFind, volumeName, MAX_PATH);
        if (!ok) break;
    }

    FindVolumeClose(hFind);
    return result;
}

この方法の強みは、「EFI を “PartitionType GUID” で決定的に判定できる」ことです。開始オフセットを IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS で突き合わせる方法もありますが、EFI 判定目的なら PartitionType を見るほうが迷いが減ります。

実務で安定させるためのポイント

  • 管理者権限前提で設計する:仮想ディスクのアタッチ、パーティション操作、フォーマットは権限不足で失敗しやすい領域です。サービスとして動かすなら権限設計(実行アカウント、UAC、特権)を先に決めます。
  • 状態反映の “待ち” を入れる:アタッチ直後、GPT 初期化直後、パーティション作成直後は OS の認識が遅れることがあります。VDS Refresh / Reenumerate 相当+リトライを組み込むと安定します。
  • MSR は「作るだけ」で触らない:フォーマットしない、ドライブレターを付けない、ボリュームとして扱おうとしない。ここを触るほど手順が壊れやすいです。
  • ドライブレター付与は “最後”:フォーマット前に付与しようとすると失敗が増えます。EFI もデータも、ファイルシステムができてからアクセスパスを付けます。
  • 0x57 は “何かの形式が違う” 合図:SetVolumeMountPointW なら末尾スラッシュや Volume GUID 形式の取り違いが最有力です。CreateVirtualDisk ならパラメータ初期化・OS/プロバイダ対応の見直しから入ります。
  • 最終的に欲しいのが EFI の Volume GUID パスなら、列挙して決める:VDS の内部 ID と混ぜない。FindFirstVolume → PartitionType 判定の流れが堅いです。

まとめ:C++ で “diskpart 相当” を再現する最短ルート

VHD/VHDX を “作ってアタッチする” だけなら Virtual Disk API で十分ですが、GPT 初期化から EFI/MSR/Primary 作成、FAT32/NTFS フォーマット、ドライブレター割り当てまでを、diskpart 相当の手順で自動化するなら、VDS(または Storage Management API)を軸に組むのが近道です。

  • diskpart 相当の抽象度で扱えるため、パーティション作成・フォーマット・アクセスパス付与までが一続きで設計できる
  • SetVolumeMountPointW の 0x57 は “ボリューム名の前提” を外していることが多く、VDS の AddAccessPath に寄せると事故が減る
  • EFI の Volume GUID パスが必要なら、FindFirstVolume/FindNextVolume で列挙して PARTITION_SYSTEM_GUID で確定させるのが堅い

あとは「権限」「状態反映の待ち」「GUID の取り違い防止」を押さえれば、C++ だけで diskpart 一連の流れを安定稼働させられます。

この記事を書いた人

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

コメント

コメントする

目次