WindowsでUSB 3.1/3.2(SuperSpeedPlus)のリンク速度やレーン数をコードから正しく取得したい場合、どのDLLをP/Invokeすべきか、どのIOCTLをどのハンドルに投げるべきかでつまずきがちです。本記事では、IOCTL_USB_GET_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATIONをC#から正しく叩くための実装手順と、kernel32.dllとkernelbase.dllの選び方を、実際に動く形を意識しながら詳しく解説します。
IOCTL_USB_GET_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATION の概要
IOCTL_USB_GET_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATION は、USB ハブの特定ポートについて「SuperSpeedPlus(USB 3.1/3.2)」に関するレーン情報(速度・レーン数)を取得するためのユーザーモード IOCTL です。ターゲットは USB ハブ デバイス(GUID_DEVINTERFACE_USB_HUB)であり、入出力バッファとして USB_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATION 構造体を使います。
この構造体は、ざっくり言うと「どのポート(ConnectionIndex)について」「Rx/Tx それぞれ何レーン」「そのレーンはどの SuperSpeedPlus 速度で動いているか」を返すためのものです。USB 3.2 Gen2x2 のような 2 レーン構成もここから判断できます。
| メンバー | 意味 | ポイント |
|---|---|---|
ConnectionIndex | 1始まりのポート番号 | ハブの N 番ポートを指定(0は無効) |
Length | 構造体のサイズ | ユーザー側で sizeof を入れるのが安全 |
RxSuperSpeedPlus.Speed | 受信レーンの SuperSpeedPlus 速度属性 | USB 3.1/3.2 仕様で定義された SSP スピード値 |
RxLaneCount | 受信レーン数(0ベース) | 実際のレーン数 = RxLaneCount + 1 |
TxSuperSpeedPlus.Speed | 送信レーンの SuperSpeedPlus 速度属性 | Rx 側と同様に SSP スピード値 |
TxLaneCount | 送信レーン数(0ベース) | 実際のレーン数 = TxLaneCount + 1 |
この IOCTL 自体は WDK/WD 10.0.26100 系(Windows 11 24H2 対応)で追加されたもので、「USB superspeed information のサポート」として USB_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATION とセットで紹介されています。
kernel32.dll と kernelbase.dll の違いと P/Invoke の正解
歴史的な背景 – なぜ2つあるのか
元々、Win32 API の多くは kernel32.dll に実装されていました。その後の Windows で内部構造が整理され、多くの汎用 API 実装は KernelBase.dll に移され、「kernel32.dll のエクスポートは内部的に KernelBase.dll へフォワードする」形になりました。
ただし「公式にアプリがリンク・P/Invoke すべき DLL」としては今でも kernel32.dll が契約の窓口 です。実装 DLL である KernelBase.dll は OS の内部構造に近く、将来のバージョンで構成やエクスポートの有無が変わる可能性があり、直接指定するメリットはほぼありません。
| DLL | 役割 | 備考 |
|---|---|---|
kernel32.dll | アプリからの公式 API エクスポート窓口 | 多くの関数は内部で KernelBase にフォワード |
KernelBase.dll | 実装本体・OS 内部用 | 存在や構造は OS バージョンに依存、直接指定は非推奨 |
結論:P/Invoke の DLL 名は kernel32.dll を使う
- C/C++ でリンクする import library も
Kernel32.libが基本。 - C# などの P/Invoke でも、互換性・可読性の観点から
kernel32.dllを指定するのが定石 です。 - 実際、Microsoft Q&A で紹介されている検証コードでも
DeviceIoControlやCreateFileは[DllImport("Kernel32.dll")]で宣言されています。
したがって、本記事のサンプルでも すべて kernel32.dll で宣言 します。
C# における DeviceIoControl の宣言例
using Microsoft.Win32.SafeHandles;
using System.Runtime.InteropServices;
internal static class NativeMethods
{
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern SafeFileHandle CreateFile(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool DeviceIoControl(
SafeFileHandle hDevice,
uint dwIoControlCode,
IntPtr lpInBuffer,
uint nInBufferSize,
IntPtr lpOutBuffer,
uint nOutBufferSize,
out uint lpBytesReturned,
IntPtr lpOverlapped);
}
SafeFileHandle を使うことでハンドルリークを防ぎつつ、ガベージコレクタに自動解放させられるのでおすすめです。
SuperSpeedPlus 情報取得の全体フロー
IOCTL_USB_GET_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATION を有効に使うには、「ハブ単位でポートを総なめ」するのが基本パターンです。
GUID_DEVINTERFACE_USB_HUBでハブ デバイスパスを列挙- 各ハブデバイスパスを
CreateFileで開く IOCTL_USB_GET_NODE_INFORMATIONでポート数 (bNumberOfPorts) を取得- ポート1〜N に対して
IOCTL_USB_GET_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATIONを発行 - 結果を解釈(レーン数と速度)して自前のモデルに格納
どのステップも「ドライバのサンプルコードや WDK がやっていること」と同じ流れで、既存ツール(UsbView など)とも親和性が高い方法です。
手順1:USB ハブ デバイスの列挙
USB ハブはデバイスインターフェイス クラス GUID_DEVINTERFACE_USB_HUB で公開されます。GUID は次の値です。
private static readonly Guid GUID_DEVINTERFACE_USB_HUB =
new Guid("F18A0E88-C30C-11D0-8815-00A0C906BED8");
SetupAPI での列挙はおなじみの3段階です。
SetupDiGetClassDevsでデバイス情報セットを取得SetupDiEnumDeviceInterfacesでインターフェイスを列挙SetupDiGetDeviceInterfaceDetailでデバイスパス(シンボリックリンク)を取得
C# での SetupAPI 宣言(抜粋)
internal static class SetupApi
{
[DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern IntPtr SetupDiGetClassDevs(
ref Guid ClassGuid,
IntPtr Enumerator,
IntPtr hwndParent,
uint Flags);
[DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern bool SetupDiEnumDeviceInterfaces(
IntPtr DeviceInfoSet,
IntPtr DeviceInfoData,
ref Guid InterfaceClassGuid,
uint MemberIndex,
ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData);
[DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern bool SetupDiGetDeviceInterfaceDetail(
IntPtr DeviceInfoSet,
ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData,
IntPtr DeviceInterfaceDetailData,
uint DeviceInterfaceDetailDataSize,
out uint RequiredSize,
IntPtr DeviceInfoData);
[DllImport("setupapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern bool SetupDiGetDeviceInterfaceDetail(
IntPtr DeviceInfoSet,
ref SP_DEVICE_INTERFACE_DATA DeviceInterfaceData,
ref SP_DEVICE_INTERFACE_DETAIL_DATA DeviceInterfaceDetailData,
uint DeviceInterfaceDetailDataSize,
out uint RequiredSize,
ref SP_DEVINFO_DATA DeviceInfoData);
[DllImport("setupapi.dll", SetLastError = true)]
internal static extern bool SetupDiDestroyDeviceInfoList(
IntPtr DeviceInfoSet);
internal const uint DIGCF_PRESENT = 0x00000002;
internal const uint DIGCF_DEVICEINTERFACE = 0x00000010;
[StructLayout(LayoutKind.Sequential)]
internal struct SP_DEVICE_INTERFACE_DATA
{
public int cbSize;
public Guid InterfaceClassGuid;
public int Flags;
public IntPtr Reserved;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct SP_DEVICE_INTERFACE_DETAIL_DATA
{
public int cbSize;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string DevicePath;
}
[StructLayout(LayoutKind.Sequential)]
internal struct SP_DEVINFO_DATA
{
public int cbSize;
public Guid ClassGuid;
public int DevInst;
public IntPtr Reserved;
}
}
ポイントは SP_DEVICE_INTERFACE_DETAIL_DATA.cbSize の値です。
- x86(32bit):
cbSize = 6 - x64(64bit):
cbSize = 8
これは構造体が「先頭に DWORD、その後に可変長 WCHAR 配列」というレイアウトになっているためで、ポインタサイズに応じてアラインメントが変わることによります。Q&A のサンプルもこの値をきちんと切り替えています。
ハブのデバイスパス列挙のイメージコード
IEnumerable<string> EnumerateUsbHubs()
{
var guid = GUID_DEVINTERFACE_USB_HUB;
IntPtr devInfo = SetupApi.SetupDiGetClassDevs(
ref guid, IntPtr.Zero, IntPtr.Zero,
SetupApi.DIGCF_DEVICEINTERFACE | SetupApi.DIGCF_PRESENT);
if (devInfo == IntPtr.Zero)
yield break;
try
{
var ifData = new SetupApi.SP_DEVICE_INTERFACE_DATA
{
cbSize = Marshal.SizeOf<SetupApi.SP_DEVICE_INTERFACE_DATA>()
};
for (uint index = 0; SetupApi.SetupDiEnumDeviceInterfaces(
devInfo, IntPtr.Zero, ref guid, index, ref ifData); index++)
{
// 必要バッファサイズを取得
SetupApi.SetupDiGetDeviceInterfaceDetail(
devInfo, ref ifData,
IntPtr.Zero, 0,
out uint requiredSize,
IntPtr.Zero);
var detail = new SetupApi.SP_DEVICE_INTERFACE_DETAIL_DATA
{
cbSize = IntPtr.Size == 8 ? 8 : 6
};
var devInfoData = new SetupApi.SP_DEVINFO_DATA
{
cbSize = Marshal.SizeOf<SetupApi.SP_DEVINFO_DATA>()
};
if (SetupApi.SetupDiGetDeviceInterfaceDetail(
devInfo, ref ifData,
ref detail,
requiredSize,
out requiredSize,
ref devInfoData))
{
yield return detail.DevicePath;
}
}
}
finally
{
SetupApi.SetupDiDestroyDeviceInfoList(devInfo);
}
}
手順2:ハブを開く(CreateFile)
列挙したデバイスパスは「シンボリックリンク名」です。これを CreateFile で 通常のファイルと同じように 開きます。Target はあくまで「USBハブ デバイス」である点が重要です。デバイス(GUID_DEVINTERFACE_USB_DEVICE)のパスを開いてもこの IOCTL は使えません。
const uint GENERIC_READ = 0x80000000;
const uint GENERIC_WRITE = 0x40000000;
const uint FILE_SHARE_READ = 0x00000001;
const uint FILE_SHARE_WRITE = 0x00000002;
const uint OPEN_EXISTING = 3;
SafeFileHandle OpenHub(string devicePath)
{
var handle = NativeMethods.CreateFile(
devicePath,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
IntPtr.Zero,
OPEN_EXISTING,
0,
IntPtr.Zero);
if (handle.IsInvalid)
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
return handle;
}
手順3:IOCTL_USB_GET_NODE_INFORMATION でポート数を取得
ハブがいくつの下流ポートを持っているかは、古くからある IOCTL_USB_GET_NODE_INFORMATION で取得します。入出力バッファは USB_NODE_INFORMATION 構造体です。
関連構造体(必要部分のみ)
enum USB_HUB_NODE
{
UsbHub = 0,
UsbMIParent = 1
}
[StructLayout(LayoutKind.Sequential)]
struct USB_HUB_DESCRIPTOR
{
public byte bDescriptorLength;
public byte bDescriptorType;
public byte bNumberOfPorts;
public ushort wHubCharacteristics;
public byte bPowerOnToPowerGood;
public byte bHubControlCurrent;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)]
public byte[] DeviceRemovable;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 64)]
public byte[] PortPowerCtrlMask;
}
[StructLayout(LayoutKind.Sequential)]
struct USB_HUB_INFORMATION
{
public USB_HUB_DESCRIPTOR HubDescriptor;
[MarshalAs(UnmanagedType.Bool)]
public bool HubIsBusPowered;
}
[StructLayout(LayoutKind.Sequential)]
struct USB_MI_PARENT_INFORMATION
{
public uint NumberOfInterfaces;
}
[StructLayout(LayoutKind.Explicit)]
struct USB_NODE_INFORMATION
{
[FieldOffset(0)] public USB_HUB_NODE NodeType;
[FieldOffset(4)] public USB_HUB_INFORMATION HubInformation;
[FieldOffset(4)] public USB_MI_PARENT_INFORMATION MiParentInformation;
}
IOCTL 定数定義(CTL_CODE マクロ相当)
IOCTL コードは CTL_CODE マクロ相当のヘルパで定義します。
static uint CTL_CODE(uint deviceType, uint function, uint method, uint access)
{
return (deviceType << 16)
| (access << 14)
| (function << 2)
| method;
}
const uint FILE_DEVICE_USB = 0x22;
const uint METHOD_BUFFERED = 0;
const uint FILE_ANY_ACCESS = 0;
const uint USB_GET_NODE_INFORMATION = 258;
const uint USB_GET_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATION = 289;
static readonly uint IOCTL_USB_GET_NODE_INFORMATION =
CTL_CODE(FILE_DEVICE_USB,
USB_GET_NODE_INFORMATION,
METHOD_BUFFERED,
FILE_ANY_ACCESS);
static readonly uint IOCTL_USB_GET_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATION =
CTL_CODE(FILE_DEVICE_USB,
USB_GET_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATION,
METHOD_BUFFERED,
FILE_ANY_ACCESS);
ポート数を取得するコード例
byte GetPortCount(SafeFileHandle hubHandle)
{
USB_NODE_INFORMATION nodeInfo = new USB_NODE_INFORMATION
{
NodeType = USB_HUB_NODE.UsbHub
};
int size = Marshal.SizeOf<USB_NODE_INFORMATION>();
IntPtr ptr = Marshal.AllocHGlobal(size);
try
{
Marshal.StructureToPtr(nodeInfo, ptr, false);
if (!NativeMethods.DeviceIoControl(
hubHandle,
IOCTL_USB_GET_NODE_INFORMATION,
ptr, (uint)size,
ptr, (uint)size,
out uint bytesReturned,
IntPtr.Zero))
{
throw new System.ComponentModel.Win32Exception(
Marshal.GetLastWin32Error());
}
nodeInfo = Marshal.PtrToStructure<USB_NODE_INFORMATION>(ptr);
return nodeInfo.HubInformation.HubDescriptor.bNumberOfPorts;
}
finally
{
Marshal.FreeHGlobal(ptr);
}
}
手順4:各ポートの SuperSpeedPlus 情報を取得
USB_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATION の C# 定義
[StructLayout(LayoutKind.Sequential)]
struct USB_DEVICE_CAPABILITY_SUPERSPEEDPLUS_SPEED
{
public uint Speed;
}
[StructLayout(LayoutKind.Sequential)]
struct USB_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATION
{
public uint ConnectionIndex;
public uint Length;
public USB_DEVICE_CAPABILITY_SUPERSPEEDPLUS_SPEED RxSuperSpeedPlus;
public uint RxLaneCount;
public USB_DEVICE_CAPABILITY_SUPERSPEEDPLUS_SPEED TxSuperSpeedPlus;
public uint TxLaneCount;
}
公式ドキュメントでもこの構造体のフィールド順は同じです。
最重要ポイント:Length に構造体サイズを入れる
Length フィールドは必ず呼び出し側で構造体サイズを設定 してください。
- IOCTL の入出力バッファ長は「
sizeof(USB_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATION)」であることが要求されています。 - 多くの Windows API と同様に、
Lengthは将来の拡張を見越した「構造体の自己申告サイズ」として扱われることが多く、ゼロのままだとSTATUS_INVALID_PARAMETERなどで失敗したり、フィールドの一部が無視される可能性があります。 - 一部のサンプルコードでは
Lengthの設定を省いていますが、実運用コードでは必ず設定することをおすすめします。
ポートごとに IOCTL を発行するループ
void DumpSuperSpeedPlusInfo(SafeFileHandle hubHandle)
{
byte portCount = GetPortCount(hubHandle);
int ssSize = Marshal.SizeOf<USB_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATION>();
for (uint port = 1; port <= portCount; port++)
{
var ssp = new USB_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATION
{
ConnectionIndex = port,
Length = (uint)ssSize // ★ここが重要
};
IntPtr ptr = Marshal.AllocHGlobal(ssSize);
try
{
Marshal.StructureToPtr(ssp, ptr, false);
bool ok = NativeMethods.DeviceIoControl(
hubHandle,
IOCTL_USB_GET_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATION,
ptr, (uint)ssSize,
ptr, (uint)ssSize,
out uint bytesReturned,
IntPtr.Zero);
if (!ok)
{
int err = Marshal.GetLastWin32Error();
// Windows 10 や未対応ハブでは ERROR_NOT_SUPPORTED(50) などになり得る
System.Diagnostics.Debug.WriteLine(
$"Port {port}: IOCTL failed. Error={err}");
continue;
}
ssp = Marshal.PtrToStructure<USB_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATION>(ptr);
uint rxLanes = ssp.RxLaneCount + 1;
uint txLanes = ssp.TxLaneCount + 1;
System.Diagnostics.Debug.WriteLine(
$"Port {port}: RxSpeed={ssp.RxSuperSpeedPlus.Speed}, " +
$"RxLanes={rxLanes}, TxSpeed={ssp.TxSuperSpeedPlus.Speed}, " +
$"TxLanes={txLanes}");
}
finally
{
Marshal.FreeHGlobal(ptr);
}
}
}
Speed の具体的な数値が何ビット/秒を意味するかは USB 3.1/3.2 仕様で定義されている SuperSpeedPlus デバイスケイパビリティのビットフィールドに従います。USB-IF では、SuperSpeed(USB 3.0/3.2 Gen1)は 5 Gbit/s、SuperSpeedPlus(Gen2)は 10 Gbit/s、Gen2x2 では 20 Gbit/s のリンク速度を規定しており、レーン数は「GenNxM の M」が対応します。
OSバージョンと 0 が返るケースの読み方
SuperSpeedPlus 関連 IOCTL は Windows 11 24H2 世代で正式に「新機能」として追加されています。Windows 10 など古い OS 上で同じコードを動かした場合、以下のような挙動が考えられます。
| パターン | DeviceIoControl の戻り値 | Speed フィールド | 考えられる原因 |
|---|---|---|---|
| OS・ハブとも未対応 | false | 未定義 | GetLastError = ERROR_NOT_SUPPORTED (50) など |
| IOCTL 自体は実装済みだが情報なし | true | 0 のまま | ポート/デバイスが SuperSpeedPlus で動作していない、またはハブが SSP 情報を報告しない |
| 正常(SuperSpeedPlus 接続) | true | 0 以外 | レーン数&速度ビットから USB 3.2 Gen2x2 などが判別可能 |
Microsoft Q&A の検証では、Windows 10 上で IOCTL を発行すると DeviceIoControl 自体は成功するものの、Rx/Tx.Speed が 0 のまま、という報告があります。これは「IOCTL が将来追加されることを前提にスタブ実装されている」か、「ハブ/スタックが単に情報を返していない」可能性が高く、必ずしもバグとは限りません。
したがって、「Speed が 0 だから失敗」と即断するのではなく、まずは DeviceIoControl の戻り値・GetLastError を確認することが重要です。
よくある落とし穴と対策
1. DLL を kernelbase.dll で宣言してしまう
- 動作する環境もありますが、将来的な互換性が不透明。
- 公式ドキュメントやサンプルは
kernel32.dllを前提としているため、トラブルシュート時に情報が検索しづらくなります。 - 常に
kernel32.dllを指定するクセを付けておくと無難です。
2. ConnectionIndex を 0 から回してしまう
ConnectionIndexは「1始まり」のポート番号です。- 0 を指定すると
STATUS_INVALID_PARAMETER相当のエラーになり得ます。
3. Length を設定していない
Lengthを 0 のまま渡すと、ドライバ側でサイズチェックに失敗することがあります。- 必ず
(uint)Marshal.SizeOf<USB_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATION>()を代入してください。
4. 構造体レイアウト(Pack)の不一致
- 多くの USB 関連構造体は 4 バイト境界を前提としていますが、
StructLayoutのPackを 1 にしてしまうとオフセットがずれ、ERROR_INVALID_USER_BUFFER (1784)などが発生します。 - 特に x64 ではデフォルトの
Pack=8のままで正しく動くことが多いので、「明示的に Pack=1 に変える」よりはデフォルトのままにして、どうしても必要な場合のみ USB ヘッダと突き合わせて調整するのがおすすめです。
5. ハブではなくデバイスのパスを開いている
- USB デバイス自体(ストレージやキーボードなど)のデバイスパスに対して、USB ハブ向け IOCTL を投げても意味がありません。
- SuperSpeedPlus 情報を取得できるのはあくまで ハブ ドライバ なので、ターゲットは必ず
GUID_DEVINTERFACE_USB_HUBです。
6. SetupAPI の cbSize を間違えている
SP_DEVICE_INTERFACE_DATA.cbSize、SP_DEVINFO_DATA.cbSizeはそれぞれMarshal.SizeOf<T>()で設定してください。SP_DEVICE_INTERFACE_DETAIL_DATA.cbSizeは IntPtr サイズに応じて 6 または 8 にする必要があります(前述)。
SuperSpeed / SuperSpeedPlus と USB 3.1/3.2 の読み解き方
USB_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATION から取得した Speed とレーン数を、USB 3.1/3.2 のマーケティング用名称(Gen1/Gen2/Gen2x2 など)にきれいにマッピングしたくなりますが、実際には USB 3.1 仕様で定義された「SuperSpeedPlus デバイスケイパビリティ構造体」のサブリンクスピード属性を解釈する必要があります。
とはいえ、実用上は次のような読み方でも十分です。
| 代表的なモード | マーケティング名称 | リンク速度 | レーン数イメージ |
|---|---|---|---|
| USB 3.2 Gen1x1 | SuperSpeed USB 5Gbps | 5 Gbit/s | 1 レーン(SS) |
| USB 3.2 Gen2x1 | SuperSpeed USB 10Gbps | 10 Gbit/s | 1 レーン(SSP) |
| USB 3.2 Gen2x2 | SuperSpeed USB 20Gbps | 10 Gbit/s × 2 レーン | 2 レーン(SSP x2) |
上表と RxLaneCount/TxLaneCount を組み合わせることで、「このポートは USB 3.2 Gen2x2 でリンクアップしていそうだ」といった推測が可能になります(正確な判定には BOS ディスクリプタなども併用するとベターです)。
小さなユーティリティとしてまとめるときの設計のヒント
ここまでの部品を組み合わせると、例えば次のような「USB ハブとポートのリンク状態を一覧する」コンソールツールが作れます。
- ステップ1:
EnumerateUsbHubs()で全ハブのデバイスパスを列挙 - ステップ2:各ハブを開き、
GetPortCount()でポート数を取得 - ステップ3:各ポートについて
DumpSuperSpeedPlusInfo()を呼び、SuperSpeedPlus 情報を収集 - ステップ4:結果を JSON や CSV に落としておけば、ケーブルやハブの検証にも使える
実機検証の際は、Windows 11 24H2 世代環境で、USB 3.2 Gen2x2 対応ポート+ケーブル+デバイスを組み合わせることで、RxLaneCount/TxLaneCount が 1(=2レーン)になるケースを狙うと挙動が確認しやすくなります。
まとめ – DLL と実装手順の整理
- DLL は
kernel32.dllを指定するのが正解。KernelBase.dllは実装詳細であり、互換性の観点から直接指定する理由はありません。 - IOCTL のターゲットは USB ハブ(
GUID_DEVINTERFACE_USB_HUB)。SetupAPI でハブを列挙し、そのパスをCreateFileで開きます。 - ポート数は
IOCTL_USB_GET_NODE_INFORMATIONで取得し、bNumberOfPortsをベースに 1 〜 N でループします。 - 各ポートに対して
IOCTL_USB_GET_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATIONを発行し、USB_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATIONのRx/TxSuperSpeedPlusとRx/TxLaneCountを読み取ります。 Lengthに構造体サイズを必ず設定し、P/Invoke の構造体レイアウト(特にcbSize、Pack)を OS 側の定義と揃えることが安定動作のカギです。- Speed が 0 だからといって即異常とは限らないことに注意し、まずは
DeviceIoControlの戻り値とGetLastErrorを確認して OS/ドライバの対応状況を切り分けましょう。
以上を押さえておけば、IOCTL_USB_GET_NODE_CONNECTION_SUPERSPEEDPLUS_INFORMATION を使って、USB 3.1/3.2 の実際のリンク速度やレーン構成をアプリケーション側から高い精度で把握できるようになります。

コメント