Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#39] Integrate with existing (main) Windows GUI thread #40

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from

Conversation

RippeR37
Copy link
Owner

@RippeR37 RippeR37 commented Feb 12, 2024

This commit adds a new class template called base::WinThreadAttachment which can be used to integrate libbase's cross-thread post-tasking with Windows native message queue system.

Example usage:

  HWND hwnd = CreateWindowEx(/* ... */);

  // This must be done after creating a window and before main loop and
  // outlive the main loop.
  // `LIBBASE_TASK_MSG_ID` here is some custom integer provided by user
  // of this library that will not be used as any other message ID in
  // the app (e.g. `WM_APP+0`).
  base::WinThreadAttachment<LIBBASE_TASK_MSG_ID> mainThread{hwnd};
  // As long as the `mainThread` lives, current thread will have
  // associated task runner that will post task to Window's message
  // queue and all such tasks will be executed between other messages
  // on that queue.

  // Main loop example
  MSG msg = {};
  while (GetMessage(&msg, NULL, 0, 0) > 0) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

Apart from that, new (Windows-only) example called win32 has been added to showcase new functionality.

This commit adds a new class template called `base::WinThreadAttachment`
which can be used to integrate libbase's cross-thread post-tasking with
Windows native message queue system.

Example usage:

```cpp
  HWND hwnd = CreateWindowEx(/* ... */);

  // This must be done after creating a window and before main loop and
  // outlive the main loop.
  // `LIBBASE_TASK_MSG_ID` here is some custom integer provided by user
  // of this library that will not be used as any other message ID in
  // the app (e.g. `WM_APP+0`).
  base::WinThreadAttachment<LIBBASE_TASK_MSG_ID> mainThread{hwnd};
  // As long as the `mainThread` lives, current thread will have
  // associated task runner that will post task to Window's message
  // queue and all such tasks will be executed between other messages
  // on that queue.

  // Main loop example
  MSG msg = {};
  while (GetMessage(&msg, NULL, 0, 0) > 0) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
```

Apart from there, new (Windows-only) example called `win32` has been
added to showcase new functionality.
@RippeR37 RippeR37 added the enhancement New feature or request label Feb 12, 2024
@RippeR37 RippeR37 self-assigned this Feb 12, 2024
Copy link

codecov bot commented Feb 12, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base (5f1b049) 85.95% compared to head (0bdf995) 85.95%.

Additional details and impacted files
@@           Coverage Diff            @@
##           develop      #40   +/-   ##
========================================
  Coverage    85.95%   85.95%           
========================================
  Files           40       40           
  Lines          840      840           
========================================
  Hits           722      722           
  Misses         118      118           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@derceg
Copy link

derceg commented Feb 25, 2024

Hi @RippeR37,

I think one change that would be worthwhile would be to switch to a message-only window. The main reason for that being that there might be multiple top-level windows in an application. In that situation, top-level windows could be created and destroyed, so attaching to a specific application window wouldn't be safe.

By creating a message-only window itself, libbase can offer its functionality to an application, regardless of how the application manages its windows. It also means that the application doesn't need to supply a LIBBASE_TASK_MSG_ID, since the message-only window can use whatever message value it likes.

That is, I think the win_thread_attachment.h should be updated to:

#pragma once

#ifdef LIBBASE_IS_WINDOWS

#include <optional>

#include <windows.h>

#include "base/message_loop/win/message_pump_win_impl.h"
#include "base/sequenced_task_runner_helpers.h"
#include "base/threading/delayed_task_manager_shared_instance.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "base/threading/task_runner_impl.h"

namespace base {

class WinThreadAttachment {
 private:
  struct Private {
   private:
    Private() = default;

    friend WinThreadAttachment;
  };

 public:
  static std::optional<WinThreadAttachment> TryCreate() {
    HWND hWnd = TryCreateMessageOnlyWindow();

    if (!hWnd) {
      return std::nullopt;
    }

    return std::make_optional<WinThreadAttachment>(hWnd, Private());
  }

  WinThreadAttachment(HWND hWnd, Private)
      : hWnd_(hWnd),
        sequence_id_(detail::SequenceIdGenerator::GetNextSequenceId()),
        message_pump_(
            std::make_shared<MessagePumpWinImpl<WM_LIBBASE_EXECUTE_TASK>>(
                1,
                hWnd_)),
        task_runner_(SingleThreadTaskRunnerImpl::Create(
            message_pump_,
            sequence_id_,
            0,
            DelayedTaskManagerSharedInstance::GetOrCreateSharedInstance())),
        scoped_sequence_id_(sequence_id_),
        scoped_task_runner_handle_(task_runner_) {
    DCHECK_EQ(WinThreadAttachment::current_instance_, nullptr);
    WinThreadAttachment::current_instance_ = this;
  }

  ~WinThreadAttachment() {
    DCHECK_NE(WinThreadAttachment::current_instance_, nullptr);
    WinThreadAttachment::current_instance_ = nullptr;

    message_pump_->Stop({});

    DestroyWindow(hWnd_);
  }

  std::shared_ptr<SingleThreadTaskRunner> TaskRunner() const {
    return task_runner_;
  }

 private:
  static HWND TryCreateMessageOnlyWindow() {
    static int static_in_this_module = 0;

    HMODULE hModule = nullptr;
    GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
                           GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
                       reinterpret_cast<LPCSTR>(&static_in_this_module),
                       &hModule);

    WNDCLASSA window_class = {};
    window_class.lpfnWndProc = WindowProc;
    window_class.lpszClassName = LIBBASE_CLASS_NAME;
    window_class.hInstance = hModule;
    window_class.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_BTNFACE + 1);
    RegisterClassA(&window_class);

    return CreateWindowA(LIBBASE_CLASS_NAME, "", 0, CW_USEDEFAULT,
                         CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
                         HWND_MESSAGE, nullptr, hModule, nullptr);
  }

  static LRESULT CALLBACK WindowProc(HWND hWnd,
                                     UINT uMsg,
                                     WPARAM wParam,
                                     LPARAM lParam) {
    switch (uMsg) {
      case WM_LIBBASE_EXECUTE_TASK: {
        DCHECK_NE(WinThreadAttachment::current_instance_, nullptr);

        const MessagePump::ExecutorId executor_id = 0;
        if (auto pending_task =
                WinThreadAttachment::current_instance_->message_pump_
                    ->GetNextPendingTask(executor_id)) {
          std::move(pending_task.task).Run();
        }
      }
    }

    return DefWindowProc(hWnd, uMsg, wParam, lParam);
  }

  inline static thread_local WinThreadAttachment* current_instance_ = nullptr;

  inline static const char LIBBASE_CLASS_NAME[] = "libbaseMessageOnlyWindow";
  static const UINT WM_LIBBASE_EXECUTE_TASK = WM_USER + 0;

  HWND hWnd_;

  SequenceId sequence_id_;
  std::shared_ptr<MessagePump> message_pump_;
  std::shared_ptr<SingleThreadTaskRunnerImpl> task_runner_;
  detail::ScopedSequenceIdSetter scoped_sequence_id_;
  SequencedTaskRunnerHandle scoped_task_runner_handle_;
};

}  // namespace base

#endif  // LIBBASE_IS_WINDOWS

The window creation can technically fail, which is the reason for the TryCreate method. It's not expected that it would happen, but it would be useful to know if it does occur. Throwing an exception from the constructor would be more elegant, but it doesn't look like libbase uses exceptions and as a library, it can't really assume that the application does either.

Aside from that, I've done some testing I think it should all work well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants