-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Using open62541 from C#
To use open62541 from C#, there are the following options:
- 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
- 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.
- 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
The following text and code was originally provided by @davy7125 in this issue: https://github.com/open62541/open62541/issues/234
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.
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).
#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;
};
#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;
}
#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);
};
#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;
}
};
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.
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.