Skip to content
Start free evaluation

C++ client and server

  • Build a server with the RAII classes of pdcomm_opcua.hpp and expose three variables.
  • Build a client with the same header and write and read values.
  • See how errors arrive as exceptions.
  • A lab folder that completed the lab setup: the native package, the environment variables, certificates in $PKI and built examples.
  • A C++17 compiler and two terminals.

pdcomm_opcua.hpp is a header-only wrapper around the C ABI. It has no library of its own and uses the same pdcomm_opcua_ffi library.

Class or function Role
options, server_options Check the ABI version and return an options structure filled with the defaults.
client Owns a client handle; provides connect, activate_anonymous, begin, poll, take and close.
server Owns a server handle; provides create, add_variable, set_value, read_value, local_address, poll and close.
response, buffer Own response and buffer handles; copy() returns the bytes and info() the response metadata.
error The exception thrown when a C ABI call fails; status() is the C ABI status and what() the error text.

Every handle is released automatically when its object goes away. The objects can be moved but not copied.

examples/opcua/opcua_server.cpp
// PDComm OPC UA C++ server example using the header-only RAII wrapper.
// Hosts ns=2;i=1001 Value (public, writable), ns=2;i=1002 Limit (public,
// read-only) and ns=2;i=1003 Protected (authenticated only) until Ctrl+C,
// SIGTERM or the optional --seconds limit.
#include <pdcomm/opcua/pdcomm_opcua.hpp>
#include <algorithm>
#include <chrono>
#include <csignal>
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
#include <thread>
#include <vector>
#include "example_io.h"
#include "ua_binary.h"
namespace ua = pdcomm::opcua;
static volatile std::sig_atomic_t stop_requested = 0;
extern "C" void on_signal(int) { stop_requested = 1; }
static std::vector<uint8_t> load(const std::string& path) {
std::ifstream file(path, std::ios::binary);
std::vector<uint8_t> data{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
if (data.empty() || data.size() > EXAMPLE_MAX_FILE) throw std::runtime_error("Invalid file " + path);
return data;
}
static std::vector<uint8_t> node_id(uint32_t id) {
uint8_t out[7];
return {out, out + ua_encode_node_id(out, 2, id)};
}
static std::vector<uint8_t> int32_variant(int32_t value) {
uint8_t out[5];
return {out, out + ua_encode_int32_variant(out, value)};
}
int main(int argc, char** argv) {
unsigned long seconds = 0;
if (argc != 6 && !(argc == 8 && std::string(argv[6]) == "--seconds")) {
std::cerr << "Usage: opcua_server_cpp BIND_ADDRESS ENDPOINT_URL SERVER_CERT_DER SERVER_KEY_PEM "
"TRUSTED_CLIENT_DER [--seconds N]\n";
return 2;
}
try {
if (argc == 8) seconds = std::stoul(argv[7]);
} catch (const std::exception&) {
seconds = 0;
}
if (argc == 8 && (seconds == 0 || seconds > 86400)) {
std::cerr << "Error: --seconds must be between 1 and 86400\n";
return 2;
}
try {
const std::string endpoint = argv[2];
const auto certificate = load(argv[3]);
auto key = load(argv[4]);
const auto trust = load(argv[5]);
auto config = ua::server_options();
config.endpoint = ua::bytes(endpoint);
config.certificate_der = ua::bytes(certificate);
config.private_key_pem = ua::bytes(key);
config.trusted_client_der = ua::bytes(trust);
auto server = ua::server::create(argv[1], config);
std::fill(key.begin(), key.end(), static_cast<unsigned char>(0)); // The SDK copied the identity.
server.add_variable(node_id(1001), "Value", int32_variant(42),
PDCOMM_OPCUA_VARIABLE_WRITABLE | PDCOMM_OPCUA_VARIABLE_PUBLIC);
server.add_variable(node_id(1002), "Limit", int32_variant(100), PDCOMM_OPCUA_VARIABLE_PUBLIC);
server.add_variable(node_id(1003), "Protected", int32_variant(7), PDCOMM_OPCUA_VARIABLE_WRITABLE);
const auto address = server.local_address().copy();
// Install the stop handlers before announcing readiness.
std::signal(SIGINT, on_signal);
std::signal(SIGTERM, on_signal);
#ifdef SIGBREAK
std::signal(SIGBREAK, on_signal); // Windows Ctrl+Break, e.g. from a service manager
#endif
std::cout << "Listening on " << std::string(address.begin(), address.end()) << std::endl;
const auto started = std::chrono::steady_clock::now();
while (!stop_requested &&
(!seconds || std::chrono::steady_clock::now() - started < std::chrono::seconds(seconds))) {
server.poll();
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
const auto snapshot = server.read_value(node_id(1001)).copy();
ua_value value;
if (ua_decode_data_value(snapshot.data(), snapshot.size(), &value) || !value.has_int32)
throw std::runtime_error("Unexpected DataValue");
server.close();
std::cout << "Final Value=" << value.int32 << "\nServer stopped" << std::endl;
return 0;
} catch (const ua::error& e) {
std::cerr << "Error: SDK status " << e.status() << ": " << e.what() << std::endl;
return 3;
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 3;
}
}
  • ua::server_options() creates the options; the example sets the endpoint URL, the server certificate and private key, and the client certificate to trust. The flags stay 0, so only secure endpoints are advertised.
  • ua::server::create binds the address. The private key is copied at that point, so the example wipes its copy right away.
  • The NodeId and value passed to add_variable are UA Binary bytes: node_id(1001) is ns=2;i=1001 and int32_variant(42) is an Int32 Variant holding 42. Variables without PDCOMM_OPCUA_VARIABLE_PUBLIC are for authenticated users only.
  • The server serves clients only while you call poll; the example calls it every millisecond.
  • On exit, the example reads the current DataValue with read_value and calls close.
examples/opcua/opcua_client.cpp
// PDComm OPC UA C++ client example using the header-only RAII wrapper.
// Opens a secure anonymous session, optionally writes an Int32 and reads it.
// Exit codes: 0 success, 1 a Bad OPC UA status, 2 invalid arguments, 3 failure.
#include <pdcomm/opcua/pdcomm_opcua.hpp>
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <fstream>
#include <iostream>
#include <iterator>
#include <map>
#include <string>
#include <thread>
#include <vector>
#include "example_io.h"
#include "ua_binary.h"
namespace ua = pdcomm::opcua;
static std::vector<uint8_t> load(const std::string& path) {
std::ifstream file(path, std::ios::binary);
std::vector<uint8_t> data{std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
if (!file.good() && !file.eof()) throw std::runtime_error("Cannot read " + path);
if (data.empty() || data.size() > EXAMPLE_MAX_FILE) throw std::runtime_error("Invalid file " + path);
return data;
}
static std::vector<uint8_t> exchange(ua::client& client, const uint8_t* request, size_t size,
std::chrono::milliseconds timeout) {
const auto id = client.begin(std::vector<uint8_t>(request, request + size));
const auto deadline = std::chrono::steady_clock::now() + timeout;
while (std::chrono::steady_clock::now() < deadline) {
client.poll();
if (auto response = client.take(id)) return response->copy();
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
throw ua::error(PDCOMM_OPCUA_RUNTIME_ERROR, "Response timed out");
}
static std::string hex(uint32_t value) {
char text[11];
std::snprintf(text, sizeof(text), "0x%08x", static_cast<unsigned>(value));
return text;
}
int main(int argc, char** argv) {
if (argc < 5 || (argc - 5) % 2) {
std::cerr << "Usage: opcua_client_cpp ENDPOINT CLIENT_CERT_DER CLIENT_KEY_PEM "
"TRUSTED_SERVER_DER [--policy aes256sha256rsapss|aes128sha256rsaoaep|"
"basic256sha256] [--mode signandencrypt|sign] [--node ID] [--write VALUE]\n";
return 2;
}
const std::map<std::string, uint32_t> policies{{"aes256sha256rsapss", PDCOMM_OPCUA_AES256SHA256RSAPSS},
{"aes128sha256rsaoaep", PDCOMM_OPCUA_AES128SHA256RSAOAEP}, {"basic256sha256", PDCOMM_OPCUA_BASIC256SHA256}};
const std::map<std::string, uint32_t> modes{{"signandencrypt", PDCOMM_OPCUA_SIGN_AND_ENCRYPT},
{"sign", PDCOMM_OPCUA_SIGN}};
uint32_t policy = PDCOMM_OPCUA_AES256SHA256RSAPSS, mode = PDCOMM_OPCUA_SIGN_AND_ENCRYPT, node = 1001;
bool write = false;
int32_t write_value = 0;
try {
for (int i = 5; i < argc; i += 2) {
const std::string name = argv[i], text = argv[i + 1];
if (name == "--policy" && policies.count(text)) policy = policies.at(text);
else if (name == "--mode" && modes.count(text)) mode = modes.at(text);
else if (name == "--node") node = static_cast<uint32_t>(std::stoul(text));
else if (name == "--write") { write = true; write_value = static_cast<int32_t>(std::stol(text)); }
else throw std::invalid_argument(name);
}
} catch (const std::exception&) {
std::cerr << "Error: invalid option\n";
return 2;
}
try {
auto config = ua::options();
const auto certificate = load(argv[2]);
auto key = load(argv[3]);
const auto trust = load(argv[4]);
config.policy = policy;
config.mode = mode;
config.timeout_ms = 5000;
config.certificate_der = ua::bytes(certificate);
config.private_key_pem = ua::bytes(key);
config.trusted_server_der = ua::bytes(trust);
auto client = ua::client::connect(argv[1], config);
std::fill(key.begin(), key.end(), static_cast<unsigned char>(0)); // The SDK copied the identity.
client.activate_anonymous();
std::cout << "Session activated (anonymous)" << std::endl;
int result = 0;
uint8_t request[256];
uint32_t service = 0, status = 0;
if (write) {
const auto size = ua_encode_write_int32(request, sizeof(request), 2, node, write_value);
const auto body = exchange(client, request, size, std::chrono::seconds(5));
if (ua_decode_write(body.data(), body.size(), &service, &status))
throw std::runtime_error("Malformed WriteResponse");
std::cout << "Write ns=2;i=" << node << " value=" << write_value << " service=" << hex(service)
<< " status=" << hex(status) << std::endl;
if ((service | status) & 0x80000000u) result = 1;
}
const auto size = ua_encode_read(request, sizeof(request), 2, node);
const auto body = exchange(client, request, size, std::chrono::seconds(5));
ua_value value;
if (ua_decode_read(body.data(), body.size(), &service, &value))
throw std::runtime_error("Malformed ReadResponse");
std::cout << "Read ns=2;i=" << node << " value="
<< (value.has_int32 ? std::to_string(value.int32) : std::string("none"))
<< " service=" << hex(service) << " status=" << hex(value.status) << std::endl;
if ((service | value.status) & 0x80000000u) result = 1;
client.close();
std::cout << "Session closed" << std::endl;
return result;
} catch (const ua::error& e) {
std::cerr << "Error: SDK status " << e.status() << ": " << e.what() << std::endl;
return 3;
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 3;
}
}
  • ua::client::connect opens the secure channel and session; the returned object owns the client.
  • exchange queues a request with begin, then repeats poll and take. take returns an empty std::optional until the response arrives.
  • Every C ABI failure arrives as ua::error. The example prints it as Error: SDK status ... and exits with code 3.

In the first terminal, run the C++ server:

터미널 창
cd "$OPCUA_SDK"
build/opcua_server_cpp 127.0.0.1:4840 opc.tcp://localhost:4840/pdcomm "$PKI/server.der" "$PKI/server.key.pem" "$PKI/client.der"
Listening on 127.0.0.1:4840

To stop after a fixed time, append --seconds N. With port 0, the operating system picks a free port and the Listening on line shows the actual address.

In the second terminal, write 11 with the C++ client:

터미널 창
cd "$OPCUA_SDK"
build/opcua_client_cpp opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" --write 11
Session activated (anonymous)
Write ns=2;i=1001 value=11 service=0x00000000 status=0x00000000
Read ns=2;i=1001 value=11 service=0x00000000 status=0x00000000
Session closed

Read again with the Aes128_Sha256_RsaOaep policy and Sign mode:

터미널 창
build/opcua_client_cpp opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" --policy aes128sha256rsaoaep --mode sign
Session activated (anonymous)
Read ns=2;i=1001 value=11 service=0x00000000 status=0x00000000
Session closed

Writing a read-only variable gives the item result BadNotWritable and exit code 1:

터미널 창
build/opcua_client_cpp opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" --node 1002 --write 5
Session activated (anonymous)
Write ns=2;i=1002 value=5 service=0x00000000 status=0x803b0000
Read ns=2;i=1002 value=100 service=0x00000000 status=0x00000000
Session closed

Pass the wrong certificate (client.der) as the server certificate to trust:

터미널 창
build/opcua_client_cpp opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/client.der"
Error: SDK status 5: OPC UA security CertificateUntrusted

ua::client::connect threw ua::error with status() 5 (PDCOMM_OPCUA_RUNTIME_ERROR). Use the error text for logs and people only; branch on status() in your program.

The C++ example accepts only aes256sha256rsapss, aes128sha256rsaoaep and basic256sha256 for --policy. With none, it prints Error: invalid option and exits with code 2.

Press Ctrl+C in the server’s terminal. The server stops on SIGINT, SIGTERM and, on Windows, Ctrl+Break.

Final Value=11
Server stopped