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 vdisk | VHD/VHDX ファイル生成 | VDS CreateVDisk または CreateVirtualDisk | Version_2 の 0x57 はパラメータ初期化ミス/非対応が混ざりやすい |
| attach vdisk | OS にディスクとして認識させる | VDS Attach または AttachVirtualDisk | 権限/リスキャン/デバイス出現待ちが必要になることがある |
| convert gpt | GPT 初期化 | IOCTL_DISK_CREATE_DISK(または VDS 経由の初期化) | 初期化後に UpdateProperties/Refresh が必要な場面がある |
| create partition efi/msr/primary | EFI/MSR/基本データ領域の作成 | VDS で Free Extent を見て GPT パーティション作成 | アライン・サイズ・GUID の組み合わせを誤ると後工程が壊れる |
| format fs=fat32/ntfs | ファイルシステム作成 | VDS の Format(IVdsVolumeMF など) | EFI は FAT32、MSR はフォーマットしない |
| assign letter=X | ドライブレター付与 | IVdsVolumeMF::AddAccessPath / SetVolumeMountPointW | SetVolumeMountPointW は “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 <windows.h>
#include <vds.h>
#include <virtdisk.h>
#include <initguid.h>
#include <comdef.h>
#include <string>
#include <vector>
#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 Partition | PARTITION_SYSTEM_GUID | 100〜260MB 程度 | FAT32 | bcdboot の配置先。ドライブレターを付けずに運用することも多い |
| Microsoft Reserved (MSR) | PARTITION_MSFT_RESERVED_GUID | 16MB(一般的) | なし | フォーマットしない。アクセスパスも付けない |
| Basic Data | PARTITION_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::AddAccessPath | SetVolumeMountPointW より前提が明確で、失敗原因を潰しやすい | 例:L”X:\\” を追加(末尾 \\ を忘れない) |
一方で、どうしても Win32 の SetVolumeMountPointW を使いたい場面(VDS を避ける、または既存実装に合わせる)もあります。その場合は次のチェックリストを必ず通します。
SetVolumeMountPointW が 0x57 になるチェックリスト
| チェック項目 | 正しい例 | 間違いやすい例 | 結果 |
|---|---|---|---|
| マウント先は “ルート” か | X:\ | X: や X:\temp | 0x57 になりやすい |
| ボリューム名は Volume GUID 形式か | \\?\Volume{GUID}\ | \\.\PhysicalDriveN / \\?\GLOBALROOT\Device\… | 0x57 になりやすい |
| ボリューム名の末尾に “\” があるか | \\?\Volume{GUID}\ | \\?\Volume{GUID} | 0x57 になりやすい |
| 対象ボリュームはフォーマット済みか | NTFS/FAT32 作成後に付与 | 未フォーマットに付与 | 失敗や不安定要因になる |
| Volume GUID の取得手順が正しいか | FindFirstVolume/FindNextVolume | VDS 内部 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 <windows.h>
#include <winioctl.h>
#include <string>
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 一連の流れを安定稼働させられます。

コメント