Skip to content

Using open62541 from C#

Davy Triponney edited this page Feb 18, 2019 · 7 revisions

To use open62541 from C#, there are the following options:

  1. Compile open62541 to a DLL and use the DLLImport mechanism. http://www.codeguru.com/csharp/csharp/cs_data/article.php/c4217/Calling-Unmanaged-Code-Part-1--simple-DLLImport.htm
  2. Build a C++ wrapper library with the Microsoft CLI (common language infrastructure) extensions. The library is written in C++ and can hence use open62541. Towards C#, the library exports managed data structures with garbage collection and so on. This is explained in detail further below.
  3. Build a open62541 wrapper with C# interop via IPC (inter process communication). For example via sockets. This was suggested by @ichrispa here: https://github.com/open62541/open62541/issues/783

Wrap open62541 in a C++/CLI library that can be used from C#

The following text and code was originally provided by @davy7125 in this issue: https://github.com/open62541/open62541/issues/234

Building open62541 with visual studio

First, I compiled open62541.h and open62541.cpp using Visual C++ 2013, which is compatible with C99. The project I created is a C++ project, excluding the Common Language Runtime Support (no managed code). I linked the project against winsock, which is w2_32.lib on Windows 7. When the build is done, open62541.dll and open62541.lib are created.

Creating a C++ / CLI project

Then I created a C++/CLI dll project. I added the .h you provided and linked against open62541.lib previously built. open62541.dll is in the project directory.

Note: for compatibility reason with my other projects I had to use Visual Studio 2010 and C99 is not enabled. I had to change these lines in open62541.h

#include <stdint.h>     =>   #include "stdint.h"
#include <stdbool.h>    =>   #include "stdbool.h"

I manually added the two files "stdint.h" and "stdbool.h" in the project.

I then created classes to interface with the library. Please note that it's in an early stage of the development and that only basic features are provided. I'm using log4net as logger. All the code has not been tested (retrieving a string or using Guid for instance).

LibOpen62541.h

#pragma once

#include <string>
#include "open62541.h"
#include "Windows.h"
#include "UAGuid.h"
#include "ILogger.h"
using namespace System;
using namespace System::Runtime::InteropServices;

// Stands as a forward declaration for the opaque struct UA_Client,
// and a hack to get rid of the LNK4248 warnings
struct UA_Client {};

// Wrapper for the library "open62541"
public ref class LibOpen62541
{
public:
  LibOpen62541(ILogger^ logger);
  ~LibOpen62541() { this->!LibOpen62541(); } // The destructor calls the finalizer
  !LibOpen62541(); // Disconnect and free unmanaged resources
  
  // Try to connect to an address (if the connection is not already done)
  // Return true if connected
  bool Connect(String^ hostAddress);

  // Retrieve a value which can be a bool, byte, char, (U)Int16/32/64, float, double or String^
  // The parameter can be a String^, an Integer or a UAGuid
  // Example: Get<Int32>("the.answer");
  template<typename T> T Get(int namespaceIndex, Object^ parameter);
  template<> String^ Get<String^>(int namespaceIndex, Object^ parameter);

private:
  template<typename T> int GetType();
  template<> int GetType<bool>()      { return UA_TYPES_BOOLEAN; }
  template<> int GetType<char>()      { return UA_TYPES_SBYTE; }
  template<> int GetType<byte>()      { return UA_TYPES_BYTE; }
  template<> int GetType<Int16>()     { return UA_TYPES_INT16; }
  template<> int GetType<UInt16>()    { return UA_TYPES_UINT16; }
  template<> int GetType<Int32>()     { return UA_TYPES_INT32; }
  template<> int GetType<UInt32>()    { return UA_TYPES_UINT32; }
  template<> int GetType<Int64>()     { return UA_TYPES_INT64; }
  template<> int GetType<UInt64>()    { return UA_TYPES_UINT64; }
  template<> int GetType<float>()     { return UA_TYPES_FLOAT; }
  template<> int GetType<double>()    { return UA_TYPES_DOUBLE; }
  template<> int GetType<UA_String>() { return UA_TYPES_STRING; }

  static std::string ConvertToStandardString(String^ managedStr);
  static UA_Guid ConvertToGuid(UAGuid^ managedGuid);

  ILogger^ m_log;
  GCHandle m_logHandle;
  UA_Client * m_client;
  bool m_connected;
};

LibOpen62541.cpp

#include "LibOpen62541.h"


// Two levels for the logger callback because functions with variable arguments
// cannot comprise managed types (such as the logger)
static void * LOG_HANDLE = NULL;
static const int BUFFER_LENGTH = 256;
static void LOG_LEVEL_2(UA_LogLevel level, UA_LogCategory category, const char *msg)
{
  if (LOG_HANDLE) {
    // Retrieve the managed logger
    using System::Runtime::InteropServices::GCHandle;
    GCHandle gch = GCHandle::FromIntPtr((IntPtr)LOG_HANDLE);
    ILogger^ log = (ILogger^)gch.Target;

    // Preparation of the message
    String ^text = "[Open62541, ";
    switch (category) {
    case UA_LOGCATEGORY_NETWORK: text += "network"; break;
    case UA_LOGCATEGORY_SECURECHANNEL: text += "secure channel";  break;
    case UA_LOGCATEGORY_SESSION: text += "session"; break;
    case UA_LOGCATEGORY_SERVER: text += "server"; break;
    case UA_LOGCATEGORY_CLIENT: text += "client"; break;
    case UA_LOGCATEGORY_USERLAND: text += "userland"; break;
    case UA_LOGCATEGORY_SECURITYPOLICY: text += "security policy"; break;
    default: text += "unknown category"; break;
    }
    text += "] " + gcnew String(msg);

    // Dispatch the message according to the severity level
    switch (level) {
    case UA_LOGLEVEL_TRACE:   log->Trace(text);   break;
    case UA_LOGLEVEL_DEBUG:   log->Debug(text);   break;
    case UA_LOGLEVEL_INFO:    log->Info(text);    break;
    case UA_LOGLEVEL_WARNING: log->Warning(text); break;
    case UA_LOGLEVEL_ERROR:   log->Error(text);   break;
    case UA_LOGLEVEL_FATAL:   log->Fatal(text);   break;
    }
  }
}

#pragma unmanaged // Remove warning C4793: 'anonymous namespace' for LOGGER
#pragma warning(disable: 4996) // Disable deprecation (vsnprintf function)
static void LOG_LEVEL_1(void *logContext, UA_LogLevel level, UA_LogCategory category, const char * msg, va_list args)
{
  char buffer[BUFFER_LENGTH];
  vsnprintf(buffer, BUFFER_LENGTH, msg, args);
  LOG_LEVEL_2(level, category, buffer);
}
#pragma warning(default: 4996) // Restore deprecation
#pragma managed



LibOpen62541::LibOpen62541(ILogger^ logger) :
  m_log(logger),
  m_client(NULL),
  m_connected(false)
{
  m_logHandle = GCHandle::Alloc(m_log);
  IntPtr pointer = GCHandle::ToIntPtr(m_logHandle);
  LOG_HANDLE = pointer.ToPointer();
}

LibOpen62541::!LibOpen62541()
{
  // Disconnect client and destroy it
  if (m_client) {
    if (m_connected)
      UA_Client_disconnect(m_client);
    UA_Client_delete(m_client);
    m_client = NULL;
  }
  m_connected = false;

  // Free the handle on the log
  // The object targeted can now be garbage collected if there is no more references to it
  m_logHandle.Free();
}

bool LibOpen62541::Connect(String^ hostAddress)
{
  // Client configuration and creation
  if (!m_client) {
    m_client = UA_Client_new();
    if (m_client == NULL) {
      m_log->Error("Failed to create a client");
      return false;
    }
  }
  
  // Connection if necessary
  if (m_client && !m_connected) {
    // Default configuration
    UA_ClientConfig *cc = UA_Client_getConfig(m_client);
    UA_ClientConfig_setDefault(cc);
    cc->logger.log = &LOG_LEVEL_1;

    UA_StatusCode  retVal = UA_Client_connect(m_client, (char *)ConvertToStandardString(m_hostAddress).c_str());
    if (retVal == UA_STATUSCODE_GOOD) {
      m_connected = true;
      m_log->Info("Connection to " + hostAddress + " => success");
    } else
      m_log->Error("Connection to " + hostAddress + " => error " + ((UInt32)retVal).ToString("X4"));
  }

  return m_connected;
}

