-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Handling p/invokes for different platforms and discussions about dllmap #4216
Comments
cc @stephentoub Related to #4032 |
|
@janhenke in theory yes, Mono just tries a few of the combinations that make sense. |
This is an interesting problem to tackle. I’m curious what is your experience with DllMap? What works well and what doesn’t? |
As long as we keep the assembly load context hook so that the host can decide how this works then I'm happy. I think the core for should try to reduce as much policy/configuration as it can ootb (this reeks of binding redirects). Doesn't the host also let you configure paths for dll imports? Maybe that can be extended to allow probing for more stuff (different extensions etc). |
@sergiy-k I'm not really an expert on DllMap, but I haven't had too many problems in my limited use. The main problem I see is where to put the stuff (.config files don't exist anymore in this new world). I'd be happy with a first step that just probes for different variations if a lib can't be found by the verbatim string. @davidfowl how would the application hook into that? |
One more thing, some of the libs in aspnet avoid dll import and instead load libraries using another A library provided by the dnx to help locate the native lib: And the usage: |
How would the app handle it? I have no idea. I don't see the core clr as an application model. I see it as a building block with configurable policy and a nice hosting API. There will be a way to express native dependencies in project.json and in nuget packages. The dnx host will hook the event and do something clever :) |
The main thing to keep in mind for this is compatibility. This means the solution needs to assume you are running a compiled .Net application (.exe/.dll) and that it has hard-coded references to library names that work on windows. Adding platform specific class abstractions will not work in this scenario. The .Net binary is already compiled and source may not be available. A good real world example is calls into the C runtime to "memcpy". The library name is entirely different on windows than other platforms "msvcrt.dll" versus "libc.so". Name mangling wont fix it and hard coded substitution presents a maintenance burden to keep up with every new runtime version release. With regard to name mangling, I would prefer to see a single, known and documented substitution take place: "MyFoo.dll" -> "libMyFoo.so". Tying more than one substitution as Mono does can open the door to file name collisions. The main issue present with this approach is case sensitivity. .Net code assumes a windows platform and is not case sensitive as unix is. So "MyFoo.dll" and "myfoo.dll" are not the same. Taking all the above into account, the only realistic solution is a "DllMap" style configuration file that can be added alongside an already compiled binary. For completeness, the proper way to specify a DllImport is to name only the library file name without extension. So the above example would be written as [DllImport("MyFoo")]. The platform specific suffix (.dll/.so) is added by the runtime. This would imply the correct behavior would be for the runtime to also add the platform specific prefix (lib). Anything beyond this would need to be specified by the "DllMap" style solution. |
One other comment regarding .so versioning: This is usually solved by providing a symlink to the explicit verison named library. |
I do believe this isn't the case. .Net is perfectly case-sensitive, but it's only the underlying filesystem implementation in Windows that's insensitive. I'm also pretty sure that the CLR doesn't assume anything, and even if it did, CoreCLR is built from the ground up to not assume these kinds of platform-specific things. |
This isn't a concern as existing assemblies don't work on .NET Core anyway and need to be recompiled. I agree that whatever the solution is it shouldn't rely on too much magic and be properly documented. |
Let me expand that a bit: This is specific to pre-existing .Net binaries that are authored by 3rd parties for which the option of recompiling is not available. Its the reason Mono implemented "DllMap". |
Pre-existing binaries are not limited to just legacy. Its possible for someone to author new .Net Core assemblies that P/Invokes the C runtime today. Its also possible that this assembly is 3rd party and released without code. |
Would it be possible to allow the library/module name of DllImport to permit a static readonly allocation somehow? That way we can point to where the library should be loaded in a class's static constructor. Right now attributes parameters are compile time things so it would be a problem. However it would avoid the need of an external .config file which apparently does not exist in .NET Core. |
The key word here is "usually". While it holds true for many libraries, it is not really a formal requirement. You do have to expect encountering a shared library with a symlink to |
Overridable AssemblyLoadContext.LoadUnmanagedDll method was designed to allow customization of the DllImport target loading. The method gets the dllname and returns the OS handle. The user supplied algorithm can probe for .so versions, use dllmap-style binding, or do something clever like dnx ;-) It would be nice to publish sample that implements dllmap using AssemblyLoadContext. |
@jkotas That's nice, but I still think there needs to be something simple built in. E.g. |
@akoeplinger Couldn't that be implemented in the PAL, or am I misunderstanding? |
@akoeplinger It sounds reasonable to add a helper API on AssemblyLoadContext for it. dotnet/coreclr#937 |
I concur that whatever solution is presented needs to be 'built-in'. Its important that it be deterministic and consistent for everyone. If everyone 'rolls their own as needed using hooks', then library providers will not be able to offer support for anything other than their own 'loader' which may not be compatible or offer the features of a customers proprietary 'loader'. |
+1 for a simple convention based plugin -1 for anything like dll map. The core clr should keep the light weight hosting model without inflicting policies and config files. Let the host implementation do that. Corerun/core console could bake something higher level in |
I have made a library for this, but it is not supported by AOT since it generates the implementation in runtime using Emit. |
As a principle of encapsulation, if coreclr is the one implementing and defining the behavior of DllImport, then it is also responsibility for allow configuration of it. The host, (corerun, etc) should have no concern over the operational details of something that is outside its scope. This is a coreclr concern, making it a host concern is just "passing the buck" to make it "someone else's problem" instead of having to deal with it. The need is that someone that is not a developer can xcopy a project to a target. If that target happens to be non Windows, there is a simple and trivial method for them or a library provider to override a hard coded library name assumption. This must work deterministically and consistently regardless if corerun is host or the runtime is hosted as part of a larger application. |
Sensible defaults is always good and I think everyone is on board with that. Policy should not be baked into the CLR, and if it is, it needs to be minimal and overridable by the host. |
Its impossible for the host to anticipate the usage or intent of DllImport in 3rd party libraries. Therefore, there will never be a need for the host to override behavior. Its entirely outside the scope of concern for the host. This is a real world example for discussion:
The presented solution of "let the host worry about it" means every host would require knowledge that memcpy is not in msvcrt.dll on linux. Instead, it should load libc.so. Mono does include built in knowledge that msvcrt should be mapped to libc on linux, so this mapping is never required by the end user or author of the library using it. The presented solution of "rewrite the library to use an abstraction" has considerable impact for authors of libraries (code refactoring and testing) and places the burden of platform knowledge into every library using DllImport. The "DllMap" solution is the only one presented so far that meets everyone's needs:
|
Just to throw something more specific into the mix, the dnx is a core CLR host, it doesn't use corerun or core console. Instead, it uses the native host APIs directly and hosts the CLR in a native process. As part of that bootup sequence, we hook AssemblyLoadContext.LoadNativeDll and have a way to load native assemblies from NuGet packages. I'm not sure I want dll map at all in that situation since as the host, the app model may not even choose to expose the same primitives. PS: It reminds me of binding redirects, which nobody wants ever again 😄 |
So how does DNX cope with the above mentioned example of memcpy? I should also point out that
Is equally valid C# and works on Linux, but not on windows. A 3rd party library could contain either. So this is not just a "other platform problem". Its also a Windows problem. Is DNX prepared to handle that mapping? |
No but I said it's ok to have something extremely basic in the core clr for something as low as memcopy. Then again if your library is calling memcopy or uname or anything directly then it should detect where it's running. Btw there's a memcopy method on Buffer now which is a better abstraction for that specific call. |
If writing my own loader is the solution at release, then I will. So will others. Some will follow Mono DllMap. Others will not. In the end there will competing 'standards' causing frustration for library providers and customers. This is why I feel its important to resolve this early. If I can only pick one platform when using DNX + P/Invoke, then its going to be Linux. |
@mjsabby Absolutely. Let's chat offline and see how we can work together to move this forward. |
Is there source code for it? Nuget is forbidden in many environments due to security constraints.
Could you provide example code usage of how you envision this working? Or is the solution still to write two different bindings/assemblies for the same API? |
I don't understand why it's possible to participate in managed loading via an event, but not unmanaged. An event similar to the Resolving event for managed assemblies would be great—let me know when the runtime is attempting to load an unmanaged library and allow me to (attempt to) provide my own handle to the loaded native library. If all of the event handlers return |
On Linux I found a practical workaround for this issue, which does not involve modifying the DllImports at all. The idea is to generate a stub shared library that is named like the Windows DLL and contains a reference to the real library name for Linux. The DllImport in .NET code remains unchanged and uses the Windows DLL name (without .dll suffix). The .NET core native loader will then load the stub shared object. This action invokes the Linux dynamic loader (ld.so) which then resolves the dependency of the stub on the real library and automatically maps all symbols from the real library into the stub. To generate a stub library do the following:
The result can be checked using the
Each stub is very small (8 KB) and thus can easily be included in cross-platform NuGet packages. A real world example of this technique can be seen at https://github.com/surban/managedCuda/tree/master/StubsForLinux |
@surban +1, note that the library cannot have a dot in the name. The loader stops guessing at the first dot. Please take a look here for an actual example: |
@yizhang82 |
Interop with native libraries on .Net Standard proposal. We have the C libraries provided by third party on Linux and Windows platforms. The library names doesn’t follow the naming convention. Say library names are linuxlib.so and winLib.dll. We want to create the .Net Standard project that uses this libraries. We are using the DllImport attribute. The challenge is how to load a correct library for the OS platform. In the .Net Standard 2.0 there is no support for this. I want to propose a simple enhancement to the .Net Standard that will solve this problem. The code below shows how a user code will look like. The map between OS and library is added to the DllImportMap class. /// This class is a part of the assembly that uses the C libraries on Windows and Linux platforms /// The map should be added to the static constructor of the class that has declarations of the external C functions /// public class ExternalFunctions { static ExternalFunctions() { DllImportMap.AddMap("MapName", new Dictionary<OSPlatform, string> { { OSPlatform.Windows, "winlib.dll" }, { OSPlatform.Linux, "linuxlib.so" } });
} The code below should be added by Microsoft to the .Net Standard /// /// This class should be implemented by Microsoft /// It should be added to the same assembly and namespace as the DllImportAttribute class. /// It allows adding a map that describes which C library should be used on which platform. /// Multiple maps can be added. /// The DllImportMap is a static class. /// public static class DllImportMap { private static Dictionary<string, Dictionary<OSPlatform, string>> masterMap = new Dictionary<string, Dictionary<OSPlatform, string>>(); public static void AddMap(string key, Dictionary<OSPlatform, string> map) { if (masterMap.ContainsKey(key)) throw new Exception(string.Format($"Key {key} already present in the masterMap")); masterMap.Add(key, map); }
} /// /// It assumed that Microsoft has a class that process the DllImportAttribute. /// A new method DllNameAnalyzer should be added. It processes the dllName parameter of the DllImportAttribute /// public class DllImportProcessingClass { /// /// The dllName parameter of the DllImportAttribute now can accept a map name. /// This method checks if a libName parameter is a map key and retrieves a library name corresponding to the OS platform /// /// A name of the library or a key in the map /// private string DllNameAnalyzer(string libName) { if (string.IsNullOrWhiteSpace(libName)) return libName; // first check if the libName is a key in the masterMap Dictionary<OSPlatform, string> map = DllImportMap.ByKey(libName); if (map == null) // not a key, so it is a library name return libName;
} |
|
Since I just removed all the glue code and native detection dependencies as well as having a single binary for the curses support, I had to write this support. .NET Core sadly does not support Mono's <dllmap> yet, so it is not possible to do this in any decent form. This hell is imposed not only on my curses binding, but on any library that needs to deal with Linux packagin peculiarities - and we are inflicing this pain on every .NET developer doing Unix variations. Anyways, to make a long story short, now I have this ugly turd of a binding to curses for the sole purpose of calling into one kind of curses on MacOS, and another on Linux. This could have been a 3 line XML configuration file if we had supported dllmap, but instead this has devolved into what could be described as poster child of bikeshedding. This is one of that many issues, lots more where this came from: https://github.com/dotnet/coreclr/issues/930
makes my life a living hell. Support for bindings to different ncurses version Since I just removed all the glue code and native detection dependencies as well as having a single binary for the curses support, I had to write this support. .NET Core sadly does not support Mono's <dllmap> yet, so it is not possible to do this in any decent form. This hell is imposed not only on my curses binding, but on any library that needs to deal with Linux packagin peculiarities - and we are inflicing this pain on every .NET developer doing Unix variations. Anyways, to make a long story short, now I have this ugly turd of a binding to curses for the sole purpose of calling into one kind of curses on MacOS, and another on Linux. This could have been a 3 line XML configuration file if we had supported dllmap, but instead this has devolved into what could be described as poster child of bikeshedding. This is one of that many issues, lots more where this came from: https://github.com/dotnet/coreclr/issues/930
@efimackerman What you propose sounds like dotnet/corefx#17135; if that's the case, would you mind "upvoting" that issue (thumbs up icon below the issue description)? |
@qmfrederik I don't see how my proposal is similar to the one you have mentioned in your comment. |
I've recently released a library which solves most (if not all) of the aforementioned issues, and bundles a cross-platform delegate-based approach into a simple and easy-to-use APIs, which also supports Mono DllMaps, as well as custom library search implementations. The backend is superficially similar to the proposed API in dotnet/corefx#17135, but is abstracted away for ease of use. On top of that, it allows some more interesting extensions to the P/Invoke system, such as direct marshalling of |
I would like to see I voiced the 'issues' I have with the alternative here: https://github.com/dotnet/corefx/issues/17135#issuecomment-374248831 |
This will break Linux because of the dll name change. There isn't a DllMap in .NET Core, so there isn't a nice way to work around this. See: https://github.com/dotnet/coreclr/issues/930
@jeffschwMSFT Look about 3 years up this issue's conversation thread for a bunch of discussion about trying to emulate Mono's XML DllMap system and why that's a very bad idea. Let's please not resurrect that now. |
👍 @jeffschwMSFT Based on a quick read of early discussion, I would also recommend against DLLMap. It is reusing a design which was conceived for a different purpose. |
So, how do I do this now in .NET-Core ?
And why does .NET load native assembly functions with compile-time attributes anyway, when it then goes forth to actually dlsym them at runtime when invoked ? And why having that feature anyway ? Right now, as I see it, I need to create a static class with a lot of delegates, then do the dlopen and dlsym manually, to replace all freetype-internals - that is, if I don't want to create 3 versions of the assembly for each platform because the shared-object-name is different. And why can't dllimport take a static function as its attribute argument, in addition to a const-string ? |
Since attribute doesn't exactly allow for a callback, here's an example of what I mean:
|
@ststeiger You're SOL with normal .NET Core. However, I've made a library the solves practically all of the issues mentioned in this thread. Might be worth checking out: https://github.com/Firwood-Software/AdvanceDLSupport |
@Nihlus: I already made a similar thing, long ago (loading oracle native-dlls in asp.net and mono). No backwards-compatibility problems whatsoever. You could now even fetch the dll-name from a database.
Then you could theoretically even use a different freetype version per tenant in the database depending on HttpContext (domain), if available ;) What we would need now is extension-attributes with extension properties, to retroactively add this to the full .net framework without changing its source. |
Yes, this is exactly what we plan to do. Tracked by https://github.com/dotnet/corefx/issues/32015. |
Closing this issue, the matter is tracked by dotnet/corefx#32015. |
Right now, coreclr has no good way to handle the differences between platforms when it comes to p/invoking native libraries.
E.g. imagine I want to call an openssl function, on Unix the library is called
libssl.so/dylib
but on Windows it islibeay32.dll
(there are countless other examples, e.g.libz.so
vszlib1.dll
).corefx "solves" this by conditionally compiling the p/invoke code for each platform with the correct library name. This seems like a major organizational overhead though and is brittle when new platforms are added.
Mono does two things to make this much easier:
DllImport
, e.g. if I specifyDllImport("myFoo")
it triesmyFoo.dll
,myFoo.so
,libmyFoo.so
and so on. In the happy case this is enough to find the library on all the platforms.In my opinion, coreclr should implement something along those lines. I know that using DllMap 1:1 is not really possible since there's no System.Configuration stack in .NET Core, but it should at least guide an alternate implementation.
What do you think?
edit I proposed adding DllMap attributes in https://github.com/dotnet/coreclr/issues/930#issuecomment-100675743 :
The text was updated successfully, but these errors were encountered: