Hi Community,
The following article is about one of the subjects I’m most passionate about that’s integration of disparate systems and cross-platform computing. I think, it’s something I had to publish and it’s one of the core components of my SIF project (Service Integration Framework). This approach can be applied to a variety of scenarios, like exposing performance counters on a *nix system that is hosted in the cloud (good example, a NAT server hosted AWS – Linux server without any UI elements but only allowing connection over HTTP/HTTPS/SSH). This also reminds me of a distributed CyberKiosk (Cyber Cafe) solution I built in Visual FoxPro like 20 years ago, unfortunately back then SOAP was pretty much unknown territory thus management and instrumentation of the networked computers was achievable through sockets only (and yes, I did try DCOM but back then it gave me more headaches than solutions).
Anyways, I’m done with the introduction so what’s this article about? Well, in a nutshell it’s part of my SIF project but it’s also also a PoC (Proof of Concept) of a Qt application hosting the CLR implemented in mono which in turn exposes a WCF service that allows me to instrument the target *nix system through SOAP, sounds cool, right? *nix systems as their Windows counterparts provide a variety of different performance counters and profiling tools, a good example can be the perf stat utility
angel@Obi-wan:~$ sudo perf stat -a
^C
Performance counter stats for 'system wide':
62568.846961 task-clock (msec) # 4.000 CPUs utilized (100.00%)
214,730 context-switches # 0.003 M/sec (100.00%)
11,889 cpu-migrations # 0.190 K/sec (100.00%)
2,019 page-faults # 0.032 K/sec
28,653,473,022 cycles # 0.458 GHz (100.00%)
39,173,539,651 stalled-cycles-frontend # 136.71% frontend cycles idle (100.00%)
<not supported> stalled-cycles-backend
9,286,112,645 instructions # 0.32 insns per cycle
# 4.22 stalled cycles per insn (100.00%)
1,977,739,115 branches # 31.609 M/sec (100.00%)
83,956,096 branch-misses # 4.25% of all branches
15.640948007 seconds time elapsed
angel@Obi-wan:~$
In order to understand how this solution was architected is important to mention that our Qt application hosts the CLR (more information on embedding mono here), once the AppDomain is created we pass a native functor to the managed world in order to bubble up messages back to the Qt application. C++11 introduced the std::function class template that’s passed to the CLR as a pointer and marshalled by our C# code, but I think it’s worthy to start from the very beginning and then we’ll describe this later.
Our Qt application implements a wrapper class (monowrapper) to interact with mono
#include "monowrapper.h"
monowrapper::monowrapper() {
}
bool monowrapper::Initialized_get() const {
return m_IsInitialized;
}
void monowrapper::UnloadAppDomain() {
if (m_IsInitialized)
mono_jit_cleanup(m_domain);
}
void monowrapper::CreateDomain(std::function<void(const char*)> logger) {
if (m_IsInitialized)
return;
m_logger = logger;
mono_set_dirs("/usr/lib/", "/etc/mono");
mono_config_parse(nullptr);
m_domain = mono_jit_init("./MonoDaemon.Service.dll");
m_assembly = mono_domain_assembly_open (m_domain, "./MonoDaemon.ServiceHost.dll");
if (m_assembly) {
MonoObject* ex = nullptr;
mono_domain_set_config(m_domain, "/etc/mono/4.5/", "machine.config");
m_monoImage = mono_assembly_get_image(m_assembly);
m_hostClass = mono_class_from_name(m_monoImage, "MonoDaemon", "ServiceHost");
m_hostInstance = mono_object_new(m_domain, m_hostClass);
auto ctorMethod = mono_class_get_method_from_name(m_hostClass, ".ctor", 0);
mono_runtime_invoke(ctorMethod, m_hostClass, nullptr, &ex);
auto hostInitializeMethod = mono_class_get_method_from_name(m_hostClass, "InitializeHost", 1);
void* args[1] = {&logger};
mono_runtime_invoke(hostInitializeMethod, m_hostInstance, args, &ex);
m_IsInitialized = true;
} else {
// Log & display error message here
}
}
The mainwindow class has an instance of the monowrapper class
#include "mainwindow.h"
#include "ui_mainwindow.h"
static const MainWindow* m_selfReference;
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
m_monoWrapper = std::unique_ptr<monowrapper>(new monowrapper);
m_selfReference = this;
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::LogMessage(const char *msg) {
auto msgAsStr = std::string(msg);
auto lstMessages = m_selfReference->findChild<QListWidget*>("lstMessages");
new QListWidgetItem(tr(msgAsStr.c_str()), lstMessages);
}
void MainWindow::on_btnStartHost_clicked()
{
std::function<void(const char*)> func = &MainWindow::LogMessage;
m_monoWrapper->CreateDomain(func);
}
void MainWindow::on_btnExitHost_clicked()
{
m_monoWrapper->UnloadAppDomain();
close();
}
Please note that I’m using a few features available in modern C++ like smart-pointers and the new std::function (which is the equivalent of Action<T> in .NET). Once our application has loaded the CLR we can see it in the “Loaded Modules” in Qt Creator (shown highlighted “mscorlib”)
In the C#/CLR side of things (mono) we can see how we get the void* from Qt as an IntPtr, we then pin it with GCHandle.Alloc (this is to prevent the GC to move it around) and we pass it across to service implementation too, therefore we can bubble up or fire notifications back to the native world (Qt application). Below the implementation of InitializeHost and ShowMessage method, please note how we need to add the null-operator to the string we’re returning through the callback otherwise our strings in the Qt application might have some “funny” characters at the end.
/// <summary>
/// Initializes the host.
/// </summary>
/// <param name="callback">Callback.</param>
public void InitializeHost (IntPtr callback) {
if (callback != IntPtr.Zero) {
ExternalCallback = GCHandle.Alloc (Marshal.GetDelegateForFunctionPointer<Utilities.QtExternalCode> (callback));
DaemonOperation.QtMessenger = new Action<string> (ShowMessage);
}
ShowMessage ("Creating binding...");
var binding = new BasicHttpBinding ();
binding.Security.Mode = BasicHttpSecurityMode.None;
ShowMessage ("Allocating endpoint addresses...");
var address = new Uri ("http://localhost:8888/monodaemon");
var addressMex = new Uri ("http://localhost:8888/monodaemon/mex");
ShowMessage ("Creating host...");
m_host = new System.ServiceModel.ServiceHost (typeof(DaemonOperation),
new Uri[] { new Uri ("http://localhost:8888/monodaemon") });
ShowMessage ("Adding behaviors...");
m_host.Description.Behaviors.Remove<System.ServiceModel.Description.ServiceMetadataBehavior> ();
m_host.Description.Behaviors.Add (new System.ServiceModel.Description.ServiceMetadataBehavior {
HttpGetEnabled = true, HttpsGetEnabled = false
});
ShowMessage ("Adding endpoints...");
m_host.AddServiceEndpoint (typeof(IDaemonOperation), binding, address);
m_host.AddServiceEndpoint (System.ServiceModel.Description.ServiceMetadataBehavior.MexContractName,
System.ServiceModel.Description.MetadataExchangeBindings.CreateMexHttpBinding (), addressMex);
ShowMessage ("Starting host...");
m_host.Open ();
}
/// <summary>
/// Shows the message.
/// </summary>
/// <param name="message">Message.</param>
protected void ShowMessage (string message) {
if (!string.IsNullOrEmpty (message)) {
Console.WriteLine (message);
var bytes = Encoding.Default.GetBytes (message);
var buffer = new byte[bytes.Length + 1];
Array.Copy (bytes, buffer, bytes.Length);
buffer [buffer.Length - 1] = (byte) '\0';
((Utilities.QtExternalCode)ExternalCallback.Target).Invoke (buffer);
}
}
As you can see, our native callback will be the interface between managed and native world. The ShowMessage method will output the notifications to the “Application Output” window in Qt Creator (System console)
but also to the ListBox (QListWidget) in the main window
The client application or the one driving the operations is a Windows Form application (depicted below)
One of the methods in our MonoDaemon (that’s the Qt application acting as WCF host, effectively) is StartRemoteProcess that can easily call a bash script or in this case we’re starting “gnome-calculator”. The image below shows my Ubuntu desktop along with “gnome-system-monitor” (highlighting qtMonoHost and mono is also visible there) and the application we started remotely (gnome-calculator)
The requests/responses are HTTP/SOAP so unlike my situation a few years back I had to do it with sockets. The WireShark image below shows the SOAP envelope returned to the client after having called the “GetRunningProcesses” method.
And that’s pretty much it, folks. Source code can be found here.
Regards,
Angel