MFC worker
- Understand how
MfcClientWorkerruns the native client on aCWinThreadand hands events to the UI thread. - Read the MFC consumer example.
- Set up a Windows MFC application that uses the worker.
What you need
Section titled “What you need”- Windows 10 or later and Visual Studio 2022 (MSVC) with the MFC component installed.
- The PDComm OPC UA SDK Windows
nativepackage (pdcomm-opcua-native-windows-AMD64.zip):include\pdcomm\opcua\,lib\pdcomm_opcua_ffi.dll.lib, the DLLs inbin\and the MFC worker headerbindings\mfc\PdcommOpcUa.hpp. Only the Windows package includes the MFC header.
1. How the worker is built
Section titled “1. How the worker is built”UI thread (receiver window) Worker thread (CWinThread)--------------------------- --------------------------Submit(request, correlation) --- queue ---> begin / poll / takeRequestStop(Drain or Abort) ------------> close, release ^ | | PostMessage(receiver, message) | events into a bounded mailbox +---- Drain() returns MfcClientEvent <-----+- The worker never touches a
CWnd. It puts events into a bounded mailbox and posts one registered message to the receiver window. - When the receiver window’s thread gets that message, it takes the queued events with
MfcClientWorker::Drain. Events are always handled on the UI thread. - You can call
SubmitandRequestStopfrom any thread. CallStartandWaitfrom the thread that owns the worker. - A worker starts once; a second
Startreturns false.
The kinds of MfcClientEvent:
kind |
Meaning | Main fields |
|---|---|---|
Ready |
The session is active. | ownerThread |
Pending |
A request was sent. | correlation, requestId |
Completed |
A validated response arrived; it can be a ServiceFault. | correlation, metadata, value |
Failed |
A request or the worker failed. | correlation (0 for a worker failure), status, phase, text |
Stopped |
The worker ended; always the last event. | orderly, aborted, accepted, completed, discarded, reason |
The phase of Failed works as in the Qt worker: a begin or response failure ends only its request, while a worker failure ends the worker and reports the remaining requests with abort. When the handle registry is full, the worker holds the response and delivers it once your application releases responses; if the registry stays full for timeoutMs, the worker ends with PDCOMM_OPCUA_LIMIT.
MfcClientConfiguration defaults to a 5000 ms timeout, the PDCOMM_OPCUA_USER_SECRET_HEADER_POLICY_ONLY flag, Aes256_Sha256_RsaPss, SignAndEncrypt and user name activation. Set anonymous to true to activate anonymously. The string fields (endpoint, applicationUri, userName) are UTF-8 CStringAs.
2. Read the example
Section titled “2. Read the example”examples/opcua-mfc/main.cpp is an MFC consumer that checks the worker’s rules one by one. It runs the SDK in-memory server with security None in the same process and uses a message-only window as the receiver.
#include "PdcommOpcUa.hpp"#include "response_fault_proxy.hpp"#include <afxwin.h>#include <chrono>#include <cstdio>#include <memory>#include <stdexcept>#include <string>#include <vector>CWinApp application;using namespace pdcomm::opcua;
// Part 6 UA Binary ReadRequest (encoding i=631) for ServerState (i=2259) Value.static const std::vector<uint8_t> kRead = { 0x01, 0x00, 0x77, 0x02, 0x00, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0, 0x00, 0x00, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0x03, 0, 0, 0, 0x01, 0, 0, 0, 0x01, 0x00, 0xd3, 0x08, 0x0d, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff};
static void Require(bool condition, const char *what) { if (!condition) throw std::runtime_error(what);}
// The UI thread polls the in-process server and pumps the receiver window until the// worker reports its terminal event. Worker events are handled only on this thread.template <typename Handler>static void Pump(server &host, MfcClientWorker &worker, UINT message, Handler &&handle, bool &stopped) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30); while (!stopped) { Require(std::chrono::steady_clock::now() < deadline, "MFC worker deadline"); host.poll(); MSG msg; while (::PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { if (msg.message == message) { for (auto &event : worker.Drain()) handle(event); } else { ::TranslateMessage(&msg); ::DispatchMessageW(&msg); } } ::Sleep(1); }}
// Pump that also runs `idle` on the UI thread after every pass, for checks that no worker// event triggers.template <typename Handler, typename Idle>static void PumpWithIdle(server &host, MfcClientWorker &worker, UINT message, Handler &&handle, Idle &&idle, bool &stopped) { const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30); while (!stopped) { Require(std::chrono::steady_clock::now() < deadline, "MFC worker deadline"); host.poll(); MSG msg; while (::PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) { if (msg.message == message) { for (auto &event : worker.Drain()) handle(event); } else { ::TranslateMessage(&msg); ::DispatchMessageW(&msg); } } idle(); ::Sleep(1); }}
// Fills the process-wide handle registry (1024 handles) with small buffers until it reports LIMIT.static void FillRegistry(server &host, std::vector<buffer> &filler) { for (int attempt = 0; attempt != 2048; ++attempt) { try { filler.push_back(host.local_address()); } catch (const error &e) { Require(e.status() == PDCOMM_OPCUA_LIMIT && !filler.empty(), "Registry filled up to LIMIT"); return; } } Require(false, "Handle registry was not filled");}
int wmain() { if (!AfxWinInit(GetModuleHandle(nullptr), nullptr, GetCommandLineW(), 0)) return 2; try { auto config = server_options(); const std::string url = "opc.tcp://127.0.0.1:0/pdcomm"; config.endpoint = bytes(url); config.flags = PDCOMM_OPCUA_ALLOW_NONE; auto host = server::create("127.0.0.1:0", config); const auto local = host.local_address().copy(); CStringA endpoint("opc.tcp://"); endpoint.Append(reinterpret_cast<const char *>(local.data()), static_cast<int>(local.size())); endpoint += "/pdcomm"; HWND receiver = ::CreateWindowExW(0, L"STATIC", L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, nullptr, nullptr); Require(receiver != nullptr, "Message-only receiver window"); const UINT message = ::RegisterWindowMessageW(L"PDComm.OpcUa.Mfc.Consumer"); MfcClientConfiguration configuration; configuration.endpoint = endpoint; configuration.anonymous = true; configuration.policy = PDCOMM_OPCUA_POLICY_NONE; configuration.mode = PDCOMM_OPCUA_MODE_NONE; configuration.flags = PDCOMM_OPCUA_ALLOW_NONE;
// Normal drain: valid and invalid requests, owned response after worker destruction. std::shared_ptr<const response> kept; std::vector<uint8_t> keptBytes; { auto worker = std::make_unique<MfcClientWorker>(configuration, receiver, message); Require(worker->Submit({}, 1) == PDCOMM_OPCUA_INVALID_INPUT, "Empty request rejected"); Require(worker->Submit(kRead, 0) == PDCOMM_OPCUA_INVALID_INPUT, "Zero correlation rejected"); Require(worker->Submit(std::vector<uint8_t>(MfcClientWorker::MaximumQueuedBytes + 1, 1), 9) == PDCOMM_OPCUA_LIMIT, "Oversize request rejected"); Require(worker->Submit(kRead, 1) == PDCOMM_OPCUA_OK, "Read admitted"); Require(worker->Submit(kRead, 1) == PDCOMM_OPCUA_BUSY, "Duplicate correlation rejected"); Require(worker->Submit(std::vector<uint8_t>(kRead.begin(), kRead.end() - 1), 2) == PDCOMM_OPCUA_OK, "Truncated request admitted"); Require(worker->Start() && !worker->Start(), "One-shot worker start"); const DWORD uiThread = ::GetCurrentThreadId(); DWORD ownerThread = 0; bool stopped = false, completed = false, invalid = false; Pump(host, *worker, message, [&](const MfcClientEvent &event) { Require(::GetCurrentThreadId() == uiThread, "Events are handled on the receiver thread"); switch (event.kind) { case MfcClientEventKind::Ready: ownerThread = event.ownerThread; break; case MfcClientEventKind::Pending: break; case MfcClientEventKind::Completed: Require(event.correlation == 1 && event.metadata.binary_id == 634 && event.metadata.service_status == 0, "Read response metadata"); kept = event.value; keptBytes = kept->copy(); completed = true; break; case MfcClientEventKind::Failed: Require(event.correlation == 2 && event.status == PDCOMM_OPCUA_INVALID_INPUT && event.phase == L"begin", "Invalid request fails alone"); invalid = true; break; case MfcClientEventKind::Stopped: Require(event.orderly && !event.aborted && event.reason == L"drained-and-closed" && event.accepted == 2 && event.completed == 2 && event.discarded == 0, "Orderly drained stop"); stopped = true; break; } if (completed && invalid) worker->RequestStop(MfcClientStop::Drain); }, stopped); Require(ownerThread != 0 && ownerThread != uiThread, "Native client runs on the worker thread"); Require(worker->Wait(10000), "Worker joined"); Require(worker->Submit(kRead, 3) == PDCOMM_OPCUA_CLOSED, "Submit after stop rejected"); } Require(kept && kept->copy() == keptBytes, "Owned response survives worker destruction");
// Abort: queued requests are discarded and reported, never claimed as an orderly close. { MfcClientWorker worker(configuration, receiver, message); Require(worker.Submit(kRead, 11) == PDCOMM_OPCUA_OK, "Queued before abort"); worker.RequestStop(MfcClientStop::Abort); Require(worker.Start(), "Abort worker start"); bool stopped = false, discarded = false; Pump(host, worker, message, [&](const MfcClientEvent &event) { if (event.kind == MfcClientEventKind::Failed) discarded = event.correlation == 11 && event.status == PDCOMM_OPCUA_CLOSED && event.phase == L"abort"; if (event.kind == MfcClientEventKind::Stopped) { Require(!event.orderly && event.aborted && event.discarded == 1, "Aborted stop"); stopped = true; } }, stopped); Require(discarded && worker.Wait(10000), "Abort discarded and joined"); }
// Response faults through an in-process proxy that changes the response to the first Read // after Arm(). A failed response ends only its request; a closed connection or a registry // that stays full ends the worker. const std::string upstream(reinterpret_cast<const char *>(local.data()), local.size()); const auto proxied = [&](const ResponseFaultProxy &proxy, uint32_t timeoutMs) { MfcClientConfiguration result = configuration; result.endpoint = ("opc.tcp://127.0.0.1:" + std::to_string(proxy.Port()) + "/pdcomm").c_str(); result.timeoutMs = timeoutMs; return result; }; const auto none = [] {};
// Aborted response: only that request fails, and the same worker serves the next one. { ResponseFaultProxy proxy(upstream, ResponseFaultProxy::Mode::Abort); MfcClientWorker worker(proxied(proxy, 5000), receiver, message); Require(worker.Start(), "Aborted-response worker start"); bool stopped = false, failed = false, answered = false; PumpWithIdle(host, worker, message, [&](const MfcClientEvent &event) { switch (event.kind) { case MfcClientEventKind::Ready: proxy.Arm(); Require(worker.Submit(kRead, 21) == PDCOMM_OPCUA_OK, "Read before the abort admitted"); break; case MfcClientEventKind::Pending: break; case MfcClientEventKind::Failed: Require(!failed && event.correlation == 21 && event.status == PDCOMM_OPCUA_RUNTIME_ERROR && event.phase == L"response" && event.text.GetLength() > 0, "Aborted response fails alone"); failed = true; Require(worker.Submit(kRead, 22) == PDCOMM_OPCUA_OK, "Read after the aborted response admitted"); break; case MfcClientEventKind::Completed: Require(failed && event.correlation == 22 && event.metadata.binary_id == 634 && event.metadata.service_status == 0, "Same worker serves the next request"); answered = true; worker.RequestStop(MfcClientStop::Drain); break; case MfcClientEventKind::Stopped: Require(event.orderly && !event.aborted && event.reason == L"drained-and-closed" && event.accepted == 2 && event.completed == 2 && event.discarded == 0, "Orderly drain after an aborted response"); stopped = true; break; } }, none, stopped); Require(failed && answered && proxy.Changed() == 1 && worker.Wait(10000), "Aborted-response worker joined"); }
// Invalid response: its request fails, the closed connection ends the worker and the // remaining request is discarded. { ResponseFaultProxy proxy(upstream, ResponseFaultProxy::Mode::Invalid); MfcClientWorker worker(proxied(proxy, 5000), receiver, message); Require(worker.Start(), "Invalid-response worker start"); bool stopped = false, failed = false, closed = false, discarded = false; PumpWithIdle(host, worker, message, [&](const MfcClientEvent &event) { switch (event.kind) { case MfcClientEventKind::Ready: proxy.Arm(); Require(worker.Submit(kRead, 31) == PDCOMM_OPCUA_OK && worker.Submit(kRead, 32) == PDCOMM_OPCUA_OK, "Reads before the invalid response admitted"); break; case MfcClientEventKind::Pending: break; case MfcClientEventKind::Failed: if (event.correlation == 31) { Require(!failed && event.status == PDCOMM_OPCUA_RUNTIME_ERROR && event.phase == L"response", "Invalid response fails its request"); failed = true; } else if (event.correlation == 0) { Require(failed && event.status == PDCOMM_OPCUA_CLOSED && event.phase == L"worker", "Closed connection ends the worker"); closed = true; } else { Require(closed && event.correlation == 32 && event.status == PDCOMM_OPCUA_CLOSED && event.phase == L"abort", "Remaining request discarded"); discarded = true; } break; case MfcClientEventKind::Completed: Require(false, "No response after an invalid response"); break; case MfcClientEventKind::Stopped: Require(!event.orderly && event.aborted && event.reason == L"native-error" && event.accepted == 2 && event.completed == 1 && event.discarded == 1, "Invalid response ends the worker"); stopped = true; break; } }, none, stopped); Require(failed && closed && discarded && proxy.Changed() == 1 && worker.Wait(10000), "Invalid-response worker joined"); }
// Full handle registry: the worker holds the response and delivers it after the release. { ResponseFaultProxy proxy(upstream, ResponseFaultProxy::Mode::Observe); MfcClientWorker worker(proxied(proxy, 5000), receiver, message); std::vector<buffer> filler; std::chrono::steady_clock::time_point forwarded{}; Require(worker.Start(), "Registry worker start"); bool stopped = false, released = false, first = false, second = false; PumpWithIdle(host, worker, message, [&](const MfcClientEvent &event) { switch (event.kind) { case MfcClientEventKind::Ready: FillRegistry(host, filler); // After connect: the worker's client already holds its handle. proxy.Arm(); Require(worker.Submit(kRead, 41) == PDCOMM_OPCUA_OK, "Read into a full registry admitted"); break; case MfcClientEventKind::Pending: break; case MfcClientEventKind::Completed: if (event.correlation == 41) { Require(released, "No response while the registry is full"); first = true; Require(worker.Submit(kRead, 42) == PDCOMM_OPCUA_OK, "Read after the held response admitted"); } else { Require(first && event.correlation == 42, "Same worker serves the next request"); second = true; worker.RequestStop(MfcClientStop::Drain); } break; case MfcClientEventKind::Failed: Require(false, "Held response failed"); break; case MfcClientEventKind::Stopped: Require(event.orderly && event.accepted == 2 && event.completed == 2 && event.discarded == 0, "Orderly drain after a held response"); stopped = true; break; } }, [&] { // The response reached the client while the registry was full, so the worker holds it. // Release the registry only after the worker had time to try to take it. if (released || proxy.Changed() == 0) return; const auto now = std::chrono::steady_clock::now(); if (forwarded == std::chrono::steady_clock::time_point{}) forwarded = now; if (now - forwarded < std::chrono::milliseconds(200)) return; filler.clear(); released = true; }, stopped); Require(first && second && worker.Wait(10000), "Registry worker joined"); }
// Registry that stays full: the worker ends with LIMIT after its timeout. { ResponseFaultProxy proxy(upstream, ResponseFaultProxy::Mode::Observe); MfcClientWorker worker(proxied(proxy, 1000), receiver, message); std::vector<buffer> filler; std::chrono::steady_clock::time_point forwarded{}, limited{}; Require(worker.Start(), "Registry timeout worker start"); bool stopped = false, discarded = false; PumpWithIdle(host, worker, message, [&](const MfcClientEvent &event) { switch (event.kind) { case MfcClientEventKind::Ready: FillRegistry(host, filler); proxy.Arm(); Require(worker.Submit(kRead, 51) == PDCOMM_OPCUA_OK, "Read into a full registry admitted"); break; case MfcClientEventKind::Pending: break; case MfcClientEventKind::Failed: if (event.correlation == 0) { Require(event.status == PDCOMM_OPCUA_LIMIT && event.phase == L"worker", "Full registry ends the worker"); limited = std::chrono::steady_clock::now(); } else { Require(event.correlation == 51 && event.status == PDCOMM_OPCUA_CLOSED && event.phase == L"abort", "Held request discarded"); discarded = true; } break; case MfcClientEventKind::Completed: Require(false, "No response while the registry is full"); break; case MfcClientEventKind::Stopped: Require(!event.orderly && event.aborted && event.reason == L"native-error" && event.accepted == 1 && event.completed == 0 && event.discarded == 1, "Registry timeout ends the worker"); stopped = true; break; } }, [&] { if (forwarded == std::chrono::steady_clock::time_point{} && proxy.Changed() == 1) forwarded = std::chrono::steady_clock::now(); }, stopped); Require(discarded && forwarded != std::chrono::steady_clock::time_point{} && limited - forwarded >= std::chrono::milliseconds(500) && worker.Wait(10000), "Held response kept until the timeout"); } host.close(); ::DestroyWindow(receiver); std::puts("OPC UA MFC consumer passed"); return 0; } catch (const std::exception &e) { std::fprintf(stderr, "OPC UA MFC consumer failed: %s\n", e.what()); return 1; }}The parts to carry into your application:
- Receiver window and message. Get a message number from
RegisterWindowMessageWand pass it to the worker’s constructor together with theHWNDthat receives events. Your dialog or main window works fine. - Submitting.
Submit(request, correlation)takes a UA Binary request and a non-zero correlation. An empty request or a zero correlation returnsPDCOMM_OPCUA_INVALID_INPUT, a duplicate correlationPDCOMM_OPCUA_BUSYand a request over 4 MiBPDCOMM_OPCUA_LIMIT. - Handling the message. Like
Pumpin the example, callDrain()when the registered message arrives and handle the events. In an ordinary MFC application, callDrain()from anON_REGISTERED_MESSAGEhandler. - Stopping.
RequestStop(MfcClientStop::Drain)finishes every admitted request and closes the session. After theStoppedevent, wait for the thread withWaitand destroy the worker. - Response lifetime. The
valueofCompletedstays valid after the worker is gone; the native handle is released with the last reference.
The rest of the example deliberately creates an aborted response, an invalid response and a full handle registry to check the failure rules. That test also builds response_fault_proxy.cpp from the same folder.
3. Build on Windows
Section titled “3. Build on Windows”Set these properties in your MFC application project:
| Setting | Value |
|---|---|
| Use of MFC | Use MFC in a Shared DLL |
| Character set | Unicode (UNICODE, _UNICODE) |
| C++ language standard | ISO C++17 or later |
| Additional include directories | The SDK’s include and bindings\mfc |
| Additional dependencies | The SDK’s lib\pdcomm_opcua_ffi.dll.lib |
| Copy next to the executable | The SDK’s bin\pdcomm_opcua_ffi.dll and the OpenSSL 3 DLLs |
With CMake, the same settings look like this. PDCOMM_OPCUA_SDK is the folder you unpacked the SDK into.
set(CMAKE_MFC_FLAG 2)add_executable(my_opcua_app WIN32 main.cpp)target_compile_features(my_opcua_app PRIVATE cxx_std_17)target_compile_definitions(my_opcua_app PRIVATE UNICODE _UNICODE)target_include_directories(my_opcua_app PRIVATE "${PDCOMM_OPCUA_SDK}/include" "${PDCOMM_OPCUA_SDK}/bindings/mfc")target_link_libraries(my_opcua_app PRIVATE "${PDCOMM_OPCUA_SDK}/lib/pdcomm_opcua_ffi.dll.lib")file(GLOB pdcomm_runtime "${PDCOMM_OPCUA_SDK}/bin/*.dll")add_custom_command(TARGET my_opcua_app POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_if_different ${pdcomm_runtime} "$<TARGET_FILE_DIR:my_opcua_app>")Build from the “x64 Native Tools Command Prompt for VS 2022”:
cmake -S . -B build -G "Visual Studio 17 2022" -A x64 -DPDCOMM_OPCUA_SDK=C:\PDComm\pdcomm-opcua-native-windows-AMD64cmake --build build --config ReleaseWhen you build and run the example main.cpp as is, it checks every rule, prints this line and exits with 0:
OPC UA MFC consumer passedCommon errors
Section titled “Common errors”| Symptom | Cause and fix |
|---|---|
| Your event handler never runs | The registered message handler doesn’t call Drain(). Events stay in the worker until you drain them. |
Submit returns PDCOMM_OPCUA_CLOSED |
The worker has already ended. A worker can’t restart, so create a new one. |
| The program can’t find a DLL at startup | pdcomm_opcua_ffi.dll and the OpenSSL DLLs aren’t next to the executable. Check the post-build copy step. |
Next steps
Section titled “Next steps”- Subscriptions
- C++ API reference: every member of
MfcClientWorker