Skip to content
Start free evaluation

MFC worker

  • Understand how MfcClientWorker runs the native client on a CWinThread and hands events to the UI thread.
  • Read the MFC consumer example.
  • Set up a Windows MFC application that uses the worker.
  • Windows 10 or later and Visual Studio 2022 (MSVC) with the MFC component installed.
  • The PDComm OPC UA SDK Windows native package (pdcomm-opcua-native-windows-AMD64.zip): include\pdcomm\opcua\, lib\pdcomm_opcua_ffi.dll.lib, the DLLs in bin\ and the MFC worker header bindings\mfc\PdcommOpcUa.hpp. Only the Windows package includes the MFC header.
UI thread (receiver window) Worker thread (CWinThread)
--------------------------- --------------------------
Submit(request, correlation) --- queue ---> begin / poll / take
RequestStop(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 Submit and RequestStop from any thread. Call Start and Wait from the thread that owns the worker.
  • A worker starts once; a second Start returns 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.

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.

examples/opcua-mfc/main.cpp
#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:

  1. Receiver window and message. Get a message number from RegisterWindowMessageW and pass it to the worker’s constructor together with the HWND that receives events. Your dialog or main window works fine.
  2. Submitting. Submit(request, correlation) takes a UA Binary request and a non-zero correlation. An empty request or a zero correlation returns PDCOMM_OPCUA_INVALID_INPUT, a duplicate correlation PDCOMM_OPCUA_BUSY and a request over 4 MiB PDCOMM_OPCUA_LIMIT.
  3. Handling the message. Like Pump in the example, call Drain() when the registered message arrives and handle the events. In an ordinary MFC application, call Drain() from an ON_REGISTERED_MESSAGE handler.
  4. Stopping. RequestStop(MfcClientStop::Drain) finishes every admitted request and closes the session. After the Stopped event, wait for the thread with Wait and destroy the worker.
  5. Response lifetime. The value of Completed stays 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.

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-AMD64
cmake --build build --config Release

When 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 passed
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.