A little backstory:
Recently, I faced a task: to create a WFX plugin for Total Commander.
A WFX (Web File System) plugin for Total Commander is a plugin that allows users to access various remote file systems and services directly from the Total Commander interface. The .wfx file is essentially a renamed .dll (dynamic-link
library).
Primarily, I work with C#, but Total Commander, for which I had to create the plugin, is written in C++ and uses LoadLibrary for dynamically loading plugins. This imposes certain constraints and requires a special approach to ensure proper interaction between the runtime environments of the two languages.
С++ and Csharp
Calling C# code from C++ is more challenging for several reasons:
- Interoperability Challenges: C++ and C# have different runtime environments. C++ compiles directly into machine code, while C# runs on the .NET virtual machine (CLR). This requires interoperability configurations, adding complexity.
- Name Mangling: C++ uses name mangling to support function overloading and templates, which makes calling C++ functions from other languages, including C#, more complicated.
- Build Process: C++ requires specific tools for compilation and linking, complicating integration with C# projects built on .NET.
On the other hand, calling C++ code from C# is simpler due to the following:
- P/Invoke: C# supports the Platform Invocation (P/Invoke) mechanism, simplifying calls to native C/C++ libraries from C# code.
- Pointer Handling: C# provides robust tools for working with pointers and unsafe code, making interaction with native libraries easier.
- Minimal Configuration Requirements: The .NET runtime provides tools and libraries to simplify interaction with native code, reducing the need for manual configuration and memory management.
The article will describe the following integration methods between C++ and C#:
- Loading the .NET Runtime manually within C++ code and injecting a .NET assembly into it.
- Using Ahead-of-Time (AOT) compilation to build native libraries.
- Using a COM component for interaction.
- Modifying IL Code after building the library.
Test program for .dll
validation
Before diving into the approaches, a simple C++ program was developed to test the functionality of created .dll
files. The program includes the call_library
function for loading a library and verifying its functionality. This approach helps identify potential issues early, ensuring reliability and stability in the final product.
void call_library(const std::string& file_name)
{
const std::wstring name(file_name.begin(), file_name.end());
const auto library_handle = LoadLibrary(name.c_str());
if (library_handle == nullptr)
{
std::cout << "Cannot open library by path '" << file_name << "'\n";
return;
}
std::cout << "Success LoadLibrary(\"" << file_name << "\")\n";
const auto proc_name = "calculate";
const auto proc_address = (proc)GetProcAddress(library_handle, proc_name);
if (proc_address == nullptr)
{
std::cout << "Cannot get proc '" << proc_name << """' in file '" << file_name << "'\n";
return;
}
std::cout << "Success GetProcAddress(\"" << proc_name << "\")\n";
std::cout << "Calculation result is " << proc_address(1, 1) << '\n';
}
Loading .NET Runtime for .NET Core 3.0+
This example is the most complex among those presented. Yet, what could be simpler than initializing the .NET Runtime and loading an assembly into it, allowing you to retrieve function pointers as needed?
This approach demands a deep understanding of both C# and C++. You must carefully configure all aspects of cross-platform interaction to ensure correct code execution. While manually initializing the .NET Runtime might seem daunting, it is one of the most powerful methods for integrating C# and C++.
This method requires meticulous attention to detail and a solid knowledge of how the .NET platform operates. At first glance, it may appear overwhelming, but once you grasp all its nuances, you will be able to seamlessly combine two powerful programming languages into a cohesive solution.
As an example, let's build a simple C# API that will be invoked from a C++ library. The key feature of this method is defining a delegate
for the method signature.
The use of a delegate is optional and can be omitted entirely if desired. To achieve this, the exported method must be
annotated with theUnmanagedCallersOnly
attribute. This attribute specifies that the method can be called directly
from unmanaged code, bypassing the need for a delegate.When invoking the
load_assembly_and_get_function_pointer
function, instead of passing the delegate name, you simply
use the constantUNMANAGEDCALLERSONLY_METHOD
. This approach streamlines the process by removing the overhead of
defining and managing a delegate while ensuring seamless interoperability between managed and unmanaged code.For a practical example of this implementation, refer to the provided
example here.
public static class Api
{
public delegate int CalculateDelegate(int a, int b);
public static int Calculate(int a, int b) => a + b;
}
To run the resulting assembly, we need to write some C++ code that initializes the .NET Runtime, loads our assembly, and provides a pointer to the desired function.
The initialization of the .NET Runtime relies on hostfxr.dll
, which provides the following functions:
-
hostfxr_initialize_for_runtime_config
- Initializes the .NET Runtime using the runtime configuration file. -
hostfxr_get_runtime_delegate
- Used to retrieve theload_assembly_and_get_function_pointer_fn
function. -
load_assembly_and_get_function_pointer_fn
- Used to obtain a pointer to a function from the .NET assembly. -
hostfxr_close
- A function to shut down the .NET Runtime.
typedef int (*proc)(int, int);
void run()
{
// Step 1: Get hostfxr procedures
const auto hostfxr_library = LoadLibraryW(L"hostfxr.dll");
const auto init_proc = "hostfxr_initialize_for_runtime_config";
const auto runtime_proc = "hostfxr_get_runtime_delegate";
const auto close_proc = "hostfxr_close";
const auto init_fptr = (hostfxr_initialize_for_runtime_config_fn)GetProcAddress(hostfxr_library, init_pr
const auto get_delegate_fptr = (hostfxr_get_runtime_delegate_fn)GetProcAddress(hostfxr_library, runtime_
const auto close_fptr = (hostfxr_close_fn)GetProcAddress(hostfxr_library, close_proc);
if (!(init_fptr && get_delegate_fptr && close_fptr))
throw std::runtime_error("Failed to load hostfxr");
const fs::path runtime_config_path = params_.assembly + L".runtimeconfig.json";
// Step 2: Initialize the .NET Core runtime
hostfxr_handle context = nullptr;
int rc = init_fptr(runtime_config_path.c_str(), nullptr, &context);
if (rc != 0 || context == nullptr)
throw std::runtime_error("Failed to initialize runtime");
// Step 3: Get the load_assembly_and_get_function_pointer delegate
load_assembly_and_get_function_pointer_fn load_assembly_and_get_function_pointer = nullptr;
rc = get_delegate_fptr(context,
hdt_load_assembly_and_get_function_pointer,
(void**)&load_assembly_and_get_function_pointer);
if (rc != 0 || load_assembly_and_get_function_pointer == nullptr)
{
close_fptr(context);
throw std::runtime_error("Failed to get load_assembly_and_get_function_pointer delegate");
}
// Step 4: Load the managed assembly and get a function pointer to the C# method
rc = load_assembly_and_get_function_pointer(
(params_.assembly + L".dll").c_str(), // assembly file
params_.type.c_str(), // full qualified type
params_.method.c_str(), // method
params_.delegate.c_str(), // delegate full qualified type
nullptr,
(void**)&proc_);
if (rc != 0 || proc_ == nullptr)
{
close_fptr(context);
throw std::runtime_error("Failed to get managed method pointer");
}
// Step 5: Call delegate
auto result = proc_(1,1);
close_fptr(context);
}
Limitations of this method:
- This method relies on the new APIs provided by
hostfxr.h
andcoreclr_delegates.h
, which are not available in earlier versions such as .NET Framework. As a result, its usage is limited to modern technologies, which may not always align with the needs of legacy projects. - For this method to work, the specific version of the .NET Runtime under which your
.dll
was built must be installed. Without the appropriate runtime, the project cannot function correctly, adding additional requirements and effort for setting up the environment.
Useful links:
Loading .NET Runtime for .NET Framework/.NET Standard
This method is fundamentally similar to the previous one but with one significant difference: it uses an older API to launch the .NET host. Despite their similarities, this approach relies on a well-established set of APIs used before newer methods were introduced. It allows you to initialize the .NET Runtime, utilize the assembly, and access the
required functions.
This method may involve additional steps and demand greater attention to detail, particularly regarding cross-platform interactions and memory management. However, mastering this approach enables you to work with older infrastructure, which can be beneficial for specific scenarios and legacy projects.
To begin, let's write the API that we will use:
Here, there is no need to define a
delegate
, as was required in the method described above.
public static class Api
{
public static int Calculate(int a, int b) => a + b;
}
Now, to run the resulting .dll
, we need to write C++ code that will handle initializing the .NET Runtime and loading the assembly into it.
In this case, coreclr.dll
is used instead of hostfxr.dll
.
Key Differences:
- Functionality Level:
coreclr.dll
operates at the application execution level and handles all aspects of managing executable code, whereashostfxr.dll
functions at the host level and is responsible for preparing the runtime environment. - Purpose:
coreclr.dll
directly executes .NET code, whilehostfxr.dll
provides the infrastructure for initializing and loading the runtime, acting as an entry point for hosts.
Functions Provided by coreclr.dll
:
-
coreclr_initialize
- Initializes and starts the .NET Runtime. -
coreclr_create_delegate
- Creates adelegate
for a function from the assembly. -
coreclr_shutdown
- Shuts down the .NET Runtime.
typedef int (*proc)(int, int);
void run()
{
const auto executable_path = fs::current_path();
const auto assembly_path = executable_path / (params_.assembly + ".dll;");
// Step 1: Load coreclr.dll
const auto coreclr_file_name = "coreclr.dll";
const auto paths = split(getenv("PATH"), ';'); // Getting env 'PATH'
const auto runtime_path = find_path(coreclr_file_name, paths); // .NET Runtime path
const auto core_clr = LoadLibraryExA(coreclr_file_name, nullptr, 0);
if (core_clr == nullptr)
throw std::runtime_error("Failed to load CoreCLR");
// Step 2: Get CoreCLR hosting functions
const auto initialize_core_clr = (coreclr_initialize_ptr)GetProcAddress(core_clr, "coreclr_initialize");
const auto create_managed_delegate = (coreclr_create_delegate_ptr)GetProcAddress(
core_clr, "coreclr_create_delegate");
const auto shutdown_core_clr = (coreclr_shutdown_ptr)GetProcAddress(core_clr, "coreclr_shutdown");
if (!(initialize_core_clr && create_managed_delegate && shutdown_core_clr))
throw std::runtime_error("Cannot get coreclr procedures");
// Step 3: Construct AppDomain properties used when starting the runtime
// Construct the trusted platform assemblies (TPA) list
std::string tpa_list(assembly_path.string());
for (const auto& file : fs::directory_iterator(runtime_path))
{
auto file_name = file.path().string();
if (std::regex_match(file_name, std::regex(".*.dll$")))
tpa_list += file_name + ';';
}
// Define CoreCLR properties
const char* property_keys[] = {
"TRUSTED_PLATFORM_ASSEMBLIES"
};
const char* property_values[] = {
tpa_list.c_str(),
};
// Step 4: Start the CoreCLR runtime
void* host_handle;
unsigned int domain_id;
int hr = initialize_core_clr(
executable_path.string().c_str(), // AppDomain base path
"SampleHost", // AppDomain friendly name
sizeof(property_keys) / sizeof(char*), // Property count
property_keys, // Property names
property_values, // Property values
&host_handle, // Host handle
&domain_id); // AppDomain ID
if (hr < 0)
throw std::runtime_error("coreclr_initialize failed - status: " + std::to_string(hr));
// Step 5: Create delegate to managed code
hr = create_managed_delegate(
host_handle,
domain_id,
params_.assembly.c_str(),
params_.type.c_str(),
params_.method.c_str(),
(void**)&proc_);
if (hr != S_OK)
{
shutdown_core_clr(host_handle, domain_id);
throw std::runtime_error("coreclr_create_delegate failed - status: " + std::to_string(hr));
}
// Step 6: Call delegate
auto result = proc_(1,1);
shutdown_core_clr(host_handle, domain_id);
}
Limitations:
- This approach works with all versions of the .NET Runtime; however, it is primarily recommended for legacy runtimes, such as the .NET Framework.
- Successful execution requires the corresponding .NET Runtime version that the
.dll
was built for to be installed on the system. If the required runtime version is missing, the library cannot be loaded or executed.
Useful links:
Using C++/CLI as a Wrapper
One of the most common and convenient methods for integrating C++ and C# is using a wrapper based on C++/CLI. This approach seamlessly bridges the two languages, leveraging the power and flexibility of each.
Firstly, a C++/CLI wrapper serves as a bridge between managed .NET code and unmanaged C++ code. This is especially useful when you need to use existing C++ code in C# projects or, conversely, integrate new .NET features into legacy C++
applications.
Additionally, C++/CLI provides excellent tools for working with pointers and memory management, enabling developers to create highly efficient and performant applications. This method also promotes better code readability and maintainability, as developers can use familiar tools and libraries.
In summary, utilizing a C++/CLI wrapper is not only a standard but also a powerful way to integrate C++ and C#, ensuring smooth and efficient interaction between these two programming languages.
To begin, let's describe the .NET library that will be called from C++.
public static class Api
{
public static int Calculate(int a, int b) => a + b;
}
Next, we create a C++/CLI library (built with the /clr
flag) where we define the header and cpp files.
// Api.h
#define EXTERN_DLL_EXPORT extern "C" __declspec(dllexport)
EXTERN_DLL_EXPORT int calculate(int a, int b);
// Api.cpp
#include "Api.h"
int calculate(int a, int b)
{
return CppCli::Api::Api::Calculate(a, b);
}
In the end, we get three files:
-
dotnet.dll
, containing the .NET assembly. -
cppcli.dll
, containing the C++/CLI assembly. -
cppcli.runtimeconfig.json
- configuration file for initializing the .NET Runtime.
To ensure that the calculate function is exported in our dll, we can use the dumpbin
utility. This tool allows us to view the list of exported functions. For example, by using the command dumpbin /exports
, we can easily check whether our calculate function is present in the dll.
This method provides a clear view of which functions are available for use and helps confirm the correctness of our library before integrating it into projects. In this way, we can not only verify the exported functions but also ensure that they are properly integrated into our application.
File Type: DLL
Section contains the following exports for cppcli.dll
00000000 characteristics
FFFFFFFF time date stamp
0.00 version
1 ordinal base
1 number of functions
1 number of names
ordinal hint RVA name
1 0 00004000 calculate = calculate
Limitations:
- This method is directly compatible only with .NET Framework and .NET Core, meaning its application is limited to these versions of the .NET runtime. Consequently, it cannot be used with other types of .NET, which should be considered when choosing an appropriate approach. For working with .NET Standard, you would need to create a wrapper library.
- Proper functioning of this method requires an installed .NET Runtime. Without the required version of the runtime, your project will not function correctly, adding specific requirements for the development environment and the installation of necessary components. Ensure that the .NET Runtime matches the version under which your library was built.
Useful links:
Modifying IL Code (.export Instruction)
One of the most unusual and intriguing methods for integrating C++ and C# is modifying the Intermediate Language (IL) code after compiling a C# project. This is a highly creative approach that allows changes to be made directly at the intermediate language level used by .NET.
This method can unlock new possibilities for interaction between the two languages, giving developers the flexibility to manipulate the code even after the compilation process is complete. While it requires a deep understanding of C#, C++, and Intermediate Language, it also presents an exciting challenge that can significantly broaden your development
horizons.
First, let's define the C# library.
public static class Api
{
public static int Calculate(int a, int b) => a + b;
}
After building the project, we will get a .dll
file. Next, we need to run the command ildasm /out:output.il filename.dll
. When we open the resulting .il
file, we can see the IL Code
. In our case, we are interested in the Calculate
function, which is represented by the following code:
.method public hidebysig static int32 Calculate(int32 a,
int32 b) cil managed
{
// Code size 4 (0x4)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: add
IL_0003: ret
} // end of method TestApi::Calculate
At the beginning of this method, you need to add an instruction in the format .export [index] as name
, so it becomes:
.method public hidebysig static int32 Calculate(int32 a,
int32 b) cil managed
{
.export [1] as calculate
// Code size 4 (0x4)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: add
IL_0003: ret
} // end of method TestApi::Calculate
After modifying the IL Code, it needs to be reassembled using the command:
ilasm /dll /out:dotnet.dll /PE64 /x64 output.il
.
Now, by applying the dumpbin /exports
utility to the new library, you can obtain the following result:
File Type: DLL
Section contains the following exports for dotnet.dll
00000000 characteristics
673106E9 time date stamp Sun Nov 10 20:18:01 2024
0.00 version
1 ordinal base
1 number of functions
1 number of names
ordinal hint RVA name
1 0 0000276A calculate
The resulting new library can be used via LoadLibrary
in C++.
Taking this idea further, you can combine .NET Framework and .export
to create a wrapper library for a library built on .NET Standard. This approach opens up additional possibilities for cross-language integration. It allows you to take advantage of both worlds: the functionality and extensive capabilities provided by .NET Framework, and the compatibility
and cross-platform support of .NET Standard.
This method enables the creation of powerful and flexible solutions that can operate in various environments while ensuring a high level of compatibility and efficiency. It requires attention to detail and knowledge of the specific features of both frameworks, but when implemented correctly, it can significantly enhance the performance and
functionality of your application.
Limitations:
- This method works only with .NET Framework. However, by using a wrapper library, it can also support .NET Standard libraries, significantly expanding its range of applications.
- Proper functioning of this approach requires an installed .NET Runtime. This means that the corresponding .NET runtime environment must be present on the user's machine for the application to execute successfully.
- This method relies on the use of the Global Assembly Cache (GAC), which allows versioning and dependency management for libraries at the system level.
- Unfortunately, this method is limited to the Windows operating system. While this restricts its use in cross-platform solutions, it is ideally suited for applications developed and run in a Windows environment.
Useful links:
Ahead-of-Time Compilation (AOT)
One of the most modern and advanced methods for integrating C# and C++ code is using the Ahead-of-time compilation(AOT). This method
involves compiling C# code directly into native machine code ahead of time, rather than relying on Just-In-Time (JIT) compilation during runtime.
AOT compilation offers numerous advantages, such as:
- Increased performance: Since the code is already compiled into machine code, there is no need to spend time on compilation during runtime, making the application execute faster.
- Reduced startup time: Applications using AOT can start up faster because there is no need to compile the code at launch.
- Lower memory consumption: AOT compilation helps reduce memory usage as the compiled code is optimized and does not require resources for runtime compilation.
This approach unlocks new possibilities for creating highly efficient and performant applications, combining the power of C# and C++ in a single project. AOT compilation is particularly valuable in scenarios requiring maximum performance and stability, such as game development, scientific applications, and other computationally intensive tasks.
However, the AOT method has its limitations:
- Executable size: AOT compilation can significantly increase the size of the final executable, as all the code is compiled ahead of time.
- Library compatibility: Some libraries and features that rely on JIT may not be supported with AOT compilation, limiting the availability of certain tools and technologies.
- Optimizations: AOT compilation may not always achieve all possible optimizations available with JIT, as JIT can make decisions based on runtime-specific data.
Within the AOT framework, the UnmanagedCallersOnly
attribute can be utilized. UnmanagedCallersOnly
is a .NET attribute that specifies a method can be called directly from native code without using interop mechanisms like P/Invoke. This allows the method to be invoked directly from C++ code, bypassing additional layers of interaction, which
can enhance performance.
Let's describe our C# library:
public static class Api
{
[UnmanagedCallersOnly(EntryPoint = "Calculate")]
public static int Calculate(int a, int b) => a + b;
}
It is also necessary to modify the .csproj
file and add:
<PropertyGroup>
<PublishAot>true</PublishAot>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
After running the command dotnet publish -r win-x64
, we obtain a .dll
file. To ensure that the library correctly exports the required functions, you can use the dumpbin
utility and execute the command dumpbin /exports
.
File Type: DLL
Section contains the following exports for dotnet.dll
00000000 characteristics
FFFFFFFF time date stamp
0.00 version
1 ordinal base
2 number of functions
2 number of names
ordinal hint RVA name
1 0 000B5060 Calculate = Calculate
2 1 001756B0 DotNetRuntimeDebugHeader = DotNetRuntimeDebugHeader
Limitations:
- Features like
Assembly.Load
andReflection
cannot be used. This restriction means that the library cannot perform dynamic assembly loading or runtime code manipulation, which might be required for more complex and flexible scenarios. - It's important to note that .NET 8 supports only the x64 architecture, while .NET 9 expands this support to include both x64 and x86 architectures. This limitation may affect the compatibility of your application with different systems and platforms, requiring careful consideration during project planning and implementation.
Useful links:
Using COM Components
The last method worth considering is the use of COM (Component Object Model) technology. This option offers the least flexibility compared to the previous solutions, as it requires implementation efforts both on the library developer's side and on the consumer's side.
COM interfaces can be useful in scenarios where the consumer of your code can only work with COM components. For example, if you are integrating your library into legacy systems or applications built on COM, this approach might be the only option.
However, despite its applicability in certain cases, this method has its limitations and can be inconvenient to use if strict compatibility with COM technologies is not required. In my case, this method was not suitable, as the project's requirements extended beyond the capabilities provided by COM.
Thus, COM technology should only be considered when you have strict compatibility requirements or other justified reasons for its use. In other cases, it may be more appropriate to explore more modern and flexible options for integrating C# and C++.
To begin, you should define a .NET library that consists of a COM interface and its implementation.
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IComInterface
{
int Calculate(int a, int b);
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public sealed class ComInterface : IComInterface
{
public int Calculate(int a, int b) => a + b;
}
Additionally, to build the COM library, you will need to modify the .csproj
file and include the following:
<PropertyGroup>
<ComVisible>true</ComVisible>
</PropertyGroup>
The next step is to register the resulting .dll
. To accomplish this, you can define a Target
that will not only publish the library as a COM component but also register it in the Global Assembly Cache (GAC).
IMPORTANT: Administrator privileges are required for
gacutil
andregasm
to work.
<Target Name="ComRegister" AfterTargets="Build">
<Message Text="Register COM component" Importance="high"/>
<Exec Command="C:\Windows\Microsoft.NET\Framework64\v4.0.30319\regasm $(TargetDir)$(AssemblyName).dll /codebase /tlb:$(AssemblyName).tlb"/>
<Message Text="Publish TLB" Importance="high"/>
<Copy SourceFiles="$(TargetDir)$(AssemblyName).tlb" DestinationFolder="$(ProjectDir)/bin/publish/"/>
<Copy SourceFiles="$(TargetDir)$(AssemblyName).dll" DestinationFolder="$(ProjectDir)/bin/publish/"/>
<Message Text="Add to GAC" Importance="high"/>
<Exec Command="gacutil /i $(TargetDir)$(AssemblyName).dll"/>
</Target>
In this case, using the LoadLibrary
function is not suitable, so it is necessary to develop an alternative client to load the library, which is now represented by a .tlb
file.
#import "../Com/bin/publish/Com.tlb" raw_interfaces_only
#include <iostream>
int main()
{
HRESULT hr = CoInitialize(nullptr); // Initialize COM
if (FAILED(hr))
{
std::cerr << "Failed to initialize COM." << std::endl;
return -1;
}
std::cout << "Successfully initialized COM." << std::endl;
try
{
// Create an instance of the COM class
Com::IComInterfacePtr comObject;
hr = comObject.CreateInstance(__uuidof(Com::ComInterface));
if (FAILED(hr))
{
std::cerr << "Failed to create COM object. Status - " << std::hex << hr << std::endl;
CoUninitialize();
return -1;
}
std::cout << "Successfully created COM object." << std::endl;
// Call the method
long result = 0;
comObject->Calculate(1, 1, &result);
std::cout << "Calculation result is " << result << '\n';
}
catch (const _com_error& e)
{
std::cerr << "COM error: " << e.ErrorMessage() << std::endl;
}
CoUninitialize(); // Clean up COM
return 0;
}
Useful links:
Benchmarks
Benchmark | Time | CPU | Iterations |
---|---|---|---|
BenchmarkFixture_call_aot/call_aot | 4.04 ns | 3.77 ns | 186666667 |
BenchmarkFixture_call_cppcli/call_cppcli | 7.81 ns | 7.85 ns | 89600000 |
BenchmarkFixture_call_framework_export/call_framework_export | 5.11 ns | 5.02 ns | 112000000 |
BenchmarkFixture_call_standard_framework_export/call_standard_framework_export | 6.29 ns | 6.14 ns | 112000000 |
call_com | 141065 ns | 107422 ns | 6400 |
BenchmarkFixture_call_old_runtime/call_old_runtime | 1257 ns | 1245 ns | 640000 |
BenchmarkFixture_call_new_runtime/call_new_runtime | 146 ns | 140 ns | 5600000 |
The benchmarks demonstrate the performance of various function call mechanisms, highlighting differences in execution time and efficiency across different scenarios. Here's a summary:
-
Fastest Calls:
- BenchmarkFixture_call_aot/call_aot: Achieved the best performance with a runtime of 4.04 ns, showcasing the efficiency of Ahead-of-Time (AOT) compilation.
- BenchmarkFixture_call_framework_export/call_framework_export: Delivered a runtime of 5.11 ns, slightly slower but still highly efficient.
-
Moderately Fast Calls:
- BenchmarkFixture_call_standard_framework_export/call_standard_framework_export: Executed in 6.29 ns, showing comparable performance to call_framework_export.
- BenchmarkFixture_call_cppcli/call_cppcli: With a runtime of 7.81 ns, the C++/CLI call is efficient but slightly slower than AOT and framework export methods.
-
Slower Calls:
- BenchmarkFixture_call_new_runtime/call_new_runtime: Performed in 146 ns, indicating that the newer runtime introduces additional overhead compared to AOT.
- BenchmarkFixture_call_old_runtime/call_old_runtime: Executed in 1257 ns, revealing significant overhead in older runtime environments.
-
Slowest Call:
- call_com: At 141065 ns, COM-based calls are the least efficient in this comparison, reflecting the additional overhead associated with the COM infrastructure.
Key Insights:
- AOT and framework exports are the most efficient calling mechanisms, achieving nanosecond-level performance.
- COM calls are significantly slower, likely due to their inherent complexity and inter-process communication overhead.
- The new runtime demonstrates improved performance over the old runtime but still lags behind AOT in raw speed.
These benchmarks highlight the importance of choosing the right execution model based on performance requirements and
use cases.
Conclusion
If the obtained results are summarized in a table, the following pros and cons can be identified:
Name | Description | Target Runtime | Pros | Cons | OS |
---|---|---|---|---|---|
AOT | Using ahead of time compilation into native .dll | .NET 8, .NET 9 | Modern, Simple implementation, Compatible with Unix/Windows | Imposes restrictions, Not supported by older Runtimes | Windows, Unix |
COM | Using COM Interop | .NET Framework, .NET Core, .NET Standard | Standardized implementation, Documentation | Requires the client to support interaction via COM | Windows |
C++/CLI | Using C++/CLI like Wrapper .dll | .NET Framework, .NET Core, .NET Standard | Easy to implement, Documentation | Requires C++, Only for Windows, Additional .dll layer | Windows |
.export instruction | Using .export instruction | .NET Framework, .NET Standard | Fun way, Supported out of the box | Difficult to implement, Only for Windows, need GAC | Windows |
New Runtime | Using pure .NET (New) Runtime | .NET Standard, .NET Core | No use of additional languages - only C++ and C#, Compatible with Unix/Windows | Complex implementation | Windows, Unix |
Old Runtime | Using pure .NET (Old) Runtime | .NET Framework, .NET Core, .NET Standard | No use of additional languages - only C++ and C# | Complex implementation | Windows, Unix |
In my case, while developing a plugin for Total Commander, I chose the AOT (Ahead-of-Time Compilation) method. This choice turned out to be optimal since my library had no external dependencies, which significantly simplified the integration process and improved performance.
This method allowed me to compile the code into native machine code ahead of time, which not only enhanced execution speed but also ensured faster application startup. By using AOT, I was able to significantly reduce overhead and optimize resource consumption. Thus, AOT became the perfect solution for my task.
If you're interested in a detailed implementation example, you can check out the results of my project here.
The choice of the right integration method always depends on the specific requirements and conditions of the project. It is important to evaluate all possible options and their limitations to find the most effective and reliable solution. I hope my advice will help you tackle your tasks.
Good luck with your projects!
Top comments (0)