Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DllImport cross-platform best practices? #8295

Closed
hintdesk opened this issue Jun 6, 2017 · 11 comments
Closed

DllImport cross-platform best practices? #8295

hintdesk opened this issue Jun 6, 2017 · 11 comments
Labels
area-Interop-coreclr question Answer questions and provide assistance, not an issue with source code or documentation.
Milestone

Comments

@hintdesk
Copy link

hintdesk commented Jun 6, 2017

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
Copy link
Contributor

wjk commented Jun 6, 2017

@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
Copy link
Contributor

ayende commented Jun 28, 2017

@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

  • x64-win
  • x86-win
  • x64-mac
  • x86-mac
  • x64-linux
  • x86-linux
  • arm-32-pi

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
Copy link

rtvd commented Apr 19, 2018

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
Copy link

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.

  • Create an assembly for each and every possible CPU architecture (32 bit, 64 bit, Intel, ARM, etc),. This is obviously awful, don't feel this needs more explanation.
  • Force the end user to ensure that a the proper library, named the same no matter the CPU architecture, platform, or version is located side-by-side with the assembly. This is currently the best option, and really only extends the same problem to the person who uses the assembly, unless they only plan to target single platform and/or architecture
  • Don't use DllImport, and simply hook into the current platforms "LoadLibrary" or "dlopen" functions, and create delegates . This is fine if you have just a few functions. Wrapping an expansive library means hundred of delegates, not exactly appealing, and twice the code.

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
Copy link
Contributor

wjk commented Jun 15, 2018

@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
Copy link
Contributor

ayende commented Jun 15, 2018

@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
Copy link
Contributor

wjk commented Jun 15, 2018

@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
Copy link

@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
Copy link
Member

jkotas commented Jun 16, 2018

@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: MonoGame/MonoGame#5339 (comment)

@jeffschwMSFT
Copy link
Member

dotnet/coreclr#19373

@jeffschwMSFT
Copy link
Member

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

@msftgits msftgits transferred this issue from dotnet/coreclr Jan 31, 2020
@msftgits msftgits added this to the Future milestone Jan 31, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 22, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-Interop-coreclr question Answer questions and provide assistance, not an issue with source code or documentation.
Projects
None yet
Development

No branches or pull requests

8 participants