I was looking for an answer also as it's my first time working with C++/CLI. So all this native managed and unmanaged combo was bit confusing to me as i come from (standard) C++.
In a nutshell:
Ref classes cannot inherit non-ref classes. And non-ref classes cannot have normal reference or pointer to a ref class because it's in garbage collected heap, which relocates objects. Once it's relocated, stored reference/pointer will keep pointing to the same place which contains something else. This is where gcroot comes.
In your C++/CLI wrapper you need to have non-ref class that will implement observer pattern. That class should somehow have or obtain managed reference to the ref class which it can keep with gcroot
member. This gcroot
member reference will be the bridge between unmanaged and managed code. Here's a working code.
There is also object pinning in gc heap but you cannot keep the object pinned for the whole life of the process, as pinning have to be done in some function on stack. I imagine that sort of thing could be possible but it's bad design and bad performance.
Native dll written in pure C++ (unmanaged), observable class and observer interface:
// PureCppDll.h
#include "stdafx.h"
#ifdef PURECPPDLL_EXPORTS
#define PURECPPDLL_API __declspec(dllexport)
#else
#define PURECPPDLL_API __declspec(dllimport)
#endif
struct PURECPPDLL_API Feature
{
int val;
};
class PURECPPDLL_API IFeatureObserver
{
public:
virtual ~IFeatureObserver() = default;
virtual void onFeatureUpdate(Feature* feature) = 0; // observer takes ownership of the feature
};
class PURECPPDLL_API NativeDllAPI
{
public:
virtual ~NativeDllAPI() = default;
virtual void addFeatureObserver(IFeatureObserver* observer) = 0;
virtual void removeFeatureObserver(IFeatureObserver* observer) = 0;
};
class PURECPPDLL_API NativeDllApiFactory
{
public:
static NativeDllAPI* create();
};
C++/CLI dll code, wiring native (unmanaged) dll and C#:
// FeatureObserver.h
#pragma once
#include "PureCppDll.h"
#include <vcclr.h>
namespace CppCLI
{
ref class CppCLIWrapper;
class FeatureObserver : public IFeatureObserver
{
public:
FeatureObserver(CppCLIWrapper^ cppcliwrapper);
~FeatureObserver();
void onFeatureUpdate(Feature* feature) override;
private:
gcroot<CppCLIWrapper^> m_cppCliWrapper;
};
}
//FeatureObserver.cpp
#include "stdafx.h"
#include "FeatureObserver.h"
#include "CppCLIWrapper.h"
namespace CppCLI
{
FeatureObserver::FeatureObserver(CppCLIWrapper^ cppcliwrapper)
: m_cppCliWrapper{ cppcliwrapper }
{
}
FeatureObserver::~FeatureObserver()
{
System::Console::WriteLine("~FeatureObserver : dieded.");
}
void FeatureObserver::onFeatureUpdate(Feature * feature)
{
System::Console::WriteLine("FeatureObserver : onFeatureUpdate.");
m_cppCliWrapper->NotifyFeatureChange(feature);
}
}
// CppCLIWrapper.h
#pragma once
#include "ManagedObject.h"
#include "PureCppDll.h"
#include "FeatureObserver.h"
#include <memory>
using namespace System;
namespace CppCLI
{
public ref class CppCLIWrapper
{
public:
CppCLIWrapper();
~CppCLIWrapper();
!CppCLIWrapper();
// slot:
void NotifyFeatureChange(Feature* feature);
// signal:
event EventHandler<MyFeatureEventArgs^>^ FeatureChangedEvent;
private:
NativeDllAPI* m_nativeDllApi;
FeatureObserver* m_featureObserver;
};
}
//CppCLIWrapper.cpp
#include "stdafx.h"
#include "CppCLIWrapper.h"
using namespace System;
namespace CppCLI
{
CppCLIWrapper::CppCLIWrapper()
: m_nativeDllApi { NativeDllApiFactory::create() }
, m_featureObserver{ new FeatureObserver{this} }
{
m_nativeDllApi->addFeatureObserver(m_featureObserver);
}
CppCLIWrapper::~CppCLIWrapper()
{
if (m_nativeDllApi != nullptr)
{
m_nativeDllApi->removeFeatureObserver(m_featureObserver);
delete m_nativeDllApi;
}
if (m_featureObserver != nullptr)
{
delete m_featureObserver;
}
System::Console::WriteLine("~CppCLIWrapper : dieded");
}
CppCLIWrapper::!CppCLIWrapper()
{
if (m_nativeDllApi != nullptr)
{
m_nativeDllApi->removeFeatureObserver(m_featureObserver);
delete m_nativeDllApi;
}
if (m_featureObserver != nullptr)
{
delete m_featureObserver;
}
System::Console::WriteLine("!CppCLIWrapper : finalized");
}
void CppCLIWrapper::NotifyFeatureChange(Feature * feature)
{
System::Console::WriteLine("CppCLIWrapper : NotifyFeatureChange {0}", feature->val);
auto myfeat = gcnew MyFeature{};
auto myfeatargs = gcnew MyFeatureEventArgs{};
myfeat->Value = feature->val;
myfeatargs->Feature = myfeat;
FeatureChangedEvent(this, myfeatargs);
delete feature;
}
}
The DataModel class library in C# for event raising from C++/CLI to C#:
using System;
namespace DataModel
{
public class MyFeature
{
public int Value { get; set; }
}
public class MyFeatureEventArgs : EventArgs
{
public MyFeature Feature { get; set; }
}
}
Finally, C# console application:
// Test.cs
using System;
using CppCLI;
namespace Test
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Starting C++/CLI wrapper for native code.");
Console.WriteLine("Thread will start producing and notifying Feature objects.");
Console.WriteLine("Press any key to stop.");
CppCLIWrapper cli = new CppCLIWrapper();
cli.FeatureChangedEvent += onFeatureChangedEvent;
Console.ReadKey();
Console.WriteLine("Program ending...");
}
}
}
I know it's an old question, I hope this helps someone.
Here's also the code for NativeDllApi class, in case you want to test it.
// PureCppDll.cpp
#include "stdafx.h"
#include "PureCppDll.h"
#include <thread>
#include <mutex>
class NativeDllAPIImpl : public NativeDllAPI
{
public:
NativeDllAPIImpl();
~NativeDllAPIImpl();
// Inherited via NativeDllAPI
virtual void addFeatureObserver(IFeatureObserver* observer) override;
virtual void removeFeatureObserver(IFeatureObserver* observer) override;
void run();
private:
void NotifyObservers();
private:
std::mutex m_mutex;
int m_value;
bool m_work;
std::thread m_worker;
std::vector<IFeatureObserver*> m_observers;
};
NativeDllAPI* NativeDllApiFactory::create()
{
return new NativeDllAPIImpl{};
}
NativeDllAPIImpl::NativeDllAPIImpl()
: NativeDllAPI {}
, m_value{ 42 }
, m_work{ true }
, m_worker{&NativeDllAPIImpl::run, this}
{
}
NativeDllAPIImpl::~NativeDllAPIImpl()
{
std::cout << "~NativeDllAPIImpl : destroying...\n";
m_work = false;
if (m_worker.joinable())
{
std::cout << "~NativeDllAPIImpl : join thread....\n";
m_worker.join();
}
std::cout << "~NativeDllAPIImpl : dieded...\n";
}
void NativeDllAPIImpl::addFeatureObserver(IFeatureObserver* observer)
{
std::cout << "NativeDllAPIImpl : addFeatureObserver\n";
std::lock_guard<std::mutex> lock{ m_mutex };
m_observers.push_back(observer);
}
void NativeDllAPIImpl::removeFeatureObserver(IFeatureObserver* observer)
{
std::cout << "NativeDllAPIImpl : removeFeatureObserver\n";
std::lock_guard<std::mutex> lock{ m_mutex };
m_observers.erase(std::remove(m_observers.begin(), m_observers.end(), observer), m_observers.end());
}
void NativeDllAPIImpl::run()
{
std::cout << "NativeDllAPIImpl : Running...\n";
while (m_work)
{
NotifyObservers();
Sleep(3000);
}
}
void NativeDllAPIImpl::NotifyObservers()
{
std::cout << "NativeDllAPIImpl : Notifying observers with value: " << m_value << '\n';
std::lock_guard<std::mutex> lock{ m_mutex };
for (auto observer : m_observers)
{
observer->onFeatureUpdate(new Feature{ m_value });
}
++m_value;
}