上面的clsInt太过简单了,无法回答这样的问题:在内核中使用类能带来什么好处?simClass工程无法回答上述问题,笔者只是借助它引出并解决一些基本问题。下面我们思考这样一个问题:就驱动本身而言,如何把内核驱动封装成一个类?
内核驱动,无外乎就是一些数据结构:驱动对象、设备对象、文件对象、IRP等;而对这些数据结构的处理就是内核函数:WDM驱动乃是分发函数(Dispatch Function),WDF乃是事件(Event)。
这不正好吗?上述二者恰好是类封装的基本要素!类者,数据加方法。笔者将把诸如驱动对象、设备对象等一切用到的数据结构,作为成员数据;把分发函数或者事件、回调,作为成员函数。一个“驱动类”就此初露峥嵘了。
想法是不错的,但遇到两个问题,下面一一说明。
定义类之前要解决的第一个问题是,一旦类对象被创建后,它的生命周期基本上要和驱动程序的生命周期相当,在哪里保存类对象呢?创建全局变量当然是一种方法,但存在多个驱动实例时就会发生冲突。在WDM驱动中,有设备扩展可以保存自己的变量。KMDF则更丰富,笔者最终决定在WDFDRIVER对象中保存类对象。达成的效果如图6-5所示。
驱动对象和设备对象是驱动程序的核心,而回调函数又是核心的核心。在图6-5中,驱动对象和设备对象的回调函数,都在DrvClass类中实现。而为了让C++类对象的生命周期和驱动对象保持一致,用一个WDMMEMORY对象将它封装起来,并作为驱动对象的子对象,由框架自动维护,在驱动对象存在时,C++类对象将一直是有效的。
首先看看怎么把一个自定义的内容保存到驱动对象中,这又要用到框架对象的“环境变量”概念了,前面我们学过给设备对象设置环境变量,现在轮到驱动对象了。让我们重新来做一遍。
图6-5 对象模块图
第1步,定义一个获取环境块指针的函数。
WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(DRIVER_CONTEXT,
GetDriverContext);
上面的宏将定义一个名称为GetDriverContext的函数,这个函数的伪代码如下:
*DRIVER_CONTEXT GetDriverContext(WDFOBJECT Object)
{
// XXX是一个固定的地址,由于未文档化,无法知道其具体定义
return (DRIVER_CONTEXT*)Object->XXX;
}
以后只需要进行如下调用,即能取得驱动对象的环境块指针(前提是传入正确的对象句柄)。
// 获取环境变量
DRIVER_CONTEXT *pContext = GetDriverContext(WdfDriver);
第2步,在WdfDriverCreate创建框架驱动对象的同时,设置环境变量的结构,通过WDF_DRIVER_CONFIG完成。下面代码的前面部分,实现了此步。
第3步,调用GetDriverContext获取环境变量,并将其封装到一个WDFMEMORY对象中,并指定第2步中创建的驱动对象为其父对象,以令框架自动维护其生命周期。下面代码的后面部分,实现了此步。
NTSTATUS DrvClass::DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath)
{
KDBG(DPFLTR_INFO_LEVEL, "DrvClass::DriverEntry");
WDFMEMORY hDriver;
WDF_OBJECT_ATTRIBUTES attributes;
WDF_DRIVER_CONFIG config;
NTSTATUS status = STATUS_SUCCESS;
WDFDRIVER WdfDriver;
// 设定驱动环境块长度
// 宏内部会调用sizeof(…)求结构体长度,并用粘连符(##)获得其名称
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attributes, DRIVER_CONTEXT);
WDF_DRIVER_CONFIG_INIT(&config, DrvClass::PnpAdd_sta);
status = WdfDriverCreate(DriverObject, // WDF驱动对象
RegistryPath,
&attributes,
&config, // 配置参数
&WdfDriver);
// 取得驱动环境块
PDRIVEDR_CONTEXT pContext = GetDriverContext(WdfDriver);
ASSERT(pContext);
pContext->par1 = (PVOID)this;
// 把类对象用WDFMEMORY对象封装后,作为WDFDRIVER对象的子对象
WDF_OBJECT_ATTRIBUTES_INIT(&attributes);
attributes.ParentObject = WdfDriver;
attributes.EvtDestroyCallback = DrvClassDestroy;
WdfMemoryCreatePreallocated(&attributes, (PVOID)this,
sizeof(DrvClass), &hDriver);
KDBG(DPFLTR_INFO_LEVEL, "this = %p", this);
return status;
}
驱动程序将在入口函数DriverEntry中动态创建一个类对象,并即刻调用方法DrvClass::DriverEntry,以创建驱动对象并将其作为对象的存储所。
以这种方法实现的妙处是,对象的维护是自动化的,我们不用操心太切。一切看上去,很是完美。下面是DrvClassDestroy函数的实现,WDF框架会在销毁内存对象时自动调用它,我们在其中销毁类对象。
VOID DrvClassDestroy(IN WDFOBJECT Object)
{
PVOID pBuf = WdfMemoryGetBuffer((WDFMEMORY)Object, NULL);
delete pBuf;
}
KMDF中的事件函数,分开来说:驱动对象有EvtDriverDeviceAdd和EvtDriverUnload,我们将实现前者;设备对象有一系列PNP/Power事件;还有其他对象的事件函数,且忽略之,详见代码。
事件函数说到底是一种回调函数。类普通成员函数,由于编译后会增加this参数,所以无法成为回调函数。只能使用类静态函数,并通过静态函数再回调成员函数。这是一种很通用的实现手段。以EvtDriverDeviceAdd事件函数为例,我们要在类中为它定义两个相关函数。
Class DrvClass
{
// 定义类静态函数,它是全局的,可以作为回调函数
static NTSTATUS PnpAdd_sta(
IN WDFDRIVER Driver,
IN PWDFDEVICE_INIT DeviceInit);
// 再定义类成员函数,将由静态函数内部调用
virtual NTSTATUS PnpAdd(
IN WDFDRIVER Driver,
IN PWDFDEVICE_INIT DeviceInit,
DrvClass* pThis);
// 其他接口函数
// ……
}
要能够通过静态函数回调成员函数,即通过PnpAdd_sta回调PnpAdd函数。前提是要能够获得对象指针,因为我们已经把对象指针保存在驱动对象的环境块中了,所以达到此目的不是难事。代码如下:
NTSTATUS DrvClass::PnpAdd_sta(IN WDFDRIVER Driver,
IN PWDFDEVICE_INIT DeviceInit)
{
// 取得环境块
PDRIVEDR_CONTEXT pContext = GetDriverContext(Driver);
// 环境块中存有对象指针
DrvClass* pThis = (DrvClass*)pContext->par1;
// 再调用成员函数
return pThis->PnpAdd(Driver, DeviceInit);
}
所有其他的事件函数,都必须采用相同的方法实现。
其实上面的内容,一直是围绕KMDF进行讲解的。DrvClass内部的DriverEntry成员函数已经讲解过了,现在看看真正的入口函数该如何定义吧。
extern "C" NTSTATUS DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
// 动态创建对象,此步在后面将被修改
DrvClass* myDriver = new(NonPagedPool, 'CY01')DrvClass();
if(myDriver == NULL)return STATUS_UNSUCCESSFUL;
return myDriver->DriverEntry(DriverObject, RegistryPath);
}
干净得不得了,驱动程序在加载之初就以快捷无比的速度向我们定义的类靠拢了。至于第1行代码动态创建对象的操作,当前这样实现已经完全可以了,但在后面将被修改,以支持多态。
如果使用WDM方式进行类封装,对于非PNP类驱动,可以在入口函数中创建控制设备对象,并把类对象保存在设备对象的设备扩展中;对于PNP类驱动,应当在AddDevice函数中建立设备栈时创建类对象,并将其保存在功能设备对象的设备扩展中。笔者会以前者为例,简单讲一下实现。WDMClass示例工程,读者参照代码,在它的基础上很容易扩展出功能更为完善的驱动程序。
这里列出具体的封装过程。首先是类定义,定义一个通用的分发函数如下:
class WDMDrvClass{
public:
static NTSTATUS DispatchFunc_sta(
DEVICE_OBJECT Device,
PIRP Irp);
virtual NTSTATUS DispatchFunc(
DEVICE_OBJECT Device,
PIRP Irp);
// 其他……
};
同理,定义一个静态函数和一个类成员函数,静态函数将通过对象指针调用成员函数。入口函数中要这样定义:
typedef struct{
WDMDrvClass pThis;
//……
}DEVICE_EXTENSION;
NTSTATUS DriverEntry( PDRIVER_OBJECT Driver,
PUNICODE_STRING Register)
{
// 创建动态对象
WDMDrvClass* pDrv = new(NonPagedPool, 'SAMP') WDMDrvClass();
// 设置分发函数,全部指向DispatchFunc_sta
for(int i = 0; i
Driver->DispatchFunction[i] = pDrv->DispatchFunc_sta;
}
// 创建控制设备对象,并同时创建设备扩展区
IoCreateDeviceObject(..., sizeof(DEVICE_EXTENSION));
// 把对象指针保存到设备扩展中
DEVICE_EXTENSION* pContext = (DEVICE_EXTENSION*)DeviceObject->DeviceExtension;
pContext->pThis = pDrv;
return STATUS_SUCCESS;
}
这一切就绪之后,我们还是来看看DispatchFunc_sta该如何实现吧。诚如我们所知,所有的驱动分发函数的第一个参数总是设备对象,正是我们所创建的那个。通过它,我们总是能够在静态函数中得到对象指针。下面是DispatchFunc_sta函数的实现。
NTSTATUS WDMDrvClass::DispatchFunc_sta(
DEVICE_OBJECT Device, PIRP Irp)
{
PDEVICE_EXTENSION pContext = Device->DeviceExtension;
WDMDrv pThis = pContext->pThis;
return pThis-> DispatchFunc(Device, Irp);
}
与上述KMDF的实现类似,其他更详细的实现内容,请参阅工程代码。