Qt/.NET — Hosting .NET code in a Qt application (1/3)
June 12, 2023 by Miguel Costa | Comments
Integration of Qt with .NET is an often sought-after feature. Given the uncertainty regarding the future of WPF, it is not surprising to see stakeholders turning to a time-tested UI framework like Qt as a way to future-proof their projects and existing .NET assets. Since its introduction in the early 2000's, .NET has evolved from its proprietary, Windows-centric origins into a free and open-source software framework, targeting multiple platforms and application domains. This makes it all the more relevant to offer a modern and practical way to integrate .NET and Qt.
That is what we're proposing with Qt/.NET, so it will be the topic of this three-part series of blog posts:
-
Hosting .NET code in a Qt application (this post)
-
Adding a QML view to a WPF application (coming soon)
-
Qt and Azure IoT in the Raspberry Pi OS (coming soon)
Qt/.NET is a standalone, header-only, C++ library that requires Qt 6 and the .NET runtime (v6 or greater). Project sources are located at code.qt.io and github.
Native interoperability
Typically, native interoperability in .NET is achieved through the Platform Invocation Services, or P/Invoke, of which there are two flavors, explicit and implicit. Explicit P/Invoke allows managed code in an assembly (i.e., a .NET DLL) to call directly into C-style functions inside native DLL's. In all but the simplest of cases, explicit P/Invoke requires awareness of low-level integration issues, such as calling convention, type safety, memory allocation, etc. Explicit P/Invoke is, therefore, better suited for making sporadic function calls into native libraries, such as the Windows API.
Interoperability through implicit P/Invoke corresponds to using C++/CLI to link managed and native code, which does have the advantage of hiding many of the low-level integration details exposed by explicit P/Invoke. However, this option relies on a platform-specific ("C++/CLI is a Windows OS specific technology"), closed-source toolchain. Such a limitation effectively defeats the purpose of integrating with a multi-platform, open-source framework like Qt.
Managed/native interoperability using P/Invoke.
Custom .NET host
Whichever the flavor, explicit or implicit, P/Invoke assumes that the initiative of managed/native interop is always on the side of .NET code. An alternative to this, one that "flips the script" on P/Invoke, is to implement a custom native host for the .NET runtime, and use that custom host to interact with managed code.
.NET applications require a native host as an entry-point, if nothing else, to start up the Common Language Runtime (CLR). The CLR is the application virtual machine that provides the running context for managed code, including a JIT compiler and garbage collector. A default "bootstrap" host is usually a part of the .exe that is generated when building a .NET application. But a native application can also implement its own custom host by means of the .NET native hosting API. The upshot is that, through the .NET hosting API, a native host is able to obtain references to .NET methods, and use those references to call into managed code, effectively achieving native/managed interoperability.
Native/managed interoperability through a custom .NET host.
From the perspective of native code, references to methods (identified in the figure above with the ⓕ glyph) are function pointers that can be used to directly invoke managed code. From the .NET side of things, a method reference is represented as a delegate. To obtain a reference to a .NET static method, the host calls a lookup function of the hosting API, providing as input the path to the target assembly, the type name, method name, and finally the associated delegate type.
At the most fundamental level, the Qt/.NET library exposes an implementation of such a custom .NET host, including facilities for method reference lookup. The result of the lookup is an instance of the QDotNetFunction<TResult, TArg...>
class, which is a functor that encapsulates the resolved function pointer and takes care of any required marshaling of parameters and return value.
|
|
Using the Qt/.NET host to resolve a .NET static method into a function pointer.
Native/managed adapter
The Qt/.NET host implementation is thus, on its own, sufficiently capable of calling into managed code. However, that only works for static methods, and requires that a compatible delegate type be defined in the same assembly as the target method. To work around these limitations, an adapter module is introduced which is able to generate, at run-time, the delegate types needed to instantiate method references and resolve the corresponding function pointers. The adapter is also responsible for several other tasks needed to bridge the native/managed divide, such as:
- Life-cycle of references to .NET objects, making sure that managed objects used by native code are not removed by the garbage collector, and also that no dangling references remain which could prevent garbage collection and cause memory leaks.
- Subscription and notification of .NET events, generating stub event handlers on the .NET side, which will then invoke native callbacks when subscribed events are raised.
Interoperability based on a custom .NET host and a native/managed adapter.
The adapter itself is not intended to be called directly from user code. Instead, the Qt/.NET C++ API encapsulates details of the adapter's interface by providing high-level proxy types (e.g. QDotNetType
, QDotNetObject
, etc.) that map to corresponding managed entities.
|
Using the Qt/.NET API to call a static method.
|
Using the Qt/.NET API to create a managed object and call an instance method.
.NET object as QObject
To achieve a seamless integration between native and managed code, it's possible to extend the QDotNetObject
class to define wrapper classes in C++ whose instances can function as proxies for .NET objects. This way, any details of the native/managed interoperability are completely hidden from calling code.
|
Wrapper for the StringBuilder .NET class.
Extending both QDotNetObject
and QObject
allows proxies of .NET objects to be used in Qt applications. This includes, for example, mapping notification of .NET events to emission of Qt signals, making it possible to connect .NET events to Qt slots.
|
QObject
wrapper for the Ping
.NET class, including conversion of events into signals.
QML UI for a .NET module
We conclude this post with excerpts from the Chronometer
example project, which is included in the Qt/.NET repository. We'll use these excerpts to illustrate, step by step, how to implement a QML application that provides a UI for an existing .NET module.
|
Chronometer
.NET class (excerpt).
The above snippet of C# code corresponds to an existing .NET asset which we want to provide a UI for. It consists of a model of a chronometer, with properties that correspond to the position of the various hands, and methods that represent the actions that can be taken when using a chronometer. For simplicity, we'll show only the code related to the ElapsedSeconds
property (i.e. the seconds hand of the chronometer), highlighted in yellow, and to the StartStop
method (i.e. the start and stop button), highlighted in orange.
Note that the Chronometer
class implements the INotifyPropertyChanged
interface, which means it will be able to notify changes to its properties by raising the PropertyChanged
event. This mechanism is used for property binding, notably in WPF.
Step 1: Defining a wrapper class
We start by defining the interface of the wrapper class (QChronometer
) that will function as a native proxy for the managed Chronometer
class. Properties of the .NET class are mapped to corresponding Qt properties, which ultimately means implementing the associated READ
functions and NOTIFY
signals. Methods of the .NET class are mapped to slots.
|
Step 2: Implementing the wrapper class
The following actions are required of the implementation of each part of the wrapper class.
QChronometer
constructor:- Invoke constructor of the .NET class and store object reference.
- Subscribe to the
"PropertyChanged"
event.
startStop
slot:- Invoke the
"StartStop"
method of the referenced .NET object. - (All other slots are implemented in the same way.)
elapsedSeconds
property read function:- Invoke the
"get_ElapsedSeconds"
method of the referenced .NET object. - Return the value that was originally returned by the .NET method.
- (All other property read functions are implemented in the same way.)
handleEvent
callback):- Cast event arguments to the
PropertyChangedEventArgs
class. - If the modified property was the
"ElapsedSeconds"
, emit theelapsedSecondsChanged
signal.- (All other property change events are handled in the same way.)
|
Step 3: Using the proxy in QML
In the QML UI specification, assuming the "chrono
" property corresponds to the wrapper object representing the .NET object, we can use its properties and slots to implement the UI. The elapsedSeconds
property, which will be synchronized with the ElapsedSeconds
property of the .NET object, will be used to calculate the rotation angle of the seconds handle. The clicked signal of a "Start/Stop" button will be connected to the startStop
slot of the wrapper, which invokes the StartStop
method of the .NET object.
|
Step 4: Putting it all together
In the application's main function, an instance of the wrapper class is created, which triggers the creation of the corresponding instance of the managed Chronometer
class. The QChronometer
wrapper is added to the QML engine as the "chrono
" property.
|
The screen capture below shows the Chronometer
example running in a Visual Studio debug session. The Start/Stop button was pressed, starting the chronometer mechanism. The number of elapsed seconds is translated into the rotation of the seconds handle.
Blog Topics:
Comments
Subscribe to our newsletter
Subscribe Newsletter
Try Qt 6.8 Now!
Download the latest release here: www.qt.io/download.
Qt 6.8 release focuses on technology trends like spatial computing & XR, complex data visualization in 2D & 3D, and ARM-based development for desktop.
We're Hiring
Check out all our open positions here and follow us on Instagram to see what it's like to be #QtPeople.