65.9K
CodeProject 正在变化。 阅读更多。
Home

基于状态机的 Win32/WinCE 程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.37/5 (9投票s)

2006年4月10日

5分钟阅读

viewsIcon

72829

downloadIcon

1295

本文介绍如何使用窗口消息挂钩技术来运行基于状态机的应用程序框架的 Win32/WinCE 程序。(开源项目)

引言

许多并发、分布式和实时应用程序必须与其他对象(称为服务提供者)紧密协作。服务由服务用户(应用程序)可用的原语(操作)集正式定义。这些原语描述了一些动作,或报告了对等组件/实体已执行的动作。服务原语可分为四类:请求、指示、响应和确认。[1]

本文介绍如何使用窗口消息挂钩技术来运行基于状态机的应用程序框架的 Win32/WinCE 程序。

为什么运行基于状态机的 Win32/WinCE 应用程序?

在对这类应用程序进行建模时,使用状态机是很自然的,因为一个必须按顺序执行一系列动作,或根据其当前状态以不同方式处理输入的(响应和指示)的应用程序,通常最好实现为状态机。

状态机应用程序框架广泛用于嵌入式系统开发。嵌入式系统开发者可以使用 Windows 平台作为建模和仿真环境,这样可以在硬件原型可用之前很久就开始软件开发和调试。在仿真环境中,开发者可以使用 Windows 程序作为服务提供者设计一些仿真器。这些仿真器将具有与目标服务提供者接口相同的接口。在目标环境中,开发者可能只需付出很少的努力即可将状态机应用程序与真实环境中的这些服务提供者集成。

然而,对于 Windows 应用程序,特别是随着智能手机和 PDA 应用程序的 WinCE 操作系统兴起,随着系统硬件和软件变得越来越复杂,以及系统本身变得越来越互联和分布式,这些方法将变得越来越重要。

传统的有限状态机实现

典型的状态机线程工作方式如下:

SmeRun() 
{ 
   do { 
      Wait for an external event which is posted to this running thread;
      if ( the event is valid) 
      { 
         Dispatch this event to active applications and trigger state transitions.
      } else break;
   } while(1);
}

这种运行模式的缺点是我们必须为状态机应用程序创建单独的线程。

Windows® 环境中的挂钩技术

在编程中,挂钩是一种使用所谓的“挂钩”技术,将一系列过程形成一个处理链。因此,在处理过的事件发生后,控制流会按照特定的顺序沿着链进行。新的挂钩注册其自己的地址作为事件的处理程序,并通常在最后调用原始处理程序。每个挂钩都需要将执行传递给前一个处理程序,最终到达默认处理程序,否则链会被中断。取消注册挂钩意味着将原始过程设置为事件处理程序。

挂钩可用于多种目的,包括调试和扩展原始功能,但也可被滥用以将(潜在恶意)代码注入事件处理程序。[2]

而且,由于基于 Windows 的应用程序是事件驱动的,因此挂钩技术看起来非常有趣。事实上,这些应用程序不进行显式函数调用(例如 C 运行时库调用)来获取输入。相反,它们等待系统将输入传递给它们。系统将应用程序的所有输入传递给应用程序中的各个窗口。每个窗口都有一个称为窗口过程的函数,每当系统有输入传递给该窗口时,系统就会调用该函数。如果我们挂钩窗口消息,当服务提供者(其他对象)将外部事件(具有特定类型的 Windows 消息)发布到此挂钩的窗口时,状态机引擎将获取事件数据,然后将其分发给活动的某个状态机应用程序。

在 UML StateWizard(一个用于并发、分布式实时应用程序开发的 UML 动态模型工具)中,该过程定义如下:

1. 挂钩窗口消息

下面的函数 MfcHookWnd() 通过子类化 HWND 对象来挂钩它,即在 Windows 的意义上,通过将自己的窗口过程插入到当前存在的任何过程(通常是 AfxWndProc)的前面。 [3]

class CEgnSubclassWnd: public CSubclassWnd 
{
public:
    CEgnSubclassWnd();
    virtual ~CEgnSubclassWnd();

    virtual LRESULT WindowProc(HWND hwnd, UINT msg, WPARAM wp,LPARAM lp);
    LRESULT OnExtEvent(MSG& WinMsg);
};

CEgnSubclassWnd::CEgnSubclassWnd()
{
}

CEgnSubclassWnd::~CEgnSubclassWnd()
{
}

LRESULT CEgnSubclassWnd::WindowProc(HWND hwnd, UINT msg, WPARAM wp,LPARAM lp)
{
    struct SME_EVENT_T* pExtEvent=NULL;
    MSG WinMsg;
    WinMsg.hwnd = hwnd;
    WinMsg.message =msg;
    WinMsg.wParam = wp;
    WinMsg.lParam = lp;
    LRESULT ret = 0;

    switch (msg)
    {
        case WM_EXT_EVENT_ID:
        // External event is triggered. App go to running state.
            OnExtEvent(WinMsg);
            break;
            default:
            break;
    }
    ret = CSubclassWnd::WindowProc(hwnd,msg,wp,lp);
    return ret;
}

2. 将事件分发给状态机

通常,所有应用程序(状态机)都只在一个线程上运行。但是,状态机引擎允许将应用程序分成一些组,并且一组中的应用程序可以同时在单独的线程上运行。下图,“使用 MFC 运行状态机”,说明了在每个组各自的线程上运行的一些应用程序组。在每个线程上,状态机引擎都会挂钩在该线程上运行的窗口。

Sample screenshot