// Generic "Get", for all types described in the template instanciations (listed below)
template<typename T> T LibOpen62541::Get(int namespaceIndex, Object^ parameter) 
{
  if (!m_connected)
    throw gcnew Exception("Not connected");

  // Preparation of the query
  UA_ReadRequest req;
  UA_ReadRequest_init(&req);
  req.nodesToRead = UA_ReadValueId_new();
  req.nodesToReadSize = 1;
  if (parameter->GetType() == String::typeid)
    req.nodesToRead[0].nodeId = UA_NodeId_fromCharStringCopy(namespaceIndex, ConvertToStandardString((String^)parameter).c_str());
  else if (parameter->GetType() == Int32::typeid)
    req.nodesToRead[0].nodeId = UA_NodeId_fromInteger(namespaceIndex, safe_cast<int>(parameter));
  else if (parameter->GetType() == UAGuid::typeid)
    req.nodesToRead[0].nodeId = UA_NodeId_fromGuid(namespaceIndex, ConvertToGuid((UAGuid^)parameter));
  else {
    UA_ReadRequest_deleteMembers(&req);
    throw gcnew ArgumentException("Invalid type for 'parameter'");
  }
  req.nodesToRead[0].attributeId = UA_ATTRIBUTEID_VALUE;

  // Get an answer and delete the query
  UA_ReadResponse resp = UA_Client_read(m_client, &req);
  UA_ReadRequest_deleteMembers(&req);

  // Process the result, then return it or throw an exception
  const UA_DataType * expectedType = &UA_TYPES[GetType<T>()];
  if (resp.responseHeader.serviceResult == UA_STATUSCODE_GOOD && resp.resultsSize > 0 && resp.results[0].hasValue &&
    UA_Variant_isScalar(&resp.results[0].value) && resp.results[0].value.type == expectedType) {
    T valRet = *(T*)resp.results[0].value.data;
    UA_ReadResponse_deleteMembers(&resp);
    return valRet;
  } else {
    String^ error = "Cannot get the value " + parameter->ToString() + ":\n" +
      "Service result = " + ((UInt32)resp.responseHeader.serviceResult).ToString("X4") + "\n" +
      "Result size = " + resp.resultsSize;
    if (resp.resultsSize > 0)
      error += "\nHas value = " + resp.results[0].hasValue + "\n" +
      "Is scalar = " + UA_Variant_isScalar(&resp.results[0].value) + "\n" +
      "Type matching = " + (expectedType == resp.results[0].value.type);
    m_log->Error(error);
    UA_ReadResponse_deleteMembers(&resp);
    throw gcnew Exception(error);
  }
}
template bool      LibOpen62541::Get<bool>     (int namespaceIndex, Object^ parameter);
template char      LibOpen62541::Get<char>     (int namespaceIndex, Object^ parameter);
template byte      LibOpen62541::Get<byte>     (int namespaceIndex, Object^ parameter);
template Int16     LibOpen62541::Get<Int16>    (int namespaceIndex, Object^ parameter);
template UInt16    LibOpen62541::Get<UInt16>   (int namespaceIndex, Object^ parameter);
template Int32     LibOpen62541::Get<Int32>    (int namespaceIndex, Object^ parameter);
template UInt32    LibOpen62541::Get<UInt32>   (int namespaceIndex, Object^ parameter);
template Int64     LibOpen62541::Get<Int64>    (int namespaceIndex, Object^ parameter);
template UInt64    LibOpen62541::Get<UInt64>   (int namespaceIndex, Object^ parameter);
template float     LibOpen62541::Get<float>    (int namespaceIndex, Object^ parameter);
template double    LibOpen62541::Get<double>   (int namespaceIndex, Object^ parameter);
template UA_String LibOpen62541::Get<UA_String>(int namespaceIndex, Object^ parameter);

// Specialization of the method "Get" for the type String^ (a conversion is needed)
template<> String^ LibOpen62541::Get<String^>(int namespaceIndex, Object^ parameter) 
{
  UA_String uaStr = Get<UA_String>(namespaceIndex, parameter);
  return gcnew String((char*)uaStr.data, 0, uaStr.length);
}

std::string LibOpen62541::ConvertToStandardString(String^ managedStr)
{
  std::string str;
  str.resize(managedStr->Length + 1);
  for (int i = 0; i < managedStr->Length; i++)
    str[i] = (char)managedStr[i];
  str[managedStr->Length] = '\0';
  return str;
}

UA_Guid LibOpen62541::ConvertToGuid(UAGuid^ managedGuid)
{
  UA_Guid guid;
  guid.data1 = managedGuid->m_data1;
  guid.data2 = managedGuid->m_data2;
  guid.data3 = managedGuid->m_data3;
  for (int i = 0; i < 8; i++)
    guid.data4[i] = managedGuid->m_data4[i];
  return guid;
}

ILogger.h

#pragma once

// Interface for a logger used in LibOpen62541
public interface class ILogger
{
public:
  void Trace(String^ message);
  void Debug(String^ message);
  void Info(String^ message);
  void Warning(String^ message);
  void Error(String^ message);
  void Fatal(String^ message);
};

UAGuid.h

#pragma once

using namespace System;

// Simple managed class to store Guid data
ref class UAGuid
{
public:
  UAGuid(UInt32 data1, UInt16 data2, UInt16 data3, array<Byte>^ data4) :
    m_data1(data1), m_data2(data2), m_data3(data3), m_data4(data4)
  {
    if (data4->Length != 8)
      throw gcnew Exception("UAGuid constructor: wrong size for data4 (should be 8)");
  }
  
  UInt32 m_data1;
  UInt16 m_data2, m_data3;
  array<Byte>^ m_data4;

  virtual String^ ToString() override
  {
    return "Guid: " + m_data1 + "/" + m_data2 + "/" + m_data3 + "/" + m_data4;
  }
};

Using the library in a C# project

To use this library in a C# project:

  • create a class inheriting from ILogger,
  • create a new LibOpen62541 by passing an instance of your logger,
  • call the method Connect with an address such as "opc.tcp://localhost:16664",
  • retrieve values with the method Get

If the server provided as an example is used, Get<Int32>(1, "the.answer") should return 42.

The dll "open62541.dll" has to be in the project directory, or found via the path.

More information

Implicit linking on Windows requires a .lib file along with the .dll file (contrary to linux which only needs a .so file). This is explained here.

I didn't try explicit linking with libopen62541.dll since it's a longer method and also because I had a memory violation error in a C# project using it.