Porting to .NET Core

Those of you following the git commits or the Discord will have noticed a series of commits related to a .NET Core 3.1 port of Librelancer. This port is now functional and is available to download in the daily section for regression testing.

Why port to .NET Core?

Librelancer has been developed on the .NET Framework/mono since approximately 2016. The technology has served it well and it would be feasible to continue developing Librelancer on the legacy netfx platform. There are, however, some benefits of moving Librelancer to .NET Core:

Access to SSE Intrinsics

.NET Core 3.1 includes the System.Runtime.Intrinsics.X86 namespace, which allows for SSE/AVX accelerated code to be written in C#. Librelancer does include SSE optimised versions of Matrix4 multiply and invert at the moment, but they involve mapping assembly into memory and doing some crazy stuff with function pointers. This code will be able to be ported to sane C# in future for maintainability and performance.


Unfortunately mono has a habit of breaking reasonably often. While in many cases the process of unifying the corefx sources has been a benefit, particularly in performance and XML correctness, things have broken where unit tests don’t cover them. I’ve also had packaging-related issues with mono several times, leading to a development stall while I try and get a working C# compiler. .NET Core 3.1 is an LTS release, which should guarantee stability for the next few years.

Self-contained Publish

Self-contained publishing allows me to distribute Librelancer binaries without worrying about a working mono or .NET Framework install being present. This is particularly useful on LTS Linux releases which contain very old versions of mono, and for when Windows installs have not yet installed the latest .NET.

Porting Pains

With porting a legacy code base (some elements dating back to 2011!) comes challenges.

Unix Differences

Mono and .NET Core handle certain things on Unix slightly differently.

Publishing Multiple Binaries and Dependency Hell

Librelancer exists not just as an engine, but as a set of modding tools. In netfx it’s possible to configure all the projects in a solution to output to the same folder, reducing duplicates and keeping everything together. The official advice on how to achieve this with .NET Core publishing is don’t. Individual publishes for each binary is not an option for Librelancer as the size bloat for one combined target already increases upload times for Appveyor substantially, so I looked into how this could be achieved with Cake.

Cake is the build scripting system that holds Librelancer together, and helps to abstract away the platform specificness of building the native libraries that Librelancer depends on. The solution I came up with is what I like to refer to as a Merged Publish. This happens in four steps:

  1. Publish each Exe/WinExe project into their own individual folders.
  2. MD5 hash each file and check for differences. If there are different versions of the same file, bail.
  3. Copy each individual folder into the output folder named by RID (e.g. librelancer-win7-x86).
  4. Clean extraneous files from the output folder.

With the checks in place this creates a combined published output that doesn’t crash due to assembly mismatches. It does, however, limit what can be included as dependencies as nothing can reference the Windows SDK System.Drawing.dll or System.Windows.Forms.dll, as they will overwrite the assemblies from the cross-platform corefx. This brings us to our next difficulty:

UI Libraries

Eto.Forms was used for the Launcher in the netfx version of Librelancer. This library worked well in providing a launcher that ran both on WPF and GTK. Unfortunately the WPF backend for Eto.Forms brings in a lot of assemblies from the full netfx and Windows SDK, causing MD5 mismatches and making the publish for windows fail. I also tried the Avalonia project, which to its credit worked well. However in the end I decided including the assemblies for another UI library when Librelancer already implements its own UI was too much bloat. The Launcher has now been replaced with one rendered by the engine natively:

Not being able to include System.Windows.Forms.dll also broke file dialog support and crash dialog support under Windows. This was worked around by including nativefiledialog for the Windows build, and using a Vista+ Task Dialog from comctl32. Linux does not yet have a working crash window backend, but it will be most likely based on invoking either KDialog or Zenity.

Lots of things have been happening with the Librelancer engine in 2020, so keep an eye on this space for more blog posts in future.

~ Callum