.NET
- Run a server and a client from one console application with the
PDComm.OpcUaassembly. - Build the example against the assembly and native library of the installed SDK.
- See how resources are released and how errors arrive.
What you need
Section titled “What you need”- The PDComm OPC UA SDK
dotnetpackage (pdcomm-opcua-dotnet-darwin-arm64.tar.gz). - The .NET 8 runtime and a .NET SDK (8 or later). This page was run with .NET SDK 10.0.401 and the .NET 8 runtime.
- The certificates in
$PKIfrom PKI and trust setup, and two terminals.
The dotnet package contains:
| Path | Contents |
|---|---|
lib/dotnet/PDComm.OpcUa.dll |
The .NET assembly |
bindings/dotnet/PDComm.OpcUa/ |
The assembly’s C# sources |
lib/ |
The native library libpdcomm_opcua_ffi.dylib and the OpenSSL 3 runtime |
examples/opcua/dotnet/ |
The example project |
1. Build the example
Section titled “1. Build the example”Unpack the package in the lab folder and copy the example project to a working folder; leave the package itself unchanged.
tar -xzf pdcomm-opcua-dotnet-darwin-arm64.tar.gzexport OPCUA_DOTNET_SDK="$PWD/pdcomm-opcua-dotnet-darwin-arm64"cp -R "$OPCUA_DOTNET_SDK/examples/opcua/dotnet" my-opcua-appcd my-opcua-appdotnet build OpcUaExample.csproj -c Release -p:PdcommOpcUaSdk="$OPCUA_DOTNET_SDK" -o outBuild succeeded. 0 Warning(s) 0 Error(s)The build copies the assembly, the native library and the OpenSSL runtime into out:
ls outOpcUaExampleOpcUaExample.deps.jsonOpcUaExample.dllOpcUaExample.pdbOpcUaExample.runtimeconfig.jsonPDComm.OpcUa.dlllibcrypto.3.dyliblibpdcomm_opcua_ffi.dyliblibssl.3.dylibThe project file takes the SDK location from the PdcommOpcUaSdk property:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <!-- Default: the SDK root that contains examples/opcua/dotnet. --> <PdcommOpcUaSdk Condition="'$(PdcommOpcUaSdk)' == ''">$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)../../..'))</PdcommOpcUaSdk> </PropertyGroup> <ItemGroup> <Reference Include="PDComm.OpcUa"> <HintPath>$(PdcommOpcUaSdk)/lib/dotnet/PDComm.OpcUa.dll</HintPath> </Reference> <!-- The native SDK library and its OpenSSL runtime are loaded from the application directory. --> <None Include="$(PdcommOpcUaSdk)/lib/*.dylib;$(PdcommOpcUaSdk)/lib/*.so*;$(PdcommOpcUaSdk)/bin/*.dll" CopyToOutputDirectory="PreserveNewest" Visible="false" /> </ItemGroup> <Target Name="ValidatePdcommOpcUaSdk" BeforeTargets="BeforeBuild"> <Error Condition="!Exists('$(PdcommOpcUaSdk)/lib/dotnet/PDComm.OpcUa.dll')" Text="Set PdcommOpcUaSdk to the extracted PDComm OPC UA SDK root." /> </Target></Project>2. Read the code
Section titled “2. Read the code”// PDComm OPC UA .NET example: a secure server and a client in one console app.// server BIND_ADDRESS ENDPOINT_URL SERVER_CERT_DER SERVER_KEY_PEM TRUSTED_CLIENT_DER [--seconds N]// client ENDPOINT CLIENT_CERT_DER CLIENT_KEY_PEM TRUSTED_SERVER_DER [--policy P] [--mode M] [--node ID] [--write VALUE]// Exit codes: 0 success, 1 a Bad OPC UA status, 2 invalid arguments, 3 failure.// Credentials are never printed.using System.Diagnostics;using System.Runtime.InteropServices;using System.Security.Cryptography;using System.Text;using PDComm.OpcUa;using PDComm.OpcUa.Example;
const string Usage = """Usage: OpcUaExample server BIND_ADDRESS ENDPOINT_URL SERVER_CERT_DER SERVER_KEY_PEM TRUSTED_CLIENT_DER [--seconds N] OpcUaExample client ENDPOINT CLIENT_CERT_DER CLIENT_KEY_PEM TRUSTED_SERVER_DER [--policy aes256sha256rsapss|aes128sha256rsaoaep|basic256sha256|none] [--mode signandencrypt|sign|none] [--node ID] [--write VALUE]""";
try{ if (args.Length >= 6 && args[0] == "server") return RunServer(args[1..]); if (args.Length >= 5 && args[0] == "client") return RunClient(args[1..]); Console.Error.WriteLine(Usage); return 2;}catch (ArgumentException e){ Console.Error.WriteLine($"Error: {e.Message}"); return 2;}catch (OpcUaException e){ Console.Error.WriteLine($"Error: SDK status {e.NativeStatus}: {e.Message}"); return 3;}catch (Exception e) when (e is IOException or InvalidDataException or TimeoutException or PlatformNotSupportedException){ Console.Error.WriteLine($"Error: {e.Message}"); return 3;}
static byte[] Load(string path){ var info = new FileInfo(path); if (!info.Exists || info.Length == 0 || info.Length > 64 * 1024) throw new IOException($"Cannot read {path}"); return File.ReadAllBytes(path);}
static Dictionary<string, string> Options(string[] values, params string[] allowed){ if (values.Length % 2 != 0) throw new ArgumentException("Options need a value"); var result = new Dictionary<string, string>(); for (int i = 0; i < values.Length; i += 2) { if (!allowed.Contains(values[i]) || !result.TryAdd(values[i], values[i + 1])) throw new ArgumentException($"Unknown or repeated option {values[i]}"); } return result;}
static int RunServer(string[] a){ var options = Options(a[5..], "--seconds"); uint seconds = 0; if (options.TryGetValue("--seconds", out var text) && (!uint.TryParse(text, out seconds) || seconds is 0 or > 86400)) throw new ArgumentException("--seconds must be between 1 and 86400"); byte[] key = Load(a[3]); Server server; try { server = Server.Create(a[0], new ServerOptions { Endpoint = a[1], CertificateDer = Load(a[2]), PrivateKeyPem = key, TrustedClientDer = Load(a[4]), }); } finally { CryptographicOperations.ZeroMemory(key); } // The SDK copied the identity. using (server) { server.AddVariable(UaBinary.NodeId(1001), "Value", UaBinary.Int32Variant(42), VariableFlags.Writable | VariableFlags.Public); server.AddVariable(UaBinary.NodeId(1002), "Limit", UaBinary.Int32Variant(100), VariableFlags.Public); server.AddVariable(UaBinary.NodeId(1003), "Protected", UaBinary.Int32Variant(7), VariableFlags.Writable); // Register the stop signals before announcing readiness. using var stop = new ManualResetEventSlim(); using var interrupt = PosixSignalRegistration.Create(PosixSignal.SIGINT, c => { c.Cancel = true; stop.Set(); }); using var terminate = PosixSignalRegistration.Create(PosixSignal.SIGTERM, c => { c.Cancel = true; stop.Set(); }); // Windows Ctrl+Break (SIGQUIT in .NET), e.g. from a service manager. using var breakSignal = OperatingSystem.IsWindows() ? PosixSignalRegistration.Create(PosixSignal.SIGQUIT, c => { c.Cancel = true; stop.Set(); }) : null; using (var address = server.LocalAddress()) Console.WriteLine($"Listening on {Encoding.UTF8.GetString(address.Copy())}"); var clock = Stopwatch.StartNew(); while (!stop.IsSet && (seconds == 0 || clock.Elapsed < TimeSpan.FromSeconds(seconds))) { server.Poll(); stop.Wait(1); } using var snapshot = server.ReadValue(UaBinary.NodeId(1001)); var value = UaBinary.DecodeDataValue(snapshot.Copy()); server.Close(); Console.WriteLine($"Final Value={value.Value}"); Console.WriteLine("Server stopped"); return 0; }}
static int RunClient(string[] a){ var options = Options(a[4..], "--policy", "--mode", "--node", "--write"); var policy = options.GetValueOrDefault("--policy", "aes256sha256rsapss") switch { "aes256sha256rsapss" => SecurityPolicy.Aes256Sha256RsaPss, "aes128sha256rsaoaep" => SecurityPolicy.Aes128Sha256RsaOaep, "basic256sha256" => SecurityPolicy.Basic256Sha256, "none" => SecurityPolicy.None, _ => throw new ArgumentException("Unknown --policy"), }; var mode = options.GetValueOrDefault("--mode", policy == SecurityPolicy.None ? "none" : "signandencrypt") switch { "signandencrypt" => SecurityMode.SignAndEncrypt, "sign" => SecurityMode.Sign, "none" => SecurityMode.None, _ => throw new ArgumentException("Unknown --mode"), }; if ((policy == SecurityPolicy.None) != (mode == SecurityMode.None)) throw new ArgumentException("--policy none and --mode none must be used together"); uint node = 1001; if (options.TryGetValue("--node", out var nodeText) && (!uint.TryParse(nodeText, out node) || node == 0)) throw new ArgumentException("Invalid --node"); int? write = null; if (options.TryGetValue("--write", out var writeText)) write = int.TryParse(writeText, out int parsed) ? parsed : throw new ArgumentException("Invalid --write");
Console.WriteLine($"PDComm OPC UA ABI 0x{Client.AbiVersion:x8}"); using (var endpoints = Client.Discover(a[0], 5000)) foreach (var (url, endpointMode, uri) in UaBinary.DecodeEndpoints(endpoints.Copy())) Console.WriteLine($"Endpoint {url} mode={(SecurityMode)endpointMode} policy={uri}"); byte[] key = Load(a[2]); Client client; try { client = Client.Connect(a[0], new ClientOptions { Policy = policy, Mode = mode, AllowNone = policy == SecurityPolicy.None, TimeoutMs = 5000, CertificateDer = Load(a[1]), PrivateKeyPem = key, TrustedServerDer = Load(a[3]), }); } finally { CryptographicOperations.ZeroMemory(key); } // The SDK copied the identity. using (client) { client.ActivateAnonymous(); Console.WriteLine("Session activated (anonymous)"); int result = 0; if (write is int value) { var (service, status) = UaBinary.DecodeWrite(Exchange(client, UaBinary.Write(node, value))); Console.WriteLine($"Write ns=2;i={node} value={value} service=0x{service:x8} status=0x{status:x8}"); if (((service | status) & 0x80000000u) != 0) result = 1; } var (readService, data) = UaBinary.DecodeRead(Exchange(client, UaBinary.Read(node))); string shown = data?.Value?.ToString() ?? "none"; uint itemStatus = data?.Status ?? 0; Console.WriteLine($"Read ns=2;i={node} value={shown} service=0x{readService:x8} status=0x{itemStatus:x8}"); if (((readService | itemStatus) & 0x80000000u) != 0) result = 1; client.Close(); Console.WriteLine("Session closed"); return result; }}
// Sends one request and polls the client until its response arrives.static byte[] Exchange(Client client, byte[] request){ uint id = client.Begin(request); var clock = Stopwatch.StartNew(); while (clock.Elapsed < TimeSpan.FromSeconds(5)) { client.Poll(); using var response = client.Take(id); if (response is not null) return response.Copy(); Thread.Sleep(1); } throw new TimeoutException("Response timed out");}- Server.
Server.Createbinds the address andServer.AddVariableadds three variables.PosixSignalRegistrationcatches Ctrl+C and SIGTERM to end the loop. - Client.
Client.Connectopens the secure channel and session; right after,CryptographicOperations.ZeroMemorywipes the private key array. - Requests.
ExchangerepeatsClient.Begin,PollandClient.Take;Takereturnsnulluntil the response arrives. - Disposal.
Client,ServerandResponseareIDisposable;usingreleases their native handles. - Errors. A failed C ABI call throws
OpcUaException, whoseNativeStatusis the C ABI status.
UaBinary.cs builds the request messages. Like ua_binary.h in the C examples, it’s a small helper that encodes only Read and Write.
using System.Buffers.Binary;using System.Text;
namespace PDComm.OpcUa.Example;
// Minimal OPC UA Binary (Part 6) helpers for this example: one Read or Write of// the Value attribute of a numeric NodeId in namespace 2, scalar Int32 only.// Use a complete encoder in production code for every service you call.internal static class UaBinary{ private const uint ReadRequest = 631, ReadResponse = 634, WriteRequest = 673, WriteResponse = 676; private const uint ServiceFault = 397, GetEndpointsResponse = 431, ValueAttribute = 13; private const byte Int32Type = 6;
public readonly record struct DataValue(uint Status, int? Value);
public static byte[] NodeId(uint id) => Encode(w => PutNodeId(w, 2, id)); public static byte[] Int32Variant(int value) => Encode(w => { w.Write(Int32Type); w.Write(value); });
public static byte[] Read(uint id) => Encode(w => { PutNodeId(w, 0, ReadRequest); PutRequestHeader(w); w.Write(0.0); w.Write(3); w.Write(1); // maxAge, timestampsToReturn=Neither, one node PutValueId(w, id); w.Write((ushort)0); w.Write(-1); // default dataEncoding });
public static byte[] Write(uint id, int value) => Encode(w => { PutNodeId(w, 0, WriteRequest); PutRequestHeader(w); w.Write(1); PutValueId(w, id); w.Write((byte)1); w.Write(Int32Type); w.Write(value); // DataValue with an Int32 value });
// Returns (ServiceResult, DataValue). A ServiceFault has no DataValue. public static (uint Service, DataValue? Value) DecodeRead(byte[] body) { var r = new Reader(body); (uint id, uint service) = r.ResponseHeader(); if (id == ServiceFault) { r.End(); return (service, null); } if (id != ReadResponse || r.Length() != 1) throw new InvalidDataException("Unexpected ReadResponse"); var value = r.DataValue(); r.SkipDiagnosticInfos(); r.End(); return (service, value); }
public static (uint Service, uint Status) DecodeWrite(byte[] body) { var r = new Reader(body); (uint id, uint service) = r.ResponseHeader(); if (id == ServiceFault) { r.End(); return (service, 0); } if (id != WriteResponse || r.Length() != 1) throw new InvalidDataException("Unexpected WriteResponse"); uint status = r.UInt32(); r.SkipDiagnosticInfos(); r.End(); return (service, status); }
public static DataValue DecodeDataValue(byte[] body) { var r = new Reader(body); var value = r.DataValue(); r.End(); return value; }
// Lists (endpoint URL, security mode, security policy URI) from GetEndpoints. public static List<(string Url, uint Mode, string Policy)> DecodeEndpoints(byte[] body) { var r = new Reader(body); (uint id, uint service) = r.ResponseHeader(); if (id != GetEndpointsResponse || (service & 0x80000000u) != 0) throw new InvalidDataException("Unexpected GetEndpointsResponse"); var result = new List<(string, uint, string)>(); for (int i = r.Length(); i > 0; --i) { string url = r.Text(); r.Text(); r.Text(); r.SkipLocalizedText(); r.UInt32(); r.Text(); r.Text(); for (int j = r.Length(); j > 0; --j) r.Text(); r.Text(); uint mode = r.UInt32(); string policy = r.Text(); for (int j = r.Length(); j > 0; --j) { r.Text(); r.UInt32(); r.Text(); r.Text(); r.Text(); } r.Text(); r.Take(1); result.Add((url, mode, policy)); } r.End(); return result; }
private static byte[] Encode(Action<BinaryWriter> body) { using var stream = new MemoryStream(); using (var writer = new BinaryWriter(stream)) body(writer); // little-endian return stream.ToArray(); } private static void PutNodeId(BinaryWriter w, ushort ns, uint id) { if (ns == 0 && id <= 0xff) { w.Write((byte)0); w.Write((byte)id); } else if (ns <= 0xff && id <= 0xffff) { w.Write((byte)1); w.Write((byte)ns); w.Write((ushort)id); } else { w.Write((byte)2); w.Write(ns); w.Write(id); } } // The client replaces authenticationToken, timestamp and requestHandle. private static void PutRequestHeader(BinaryWriter w) { PutNodeId(w, 0, 0); w.Write(0L); w.Write(0u); w.Write(0u); w.Write(-1); w.Write(5000u); PutNodeId(w, 0, 0); w.Write((byte)0); } private static void PutValueId(BinaryWriter w, uint id) { PutNodeId(w, 2, id); w.Write(ValueAttribute); w.Write(-1); }
private sealed class Reader(byte[] data) { private int offset; public ReadOnlySpan<byte> Take(int n) { if (n < 0 || n > data.Length - offset) throw new InvalidDataException("Truncated OPC UA message"); var span = data.AsSpan(offset, n); offset += n; return span; } public byte Byte() => Take(1)[0]; public ushort UInt16() => BinaryPrimitives.ReadUInt16LittleEndian(Take(2)); public uint UInt32() => BinaryPrimitives.ReadUInt32LittleEndian(Take(4)); public int Length() { int n = (int)UInt32(); if (n < -1 || n > data.Length - offset) throw new InvalidDataException("Invalid OPC UA length"); return n; } public string Text() { int n = Length(); return n <= 0 ? "" : Encoding.UTF8.GetString(Take(n)); } public void End() { if (offset != data.Length) throw new InvalidDataException("Trailing OPC UA bytes"); } public void SkipNodeId() { switch (Byte()) { case 0: Take(1); break; case 1: Take(3); break; case 2: Take(6); break; case 3: case 5: Take(2); Text(); break; case 4: Take(18); break; default: throw new InvalidDataException("Invalid NodeId"); } } public void SkipLocalizedText() { byte mask = Byte(); if ((mask & 1) != 0) Text(); if ((mask & 2) != 0) Text(); } public void SkipDiagnosticInfo(int depth = 0) { byte mask = Byte(); if (depth > 4 || (mask & 0x80) != 0) throw new InvalidDataException("Invalid DiagnosticInfo"); foreach (byte bit in new byte[] { 0x01, 0x02, 0x08, 0x04 }) if ((mask & bit) != 0) Take(4); if ((mask & 0x10) != 0) Text(); if ((mask & 0x20) != 0) Take(4); if ((mask & 0x40) != 0) SkipDiagnosticInfo(depth + 1); } public void SkipDiagnosticInfos() { for (int i = Length(); i > 0; --i) SkipDiagnosticInfo(); } public (uint Id, uint Service) ResponseHeader() { uint id = Byte() switch { 0 => Byte(), 1 => Byte() == 0 ? UInt16() : throw new InvalidDataException("Unexpected namespace"), 2 => UInt16() == 0 ? UInt32() : throw new InvalidDataException("Unexpected namespace"), _ => throw new InvalidDataException("Invalid encoding NodeId"), }; Take(12); uint service = UInt32(); SkipDiagnosticInfo(); for (int i = Length(); i > 0; --i) Text(); SkipNodeId(); byte encoding = Byte(); if (encoding is 1 or 2) Text(); else if (encoding != 0) throw new InvalidDataException("Invalid ExtensionObject"); return (id, service); } public DataValue DataValue() { byte mask = Byte(); int? value = null; uint status = 0; if ((mask & 0xc0) != 0) throw new InvalidDataException("Invalid DataValue"); if ((mask & 0x01) != 0) { if (Byte() != Int32Type) throw new InvalidDataException("Only scalar Int32 values are shown by this example"); value = (int)UInt32(); } if ((mask & 0x02) != 0) status = UInt32(); if ((mask & 0x04) != 0) Take(8); if ((mask & 0x10) != 0) Take(2); if ((mask & 0x08) != 0) Take(8); if ((mask & 0x20) != 0) Take(2); return new DataValue(status, value); } }}3. Start the server
Section titled “3. Start the server”In the first terminal, from the my-opcua-app folder, run the server:
dotnet out/OpcUaExample.dll server 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:48404. Run the client
Section titled “4. Run the client”In the second terminal, from the my-opcua-app folder, write -5 and read it back:
dotnet out/OpcUaExample.dll client opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" --write -5After the endpoint list, the output is:
Session activated (anonymous)Write ns=2;i=1001 value=-5 service=0x00000000 status=0x00000000Read ns=2;i=1001 value=-5 service=0x00000000 status=0x00000000Session closedThe options match the C example (--policy, --mode, --node, --write). Read with Basic256Sha256 and Sign:
dotnet out/OpcUaExample.dll client opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" --policy basic256sha256 --mode signSession activated (anonymous)Read ns=2;i=1001 value=-5 service=0x00000000 status=0x00000000Session closed5. See the errors
Section titled “5. See the errors”Request the None policy, which the server doesn’t advertise:
dotnet out/OpcUaExample.dll client opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/server.der" --policy nonePDComm OPC UA ABI 0x00000002Error: SDK status 5: Requested OPC UA endpoint not advertisedPass the wrong trusted server certificate:
dotnet out/OpcUaExample.dll client opc.tcp://localhost:4840/pdcomm "$PKI/client.der" "$PKI/client.key.pem" "$PKI/client.der"Error: SDK status 5: OPC UA security CertificateUntrustedBoth exit with code 3.
6. Clean up
Section titled “6. Clean up”Press Ctrl+C in the server’s terminal:
Final Value=-5Server stoppedCommon errors
Section titled “Common errors”| Symptom | Cause and fix |
|---|---|
Build error Set PdcommOpcUaSdk to the extracted PDComm OPC UA SDK root. |
The -p:PdcommOpcUaSdk path has no lib/dotnet/PDComm.OpcUa.dll. Pass the folder you unpacked the package into. |
The server ignores kill -INT |
A script started the server in the background (&), so the shell set SIGINT to be ignored, and the .NET runtime keeps that setting. Send SIGTERM with kill -TERM. |