12

We're trying to develop a cross-platform app for desktops (Windows and Mac) using C#. The app contains a massive amount of platform dependent stuff and our team lead wants us to write all of that code in C#. The easy way to do this would be to write wrappers in C++ and just reference the libraries in the C# code and let the C++ libraries deal with the platform stuff... alas, that is not to be.

I'm trying to set up a library using Visual Studio for Mac. I'm hoping to be able to provide a single interface in a single library to grant access to the native text to speech library on the current platform -- preferably without making two assemblies. The text-to-speech library in C# for Windows isn't available on Mac, so we don't really have an alternative option than to provide some bridge ourselves.

I'm happy with having the library do operating system lookup to determine which operating system it's running on and/or attempting to load a bunch of native libraries and just using which ever library will load.

If I must, having a single project that will allow me to write two implementations will have to do. I haven't found a way to create a library project in Visual Studio for Mac, that will allow me do so however. The only type of project that will allow multiple implementations seem to require that the implementations are either for iOS or for Android.

  • What .NET runtime are you going to use? – Euphoric Apr 10 '18 at 12:10
  • 2
    Xamarin does something similar. Maybe with build configs? – Ewan Apr 10 '18 at 12:13
  • 2
    Its worth noting that Visual Studio for mac isnt really visual studio, Its just a rebadged Xamarin studio. Maybe theres something in the Xamarin docs? – richzilla Apr 10 '18 at 12:16
  • 3
    The best approach is going to be different assemblies... One for the interfaces and common code and one each for the target platform. However, you can multi-target and use #if statements to swap out implementations. The downside is that anything that is not enabled isn't evaluated so it can easily get out of date. – Berin Loritsch Apr 10 '18 at 12:17
  • When you say platform dependent references, are you talking native code or all C# code? – Berin Loritsch Apr 10 '18 at 12:20
4

You have a good question. There's probably some trade-offs with your solution. The ultimate answer really depends on what you mean by platform dependent. For example, if you are starting up a process to start external applications and you are simply switching between one application and another, you can probably handle that without too much complication. If you are talking P/Invoke with native libraries, then there's a little more that has to be done. However if you are linking with libraries that only exist on one platform, you probably will need to use multiple assemblies.

External apps

You probably won't need to use #if statements in this situation. Just set up some interfaces, and have one implementation per platform. Use a factory to detect the platform and provide the right instance.

In some cases it's just a binary compiled for a specific platform but the name of the executable and all the parameters are defined the same. In that case it's a matter of resolving the right executable. For a bulk audio converter app that could run across Windows and Linux, I had a static initializer resolve the binary name.

public class AudioProcessor
{
    private static readonly string AppName = "lame";
    private static readonly string FullAppPath;

    static AudioProcessor()
    {
        var platform = DetectPlatform();
        var architecture = Detect64or32Bits();

        FullAppPath = Path.combine(platform, architecture, AppName);
    }
}

Nothing fancy here. Just good ol' fashioned classes.

P/Invoke

P/Invoke is a bit trickier. The bottom line is that you need to ensure the right version of the native library is loaded. On windows you would P/Invoke SetDllDirectory(). Different platforms may not need that step. So this is where things can get messy. You may need to use #if statements to control which call is used to control resolving your library path--particularly if you are including it in your distribution package.

Linking to completely different platform dependent libraries

The old school multi-targeting approach may be useful here. However it does come with a lot of ugliness. In the days where some projects attempted to have the same DLL target Silverlight, WPF, and potentially UAP, you would have to compile the application multiple times with different compilation tags. The challenge with each of the platforms above is that while they share the same concepts, the platforms are sufficiently different that you have to work around those differences. This is where we get into the hell of #if.

This approach also requires hand editing the .csproj file to handle platform dependent references. Since your .csproj file is an MSBuild file, it's entirely possible to do in a known and predictable manner.

#if hell

You can turn on and off sections of code using #if statements so it is effective at handling the minor differences between the applications. On the surface it sounds like a good idea. I even used it as a means to turn on and off bounding box visualization to debug drawing code.

The number 1 problem with #if is that none of the code that is turned off is evaluated by the parser. You may have latent syntax errors, or worse, logic errors waiting for you to recompile the library. This becomes even more problematic with refactoring code. Something as simple as renaming a method or changing the order of parameters would normally be handled OK, but because the parser never evaluates anything turned off by the #if statement you suddenly have broken code that you won't see until you recompile.

All of my debug code that was written in that manner had to be rewritten after a series of refactorings broke it. During the rewrite, I used a global config class to turn on and off those features. That made it refactor tool proof, but such a solution doesn't help when the API is completely different.

My preferred method

My preferred method, based on many painful lessons learned, and even based on Microsoft's own example, is to use multiple assemblies.

One core NetStandard assembly would define all the interfaces and contain all the common code. The platform dependent implementations would be in a separate assembly that would add features when included.

This approach is exemplified by the new Configuration API and the current Identity architecture. As you need more specific integrations, you simply add those new assemblies. Those assemblies also provide extension functions to incorporate itself into your set up. If you are using a dependency injection approach, those extension methods allow the library to register it's services.

This is about the only way I know of to avoid #if hell, and satisfy a substantially different environment.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.