Why?
When writing your first .NET Core application, the thing that strikes you is how the application is executing. Even .NET Core console application is built to a DLL file, not to EXE one. It is run with dotnet command providing a standard host for our DLL execution. The question may arise: would it be beneficial to create our custom host for .NET Core application execution? Apart from hypothetical optimization and performance enhancement at the moment, I see two benefits of the custom hosting.
The first one is protection of .NET Core binaries against reflection. As you may see, ordinary .NET Framework reflectors cannot reflect .NET Core DLL over. But it is relatively easy to write a dedicated reflector for .NET Core similarly to what Lutz Roeder did in the early days of .NET for the Framework binaries. .NET Core reflector would not be so popular since in a vast majority of the cases, .NET Core code is running in server side and therefore is not available for code finding reflection. But in cases when .NET Core DLLs are running on the client side, anti-reflection protection may be quite useful. In the simplest case, such a protection may be achieved with custom hosting as follows. Bytes in the DLL saved in disk are reshuffling according to some algorithm, and the custom host places the bytes in proper order in memory. Similar idea for .NET Framework for Windows was implemented in my article Anti-Reflector .NET Code Protection.
The second and IMHO more important benefit is connecting with upgrading of legacy software. Indeed, there are piles of old code around. Millions of lines written in ANSI C are built in both Windows and Linux and successfully serve customers to the day. And there is a trap with this code: huge resources and time are required to rewrite it, it is extremely difficult to find people to support these dinosaurs, and those creatures are still very much in use. Recently a friend of mine noted production C file written in 1993. This reminded me my childhood when in 1975 I attended a metallurgical plant where my father worked and saw there functioning steam engine on which was written "Birmingham 1895" (that was the last day of its service). But unlike the steam engine, the C file is still operational and its product is still in high demand. One of the promising ways to get out of the legacy code trap is to use new programming approaches / languages for development of new features and gradually and carefully rewrite the problematic old parts. This implies close and seamless collaboration between the old and new components. And custom .NET Core hosting can provide such a collaboration. Recently, I came across this article providing explanation and code sample for .NET Core custom hosting. Based on this code in this work, I try to present more general infrastructure for .NET Core custom hosting.
Compact software sample of this article is capable of hosting .NET Core component in unmanaged C/C++ code and ensure reciprocal methods calls between the parts. It runs in Windows and Linux alike.
Managed - Unmanaged Reciprocal Calls Mechanism
In order to host NET Core components, unmanaged C/C++ native application includes a C++ gateway class GatewayToManaged
which header file GatewayToManaged.h is given below:
#pragma once
#include "coreclrhost.h"
using namespace std;
typedef bool (*unmanaged_callback_ptr)(const char* actionName, const char* jsonArgs);
typedef char* (*managed_direct_method_ptr)(const char* actionName,
const char* jsonArgs, unmanaged_callback_ptr unmanagedCallback);
class GatewayToManaged
{
public:
GatewayToManaged();
~GatewayToManaged();
bool Init(const char* path);
char* Invoke(const char* funcName, const char* jsonArgs, unmanaged_callback_ptr unmanagedCallback);
bool Close();
private:
void* _hostHandle;
unsigned int _domainId;
managed_direct_method_ptr _managedDirectMethod;
void BuildTpaList(const char* directory, const char* extension, string& tpaList);
managed_direct_method_ptr CreateManagedDelegate();
#if WINDOWS
HMODULE _coreClr;
#elif LINUX
void* _coreClr;
#endif
};
Its method Init()
takes path to executing application and performs the following:
- loads
CoreCLR
components, - constructs Trusted Platform Assemblies (TPA) list,
- defines main
CoreCLR
properties, - starts the .NET Core runtime and creates the default (and only)
AppDomain
, and finally - creates an object
managed_direct_method_ptr _managedDirectMethod
permitting to call managed delegate.
In order to have uniform mechanism for any direct unmanaged-to-managed calls and for managed-to-unmanaged callbacks, we define one type (signature) for each case. Presented above header file GatewayToManaged.h provides those types: managed_direct_method_ptr
for direct managed delegate and unmanaged_callback_ptr
for unmanaged callback.
So, to call managed methods from unmanaged code, we need:
- create object of class
GatewayToManaged
- call its method
Init()
and then - perform actual managed call with method
Invoke()
which takes name of managed function and its arguments as stringified JSON object, pointer to unmanaged callback and returns char*
.
In the managed side component (DLL), GatewayLib contains public static class Gateway
which provides method ManagedDirectMethod()
shown below:
[return: MarshalAs(UnmanagedType.LPStr)]
public static string ManagedDirectMethod(
[MarshalAs(UnmanagedType.LPStr)] string funcName,
[MarshalAs(UnmanagedType.LPStr)] string jsonArgs,
UnmanagedCallbackDelegate dlgUnmanagedCallback)
{
_logger.Info($"ManagedDirectMethod(actionName: {funcName}, jsonArgs: {jsonArgs}");
string strRet = null;
if (_worker.Functions.TryGetValue(funcName,
out Func<string, string, UnmanagedCallbackDelegate, string> directCall))
{
try
{
strRet = directCall(funcName, jsonArgs, dlgUnmanagedCallback);
}
catch (Exception e)
{
strRet = $"ERROR in \"{funcName}\" invoke:{Environment.NewLine} {e}";
_logger.Error(strRet);
}
}
return strRet;
}
This method is called from unmanaged code, as it was shown above.
Method ManagedDirectMethod()
activates actual method specified by unmanaged caller using object IWorker _worker
. Interface IWorker
provides dictionary getter Dictionary<string, Func<string, string, UnmanagedCallbackDelegate, string>> Functions { get; }
Required unmanaged call handler is retrieved from the dictionary by funcName
key and is called with directCall()
. We assume that dictionary _worker.Functions
is filled on construction of the object and is not changed later - so it is thread safe.
Argument dlgUnmanagedCallback
is passed on to the handler and may be later used to asynchronous calls of unmanaged code. In our sample, processing of managed-to-unmanaged call is implemented with plain C function bool UnmanagedCallback(const char* actionName, const char* jsonArgs)
in file callback.c . To keep things uniform, arguments for both sides calls are given as JSON string
s. For parsing JSON in C code, I took this open source JSON parser.
With provided simple infrastructure, it will be easy to write code in both sides. Unmanaged part should create object of class GatewayToManaged
and run its method Init()
. After this, the gateway object is ready for invocation of managed methods. Unmanaged code should also provide appropriate callbacks if it expects they will be called by managed part. And managed part should simply implement methods to be called from its unmanaged host. Those methods will use unmanaged callbacks (which they got as argument) for host notification.
How to Run Code Sample
The sample was built and tested with .NET Core 2.1.7. But I don't see any reason why it wouldn't work in version 2.2 or any other versions of 2.1.
Important note. In the sample, Core CLR directory is hardcoded. It is defined as:
- C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.1.7 for Windows and
- /usr/share/dotnet/shared/Microsoft.NETCore.App/2.1.7 for Linux
If location in your machine is different, then the demo will not work, and source should be rebuilt with proper location (that is definition of CORECLR_DIR
in file GatewayToManaged.cpp).
To run demo, please unzip it and run host application file that is Host.exe for Windows and Host.out for Linux. Application will output in console. Messages from unmanaged code will be written in your console standard color, whereas managed code messages will by written in cyan. Both sides transfer objects of Device
class and its unmanaged peer struct Device
in C code. Unmanaged host calls (with mediation of Gateway.ManagedDirectMethod()
) methods GetDevice()
, SubscribeForDevice()
and UnsubscribeFromDevice()
of Worker component. On its execution, the first method calls unmanaged callback once, while the second method starts timer which calls unmanaged callback repeatedly providing streaming of device data from managed part to unmanaged one. The third method is called to unsubscribe from the device data streaming (it actually stops the timer to keep things simple).
Sources should be built and run as follows.
Windows
After you defined proper location of Core CLR directory, please build the entire solution.
Important note. Bitness (32- or 64-bit) of CoreCLR
should match with bitness in which the host was built. In our case the entire solution should be built in x64 mode.
Result will be placed to your $(SolutionDir)\x64 directory. Run Host.exe from there.
Linux
Since my development environment is Windows, I will describe how to move appropriate files to Linux (Ubuntu 18.04 in my case) and perform there only additional actions. Create directory Hosting and copy there all *.h, *.c and *.cpp files from your Windows directory $(SolutionDir)\Host and all [managed] DLL files from Windows directory $(SolutionDir)\x64. Managed DLLs will be used as they are, and unmanaged code should be built. The latter is performed with the following command:
g++ -o Host.out -D LINUX jsmn.c GatewayToManaged.cpp SampleHost.cpp -ldl
It will produce file Host.out that should run with ./Host.out command.
Conclusions
This work provides compact infrastructure for custom hosting of .NET Core component in unmanaged C/C++ code and ensures reciprocal methods calls between the parts. It runs in Windows and Linux alike. Such an approach may be useful for anti-reflect protection of .NET Core code and, most important, for upgrade of legacy code with new features written in .NET Core.