Hungry Mind , Blog about everything in IT - C#, Java, C++, .NET, Windows, WinAPI, ...

Показаны сообщения с ярлыком Under the Hood. Показать все сообщения
Показаны сообщения с ярлыком Under the Hood. Показать все сообщения

CLR Execution Context

В очередной раз стало не хватать свежачка из внутренностей .NET Framework и CLR - решил поизучать потоки и домены, а точнее контекст, в котором выполняется наш прекрасный код.

Начнем с примера:

public class ContextInspector : MarshalByRefObject
{
    public void DumpContext(Object state)
    {
        var domain = AppDomain.CurrentDomain;
        var domainName = domain.IsDefaultAppDomain() ? "Default Domain" : domain.FriendlyName;
        Console.WriteLine("#" + Thread.CurrentThread.ManagedThreadId + ": Domain = " + domainName);
    }
 
    public void QueueDumpContext()
    {
        ThreadPool.QueueUserWorkItem(DumpContext);
    }
}
 
public class Program1
{
    private static void Main(string[] args)
    {
        new ContextInspector().QueueDumpContext();
        Thread.Yield();
        var domain = AppDomain.CreateDomain("Custom Domain");
        var type = typeof(ContextInspector);
        var contextInspector = (ContextInspector)domain.CreateInstanceAndUnwrap(type.Assembly.FullNametype.FullName);
        // We just need the same thread pool thread at least once
        contextInspector.QueueDumpContext();
        contextInspector.QueueDumpContext();
        Console.ReadLine();
    }
}

Программа помещает вызов метода DumpContext в очередь пула потоков CLR из основного домена приложения (Default Domain), а после из специализированного (Custom Domain), созданного мною. Точный вывод программы не очень интересен, но следующие две строки порождают важные вопросы:

#6: Domain = Default Domain
#6: Domain = Custom Domain

Почему один и тот же рабочий поток находится в разных доменах? Чем мотивировано такое поведение и кто его обеспечивает?

Вполне логично ожидать выполнения рабочих единиц (Work Items) пула потоков в окружении, которое максимально напоминает исходное. Фактически, единственное различие должно быть в потоке выполнения и всем, что с этим прямо или косвенно связано. И здесь CLR вместе с BCL делают нам, разработчикам, большое одолжение - запоминают наиболее важные аспекты контекста выполнения и разворачивают (применяют) их в потоках пула перед выполнением пользовательских рабочих единиц. Необходимо напомнить, что классы mscorlib.dll (в т.ч. ThreadPool) загружаются единожды в т.н. Shared Domain и поэтому они (вместе с состоянием, результатом JIT компиляции IL кода и пр.) используются всеми классами всех без исключения доменов. Подробнее об этом можно почитать здесь.

Пул потоков CLR реализован как связка управляемого кода в лице класса ThreadPool и неуправляемой составляющей внутри библиотеки clr.dll, классы ThreadpoolMgr и ThreadPoolNative. Давайте рассмотрим первую часть с доступным исходным кодом из Reference Source, файл ThreadPool.cs, основные сущности:

  1. Статический класс ThreadPoolGlobals.

    Содержит глобальное состояние пула потоков для каждого домена. Напомню, что статические члены классов привязаны к доменам даже в случае загрузки их (классов) в Shared Domain. Одним из полей является экземпляр ThreadPoolWorkQueue. Хочу обратить внимание, что начиная с версии 4.0 CLR поддерживает отдельную очередь рабочих единиц для каждого пользовательского домена и равномерно распределяет время своих потоков между ними.

  2. Класс ThreadPoolWorkQueue.

    Умная очередь рабочих единиц - полностью управляемый код с плюшками типа Work Stealing.

  3. Статический класс ThreadPool

    Интерфейс взаимодействия между пользовательским кодом и всей инфраструктутой пула потоков CLR. Подавляющее большинство методов объявлены как static extern и реализованы в clr.dll.

Разберем процесс попадания рабочей единицы в пул и ее диспатчеризацию. Все начинается с метода QueueUserWorkItem, за которым следует добавление элемента в очередь и вызов RequestWorkerThread:

[InlinedCallFrame: 00000000004ec728] System.Threading.ThreadPool.RequestWorkerThread()
DomainNeutralILStubClass.IL_STUB_PInvoke()
System.Threading.ThreadPoolWorkQueue.Enqueue(System.Threading.IThreadPoolWorkItem, Boolean)
System.Threading.ThreadPool.QueueUserWorkItemHelper(System.Threading.WaitCallback, System.Object, System.Threading.StackCrawlMark ByRef, Boolean)
System.Threading.ThreadPool.QueueUserWorkItemHelper(System.Threading.WaitCallback, System.Object, System.Threading.StackCrawlMark ByRef, Boolean)
System.Threading.ThreadPool.QueueUserWorkItem(System.Threading.WaitCallback, System.Object)

Метод RequestWorkerThread объявлен как static extern, его реализация находится вот здесь:

bp clr!ThreadPoolNative::RequestWorkerThread

И до безобразия проста:

call    clr!GetThread
mov     rcx,qword ptr [rax+10h]
...
call    clr!ThreadpoolMgr::SetAppDomainRequestsActive
...
call    clr!ThreadpoolMgr::QueueUserWorkItem
...

Текущий домен маркируется как требующий диспатчеризации из очереди, а сбивающий с толку вызов ThreadpoolMgr::QueueUserWorkItem на самом деле взаимодействует с приложением-хостом через clr!CorHost2::m_HostThreadpoolManager.

Теперь проанализируем как пул обрабатывает запрос на рабочий поток. Каждый из них начинает свою жизнь следующим образом:

clr!ThreadpoolMgr::WorkerThreadStart
clr!Thread::intermediateThreadProc+0x7d
KERNEL32!BaseThreadInitThunk+0xd
ntdll!RtlUserThreadStart+0x1d

clr!ThreadpoolMgr::WorkerThreadStart производит некую инициализацию:

...
call    clr!ClrFlsSetThreadType
...
call    qword ptr [clr!_imp_CoInitializeEx]
...

Далее вызов clr!ThreadpoolMgr::ExecuteWorkRequest выполняет следующий код по стеку:

clr!Frame::Push+0x90
clr!Frame::Pop+0x86
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0x2bd
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0x23b
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0xb4

Инструкция clr!Frame::Push+0x90 - call clr!ManagedThreadCallState::IsAppDomainEqual проверяет текущий домен на соответствие необходимому:

