Skip to content

Cross-platform COM interop library for .NET Core 2.1 or newer

License

Notifications You must be signed in to change notification settings

Const-me/ComLightInterop

Repository files navigation

This repository contains crossplatform COM interop library for .NET core.

Motivation

I’m programming C++ and C#, often in the same project. Both languages are object oriented, and yet outside Windows there’s no easy way to consume objects across the native/managed boundary. When an API only has a few methods, C interop is fine. But as the API surface grows, supporting wrappers on both sides of the interop becomes time consuming and error prone. This project solves it by allowing to interop directly through OO APIs.

Using the library

It's on nuget.org.

The current version targets 3 platforms, .NET framework 4.7.2, .NET 8.0, and VC++, and requires Visual Studio 2022 to build.
The final version which supported .NET Core 2.1 and Visual Studio 2017 is 1.3.8.

VC++ is Windows only. To build Linux shared libraries implementing or consuming COM objects, please add build/native directory from that package to C++ include paths. Or ComLightLib directory from this repository. For cmake see include_directories command, or use some other method, depending on your C++ build system, and compiler.

Keep in mind .NET assemblies are often AnyCPU, C++ libraries are not, please make sure you’re building your native code for the correct architecture.

The library only supports IUnknown-based interfaces, it doesn’t handle IDispatch. You can only use simple types in your interfaces: primitives, structures, strings, pointers, function pointers, but not VARIANT or SAFEARRAY.

BSTR strings probably won’t work on Linux either, I haven’t tested.

Examples

See Demos/HelloWorldCpp/HelloWorld.cpp and Demos/HelloWorldCS/HelloWorld.cs for “hello world” example. As you see, both C++ and C# sides of the interop are about 10 lines of code each.

Slightly more complex example is Demos/StreamsCpp and Demos/StreamsCS, it marshals .NET streams both directions.

A lot of things happening under the hood in the corresponding runtime libraries, ComLightLib for C++ and especially in ComLight for .NET. But the API is simple, at least until you want to implement custom marshallers, or do something equally advanced with the library.

Technical Details

The C++ interop library is in the ComLightLib library. It implements a few template classes, providing functionality comparable to a small subset of ATL.

When building on Linux, that library is header only, you don’t actually need to build it. On Windows there’s one .cpp file, server\freeThreadedMarshaller.cpp, required for better interop with the desktop edition of .NET runtime. On Windows, you either need to build the static library, or include that .cpp into the build system of the consuming project. If you’re using visual studio and install the nuget package, the package will reference that .cpp file from your project, so it will be compiled.

A small C++ demo is implemented in NativeLibrary project. On Windows, the NativeLibrary.vcxproj project builds comtest.dll. Only tested with Visual Studio 2017. On Linux, that project builds builds libcomtest.so. It uses cmake. Only tested with gcc 8.2.0, should be portable to any C++/14 compiler.

When building on Windows, my implementation is binary compatible with Microsoft’s COM. ComLightDesktop project builds a Windows console application which consumes an object from that dll using the built-in COM interop from the desktop version of .NET framework.

The managed side of the interop is implemented in ComLight project. It targets .NET core 2.2. Tested on both Windows and Linux. PortableClient project builds a cross-platform test application.

How it Works

I'm using Reflection.Emit to build delegate types, marked with [UnmanagedFunctionPointer] attribute. The delegates have one more parameter of type IntPtr, for the native this pointer. I copy the rest of the parameters from the managed interface methods, along with their custom attributes, if any.

Then I use Marshal.GetDelegateForFunctionPointer with these delegates to expose a C++ object to .NET, or Marshal.GetFunctionPointerForDelegate to build virtual method table wrapping a C# object for use by C++.

Performance

The interop code in ComLight assembly uses Reflection.Emit and System.Linq.Expressions to generate boilerplate code in runtime. The generated code runs pretty fast.

Specifically, on my PC, both Linux and Windows versions are spending about 20 nanoseconds per C# -> C++ call, that’s about 70 CPU cycles.

The other way, calling from C++ to .NET, is even faster on Windows, 15 nanoseconds per call, about 50 cycles. Strangely enough, on Linux it’s the same 20 nanoseconds both ways.

For optimal performance, I recommend designing your code so the objects are relatively long-lived. If you’re going to create and destroy these managed wrappers at a rate much higher than 1 kHz, especially if these objects have methods with custom marshaled arguments (e.g. if they create or consume other COM objects), the library gonna waste noticeable CPU time compiling .NET expressions into CIL and then into x86/AMD64/ARM code. Not good for performance.

I’ve tested on Windows 10 and Ubuntu Linux running on AMD64 CPUs, also Debian Linux running on ARM v7.1 SoC, with .NET core 2.2. Should also work on .NET Core 2.1 LTS.

Known Issues

In C++, using namespace ComLight; statement breaks compilation when that statement is before ComLight headers. This source code fails to compile on Windows, complaining about ambiguous IUnknown symbol:

using namespace ComLight;
#include "../ComLightLib/comLightServer.h"

If you want to import the complete namespace as opposed to individual types, do it after you have included the headers, like this:

#include "../ComLightLib/comLightServer.h"
using namespace ComLight;

About

Cross-platform COM interop library for .NET Core 2.1 or newer

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published