前幾天我通過改造微軟的vhidmini2這個驅動示例,寫了一個umdf的虛擬hid鍵盤,然後我發現,微軟還提供了一個叫Virtual Hid Framework(VHF)的框架,專門用來實現虛擬hid設備,在kmdf和umdf上都支持(文檔這麼説的),所以就想着用VHF來重寫一下上次的那個虛擬hid鍵盤。
0 VHF概述
使用VHF開發的驅動程序叫做源驅動程序,源驅動程序的作用是控制VHF設備對象的生命週期,以及為VHF設備對象提供數據。下面這張官方文檔中的設備樹顯示了它們之間的層次關係。
綠色框的FDO指的是源驅動程序,就是我們要開發的部分,開發時需要引用Vhfkm.lib(在umdf中是Vhfum.lib)來使用vhf提供的api。PDO是物理設備對象,通常是上級設備的FDO枚舉出來的設備,對於虛擬設備來説,一般是通過devgen等方式生成的設備。PDO一般會顯示在設備管理器中,未安裝驅動時顯示為Unknown Device,源驅動程序就是安裝在這個設備上的。Vhf.sys是VHF框架的核心,作為LowerFilter安裝在源驅動程序上,過濾PDO到源驅動程序之間的請求,這些請求一般是設備生命週期相關的請求,例如PNP事件等,與HID功能無關。Vhf.sys會枚舉出一個PDO來,這個是實現虛擬HID設備功能的PDO,系統會在它上面安裝HidClass的驅動。
VHF之於虛擬hid設備的開發,就像WPF之於桌面應用開發,雖然實現的自由度會稍微受限,但確實方便很多。使用VHF,不需要自己去處理複雜的IRP請求,其緩衝策略也有默認的實現,只需要設置好幾個回調函數就行了,給我的感覺真的是像開發WPF應用一樣。
雖然VHF用起來很方便,但是官方文檔較少,示例代碼也不完整,所以用起來還是遇到不少困難。我最初還是想用umdf的驅動來實現虛擬hid鍵盤,但實在是調不通,文檔和示例代碼大多是基於kmdf的,搞不懂到底是哪裏有問題,所以就先實現了一個kmdf的。
使用VHF的步驟非常簡單,就分為兩步:1.編寫初始化代碼;2.編寫處理請求的回調函數。
下面先介紹一下可能會用到的主要函數和數據結構,然後再説明如何編寫實例代碼。
1 初始化相關的函數和數據結構
初始化需要依次使用VHF_CONFIG_INIT、VhfCreate、VhfStart三個函數,這三個函數是不是光看名稱就很容易理解?
1.1 VHF_CONFIG_INIT
FORCEINLINE
VOID
VHF_CONFIG_INIT(
_Out_
PVHF_CONFIG Config,
#ifdef _KERNEL_MODE
_In_
PDEVICE_OBJECT DeviceObject,
#else
_In_
HANDLE FileHandle,
#endif
_In_
USHORT ReportDescriptorLength,
_In_reads_bytes_(ReportDescriptorLength)
PUCHAR ReportDescriptor
)
VHF_CONFIG_INIT函數的作用是初始化一個VHF_CONFIG的結構體,VHF_CONFIG結構體用來指定VHF框架對象的一些屬性,例如PID、VID、回調函數指針等。
DeviceObject指定一個WDM設備對象與VHF關聯,通常就是當前的設備對象。在kdmf中,可以通過WdfDeviceWdmGetDeviceObject來獲取與WDF設備對象關聯的WDM設備對象。
1.2 VhfCreate
NTSTATUS VhfCreate(
[in] PVHF_CONFIG VhfConfig,
[out] VHFHANDLE *VhfHandle
);
VhfCreate函數的作用是使用剛剛初始化的VHF_CONFIG指定的配置,去創建一個VHF設備對象,調用成功的話,VhfHandle就是新創建的VHF設備對象的句柄。
1.3 VhfStart
NTSTATUS VhfStart(
[in] VHFHANDLE VhfHandle
);
VhfStart函數的作用就是啓動剛剛創建的VHF設備對象。
1.4 VhfDelete
VOID VhfDelete(
[in] VHFHANDLE VhfHandle,
[in] BOOLEAN Wait
);
在設備或驅動卸載之前,需要調用VhfDelete方法刪除掉VHF設備對象。未正常刪除VHF設備對象的話,系統會提示設備已更改,需要重啓系統。
2 處理請求相關的函數和數據結構
2.1 EVT_VHF_ASYNC_OPERATION
源驅動程序可以支持這些異步請求:GetFeature、 SetFeature、 WriteReport、 GetInputReport。在VHF_CONFIG結構體中設置相應的回調函數:EvtVhfAsyncOperationGetFeature、EvtVhfAsyncOperationSetFeature、EvtVhfAsyncOperationWriteReport、EvtVhfAsyncOperationGetInputReport,然後在VHF處理這些請求時,就會調用這些回調。
這些回調的類型都是EVT_VHF_ASYNC_OPERATION,定義如下:
EVT_VHF_ASYNC_OPERATION EvtVhfAsyncOperation;
VOID EvtVhfAsyncOperation(
[in] PVOID VhfClientContext,
[in] VHFOPERATIONHANDLE VhfOperationHandle,
[in, optional] PVOID VhfOperationContext,
[in] PHID_XFER_PACKET HidTransferPacket
)
{...}
VhfClientContext是回調的上下文參數,是在初始化時通過VHF_CONFIG結構體設置的。VhfOperationHandle是這次異步操作的句柄,通常用於設置異步操作的結果。HidTransferPacket是請求報告的數據包。
2.2 VhfAsyncOperationComplete
NTSTATUS VhfAsyncOperationComplete(
[in] VHFOPERATIONHANDLE VhfOperationHandle,
[in] NTSTATUS CompletionStatus
);
當源驅動程序處理完異步請求之後,必須要用回調傳入的VhfOperationHandle參數,調用VhfAsyncOperationComplete函數來設置此次異步請求的結果。
2.3 VhfReadReportSubmit
NTSTATUS VhfReadReportSubmit(
[in] VHFHANDLE VhfHandle,
[in] PHID_XFER_PACKET HidTransferPacket
);
源驅動程序可以通過VhfReadReportSubmit函數向VHF提交一個輸入報告,然後由VHF決定何時將該報告提交給系統。
2.4 EVT_VHF_READY_FOR_NEXT_READ_REPORT
源驅動程序也可以自己決定何時將輸入報告提交給系統。可以通過VHF_CONFIG的EvtVhfReadyForNextReadReport字段來設置一個EVT_VHF_READY_FOR_NEXT_READ_REPORT類型的回調,它的定義如下:
EVT_VHF_READY_FOR_NEXT_READ_REPORT EvtVhfReadyForNextReadReport;
VOID EvtVhfReadyForNextReadReport(
[in] PVOID VhfClientContext
)
{...}
如果設置了EvtVhfReadyForNextReadReport回調,則當VHF準備好將緩衝區提交給系統時調用這個回調,然後由源驅動程序決定何時向緩衝區中填充輸入報告。
源驅動程序仍然通過調用VhfReadReportSubmit來填充輸入報告,一旦調用VhfReadReportSubmit後,VHF會盡快提交緩衝區,然後,直到下一次VHF調用EvtVhfReadyForNextReadReport回調後,源驅動程序才可以再次提交輸入報告。
如果實現的是鍵盤、鼠標、觸摸這類輸入設備的話,一般而言,在啓動VHF設備對象後EvtVhfReadyForNextReadReport會立即被調用。
3 實例代碼演示
還是以虛擬HID鍵盤為例,下面會從項目創建開始,完整演示一下用VHF框架實現虛擬HID設備的過程。
3.1 項目創建
這裏通過VS2022來創建項目,在創建項目前需要先完整地安裝好WDK,WDK怎麼安裝官方有詳細的文檔,這裏就不講了。
項目模板就選擇Kernel Mode Driver(KMDF)或者Kernel Mode Driver, Empty(KMDF),如果選擇空模板的話,就要自己實現DriverEntry等函數,這裏我選了Kernel Mode Driver(KMDF)模板。