0:005> g
Breakpoint 1 hit
clr!Frame::Push+0x95:
000007fe`f38035d1 85c0            test    eax,eax
0:005> r eax
eax=0

и изменяет его в случае неравенства вызовом clr!AppDomain::EnterContext:

clr!AppDomain::EnterContext
clr!Thread::DoADCallBack+0x21d
clr!Frame::Push+0xd6
clr!Frame::Pop+0x86
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0x2bd
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0x23b
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0xb4
clr!ThreadpoolMgr::ExecuteWorkRequest+0x4c
clr!ThreadpoolMgr::WorkerThreadStart+0xf3
clr!Thread::intermediateThreadProc+0x7d
KERNEL32!BaseThreadInitThunk+0xd
ntdll!RtlUserThreadStart+0x1d

который просто прыгает в другую функцию:

jmp     clr!Thread::EnterContextRestricted

И в упрощенной форме смена домена осуществляется записью его в Thread Local Storage:

mov     ecx,dword ptr [clr!gAppDomainTLSIndex]
call    qword ptr [clr!_imp_TlsSetValue]

Далее мы возвращаемся к clr!Frame::Push+0x90 и еще раз проверяем текущий домен:

clr!Frame::Push+0x90
clr!Frame::Pop+0x86
clr!ManagedPerAppDomainTPCount::DispatchWorkItem+0x2bd
clr!ReturnToPreviousAppDomainHolder::Init+0x39
clr!Thread::DoADCallBack+0x234

На этот раз имеет место равенство и мы попадаем в clr!QueueUserWorkItemManagedCallback, где происходят следующие вызовы:

mscorlib_ni+0x49c790
clr!CallDescrWorkerInternal+0x83
clr!CallDescrWorkerWithHandler+0x4a
clr!MethodDescCallSite::CallTargetWorker+0x2e6
clr!QueueUserWorkItemManagedCallback+0x2a

mscorlib_ni+0x49c790 - это на самом деле метод PerformWaitCallback класса _ThreadPoolWaitCallback:

// 
// This type is necessary because VS 2010's debugger looks for a method named _ThreadPoolWaitCallbacck.PerformWaitCallback 
// on the stack to determine if a thread is a ThreadPool thread or not.  We have a better way to do this for .NET 4.5, but
// still need to maintain compatibility with VS 2010.  When compat with VS 2010 is no longer an issue, this type may be 
// removed.
//
internal static class _ThreadPoolWaitCallback
{ 
    [System.Security.SecurityCritical]
    static internal bool PerformWaitCallback() 
    { 
        return ThreadPoolWorkQueue.Dispatch();
    } 
}

Как вы видите - один из потоков пула был выделен для диспатчеризации рабочих единиц одного из пользовательских доменов.

Рассмотрим ключевые моменты метода Dispatch:

  1. Извлекает и исполняет элементы из очереди пока не кончился квант времени отведенный пулом: while ((Environment.TickCount - quantumStartTime) < ThreadPoolGlobals.tpQuantum).

    Это скорее оптимизация, призванная уменьшить количество переходов между clr.dll и mscorlib.dll, точнее не тратить больше времени на эти переходы нежели на сами рабочие единицы.

    Кстати, ТэПэ квантум по-умолчанию равен 30 миллисекундам, а это два кванта операционной системы (в обычных условиях). Наверное это попытка гарантировать минимум один целый квантум для метода.

  2. После исполнения каждой рабочей единицы пул потоков уведомляется об этом; пул может приказать диспатчеру немедленно выйти из метода:
    //
    // Notify the VM that we executed this workitem.  This is also our opportunity to ask whether Hill Climbing wants 
    // us to return the thread to the pool or not.
    // 
    if (!ThreadPool.NotifyWorkItemComplete()) 
        return false;
    
  3. Каждый раз в случае непустой очереди вызывает workQueue.EnsureThreadRequested(), тем самым запрашивая дополнительные потоки у пула (но следит чтобы количество неудовлетворенных запросов было всегда меньше количества процессоров в системе):
    //
    // If we found work, there may be more work.  Ask for another thread so that the other work can be processed 
    // in parallel.  Note that this will only ask for a max of #procs threads, so it's safe to call it for every dequeue.
    //
    workQueue.EnsureThreadRequested();
       
  4. Первым действием уменьшает счетчик неудовлетворенных запросов с помощью workQueue.MarkThreadRequestSatisfied():
    //
    // Update our records to indicate that an outstanding request for a thread has now been fulfilled. 
    // From this point on, we are responsible for requesting another thread if we stop working for any 
    // reason, and we believe there might still be work in the queue.
    // 
    // Note that if this thread is aborted before we get a chance to request another one, the VM will
    // record a thread request on our behalf.  So we don't need to worry about getting aborted right here.
    //
    workQueue.MarkThreadRequestSatisfied(); 
       

Резюме:

  • ThreadPool управляет очередью задач своего домена, запрашивает ресурсы у пула потоков и за отведенное время исполняет выполняет максимальное количество единиц из очереди.
  • CLR управляет пулом потоков и распределяет процессорное время между очередями всех существующих доменов, которые инициализировали ThreadPool (фактически минимум один раз поместили задачу в очередь). Кроме этого, CLR накатывает правильный низкоуровневый контекст прежде чем передавать управление в класс ThreadPool.

Кстати, clr!ThreadpoolMgr::WorkerThreadStart в цикле обработки запросов передает управление clr!Thread::SetApartment, что приводит к последовательным вызовам CoUninitialize и CoInitializeEx(NULL, COINIT_MULTITHREADED). А значит состояние ole32.dll постоянно очищается и все потоки пула живут в MTA.

Следующий шаг - более тщательное изучение класса ThreadPool, начну с метода QueueUserWorkItemHelper (кстати, обратите внимание на коментарии к коду, они подтверждают часть написанного выше):

//ThreadPool has per-appdomain managed queue of work-items. The VM is
//responsible for just scheduling threads into appdomains. After that 
//work-items are dispatched from the managed queue. 
[System.Security.SecurityCritical]  // auto-generated
private static bool QueueUserWorkItemHelper(WaitCallback callBack, Object state, ref StackCrawlMark stackMark, bool compressStack ) 
{
    bool success =  true;

    if (callBack != null) 
    {
                //The thread pool maintains a per-appdomain managed work queue. 
        //New thread pool entries are added in the managed queue. 
        //The VM is responsible for the actual growing/shrinking of
        //threads. 

        EnsureVMInitialized();

        // 
        // If we are able to create the workitem, we need to get it in the queue without being interrupted
        // by a ThreadAbortException. 
        // 
        try { }
        finally 
        {
            QueueUserWorkItemCallback tpcallBack = new QueueUserWorkItemCallback(callBack, state, compressStack, ref stackMark);
            ThreadPoolGlobals.workQueue.Enqueue(tpcallBack, true);
            success = true; 
        }
    } 
    else 
    {
        throw new ArgumentNullException("WaitCallback"); 
    }
    return success;
}

Экземпляры QueueUserWorkItemCallback добавляются в очередь и на этом финита. Но интерес здесь вызывает уже конструктор QueueUserWorkItemCallback:

internal QueueUserWorkItemCallback(WaitCallback waitCallback, Object stateObj, bool compressStack, ref StackCrawlMark stackMark) 
{
    callback = waitCallback; 
    state = stateObj; 
    if (compressStack && !ExecutionContext.IsFlowSuppressed())
    { 
        // clone the exection context
        context = ExecutionContext.Capture(
            ref stackMark,
            ExecutionContext.CaptureOptions.IgnoreSyncCtx | ExecutionContext.CaptureOptions.OptimizeDefaultCase); 
    }
} 

Обратите внимание на использование параметров compressStack и stackMark вместе с классом ExecutionContext. Без дальнейшего углубления читатель может сообразить что здесь происходит - захват текущего контекста выполнения вместе с неким анализом стека в переменную-член QueueUserWorkItemCallback. Возможности ExecutionContext мимолетно описаны Рихтером в известной книге CLR via C#, а нам самое время переместиться в файл ExecutionContext.cs и проанализировать класс более детально.

Во-первых, он предлагает шесть доступных пользовательскому коду методов:

  1. static ExecutionContext Capture()
  2. ExecutionContext CreateCopy()
  3. static bool IsFlowSuppressed()
  4. static AsyncFlowControl SuppressFlow()
  5. static void RestoreFlow()
  6. static void Run(ExecutionContext executionContext, ContextCallback callback, Object state)

Все эти методы описаны в документации к классу; ключевые особенности:

  1. Capture захватывает текущий контекст в экземпляр своего же класса.

    Здесь стоит остановиться для изучения тела метода (MSDN сокрушает наше мировозрение одним предложением Captures the execution context from the current thread - прям захватывает и пиздец, без компромиссов):

    public static ExecutionContext Capture()
    { 
        // set up a stack mark for finding the caller
        StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller; 
        return ExecutionContext.Capture(ref stackMark, CaptureOptions.None); 
    }
    
    static internal ExecutionContext Capture(ref StackCrawlMark stackMark, CaptureOptions options)
    { 
        ExecutionContext.Reader ecCurrent = Thread.CurrentThread.GetExecutionContextReader();
    
        // check to see if Flow is suppressed 
        if (ecCurrent.IsFlowSuppressed)
            return null; 
    
        //
        // Attempt to capture context.  There may be nothing to capture...
        // 
    
    #if FEATURE_IMPERSONATION || FEATURE_COMPRESSEDSTACK 
        // capture the security context 
        SecurityContext secCtxNew = SecurityContext.Capture(ecCurrent, ref stackMark);
    #endif // #if FEATURE_IMPERSONATION || FEATURE_COMPRESSEDSTACK 
    #if FEATURE_CAS_POLICY
         // capture the host execution context
        HostExecutionContext hostCtxNew = HostExecutionContextManager.CaptureHostExecutionContext();      
    #endif // FEATURE_CAS_POLICY 
    
    #if FEATURE_SYNCHRONIZATIONCONTEXT 
        SynchronizationContext syncCtxNew = null; 
    #endif
        LogicalCallContext logCtxNew = null; 
    
        if (!ecCurrent.IsNull)
        {
    #if FEATURE_SYNCHRONIZATIONCONTEXT 
            // capture the [....] context
            if (0 == (options & CaptureOptions.IgnoreSyncCtx)) 
                syncCtxNew = (ecCurrent.SynchronizationContext == null) ? null : ecCurrent.SynchronizationContext.CreateCopy(); 
    #endif // #if FEATURE_SYNCHRONIZATIONCONTEXT
    
            // copy over the Logical Call Context
            if (ecCurrent.LogicalCallContext.HasInfo)
                logCtxNew = ecCurrent.LogicalCallContext.Clone();
        } 
    
        // 
        // If we didn't get anything but defaults, and we're allowed to return the 
        // dummy default EC, don't bother allocating a new context.
        // 
        if (0 != (options & CaptureOptions.OptimizeDefaultCase) &&
    #if FEATURE_IMPERSONATION || FEATURE_COMPRESSEDSTACK
            secCtxNew == null &&
    #endif 
    #if FEATURE_CAS_POLICY
            hostCtxNew == null && 
    #endif // FEATURE_CAS_POLICY 
    #if FEATURE_SYNCHRONIZATIONCONTEXT
            syncCtxNew == null && 
    #endif // #if FEATURE_SYNCHRONIZATIONCONTEXT
            (logCtxNew == null || !logCtxNew.HasInfo))
        {
            return s_dummyDefaultEC; 
        }
    
        // 
        // Allocate the new context, and fill it in.
        // 
        ExecutionContext ecNew = new ExecutionContext();
    #if FEATURE_IMPERSONATION || FEATURE_COMPRESSEDSTACK
        ecNew.SecurityContext = secCtxNew;
        if (ecNew.SecurityContext != null) 
            ecNew.SecurityContext.ExecutionContext = ecNew;
    #endif 
    #if FEATURE_CAS_POLICY 
        ecNew._hostExecutionContext = hostCtxNew;
    #endif // FEATURE_CAS_POLICY 
    #if FEATURE_SYNCHRONIZATIONCONTEXT
        ecNew._syncContext = syncCtxNew;
    #endif // #if FEATURE_SYNCHRONIZATIONCONTEXT
        ecNew.LogicalCallContext = logCtxNew; 
        ecNew.isNewCapture = true;
    
        return ecNew; 
    }
    

    Без оглядки на все эти #if FEATURE_XYZ происходит следующее:

    1. SecurityContext.Capture(ecCurrent, ref stackMark)

      Класс SecurityContext по структуре и функциям напоминает ExecutionContext, но работает лишь с безопасностью и ее политиками. Лень углубляться сюда, но ключевые аспекты - Impersonation, WindowsIdentity и CAS Security.

    2. HostExecutionContextManager.CaptureHostExecutionContext()

      Производит захват контекста приложения-хоста текущего домена.

    3. ecCurrent.SynchronizationContext.CreateCopy()

      При условии соответствующих опций, текущий SynchronizationContext потока клонируется.

    4. ecCurrent.LogicalCallContext.Clone()

      Если текущий LogicalCallContext не пустой - он также клонируется.

    5. Всяческие оптимизации и сборка всего этого в один объект.
  2. Методы IsFlowSuppressed, SuppressFlow и RestoreFlow позволяют запретить захват контекста с помощью Capture, что внятно описано Рихтером в упомянутой выше книге.
  3. Метод Run, естественно, применяет переданный контекст на текущий поток, исполняет код делегата и в конце восстанавливает контекст до исходного состояния. Применяет - фактически прописывает его в поле m_ExecutionContext класса Thread (да-да, именно там хранится вся эта байда).

Класс ThreadPool содержит еще один метод для помещения рабочей задачи в очередь - UnsafeQueueUserWorkItem. Взглянем на пару похожих методов:

[System.Security.SecuritySafeCritical]  // auto-generated 
[MethodImplAttribute(MethodImplOptions.NoInlining)] // Methods containing StackCrawlMark local var has to be marked non-inlineable
public static bool QueueUserWorkItem(
     WaitCallback           callBack,     // NOTE: we do not expose options that allow the callback to be queued as an APC
     Object                 state 
     )
{ 
    StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller; 
    return QueueUserWorkItemHelper(callBack,state,ref stackMark,true);
} 

[System.Security.SecurityCritical]  // auto-generated_required
[MethodImplAttribute(MethodImplOptions.NoInlining)] // Methods containing StackCrawlMark local var has to be marked non-inlineable 
public static bool UnsafeQueueUserWorkItem(
     WaitCallback           callBack,     // NOTE: we do not expose options that allow the callback to be queued as an APC 
     Object                 state 
     )
{ 
    StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
    return QueueUserWorkItemHelper(callBack,state,ref stackMark,false);
}

Разница в поведении - параметр compressStack, он влияет на захват и развертывание контекста. Метод UnsafeQueueUserWorkItem не оперирует контекстом - не сохраняет его как часть рабочего элемента очереди, не восстанавливает перед исполнением задачи. Также отключить перенос контекста можно выполнив ThreadPool.SuppressFlow(). Но здесь есть небольшой нюанс - оба метода отмечены как [SecurityCritical], что требует Full Trust у вызывающего метода. Безопасность здесь на высоте - код без Full Trust не может использовать пул для выполнения задач вне собственного контекста. Те же ограничения касаются других классов, например SecurityContext.

Разработчик обязан знать о наличии пар таких методов и разнице между ними. ThreadPool не так прост как кажется, затраты его обслуживания могут оказаться выше затрат исполнения элементов очереди.

Где еще используется ExecutionContext?

  • System.Threading.Thread
  • System.Threading.Timer
  • System.Windows.Forms.Control
  • System.Windows.Threading.DispatcherOperation
  • Классы семейства Task Parallel Library
  • System.IO.Stream
  • System.Net.Sockets.Socket
  • System.Data.SqlClient.SqlDataReader
  • И многие другие классы с асинхронными операциями.

У перечисления CaptureOptions есть элемент IgnoreSyncCtx. Он предотвращает захват и клонирование SynchronizationContext следующими классами:

  • System.IO.Stream
  • System.Threading.Overlapped, который используется многими классами с поддержкой Overlapped IO - Socket, MessageQueue, Pipe, PipeStream, HttpListener, HttpListenerRequest и т.д.
  • System.Threading.Tasks.*
  • System.Threading.Thread
  • System.Threading.ThreadPool
  • System.Threading.Timer
  • System.Runtime.CompilerServices.AsyncTaskMethodBuilder
  • System.Runtime.CompilerServices.AsyncVoidMethodBuilder

CaptureOptions могут использовать лишь классы самой mscorlib.dll, соответствующие методы объявлены как internal. Пользовательский код может захватить лишь весь контекст целиком.

Теперь настало время для двух важных граждан ExecutionContext - LogicalCallContext и IllogicalCallContext из пространства имен (внимание!) System.Runtime.Remoting.Messaging. Сразу напишу, что честных механизмов получить сами эти объекты нет, все спрятано как internal. Для чтения и модификации этих контейнеров нам с барского плеча подарили класс CallContext со следующими операциями (статическими):

  1. void FreeNamedDataSlot(String name) - удаляет элемент из контейнера с соответствующим именем.
  2. Object LogicalGetData(String name) - читает значение из текущего LogicalCallContext.
  3. void LogicalSetData(String name, Object data) - записывает туда значение.
  4. Object GetData(String name) - делегирует в первую очередь в LogicalGetData, а в случае неудачи читает из текущего IllogicalCallContext.
  5. void SetData(String name, Object data) - здесь немного интересней:
    [System.Security.SecurityCritical]  // auto-generated 
    public static void SetData(String name, Object data)
    { 
        if (data is ILogicalThreadAffinative) 
        {
            LogicalSetData(name, data); 
        }
        else
        {
            ExecutionContext ec = Thread.CurrentThread.GetMutableExecutionContext(); 
            ec.LogicalCallContext.FreeNamedDataSlot(name);
            ec.IllogicalCallContext.SetData(name, data); 
        } 
    }
    

    Значение проверяется на интерфейс-маркер ILogicalThreadAffinative, после вызов делегируется в LogicalCallContext (маркер присутствует) или IllogicalCallContext (маркер отсутствует).

  6. Пара методов Header[] GetHeaders() иvoid SetHeaders(Header[] headers) - оперируют заголовками типа Header, внеполосные данные, которые исполняемая среда присоединяет к запросу на вызов метода из другого контекста.

Теперь рассмотрим классы, методы и всеразличные манипуляции, связанные с этими контейнерами:

  • Пара методов BeginInvoke/EndInvoke у классов-делегатов.

    Да-да, нубасики, эти методы не так просты какими они кажутся. Во-первых, код Invoke, BeginInvoke и EndInvoke порождает JIT компилятор во время выполнения (в сборках эти методы отмечены атрибутом [MethodImpl(0, MethodCodeType=MethodCodeType.Runtime)]):

    0:000> !DumpMT -md 000007fef1c680c0
    EEClass:         000007fef15c45d8
    Module:          000007fef15c1000
    Name:            System.Action
    mdToken:         0000000002000015
    File:            C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
    BaseSize:        0x40
    ComponentSize:   0x0
    Slots in VTable: 16
    Number of IFaces in IFaceMap: 2
    --------------------------------------
    MethodDesc Table
               Entry       MethodDesc    JIT Name
    000007fef1a2c480 000007fef15ca9d0 PreJIT System.Object.ToString()
    000007fef1ab75e0 000007fef16a1898 PreJIT System.MulticastDelegate.Equals(System.Object)
    000007fef1a36c40 000007fef16a1908 PreJIT System.MulticastDelegate.GetHashCode()
    000007fef1aad790 000007fef15caa18 PreJIT System.Object.Finalize()
    000007fef1abeb70 000007fef16a7c38 PreJIT System.Delegate.DynamicInvokeImpl(System.Object[])
    000007fef1b24010 000007fef16a18e0 PreJIT System.MulticastDelegate.GetInvocationList()
    000007fef19ba2c8 000007fef16a1918 PreJIT System.MulticastDelegate.GetMethodImpl()
    000007fef1ab70d0 000007fef16a18d0 PreJIT System.MulticastDelegate.CombineImpl(System.Delegate)
    000007fef1ab7450 000007fef16a18d8 PreJIT System.MulticastDelegate.RemoveImpl(System.Delegate)
    000007fef2287eb0 000007fef16a7ce0 PreJIT System.Delegate.Clone()
    000007fef22886e0 000007fef16a1890 PreJIT System.MulticastDelegate.GetObjectData(System.Runtime.Serialization.SerializationInfo, System.Runtime.Serialization.StreamingContext)
    000007fef1a2eb30 000007fef16a1910 PreJIT System.MulticastDelegate.GetTarget()
    000007fef19f08c8 000007fef16ad2d0   NONE System.Action.Invoke()
    000007fef19f08a0 000007fef16ad2e8   NONE System.Action.BeginInvoke(System.AsyncCallback, System.Object)
    000007fef19f08b0 000007fef16ad300   NONE System.Action.EndInvoke(System.IAsyncResult)
    000007fef19f08c0 000007fef16ad2b8   NONE System.Action..ctor(System.Object, IntPtr)
    

    В результате, например, код

    Action action = () => Thread.Yield();
    var asyncResult = action.BeginInvoke(nullnull);
    action.EndInvoke(asyncResult);
    return;

    приводит к следующему стеку вызовов:

    (MethodDesc 000007fef15cff58 +0x21 System.Threading.ThreadPool.QueueUserWorkItem(System.Threading.WaitCallback, System.Object)), calling (MethodDesc 000007fef15d01c0 +0 System.Threading.ThreadPool.QueueUserWorkItemHelper(System.Threading.WaitCallback, System.Object, System.Threading.StackCrawlMark ByRef, Boolean))
    (MethodDesc 000007fef1796e78 +0x305 System.Runtime.Remoting.Proxies.RemotingProxy.Invoke(System.Object, System.Runtime.Remoting.Proxies.MessageData ByRef)), calling (MethodDesc 000007fef15cff58 +0 System.Threading.ThreadPool.QueueUserWorkItem(System.Threading.WaitCallback, System.Object))
    clr!CTPMethodTable__CallTargetHelper3+0x12
    clr!MemberLoader::FM_GetStrCompFunc+0x8b, calling clr!CTPMethodTable__CallTargetHelper3
    clr!MethodDesc::IsCtor+0x12, calling clr!MethodDesc::GetAttrs
    clr!CTPMethodTable::OnCall+0x1ce, calling clr!MemberLoader::FM_GetStrCompFunc+0x1c
    clr!CTPMethodTable::PreCall+0x6a, calling clr!TPMethodFrame::GetSlotNumber
    clr!TransparentProxyStub_CrossContextPatchLabel+0xa, calling clr!CTPMethodTable::OnCall
    clr!ThePreStub+0x5a, calling clr!PreStubWorker
    (MethodDesc 000007fe93493968 +0x75 ConsoleApplication.Program1.Main(System.String[])), calling 000007fef19f08a0 (stub for System.Action.BeginInvoke(System.AsyncCallback, System.Object))
    

    Или в упрощенной форме:

    System.Threading.ThreadPool.QueueUserWorkItem(System.Threading.WaitCallback, System.Object)
    System.Runtime.Remoting.Proxies.RemotingProxy.Invoke(System.Object, System.Runtime.Remoting.Proxies.MessageData ByRef)
    [TPMethodFrame: 00000000005febb8] System.Action.BeginInvoke(System.AsyncCallback, System.Object)
    ConsoleApplication.Program1.Main(System.String[]) [d:\Development\Projects\CLRExecutionContext\ConsoleApplication\Program.cs @ 52]
    

    Обратите внимание на обилие всяких ТэПэ в стеках. Открыв файл remotingproxy.cs, можно обнаружить следующий метод:

    // Invoke for case where call is in the same context as the server object
    // (This special static method is used for AsyncDelegate-s ... this is called 
    // directly from the EE) 
    private static void Invoke(Object NotUsed, ref MessageData msgData)
    

    И такой незамысловатый его код:

    case Message.BeginAsync:
    case Message.BeginAsync | Message.OneWay: 
        // pick up call context from the thread 
        m.Properties[Message.CallContextKey] =
            Thread.CurrentThread.GetMutableExecutionContext().LogicalCallContext.Clone();
        ar = new AsyncResult(m);
        AgileAsyncWorkerItem  workItem =
            new AgileAsyncWorkerItem(
                    m, 
                    ((callType & Message.OneWay) != 0) ?
                        null : ar, d.Target); 
    
        ThreadPool.QueueUserWorkItem(
            new WaitCallback( 
                    AgileAsyncWorkerItem.ThreadPoolCallBack),
            workItem);
    
        if ((callType & Message.OneWay) != 0) 
        {
            ar.SyncProcessMessage(null); 
        } 
        m.PropagateOutParameters(null, ar);
        break; 
    case (Message.EndAsync | Message.OneWay):
        return;
    
    case Message.EndAsync: 
        // This will also merge back the call context
        // onto the thread that called EndAsync 
        RealProxy.EndInvokeHelper(m, false);
        break;
    

    AgileAsyncWorkerItem находится в том же файле:

    internal class AgileAsyncWorkerItem 
    {
        private IMethodCallMessage _message; 
        private AsyncResult        _ar;
        private Object             _target;
    
        [System.Security.SecurityCritical]  // auto-generated 
        public AgileAsyncWorkerItem(IMethodCallMessage message, AsyncResult ar, Object target)
        { 
            _message = new MethodCall(message); 
            _ar = ar;
            _target = target; 
        }
    
        [System.Security.SecurityCritical]  // auto-generated
        public static void ThreadPoolCallBack(Object o) 
        {
            ((AgileAsyncWorkerItem) o).DoAsyncCall(); 
        } 
    
    
        [System.Security.SecurityCritical]  // auto-generated
        public void DoAsyncCall()
        {
            (new StackBuilderSink(_target)).AsyncProcessMessage(_message, _ar); 
        }
    }
    

    Как видите, для выполнения вызова используется класс StackBuilderSink, метод AsyncProcessMessage, который применяет LogicalCallContext на поток выполнения:

    LogicalCallContext callCtx =  (LogicalCallContext) 
        mcMsg.Properties[Message.CallContextKey];
    
    ...
    
    // install call context onto the thread, holding onto 
    // the one that is currently on the thread 
    
    oldCallCtx = CallContext.SetLogicalCallContext(callCtx); 
    isCallContextSet = true;
    

    Обратите внимение как метод void Invoke(Object NotUsed, ref MessageData msgData) возвращает экземпляр IAsyncResult с помощью m.PropagateOutParameters(null, ar); он объявлен в файле RealProxy.cs:

    [MethodImplAttribute(MethodImplOptions.InternalCall)]
    public extern void PropagateOutParameters(Object[] OutArgs, Object retVal); 
    

    EndInvoke приводит к вызову RealProxy.EndInvokeHelper(m, false), который выполняет слияние текущего контекста и удаленного:

    // Merge the call context back into the thread that 
    // called EndInvoke
    Thread.CurrentThread.GetMutableExecutionContext().LogicalCallContext.Merge( 
        mrm.LogicalCallContext); 
    

    Из-за всего этого производительность предложенной APM модели для делегатов может, мягко говоря, не порадовать любителей многопоточного программирования. Уверен, польза от LogicalCallContext в одном случае из миллиона, а страдают все. Браво, Microsoft! Особенно порадовала эта бредятина в стеке вместе с PropagateOutParameters.

  • Семейство классов пространства ммен System.Runtime.Remoting (в котором и объявлен LogicalCallContext), естественно, используют его в любых удаленных операциях, поддерживаемых подсистемой Remoting. Вспоминаем ContextBoundObject, __TransparentProxy, всевозможные XyzSink и пр.

На этом пока все. До новых встреч, мой любимый бред bread!

Focus and Windows activation (part 1)

Недавно я плотно занимался созданием выпадающего окна, в котором есть кнопки, поле ввода, а также элемент управления типа "список". В одном случае окно работало независимо, как меню, в другом - привязывалось к полю ввода на родительской форме (собственное поле ввода не отображалось), текст которого служил фильтром для элементов списка. Создание выпадающих окон - очень сложная задача, много нюансов связанных с активацией окон, фокусом ввода, определением ситуаций когда окно необходимо закрыть и т.д. Эта статья - попытка доходчиво объяснить на примерах как работает активация окон и как функционирует фокус ввода.

Любой UI поток имеет состояние. Это состояние хранится где-то в недрах ОС, но доступно из пользовательского режима при помощи следующих функций:

  • GetGUIThreadInfo

    Это, пожалуй, самая-самая функция, которая демонстрирует составляющие UI состояния потока. Возвращаемая структура GUITHREADINFO хранит следующие значения:

     typedef struct tagGUITHREADINFO {
      DWORD cbSize;
      DWORD flags;
      HWND  hwndActive;
      HWND  hwndFocus;
      HWND  hwndCapture;
      HWND  hwndMenuOwner;
      HWND  hwndMoveSize;
      HWND  hwndCaret;
      RECT  rcCaret;
    } GUITHREADINFO, *PGUITHREADINFO;
    
    1. flags

      Флажки состояний потока - мигающая каретка, меню, окно в процессе изменения размера или положения и т.п.

    2. hwndActive

      Дескриптор активного окна потока.

    3. hwndFocus

      Дескриптор окна потока, которое имеет логический фокус.

    4. hwndCapture

      Дескриптор окна потока, которое захватило мышь вызовом SetCapture.

    5. hwndMenuOwner

      Дескриптор окна потока, которое владеет текущим меню.

    6. hwndMoveSize

      Дескриптор окна потока, которое в процессе изменения размера или положения.

    7. hwndCaret

      Дескриптор окна потока, которое отображает каретку.

    8. rcCaret

      Прямоугольная область каретки.

  • GetActiveWindow

    Возвращает значение hwndActive структуры GUITHREADINFO текущего потока.

  • GetFocus

    Возвращает значение hwndFocus структуры GUITHREADINFO текущего потока.

  • GetCapture

    Возвращает значение hwndCapture структуры GUITHREADINFO текущего потока.

В системе может быть активно множество потоков, каждый из которых, являясь UI потоком (IsGUIThread возвращает истину, а также позволяет явно активировать поток в качестве UI потока; эта активация выполняется автоматически в момент первого использования функций для работы с окнами), имеет вышеописанное состояние. Но пользовательский ввод с клавиатуры попадает лишь в определенное окно, дескриптор которого совпадает с hwndFocus одного из потоков. Почему так происходит? Потому, что существует глобальное состояние рабочего стола ОС, а в него входят так называемые Foreground Thread и Foreground Window. Foreground Window - это окно верхнего уровня, которое содержит (или же само таковым является) элемент управления с физическим фокусом. Логический фокус - это состояние потока, физический фокус - состояние рабочего стола и привязка клавиатуры к потоку и окну. Foreground Thread владеет окном с физическим фокусом, в этот поток будут попадать клавиатурные сообщения. Foreground Thread не обязательно владеет Foreground Window, поскольку элемент управления может быть создать в другом потоке и размещен на окне верхнего уровня. Функция GetForegroundWindow позволяет узнать какое окно сейчас активно с точки зрения пользовательского ввода.

Стоит заметить, что Active Window и Foreground Window - это top-level окна, без стиля WS_CHILD, а Focus Window может быть как top-level окном, так и любым его дочерним окном.

Тестовое приложение.

Тестовое приложение состоит из нескольких оконных классов - CTopLevelWnd, CTracingButton и CTracingEdit. Обработчики интересных нам сообщений содержат трассировочный код, который отправляет полезную информацию в лог. Текст окон содержит идентификатор оконного потока, а за ним - дескриптор окна.

Трассировочная информация содержит следующие элементы:

  1. --> отмечает точку входа в обработчик, <-- - выхода; <-> идентифицирует операцию;
  2. далее следует идентификатор текущего потока;
  3. после имя класса (если мы попали в обработчик оконного сообщения);
  4. в скобках указывается дескриптор окна (если это окно либо элемент управления);
  5. потом имя функции или метода с параметрами;
  6. завершает это все статус рабочего стола/потока - Foreground Window (FW), Active Window (AW) и Focused Window (F).

На все потоки установлены хуки WH_GETMESSAGE и WH_CBT, трассирующие интересные события.

Цикл сообщений - свой. Каждому вызову GetMessage предшествует PeekMessage с флагом PM_NOREMOVE. Это сделано для того, чтобы мы видели сообщения, которые вытягивает сам поток, а также сообщения, которые вынимает оттуда ОС без нашего ведома.

Сценарий 1 - активация\деактивация окна приложения.

Scenario 1 Рассмотрим следующий сценарий: окно приложения неактивно, окно Notepad активно. Пользователь производит щелчок мышью по текстовому полю ввода окна приложения. Окно активируется, заголовок меняет свой внешний вид, фокус ввода получает поле ввода. Далее щелчок по окну Notepad. Окно деактивируется. Трассировочный лог происходящего приведен ниже, вместе с детальным разбором всех событий.

<-> 1AC4: GetMsgProc(wParam = PM_NOREMOVE, lParam = { message = WM_LBUTTONDOWN } { FW = 0, AW = 0, F = 0 }
--> 1AC4: CTracingEdit(1709F4)::OnMouseActivate(pDesktopWnd = B0B70, nHitTest = 1, message = 513) { FW = 0, AW = 0, F = 0 }
   --> 1AC4: CTopLevelWnd(B0B70)::OnMouseActivate(pDesktopWnd = B0B70, nHitTest = 1, message = 513) { FW = 0, AW = 0, F = 0 }
   <-- 1AC4: CTopLevelWnd(B0B70)::OnMouseActivate(pDesktopWnd = B0B70, nHitTest = 1, message = 513) { FW = 0, AW = 0, F = 0 }
<-- 1AC4: CTopLevelWnd(1709F4)::OnMouseActivate(pDesktopWnd = B0B70, nHitTest = 1, message = 513) { FW = 0, AW = 0, F = 0 }
<-> 1AC4: CBTProc(HCBT_ACTIVATE, hWnd = B0B70, AS = { hWndActive = 0, fMouse = 1 }) { FW = 0, AW = 0, F = 0 }
--> 1AC4: CTopLevelWnd(B0B70)::OnActivateApp(bActive = 1, dwThreadID = 0) { FW = B0B70, AW = B0B70, F = 0 }
<-- 1AC4: CTopLevelWnd(B0B70)::OnActivateApp(bActive = 1, dwThreadID = 0) { FW = B0B70, AW = B0B70, F = 0 }
--> 1AC4: CTopLevelWnd(B0B70)::OnNcActivate(bActive = 1) { FW = B0B70, AW = B0B70, F = 0 }
<-- 1AC4: CTopLevelWnd(B0B70)::OnNcActivate(bActive = 1) { FW = B0B70, AW = B0B70, F = 0 }
--> 1AC4: CTopLevelWnd(B0B70)::OnActivate(nState = 2, pWndOther = 0, bMinimized = 0) { FW = B0B70, AW = B0B70, F = 0 }
   <-> 1AC4: CBTProc(HCBT_SETFOCUS, hSetFocusWnd = B0B70, hKillFocusWnd = 0) { FW = B0B70, AW = B0B70, F = 0 }
   --> 1AC4: CTopLevelWnd(B0B70)::OnSetFocus(pOldWnd = 0) { FW = B0B70, AW = B0B70, F = B0B70 }
   <-- 1AC4: CTopLevelWnd(B0B70)::OnSetFocus(pOldWnd = 0) { FW = B0B70, AW = B0B70, F = B0B70 }
<-- 1AC4: CTopLevelWnd(B0B70)::OnActivate(nState = 2, pWndOther = 0, bMinimized = 0) { FW = B0B70, AW = B0B70, F = B0B70 }
<-> 1AC4: GetMsgProc(wParam = PM_REMOVE, lParam = { message = WM_LBUTTONDOWN } { FW = B0B70, AW = B0B70, F = B0B70 }
--> 1AC4: CTracingEdit(1709F4)::OnLButtonDown(nFlags = 1, point = {114, 10}) { FW = B0B70, AW = B0B70, F = B0B70 }
   <-> 1AC4: CBTProc(HCBT_SETFOCUS, hSetFocusWnd = 1709F4, hKillFocusWnd = B0B70) { FW = B0B70, AW = B0B70, F = B0B70 }
   --> 1AC4: CTopLevelWnd(B0B70)::OnKillFocus(pNewWnd = 1709F4) { FW = B0B70, AW = B0B70, F = 1709F4 }
   <-- 1AC4: CTopLevelWnd(B0B70)::OnKillFocus(pNewWnd = 1709F4) { FW = B0B70, AW = B0B70, F = 1709F4 }
   --> 1AC4: CTracingEdit(1709F4)::OnSetFocus(pOldWnd = B0B70) { FW = B0B70, AW = B0B70, F = 1709F4 }
   <-- 1AC4: CTracingEdit(1709F4)::OnSetFocus(pOldWnd = B0B70) { FW = B0B70, AW = B0B70, F = 1709F4 }
<-- 1AC4: CTracingEdit(1709F4)::OnLButtonDown(nFlags = 1, point = {114, 10}) { FW = B0B70, AW = B0B70, F = 1709F4 }
<-> 1AC4: GetMsgProc(wParam = PM_NOREMOVE, lParam = { message = WM_LBUTTONUP } { FW = B0B70, AW = B0B70, F = 1709F4 }
<-> 1AC4: GetMsgProc(wParam = PM_REMOVE, lParam = { message = WM_LBUTTONUP } { FW = B0B70, AW = B0B70, F = 1709F4 }
--> 1AC4: CTracingEdit(1709F4)::OnLButtonUp(nFlags = 0, point = {114, 10}) { FW = B0B70, AW = B0B70, F = 1709F4 }
<-- 1AC4: CTracingEdit(1709F4)::OnLButtonUp(nFlags = 0, point = {114, 10}) { FW = B0B70, AW = B0B70, F = 1709F4 }

--> 1AC4: CTopLevelWnd(B0B70)::OnNcActivate(bActive = 0) { FW = 0, AW = B0B70, F = 1709F4 }
<-- 1AC4: CTopLevelWnd(B0B70)::OnNcActivate(bActive = 0) { FW = 20830, AW = B0B70, F = 1709F4 }
--> 1AC4: CTopLevelWnd(B0B70)::OnActivate(nState = 0, pWndOther = 0, bMinimized = 0) { FW = 20830, AW = B0B70, F = 1709F4 }
<-- 1AC4: CTopLevelWnd(B0B70)::OnActivate(nState = 0, pWndOther = 0, bMinimized = 0) { FW = 20830, AW = B0B70, F = 1709F4 }
--> 1AC4: CTopLevelWnd(B0B70)::OnActivateApp(bActive = 0, dwThreadID = 370) { FW = 20830, AW = 0, F = 1709F4 }
<-- 1AC4: CTopLevelWnd(B0B70)::OnActivateApp(bActive = 0, dwThreadID = 370) { FW = 20830, AW = 0, F = 1709F4 }
--> 1AC4: CTracingEdit(1709F4)::OnKillFocus(pNewWnd = 0) { FW = 20830, AW = 0, F = 0 }
<-- 1AC4: CTracingEdit(1709F4)::OnKillFocus(pNewWnd = 0) { FW = 20830, AW = 0, F = 0 }


  1. <-> 1AC4: GetMsgProc(wParam = PM_NOREMOVE, lParam = { message = WM_LBUTTONDOWN } { FW = 0, AW = 0, F = 0 }

    В очереди появилось сообщение WM_LBUTTONDOWN. Но ОС пока еще ничего не делает. Действия ниже ОС выполняет лишь когда сообщение изымается из очереди вызовом GetMessage или PeekMessage с флагом PM_REMOVE (а именно эти вызовы будут выше по стеку нежели выполняющийся далее код).


  2. --> 1AC4: CTracingEdit(1709F4)::OnMouseActivate(pDesktopWnd = B0B70, nHitTest = 1, message = 513) { FW = 0, AW = 0, F = 0 }
       --> 1AC4: CTopLevelWnd(B0B70)::OnMouseActivate(pDesktopWnd = B0B70, nHitTest = 1, message = 513) { FW = 0, AW = 0, F = 0 }
       <-- 1AC4: CTopLevelWnd(B0B70)::OnMouseActivate(pDesktopWnd = B0B70, nHitTest = 1, message = 513) { FW = 0, AW = 0, F = 0 }
    <-- 1AC4: CTopLevelWnd(1709F4)::OnMouseActivate(pDesktopWnd = B0B70, nHitTest = 1, message = 513) { FW = 0, AW = 0, F = 0 }

    Вызван обработчик сообщения WM_MOUSEACTIVATE класса CTracingEdit. ОС посылает это сообщение окну, которое находилось под курсором мыши во время щелчка. Код возврата сообщает ОС что следует делать далее - активировать окно или нет, пропускать сообщение о нажатии или же удалить его из очереди. Обработчик по умолчанию посылает это же сообщение родительскому окну. Обработкик корневых окон возвращает MA_ACTIVATE, что приводит к активации окна и получению сообщений о щелчке.

    При такой схеме элемент управления может определить реакцию окна.

    Стоит заметить, что сообщение синхронное и его обработчик вызывается из кода режима ядра, управление которому передается вызовом функции GetMessage из кода приложения. Если быть совсем точным, то процедура активации в даном сценарии происходит за один вызов GetMessage, которая в результате вернет WM_LBUTTONDOWN (см. далее).

    >	Win32App.exe!CTracingEdit::OnMouseActivate(CWnd * pDesktopWnd, unsigned int nHitTest, unsigned int message)  Line 115	C++
     	Win32App.exe!CWnd::OnWndMsg(unsigned int message, unsigned int wParam, long lParam, long * pResult)  Line 2375 + 0x2f bytes	C++
     	Win32App.exe!CWnd::WindowProc(unsigned int message, unsigned int wParam, long lParam)  Line 2087 + 0x20 bytes	C++
     	Win32App.exe!AfxCallWndProc(CWnd * pWnd, HWND__ * hWnd, unsigned int nMsg, unsigned int wParam, long lParam)  Line 257 + 0x1c bytes	C++
     	Win32App.exe!AfxWndProc(HWND__ * hWnd, unsigned int nMsg, unsigned int wParam, long lParam)  Line 420	C++
     	user32.dll!_InternalCallWinProc@20()  + 0x23 bytes	
     	user32.dll!_UserCallWinProcCheckWow@32()  + 0xb3 bytes	
     	user32.dll!_DispatchClientMessage@20()  + 0x4b bytes	
     	user32.dll!___fnDWORD@4()  + 0x24 bytes	
     	ntdll.dll!_KiUserCallbackDispatcher@12()  + 0x2e bytes	
     	user32.dll!_DispatchClientMessage@20() 	
     	user32.dll!_NtUserGetMessage@16()  + 0xc bytes	
     	user32.dll!_GetMessageW@16()  + 0x2b bytes	
     	Win32App.exe!AfxInternalPumpMessage()  Line 153 + 0x13 bytes	C++
     	Win32App.exe!AfxPumpMessage()  Line 193	C++
     	Win32App.exe!wWinMain(HINSTANCE__ * hInstance, HINSTANCE__ * hPrevInstance, wchar_t * lpCmdLine, int nCmdShow)  Line 135 + 0x5 bytes	C++
    

  3. <-> 1AC4: CBTProc(HCBT_ACTIVATE, hWnd = B0B70, AS = { hWndActive = 0, fMouse = 1 }) { FW = 0, AW = 0, F = 0 }

    ОС сообщает приложению, что окно B0B70 сейчас будет активировано. Обратите внимание, что Foreground Window (FW), Active Window (AW) и Focused Window (F) - не определены (равны нулю).


  4. --> 1AC4: CTopLevelWnd(B0B70)::OnActivateApp(bActive = 1, dwThreadID = 0) { FW = B0B70, AW = B0B70, F = 0 }
    <-- 1AC4: CTopLevelWnd(B0B70)::OnActivateApp(bActive = 1, dwThreadID = 0) { FW = B0B70, AW = B0B70, F = 0 }

    Обработчик сообщения WM_ACTIVATEAPP. Посылается всем окнам верхнего уровня, которые принадлежат потоку активируемого окна, тоесть 1AC4. Окно в этом сценарии всего одно. Foreground Window (FW) и Active Window (AW) уже установлены.


  5. --> 1AC4: CTopLevelWnd(B0B70)::OnNcActivate(bActive = 1) { FW = B0B70, AW = B0B70, F = 0 }
    <-- 1AC4: CTopLevelWnd(B0B70)::OnNcActivate(bActive = 1) { FW = B0B70, AW = B0B70, F = 0 }

    Обработчик сообщения WM_NCACTIVATE. Код возврата в этом случае игнорируется.


  6. --> 1AC4: CTopLevelWnd(B0B70)::OnActivate(nState = 2, pWndOther = 0, bMinimized = 0) { FW = B0B70, AW = B0B70, F = 0 }

    Обработчик сообщения WM_ACTIVATE. По умолчанию вызывает SetFocus(self), устанавливает фокус на активируемое окно.


  7. <-> 1AC4: CBTProc(HCBT_SETFOCUS, hSetFocusWnd = B0B70, hKillFocusWnd = 0) { FW = B0B70, AW = B0B70, F = 0 }

    ОС сообщает приложению, что фокус ввода собирается переместиться из ниоткуда (hKillFocusWnd = 0) в окно B0B70 (это CTopLevelWnd). Focused Window (F) пока что неопределен.


  8. --> 1AC4: CTopLevelWnd(B0B70)::OnSetFocus(pOldWnd = 0) { FW = B0B70, AW = B0B70, F = B0B70 }
    <-- 1AC4: CTopLevelWnd(B0B70)::OnSetFocus(pOldWnd = 0) { FW = B0B70, AW = B0B70, F = B0B70 }

    Уведомление о попадании фокуса в окно.


  9. <-- 1AC4: CTopLevelWnd(B0B70)::OnActivate(nState = 2, pWndOther = 0, bMinimized = 0) { FW = B0B70, AW = B0B70, F = B0B70 }

    Выход из обработчика WM_ACTIVATE.


  10. <-> 1AC4: GetMsgProc(wParam = PM_REMOVE, lParam = { message = WM_LBUTTONDOWN } { FW = B0B70, AW = B0B70, F = B0B70 }

    Здесь ОС сделала все, что хотела, поэтому собирается вернуть управление в приложение.


  11. --> 1AC4: CTracingEdit(1709F4)::OnLButtonDown(nFlags = 1, point = {116, 13}) { FW = B0B70, AW = B0B70, F = B0B70 }

    Нажатие мыши наконец дошло до элемента управления.


  12. <-> 1AC4: CBTProc(HCBT_SETFOCUS, hSetFocusWnd = 1709F4, hKillFocusWnd = B0B70) { FW = B0B70, AW = B0B70, F = B0B70 }

    Эдемент управления реагирует на сообщение изменением фокуса. Поле ввода забирает фокус на себя, о чем говорят следующие строки.


  13. --> 1AC4: CTopLevelWnd(B0B70)::OnKillFocus(pNewWnd = 1709F4) { FW = B0B70, AW = B0B70, F = 1709F4 }
    <-- 1AC4: CTopLevelWnd(B0B70)::OnKillFocus(pNewWnd = 1709F4) { FW = B0B70, AW = B0B70, F = 1709F4 }
    --> 1AC4: CTracingEdit(1709F4)::OnSetFocus(pOldWnd = B0B70) { FW = B0B70, AW = B0B70, F = 1709F4 }
    <-- 1AC4: CTracingEdit(1709F4)::OnSetFocus(pOldWnd = B0B70) { FW = B0B70, AW = B0B70, F = 1709F4 }

  14. <-- 1AC4: CTracingEdit(1709F4)::OnLButtonDown(nFlags = 1, point = {116, 13}) { FW = B0B70, AW = B0B70, F = 1709F4 }

    Здесь мы наконец возвращаемся к циклу сообщений.


  15. <-> 1AC4: GetMsgProc(wParam = PM_NOREMOVE, lParam = { message = WM_LBUTTONUP } { FW = B0B70, AW = B0B70, F = 1709F4 }
    <-> 1AC4: GetMsgProc(wParam = PM_REMOVE, lParam = { message = WM_LBUTTONUP } { FW = B0B70, AW = B0B70, F = 1709F4 }

    И получаем из очереди WM_LBUTTONUP.


  16. --> 1AC4: CTracingEdit(1709F4)::OnLButtonUp(nFlags = 0, point = {116, 13}) { FW = B0B70, AW = B0B70, F = 1709F4 }
    <-- 1AC4: CTracingEdit(1709F4)::OnLButtonUp(nFlags = 0, point = {116, 13}) { FW = B0B70, AW = B0B70, F = 1709F4 }

    Обработчик которого не делает ничего полезного.

Какие выводы можно сделать из вышенаписанного?

  1. Элементы управления могут управлять активацией корневых окон.
  2. Код ОС, активирующий окна и устанавливающий фокус ввода, выполняется после попадания асинхронного сообщения в очередь и до изымания этого сообщения из очереди приложением (если окно активируется мышью). В противном случае - просто ниже по стеку от вызовов GetMessage или PeekMessage с флагом PM_REMOVE.
  3. Если окно активируется не мышью, а другим способом (Alt-TAB, например), - происходит все то же, за исключением обработчиков WM_MOUSEACTIVATE, WM_LBUTTONDOWN + WM_LBUTTONUP (GetMessage не изымает асинхронные сообщения из очереди, а все процедуры активации происходят без выхода из GetMessage).
  4. ОС не восстанавливает фокус ввода. Если окно было активно и фокус ввода находился на кнопке - то при деактивации и последующей активации он автоматически не вернется обратно на кнопку. За этим нужно сделить самостоятельно. Или воспользоваться оконными процедурами диалоговых окон, которые выполняют эти действия за нас.
  5. Лучшее место для восстановления фокуса - обработчик WM_ACTIVATE. Если при выходе из него фокус все еще неопределен - ОС устанавливает его на активируемое окно, так же, как делает это обработчик WM_ACTIVATE по умолчанию.
  6. При щелчке на элементы управления (кнопки, списки, поля воода) ОС автоматически не устанавливает фокус ввода в целевое окно. Этим занимаются сами элементы управления, обрабатывая WM_?BUTTONDOWN.

При деактивации окна вызываются обработчики сообщений WM_NCACTIVATE, WM_ACTIVATE, WM_ACTIVATEAPP, WM_KILLFOCUS. Обратите внимание как изменяется состояние рабочего стола и потока во время вызова обработчиков по умолчанию. Например на момент входа в CTopLevelWnd::OnNcActivate Foreground Window неопределен (равен нулю), а после вызова обработчика по умолчанию - он уже равен дескриптору окна Notepad. Поскольку пачка всех этих сообщений обрабатывается синхронно - то они посылаются последовательно активирующимся окнам и деактивирующимся, даже если окна принадлежат разным потокам или даже процессам.

В следующей части мы рассмотрим этот сценарий более детально - посмотрим как перемешиваются сообщения активирущегося окна и деактивирующегося. И как можно управлять активацией, вместе с ломанием стереотипа о том, что на рабочем столе может быть лишь одно активированное окно, а также единственное окно с логическим фокусом ввода.

How to prevent MFC dialog closing on Enter and Escape keys

In this article I explain how to prevent an MFC dialog from handling the Enter and Escape keys and not pass it on. With all required details for you to completely understand why does it work the way it does. There are several possible solutions, so you can choose the most suitable one.

The problem is the following: you have a dialog, no matter - top level window or just a child container; you press Escape or Enter keys - the dialog disappears, even if there are no OK and Cancel buttons on the dialog. It's very funny seeing a child dialog drops out it's parent.

Let's see why this happens:

OrderXMLReaderShell.exe!CFilterEditorDlgBase::OnCancel()  Line 61 C++
  mfc100ud.dll!_AfxDispatchCmdMsg(CCmdTarget * pTarget, unsigned int nID, int nCode, void (void)* pfn, void * pExtra, unsigned int nSig, AFX_CMDHANDLERINFO * pHandlerInfo)  Line 82 C++
  mfc100ud.dll!CCmdTarget::OnCmdMsg(unsigned int nID, int nCode, void * pExtra, AFX_CMDHANDLERINFO * pHandlerInfo)  Line 381 + 0x27 bytes C++
  mfc100ud.dll!CDialog::OnCmdMsg(unsigned int nID, int nCode, void * pExtra, AFX_CMDHANDLERINFO * pHandlerInfo)  Line 87 + 0x18 bytes C++
  mfc100ud.dll!CWnd::OnCommand(unsigned int wParam, long lParam)  Line 2729 C++
  mfc100ud.dll!CWnd::OnWndMsg(unsigned int message, unsigned int wParam, long lParam, long * pResult)  Line 2101 + 0x1e bytes C++
  mfc100ud.dll!CWnd::WindowProc(unsigned int message, unsigned int wParam, long lParam)  Line 2087 + 0x20 bytes C++
  mfc100ud.dll!AfxCallWndProc(CWnd * pWnd, HWND__ * hWnd, unsigned int nMsg, unsigned int wParam, long lParam)  Line 257 + 0x1c bytes C++
  mfc100ud.dll!AfxWndProc(HWND__ * hWnd, unsigned int nMsg, unsigned int wParam, long lParam)  Line 420 C++
  mfc100ud.dll!AfxWndProcBase(HWND__ * hWnd, unsigned int nMsg, unsigned int wParam, long lParam)  Line 420 + 0x15 bytes C++
  user32.dll!_InternalCallWinProc@20()  + 0x23 bytes 
  user32.dll!_UserCallWinProcCheckWow@32()  + 0xb3 bytes 
  user32.dll!_SendMessageWorker@20()  + 0xee bytes 
  user32.dll!_SendMessageW@16()  + 0x49 bytes 
  user32.dll!_IsDialogMessageW@8()  + 0xef46 bytes 
> mfc100ud.dll!CWnd::IsDialogMessageW(tagMSG * lpMsg)  Line 198 C++
  mfc100ud.dll!CWnd::PreTranslateInput(tagMSG * lpMsg)  Line 4713 C++
  mfc100ud.dll!CDialog::PreTranslateMessage(tagMSG * pMsg)  Line 82 C++
  OrderXMLReaderShell.exe!CFindDlg::PreTranslateMessage(tagMSG * pMsg)  Line 56 C++
  mfc100ud.dll!CWnd::WalkPreTranslateTree(HWND__ * hWndStop, tagMSG * pMsg)  Line 3311 + 0x14 bytes C++
  mfc100ud.dll!AfxInternalPreTranslateMessage(tagMSG * pMsg)  Line 233 + 0x12 bytes C++
  mfc100ud.dll!CWinThread::PreTranslateMessage(tagMSG * pMsg)  Line 777 + 0x9 bytes C++
  mfc100ud.dll!AfxPreTranslateMessage(tagMSG * pMsg)  Line 252 + 0x11 bytes C++
  mfc100ud.dll!AfxInternalPumpMessage()  Line 178 + 0x18 bytes C++
  mfc100ud.dll!CWinThread::PumpMessage()  Line 900 C++
  mfc100ud.dll!CWinThread::Run()  Line 629 + 0xd bytes C++
  mfc100ud.dll!CWinApp::Run()  Line 832 C++
  mfc100ud.dll!AfxWinMain(HINSTANCE__ * hInstance, HINSTANCE__ * hPrevInstance, wchar_t * lpCmdLine, int nCmdShow)  Line 47 + 0xd bytes C++
  OrderXMLReaderShell.exe!wWinMain(HINSTANCE__ * hInstance, HINSTANCE__ * hPrevInstance, wchar_t * lpCmdLine, int nCmdShow)  Line 26 C++
  OrderXMLReaderShell.exe!__tmainCRTStartup()  Line 547 + 0x2c bytes C
  OrderXMLReaderShell.exe!wWinMainCRTStartup()  Line 371 C
  kernel32.dll!@BaseThreadInitThunk@12()  + 0x12 bytes 
  ntdll.dll!___RtlUserThreadStart@8()  + 0x27 bytes 
  ntdll.dll!__RtlUserThreadStart@8()  + 0x1b bytes 

The execution flow got into CDialog::OnCancel virtual method. Here is the default MFC's implementation:

void CDialog::OnCancel()
{
 EndDialog(IDCANCEL);
}

It simply ends the dialog, thus destroying the window.

Lets find out how do we get there. The control flow is the following:

  1. Escape key is down
  2. WM_KEYDOWN message is queued into message queue
  3. AfxInternalPumpMessage gets the message from the queue and invokes AfxPreTranslateMessage
  4. CWnd::IsDialogMessage is reached
  5. IsDialogMessage WinAPI function is called by CWnd::IsDialogMessage; this function is the core of dialog management - it does things like keyboard navigation.
  6. There is a user32.dll!_SendMessageW@16() call from IsDialogMessageW
  7. Which brings us to CWnd::OnCommand with wParam equals to IDCANCEL
  8. End finally to CDialog::OnCancel

IsDialogMessageW examines the message and invokes SendMessage(hwndDlg, WM_COMMAND, MAKELONG(IDCANCEL, BN_CLICKED), ...);.

Short summary - MFC calls IsDialogMessage API function, which in turns emulates IDCANCEL button click, which is being handled by MFC eventually destroying the dialog.

It is not obvious why IsDialogMessageW emulates IDOK and IDCANCEL button clicks. I believe this is done to make developer's life easier. Imagine you have no control over the message loop, can't do any message pre translation. You'll have to handle Enter and Escape keys for every control on the dialog. That's pain.

What can we do to solve the problem? You can't change OS behavior for sure, but can do the following:

  1. Override CDialog::OnOK/OnCancel and do nothing

    The following applies:

    • Dialog is not destroyed on Enter or Escape
    • Focused inline controls don't receive WM_KEYDOWN for VK_ENTER and VK_ESCAPE codes
    • OK/Cancel button clicks have no effect
  2. Override CDialog::PreTranslateMessage and don't call the base if pMsg->message == WM_KEYDOWN && (pMsg->wParam == VK_ESCAPE || pMsg->wParam == VK_RETURN)

    If CDialog::PreTranslateMessage returns TRUE the following applies:

    • Dialog is not destroyed on Enter or Escape
    • Focused inline controls don't receive WM_KEYDOWN for VK_RETURN and VK_ESCAPE codes, unless you do dispatch them before CDialog::PreTranslateMessage returns
    • OK/Cancel button clicks have desired effect

    If CDialog::PreTranslateMessage returns FALSE the following applies:

    • MFC calls parent's PreTranslateMessage (which brings us back if the parent is a CDialog instance)
  3. Override CWinThread::PreTranslateMessage, examine the message and don't call the base

    The following applies:

    • CWnd::WalkPreTranslateTree is not invoked, thus disabling message pre translation along with IsDialogMessage call (specific messages only, it's not as scary as it seems, but hits the performance and hard to implement properly)

      If CWinThread::PreTranslateMessage returns TRUE the following applies:

      • Message is not dispatched
      • Focused inline controls don't receive WM_KEYDOWN for VK_RETURN and VK_ESCAPE codes

      If CWinThread::PreTranslateMessage returns FALSE the following applies:

      • Message is dispatched
      • Focused inline controls receive WM_KEYDOWN for VK_RETURN and VK_ESCAPE codes
    • The behavior is application wide

The very best option would be to override CWnd::IsDialogMessage and return in case of VK_RETURN and VK_ESCAPE, but this is impossible since CWnd::IsDialogMessage is not virtual. CWnd::IsDialogMessage is an API function wrapper, the closest item to modify the behavior.

So which method is the best? Each does the trick, but... Option 3 has serious drawback. Option 1 and 2 are class specific. I vote for option 2. You could create some base CDialog class with the trick and use it for all your dialogs.

Copyright 2007-2011 Chabster