dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.48k stars 4.76k forks source link

DllImport cross-platform best practices? #8295

Closed hintdesk closed 4 years ago

hintdesk commented 7 years ago

Hi, I would like to connect to my Canon DSLR camera on Windows and Mac over Canon EDSDK. In Windows, I can use DllImport to call C++ function of the .dll files but I don't know how to use DllImport in Mac.

I would like to ask 2 questions.

Question 1. With Mono, I can do call external framework in Mac like

[DllImport("@executable_path/../Frameworks/EDSDK.framework/EDSDK")]

How can I do the same thing with .NET core? Is there "@executable_path" in .NET Core.

Question 2. For example, I have an import

   [DllImport("Win/EDSDK.dll")]
   public extern static uint EdsInitializeSDK();

How should I make this import cross-platform? Does .NET Core have macros like this

#if WIN
   [DllImport("Win/EDSDK.dll")]
   public extern static uint EdsInitializeSDK();
#elif MAC
   [DllImport("Mac/EDSDK.framework/EDSDK")]
   public extern static uint EdsInitializeSDK();

Thank you.

wjk commented 7 years ago

@hintdesk I just saw your issue and wanted to throw in my two cents.

For your first question, as far as I know this won't work under CoreCLR. The @executable_path syntax is actually handled by the macOS dynamic loader, and is meaningless under Windows or Linux. If Mono handles it correctly on Mac, it's because they extended their P/Invoke loading algorithm to deal with it specifically.

As for your second question, this will definitely work. This is the correct and preferred way to do this sort of dynamic library lookup, IMHO. The way I'd recommend doing it is to place the path/name of the DLL in a const string variable, and then wrap that variable definition with #if statements. You can then use the variable in the [DllImport] attributes and the proper definition will be inserted at compile time. This approach reduces the amount of code duplication (since you don't have multiple copies of the P/Invoke function definition itself, you don't have to worry about keeping them all in sync if you change a parameter type or something).

Hope this helps!

ayende commented 7 years ago

@wjk The problem with this approach is that this is really bad experience for us. Consider the case where we support. 64 / 32 bits

Windows, Linux, Mac, Pi

And we use a native library that needs to be shipped for each of those. Ideally, I want to have just a single nuget package, but with the const and #if approach I'm forced to have

That makes things like distributing nuget packages very awkward.

It would be much nicer if we could have something like Mono dllmap that allow to configure that. For that matter, looking at https://github.com/dotnet/coreclr/issues/10520, is there documentation on the probing behavior used?

rtvd commented 6 years ago

I am new to .NET and the first thing I tried was to check its performance as I often need to squeeze out everything I can from the code. So I tried P/Invoke and it was a touch faster than JNI. However, it is absolutely not clear how to use it in cross-platform applications. There are lots of tutorials but everything I saw was either P/Invoke on a single platform or Mono's dllmap or it was something involving platform-specific #if statements in C# code which is not only ugly but also I suppose means that the code needs to have several compiled versions for different platforms, even though it is in C#. I guess the "@executable_path" trick would be really nice to have too because on non-windows platforms the location of the executable is not looked at at when searching for shared libraries. So it is not obvious how to reference shared libraries which are application-specific and probably should be put alongside the library/application rather that in a system-wide location.

It would be absolutely amazing had it been possible to use P/Invoke in multi-platform .NET Core applications and libraries. Ideally it would have the same compiled C# code for all platforms and multiple platform-specific shared libraries. So far it seems that is not possible.

ForeverZer0 commented 6 years ago

This issue with DllImport completely breaks the entire "single assembly" concept, and truly makes any type of cross-platform assembly that relies on an unmanaged library either impossible or too cumbersome to upkeep.

As far as I have discovered, I have a very limited set of options, each of which is terrible, especially when dealing with an assembly to support both 32 and 64 bit unmanaged libraries.

The perfect solution would be to simply be able to use a non-constant name for DllImport, though I understand that restriction is not specific to DllImport, but a restriction of Attributes in general.

Another solution, one that I have kicked around implementing myself is to create a "DllImport" type of function that also uses the appropriate underlying platform "LoadLibrary", and using reflection to generate methods from the function pointers, but this seems inelegant and clumsy, as well as prone to problems.

I understand the many naming conventions and file extensions of different platforms is a serious issue, but with would it not be possible for dare I even say an elaborate regular expression as a last ditch effort to find a missing lib, or even simpler (maybe) yet, a way to use a "resolve" event that gets raised when a native lib cannot be found (just like the AssemblyResolve event works for managed libraries). This would allow the end user to implement their own logic and search patterns to find the file, but not enforce any type of behavior.

wjk commented 6 years ago

@ForeverZer0 Have you looked into AssemblyLoadContext? You will need to implement some logic through subclassing it and overriding LoadUnmanagedDll(), but using this class it is very possible to implement a system that determines what path to load the library from at runtime. Better yet, you can still use the standard P/Invoke syntax with it!

The only "gotcha" I can see is that you must load the assembly that contains the P/Invoke in question using LoadFromAssemblyPath(), called on an instance of your custom subclass, so that the CLR knows to ask that instance for the path of the P/Invoke when one is required. If you don't the standard CoreCLR probing logic will be used, and you'll probably get a DllNotFoundException.

Hope this helps!

ayende commented 6 years ago

@wjk Is there a way to do this on the default context? I'm controlling my own process, so I would like to do it for the whole system.

wjk commented 6 years ago

@ayende I don't think so, unfortunately. The best I can think of is to have the entry point DLL do nothing but create an assembly load context and then use reflection to load the real entry point using that context. As lame as it sounds, as far as I know it's the only way to apply a custom assembly load context to the entire system. As I understand it all assemblies (directly or indirectly, explicitly through reflection or implicitly via compile-time references) loaded by an assembly in a certain load context will inherit that load context.

ForeverZer0 commented 6 years ago

@wjk Looking into your suggestions now, and it does seem plausible to implement. I am making a rather large wrapper, with hundreds of external functions. The library has different builds for each platform and processor architecture, with platform specific naming conventions for each.

So using the above method, creating my own LoadContext subclass, I could use a single name for the "DllImport", and when use the overridden "LoadUnmanagedDLL" to return the proper handle after determining the platform, CPU, etc, etc?

If so, I suppose I have a few questions.

  1. Is this function going to be called once when first looking for the specified DLL, each time a method is invoked, or once each the first time a for each method using it is invoked?
  2. Is the context "stored" where subscribing I can save the handle to a field to later be unloaded?
  3. Is loading the assembly in a static constructor plausible, or is this something the user will simply have to do themselves?
jkotas commented 6 years ago

@ForeverZer0 The best way to handle this in .NET Core is put the native libraries into platform specific folders in the NuGet package for you library and the rest will happen automatically. And you can just keep using regular DllImport.

There are number of packages that do this today. For example, Mono Game for .NET Core does this: https://github.com/MonoGame/MonoGame/issues/5339#issuecomment-353751447

jeffschwMSFT commented 6 years ago

https://github.com/dotnet/coreclr/pull/19373

jeffschwMSFT commented 5 years ago

Closing out question. Though please let us know if you still have a question around this topic.