項目創建後,右鍵項目,點擊屬性,在項目屬性面板中,選擇鏈接器-輸入,在附加依賴項中添加vhfkm.lib,然後在頭文件中包含vhf.h

3.2 修改INF文件
Vhf.sys需要安裝為源驅動程序的Lower Filter驅動,這可以通過INF文件來指定(僅限通過INF文件安裝的情況)。模板中包含默認的INF文件,我們需要在INF文件的DDInstall.HW部分中添加一個AddReg指令(如果沒有DDInstall.HW部分則添加一個),再添加一個對應的AddReg部分。類似下面這樣:
[vhfkeyboardkm_Device.NT.HW]
AddReg = vhfkeyboardkm_Device.NT.AddReg
[vhfkeyboardkm_Device.NT.AddReg]
HKR,,"LowerFilters",0x00010000,"vhf"
3.3 初始化VHF設備對象的代碼
模板實現的是一個PNP樣式的驅動程序,它在EvtDriverDeviceAdd事件中完成WDF設備對象的創建和初始化,我們在它創建WDF設備對象後初始化VHF設備對象。模板的代碼如下:
NTSTATUS
vhfkeyboardkmEvtDeviceAdd(
_In_ WDFDRIVER Driver,
_Inout_ PWDFDEVICE_INIT DeviceInit
)
{
NTSTATUS status;
UNREFERENCED_PARAMETER(Driver);
PAGED_CODE();
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Entry");
status = vhfkeyboardkmCreateDevice(DeviceInit);
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Exit");
return status;
}
NTSTATUS
vhfkeyboardkmCreateDevice(
_Inout_ PWDFDEVICE_INIT DeviceInit
)
{
WDF_OBJECT_ATTRIBUTES deviceAttributes;
PDEVICE_CONTEXT deviceContext;
WDFDEVICE device;
NTSTATUS status;
VHF_CONFIG vhfConfig;
PDEVICE_OBJECT pdo;
PAGED_CODE();
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&deviceAttributes, DEVICE_CONTEXT);
deviceAttributes.EvtCleanupCallback = EVT_CONTEXT_CLEANUP;
status = WdfDeviceCreate(&DeviceInit, &deviceAttributes, &device);
if (NT_SUCCESS(status)) {
deviceContext = DeviceGetContext(device);
RtlZeroMemory(deviceContext, sizeof(DEVICE_CONTEXT));
//status = WdfDeviceCreateDeviceInterface(
// device,
// &GUID_DEVINTERFACE_vhfkeyboardkm,
// NULL // ReferenceString
// );
//if (NT_SUCCESS(status)) {
// //
// // Initialize the I/O Package and any Queues
// //
// status = vhfkeyboardkmQueueInitialize(device);
//}
}
return status;
}
先把模板中創建設備接口和初始化事件隊列的代碼刪掉,我們可以通過pid、vid來訪問虛擬設備,暫時不需要設備接口,而事件主要由VHF處理,所以也暫時不需要事件隊列。然後加入我們初始化VHF設備對象的代碼:
typedef struct _DEVICE_CONTEXT
{
UCHAR Data[8];
VHFHANDLE VhfHandle;
} DEVICE_CONTEXT, *PDEVICE_CONTEXT;
NTSTATUS
vhfkeyboardkmCreateDevice(
_Inout_ PWDFDEVICE_INIT DeviceInit
)
{
WDF_OBJECT_ATTRIBUTES deviceAttributes;
PDEVICE_CONTEXT deviceContext;
WDFDEVICE device;
NTSTATUS status;
VHF_CONFIG vhfConfig;
PDEVICE_OBJECT pdo;
PAGED_CODE();
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&deviceAttributes, DEVICE_CONTEXT);
deviceAttributes.EvtCleanupCallback = EVT_CONTEXT_CLEANUP;
status = WdfDeviceCreate(&DeviceInit, &deviceAttributes, &device);
if (NT_SUCCESS(status)) {
deviceContext = DeviceGetContext(device);
RtlZeroMemory(deviceContext, sizeof(DEVICE_CONTEXT));
pdo = WdfDeviceWdmGetDeviceObject(device);
KdPrint(("Device Object Pointer: %p\n", pdo));
if (pdo == NULL)
{
KdPrint(("Invalid Device Object Pointer\n"));
return STATUS_INVALID_PARAMETER;
}
VHF_CONFIG_INIT(&vhfConfig, pdo, sizeof(G_DefaultReportDescriptor), G_DefaultReportDescriptor);
KdPrint(("VHF configuration initialized\n"));
vhfConfig.EvtVhfAsyncOperationWriteReport = EvtVhfWriteReport;
vhfConfig.VendorID = 0xDEED;
vhfConfig.ProductID = 0xFEED;
vhfConfig.VersionNumber = 0x101;
vhfConfig.VhfClientContext = device;
status = VhfCreate(&vhfConfig, &deviceContext->VhfHandle);
if (!NT_SUCCESS(status))
{
KdPrint(("VhfCreate failed with status: 0x%08x\n", status));
return status;
}
KdPrint(("VhfCreate succeeded\n"));
status = VhfStart(deviceContext->VhfHandle);
if (!NT_SUCCESS(status))
{
KdPrint(("VhfStart failed with status: 0x%08x\n", status));
return status;
}
}
return status;
}
簡單來説就是先初始化VHF_CONFIG,然後設置回調和其他屬性。注意這裏將WDF設備對象device設置為回調的上下文,是為了在回調中獲取WDF設備對象的設備上下文。然後調用VhfCreate創建VHF設備對象,創建出的VHF設備對象的句柄保存到WDF設備對象的設備上下文中。最後調用VhfStart啓動VHF設備對象。
報告描述符是通過VHF_CONFIG傳給VHF的,還是分為鍵盤(report_id=2)和Vendor definded(report_id=1)兩個頂級集合,定義如下:
UCHAR G_DefaultReportDescriptor[] = {
0x06,0x00, 0xFF, // USAGE_PAGE (Vender Defined Usage Page)
0x09,0x01, // USAGE (Vendor Usage 0x01)
0xA1,0x01, // COLLECTION (Application)
0x85,0x01, // REPORT_ID (1)
0x09,0x01, // USAGE (Vendor Usage 0x01)
0x15,0x00, // LOGICAL_MINIMUM(0)
0x26,0xff, 0x00, // LOGICAL_MAXIMUM(255)
0x75,0x08, // REPORT_SIZE (0x08)
0x96,(FEATURE_REPORT_SIZE_CB & 0xff), (FEATURE_REPORT_SIZE_CB >> 8), // REPORT_COUNT
0xB1,0x00, // FEATURE (Data,Ary,Abs)
0x09,0x01, // USAGE (Vendor Usage 0x01)
0x75,0x08, // REPORT_SIZE (0x08)
0x96,(INPUT_REPORT_SIZE_CB & 0xff), (INPUT_REPORT_SIZE_CB >> 8), // REPORT_COUNT
0x81,0x00, // INPUT (Data,Ary,Abs)
0x09,0x01, // USAGE (Vendor Usage 0x01)
0x75,0x08, // REPORT_SIZE (0x08)
0x96,(OUTPUT_REPORT_SIZE_CB & 0xff), (OUTPUT_REPORT_SIZE_CB >> 8), // REPORT_COUNT
0x91,0x00, // OUTPUT (Data,Ary,Abs)
0xC0, // END_COLLECTION
0x05, 0x01, // USAGE_PAGE (Generic Desktop)
0x09, 0x06, // USAGE (Keyboard)
0xA1, 0x01, // COLLECTION (Application)
0x85, 0x02, // REPORT_ID (2)
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x19, 0xE0, // USAGE_MINIMUM (Left Control)
0x29, 0xE7, // USAGE_MAXIMUM (Right GUI)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x01, // LOGICAL_MAXIMUM (1)
0x75, 0x01, // REPORT_SIZE (1)
0x95, 0x08, // REPORT_COUNT (8)
0x81, 0x02, // INPUT (Data, Var, Abs)
0x95, 0x01, // REPORT_COUNT (1)
0x75, 0x08, // REPORT_SIZE (8)
0x81, 0x03, // INPUT (Const, Var, Abs)
0x95, 0x06, // REPORT_COUNT (6)
0x75, 0x08, // REPORT_SIZE (8)
0x15, 0x00, // LOGICAL_MINIMUM (0)
0x25, 0x65, // LOGICAL_MAXIMUM (101)
0x05, 0x07, // USAGE_PAGE (Keyboard)
0x19, 0x00, // USAGE_MINIMUM (No Event)
0x29, 0x65, // USAGE_MAXIMUM (Keyboard Application)
0x81, 0x00, // INPUT (Data, Ary, Abs)
0xC0, // END_COLLECTION
};
3.4 實現鍵盤功能
這裏我們還是實現下面這個功能:
應用程序向源驅動程序發送一個鍵值,然後虛擬鍵盤就模擬這個鍵的按下,如果發送0,則模擬抬起。
3.4.1 使用默認緩衝策略
由於請求處理和緩衝策略都是VHF實現的,所以我們要做的很少,只需要註冊EvtVhfAsyncOperationWriteReport的回調來接收應用程序發送的鍵值,然後調用VhfReadReportSubmit提交一個輸入報告就可以了,回調函數的代碼如下:
typedef struct _HIDMINI_KBD_INPUT_REPORT {
UCHAR ReportId;
UCHAR Data[8];
} HIDMINI_KBD_INPUT_REPORT, * PHIDMINI_KBD_INPUT_REPORT;
typedef struct _HIDMINI_OUTPUT_REPORT {
UCHAR ReportId;
UCHAR Data;
USHORT Pad1;
ULONG Pad2;
} HIDMINI_OUTPUT_REPORT, * PHIDMINI_OUTPUT_REPORT;
VOID EvtVhfWriteReport(
_In_ PVOID VhfClientContext,
_In_ VHFOPERATIONHANDLE VhfOperationHandle,
_In_ PVOID VhfOperationContext,
_In_ PHID_XFER_PACKET HidTransferPacket
)
{
ULONG reportSize;
NTSTATUS status;
PHIDMINI_OUTPUT_REPORT outputReport;
PDEVICE_CONTEXT deviceContext;
HID_XFER_PACKET inputPacket;
HIDMINI_KBD_INPUT_REPORT inputReport;
UNREFERENCED_PARAMETER(VhfOperationContext);
status = STATUS_SUCCESS;
deviceContext = DeviceGetContext(VhfClientContext);
if (HidTransferPacket->reportId != 1)
{
status = STATUS_INVALID_PARAMETER;
KdPrint(("WriteReport: unkown report id %d\n", HidTransferPacket->reportId));
goto Exit;
}
reportSize = sizeof(HIDMINI_OUTPUT_REPORT);
if (HidTransferPacket->reportBufferLen < reportSize) {
status = STATUS_INVALID_BUFFER_SIZE;
KdPrint(("WriteReport: invalid input buffer. size %d, expect %d\n",
HidTransferPacket->reportBufferLen, reportSize));
goto Exit;
}
outputReport = (PHIDMINI_OUTPUT_REPORT)HidTransferPacket->reportBuffer;
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "data:%08x\n", outputReport->Data);
RtlZeroMemory(&inputReport, sizeof(HIDMINI_KBD_INPUT_REPORT));
inputReport.ReportId = 2;
inputReport.Data[2] = outputReport->Data;
inputPacket.reportBuffer = (PUCHAR)&inputReport;
inputPacket.reportBufferLen = sizeof(HIDMINI_KBD_INPUT_REPORT);
inputPacket.reportId = 2;
VhfReadReportSubmit(deviceContext->VhfHandle, &inputPacket);
Exit:
VhfAsyncOperationComplete(VhfOperationHandle, status);
}
這個函數從入參的PHID_XFER_PACKET中獲取了應用程序傳進來的鍵值,然後創建一個新的PHID_XFER_PACKET來填充輸入報告,再調用VhfReadReportSubmit提交輸入報告給VHF,最後調用VhfAsyncOperationComplete來設置回調的結果。
注意這裏調用VhfReadReportSubmit時用到的VhfHandle是我們之前保存在WDF設備對象的設備上下文中的,先前我們將WDF的設備對象設置為回調的上下文參數,也就是VhfClientContext,所以在這裏可以通過DeviceGetContext來獲取設備上下文。
3.4.2 使用EVT_VHF_READY_FOR_NEXT_READ_REPORT控制緩衝方案
上面的例子我們使用的默認的緩衝策略,通過VhfReadReportSubmit提交輸入報告後,由VHF決定何時將輸入報告提交給系統。
也可以使用EVT_VHF_READY_FOR_NEXT_READ_REPORT來控制緩衝策略,這需要在初始化VHF設備對象時註冊EvtVhfReadyForNextReadReport回調。當VHF調用EvtVhfReadyForNextReadReport回調時,意味着源驅動程序可以通過VhfReadReportSubmit向VHF提交一次輸入報告,然後VHF會盡快將輸入報告提交給系統。
使用EVT_VHF_READY_FOR_NEXT_READ_REPORT的代碼時,需要編寫EvtVhfReadyForNextReadReport回調,然後修改EvtVhfWriteReport和DEVICE_CONTEXT的代碼
typedef struct _DEVICE_CONTEXT
{
BOOLEAN Ready;
UCHAR Data[8];
VHFHANDLE VhfHandle;
} DEVICE_CONTEXT, *PDEVICE_CONTEXT;
VOID EvtVhfReadyForNextReadReport(
_In_ PVOID VhfClientContext
)
{
PDEVICE_CONTEXT deviceContext = DeviceGetContext(VhfClientContext);
KdPrint(("EvtVhfReadyForNextReadReport...Entry\n"));
deviceContext->Ready = TRUE;
}
VOID EvtVhfWriteReport(
_In_ PVOID VhfClientContext,
_In_ VHFOPERATIONHANDLE VhfOperationHandle,
_In_ PVOID VhfOperationContext,
_In_ PHID_XFER_PACKET HidTransferPacket
)
{
ULONG reportSize;
NTSTATUS status;
PHIDMINI_OUTPUT_REPORT outputReport;
PDEVICE_CONTEXT deviceContext;
HID_XFER_PACKET inputPacket;
HIDMINI_KBD_INPUT_REPORT inputReport;
UNREFERENCED_PARAMETER(VhfOperationContext);
status = STATUS_SUCCESS;
deviceContext = DeviceGetContext(VhfClientContext);
if (HidTransferPacket->reportId != CONTROL_COLLECTION_REPORT_ID)
{
status = STATUS_INVALID_PARAMETER;
KdPrint(("WriteReport: unkown report id %d\n", HidTransferPacket->reportId));
goto Exit;
}
reportSize = sizeof(HIDMINI_OUTPUT_REPORT);
if (HidTransferPacket->reportBufferLen < reportSize) {
status = STATUS_INVALID_BUFFER_SIZE;
KdPrint(("WriteReport: invalid input buffer. size %d, expect %d\n",
HidTransferPacket->reportBufferLen, reportSize));
goto Exit;
}
outputReport = (PHIDMINI_OUTPUT_REPORT)HidTransferPacket->reportBuffer;
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "data:%08x\n", outputReport->Data);
deviceContext->Data[2] = outputReport->Data;
RtlZeroMemory(&inputReport, sizeof(HIDMINI_KBD_INPUT_REPORT));
if (deviceContext->Ready)
{
deviceContext->Ready = FALSE;
inputReport.ReportId = 2;
inputReport.Data[2] = deviceContext->Data[2];
inputPacket.reportBuffer = (PUCHAR)&inputReport;
inputPacket.reportBufferLen = sizeof(HIDMINI_KBD_INPUT_REPORT);
inputPacket.reportId = 2;
status = VhfReadReportSubmit(deviceContext->VhfHandle, &inputPacket);
if (!NT_SUCCESS(status))
{
KdPrint(("VhfReadReportSubmit failed: 0x%08x", status));
}
}
Exit:
VhfAsyncOperationComplete(VhfOperationHandle, status);
}
這裏在DEVICE_CONTEXT結構體中新增了一個Ready字段,當EvtVhfReadyForNextReadReport被調用時,將它設為TRUE。然後當EvtVhfWriteReport被調用時,首先檢查Ready字段,如果為TRUE,則提交輸入報告。
像鍵盤這類輸入設備,系統會持續請求輸入報告,所以正常情況下,提交輸入報告後,VHF就會馬上再次調用EvtVhfReadyForNextReadReport。
3.5 驅動安裝方法
將vhfkeyboardkm.sys, vhfkeyboardkm.inf, vhfkeyboardkm.cat三個文件拷貝到需要安裝的設備上,使用管理員權限的cmd命令行輸入
devcon.exe install vhfkeyboardkm.inf Root\vhfkeyboardkm
上面的vhfkeyboardkm默認情況下應替換為項目名稱。devcon.exe會隨WDK一起安裝,如果找不到可以全局搜索一下。
如果是測試簽名,需要設備開啓測試模式才能安裝,開啓測試模式的方法是,在管理員權限的cmd命令行輸入
bcdedit.exe /set testsigning on
然後重啓。
3.6 應用程序
應用程序的話還是用這份代碼,這份代碼監聽Alt/Ctrl+字母鍵的全局快捷鍵,在Alt+字母鍵時,向源驅動程序發送該字母鍵的鍵值,在Ctrl+字母鍵時,向源驅動程序發送0. 最後實現的效果跟上一次使用hidmini2實例改造的驅動程序效果一樣。
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using HidSharp;
namespace VirtualHidKbdClient
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private const int AltKeyEventId = 9000;
private bool PressedKey = false;
private HidStream _stream;
[DllImport("user32.dll")]
public static extern uint MapVirtualKey(uint uCode, uint type);
[DllImport("user32.dll")]
public static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
[DllImport("user32.dll")]
public static extern bool UnregisterHotKey(IntPtr hWnd, int id);
public MainWindow()
{
InitializeComponent();
SourceInitialized += MainWindow_SourceInitialized;
}
private void MainWindow_SourceInitialized(object? sender, EventArgs e)
{
var handle = new WindowInteropHelper(this).Handle;
var source = HwndSource.FromHwnd(handle);
source?.AddHook(HwndHook);
//註冊Alt+F1和Alt+字母鍵的全局快捷鍵
for (int i = (int)Key.A; i <= (int)Key.Z; i++)
{
RegisterHotKey(handle, AltKeyEventId, (uint)ModifierKeys.Alt, (uint)KeyInterop.VirtualKeyFromKey((Key)i));
}
RegisterHotKey(handle, AltKeyEventId, (uint)ModifierKeys.Alt, (uint)KeyInterop.VirtualKeyFromKey(Key.F1));
//找到虛擬設備下面的vendor defined設備
var devices = DeviceList.Local.GetHidDevices(0xDEED, 0xFEED);
foreach (var d in devices)
{
Console.WriteLine(d.DevicePath);
}
var device = devices.FirstOrDefault(d => !d.DevicePath.EndsWith("kbd"));
_stream = device.Open();
}
private IntPtr HwndHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
const int wmHotkey = 0x0312;
switch (msg)
{
case wmHotkey:
switch (wParam.ToInt32())
{
case AltKeyEventId:
var lp = lParam.ToInt32();
var virtualKey = lp >> 16;
var key = KeyInterop.KeyFromVirtualKey(virtualKey);
//虛擬鍵碼到HID鍵碼的轉換,字母鍵是連續的,所以可以簡單寫成這樣
var hidKey = virtualKey - 61;
var charKey = (char)MapVirtualKey((uint)virtualKey, 2);
if (key == Key.F1 && PressedKey)
{
_stream.Write([1, 0]);
MessageBox.Show($"cancel long press");
PressedKey = false;
}
else if(key != Key.F1 && !PressedKey)
{
PressedKey = true;
_stream.Write([1, (byte)hidKey]);
MessageBox.Show($"long press {charKey}");
}
break;
}
break;
}
return IntPtr.Zero;
}
protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
var handle = new WindowInteropHelper(this).Handle;
//關閉窗口時取消註冊
UnregisterHotKey(handle, AltKeyEventId);
//關閉窗口時控制虛擬設備按鍵抬起
_stream.Write([1, 0]);
_stream.Close();
}
}
}
4 在UMDF中使用VHF
據VHF的文檔描述,VHF是支持UMDF的,其使用方法和KMDF也大差不差,只是在初始化的時候不太一樣。
在前面介紹VHF_CONFIG_INIT函數的時候,可以看到函數定義裏有一個條件編譯,這個條件編譯指示,在KMDF中,第二個參數應傳入WDM設備對象,而在UMDF中,第二個參數應傳入一個IoTarget.
據文檔描述,這裏的IoTarget應該是通過WdfIoTargetCreate創建,然後通過WdfIoTargetOpen打開的。但我調試了很久沒有調通,實在搞不明白IoTarget應該關聯什麼樣一個對象,總是在WdfIoTargetOpen這裏報錯。