当状态机引擎收到专用于状态机应用程序的消息时,状态机引擎会将此消息转换为外部事件,并将其分发到目标应用程序端口(如果不是 null);否则,它会将消息分发给在同一应用程序线程上下文中运行的所有活动应用程序。

当挂钩的窗口被销毁时,窗口消息挂钩会自动移除。

/*****************************************************
* DESCRIPTION: Just like SmeRun(), this function
* dispatches external event to applications. 
* INPUT: 
* OUTPUT: None.
* NOTE: 
******************************************************/
struct SME_EVENT_T * GetEventFromQueue();
BOOL DispatchInternalEvents(SME_THREAD_CONTEXT_PT pThreadContext);
BOOL DispatchEventToApps(SME_THREAD_CONTEXT_PT pThreadContext, 
                         SME_EVENT_T *pEvent);

LRESULT CEgnSubclassWnd::OnExtEvent(MSG& WinMsg)
{
    SME_APP_T *pApp;
    SME_THREAD_CONTEXT_PT pThreadContext=NULL;
    SME_EVENT_T *pEvent=TranslateEvent(&WinMsg);
    if (pEvent==NULL)
        return 0;
    if (g_pfnGetThreadContext)
        pThreadContext = (*g_pfnGetThreadContext)();
    if (!pThreadContext) return 0;
        pApp = pThreadContext->pActAppHdr;

    /* Call hook function on an external event coming. */
    if (pThreadContext->fnOnEventComeHook)
        (*pThreadContext->fnOnEventComeHook)
           (SME_EVENT_ORIGIN_EXTERNAL, pEvent);
    /* Dispatch it to active applications.*/
    DispatchEventToApps(pThreadContext, pEvent);
    DispatchInternalEvents(pThreadContext);
    /* Free external event if necessary. */
    if (pThreadContext->fnDelExtEvent && pEvent)
    {
        (*pThreadContext->fnDelExtEvent)(pEvent);
        // Engine should delete this event, because
        // translation of external event
        // will create an internal event. 
        SmeDeleteEvent(pEvent); 
    }

    return 0;
}

CEgnSubclassWnd EngSubclassWnd;
BOOL MfcHookWnd(HWND hWndHooked)
{
    if (hWndHooked==NULL || !IsWindow(hWndHooked))
        return FALSE;
    CWnd *pWnd = CWnd::FromHandle(hWndHooked);
    return EngSubclassWnd.HookWindow(pWnd); 
}

BOOL MfcUnhookWnd(HWND hWndHooked)
{
    if (hWndHooked==NULL || !IsWindow(hWndHooked))
        return FALSE;
    CWnd *pWnd = CWnd::FromHandle(hWndHooked);
    return EngSubclassWnd.HookWindow(pWnd); 
}

示例

假设我们有一个简单的播放器应用程序,其状态图如下:

Sample screenshot

下面的示例演示了如何挂钩对话框消息并将外部事件分发给 Player 状态机应用程序。声明一个应用程序线程上下文。当对话框打开时,通过 SmeInitEngine() 在线程上下文中初始化状态机引擎。在状态机引擎的 Windows 版本中,此函数将根据隐含的信息自动初始化给定的线程上下文。

  1. SmeSetExtEventOprProc() 用于通过 Windows API GetMessage()PostThreadMessage() 设置外部事件处理函数。
  2. SmeSetMemOprProc() 用于通过 newdelete 操作符设置动态内存管理函数。
  3. SmeSetTlsProc() 用于通过 Windows API TlsGetValue()TlsSetValue() 设置线程局部存储过程函数。

然后挂钩对话框消息。在应用程序线程中激活 Player 应用程序。如果触发了外部事件,它会调用 MfcPostExtIntEventToWnd() 函数将外部事件发布到对话框。此函数将如下所示的 WM_EXT_EVNET_ID Windows 消息发布到对话框。

#define WM_EXT_EVENT_ID (0xBFFF)

当状态机引擎收到此消息时,它会将此消息转换为外部事件,并将其分发到目标应用程序端口(如果不是 null);否则,它会将消息分发给在同一应用程序线程上下文中运行的所有活动应用程序。

// The application thread context. 
SME_THREAD_CONTEXT_T g_AppThreadContext; 
// Declare the Player state machine application variable. 
SME_DEC_EXT_APP_VAR(Player); 

BOOL CSamplePlayerMfcDlg::OnInitDialog() 
{ 
   CDialog::OnInitDialog(); 
    
   .... 
   // Initialize engine. 
   g_AppThreadContext.nAppThreadID = 0; 
   SmeInitEngine(&g_AppThreadContext); 
   // Hook dialog message. 
   MfcHookWnd(GetSafeHwnd()); 
   SmeActivateApp(&SME_GET_APP_VAR(Player),NULL); 
} 

void CSamplePlayerMfcDlg::OnButtonPower() 
{ 
   MfcSendExtIntEventToWnd(EXT_EVENT_ID_POWER, 
                           0, 0, NULL, GetSafeHwnd()); 
} 

void CSamplePlayerMfcDlg::OnButtonPause() 
{ 
   // TODO: Add your control notification handler code here 
   MfcSendExtIntEventToWnd(EXT_EVENT_ID_PAUSE_RESUME, 
                           0, 0, NULL, GetSafeHwnd()); 
}

当挂钩的窗口被销毁时,窗口消息挂钩会自动移除。

对主题感兴趣?

您可以在 UML StateWizard 开源项目官方网站下载更多信息。[4]

注释和参考文献

  1. 计算机网络,Andrew S.Tanenbaum
  2. 维基百科,领先的用户贡献百科全书
  3. Microsoft Systems Journal,1997 年 3 月
  4. UML StateWizard 项目托管在 Sourceforge.net

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.