-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
How to address other problem scenarios previously solved by dllmap #37213
Comments
Let me provide a concrete example of some of the problems described here. The past few days I've been attempting to add proper .NET Core support to the FNA game framework, a C# project that makes heavy use of a small handful of native libraries for window creation/input, audio handling, and graphics API calls (OpenGL/DirectX/Metal/etc). Most of the FNA predates .NET Core and has historically relied on Mono's DllMap feature for native library discovery on all its supported platforms. As you can see from FNA's dllmap config file, the native library names on macOS/Linux/BSD are substantially different from their Windows counterparts, so the automatic "try adding lib as a prefix" DLL name checking doesn't work here. This means that for CoreCLR we need to use the NativeLibrary API to load the appropriate library. Thankfully basic DllMap-like renaming functionality is fairly easy to implement with NativeLibrary. The trick is figuring out where to initialize it, i.e. where to call My next attempt was to call the NativeLibrary initialization code in all possible places within FNA that might serve as the entry point. Thankfully there are relatively few potential entry points, but this was already getting awkward since it meant that DllMap-loading code was spreading throughout the codebase. Instead of classes containing only logic relevant to their internal operation, they were starting to contain runtime bookkeeping. However, this strategy was also fatally flawed, because it was making the assumption that the end user would call into FNA before they made any calls into FNA's dependencies. That quickly fell apart when I encountered some games that called into SDL2-CS first, before NativeLibrary had a chance to initialize. This is totally valid behavior on the game's part, but for us it's very problematic, because it leaves us with only two realistic options for NativeLibrary initialization:
There is no clear solution to these problems. The only viable way forward that I can see is setting these mappings up before runtime, not during. My ideal scenario would be a straight-up implementation of Mono's DllMap feature into CoreCLR (maybe even implemented on top of NativeLibrary, but handled by the runtime and called at module init time), but even adding some of these configuration options to |
It's not the most ideal, but the suggested solution in the previous discussion of
@TheSpydog Have you explored using module initializers for FNA? Official support is planned for C# 9.0, but you can use them today through post-build tools like Fody or InjectModuleInitializer. |
Yes, and thanks for the links. While this sounds generally useful, I don't think this is a feasible solution in our case, for the following reasons:
|
It would be useful to have a concrete examples to discuss this on.
ICU is used by the .NET runtime. We are very well aware of this ICU quirk. Another problem with ICU library is that it has very chatty APIs and paying the PInvoke overhead for each call is prohibitive. We solved both these problems by using a shim native library. dllmap would not be sufficient to solve this problem.
I agree that the current NuGet standard for distributing native libraries with NuGet packages is less than ideal. I would be nice to have a standard for distributing the sources for native libraries as part of the NuGet package and use these sources to recompile the native library as fallback when the right native asset is not available as precompiled. I believe other ecosystem (e.g. Python) have capability like this. This is independent problem from dllmap.
If the library has the same APIs accross operating systems, this is not normally required. The default native library probing logic appends platform specific suffix as appropriate.
You should be able to do this, without any additional overhead, using function pointers that are coming with C# 9.
It is a common best practice to have all PInvokes for the given library defined in one type. My recommendation would be to refactor the project to follow this best practice, and then it is pretty straightforward to install the custom
My recommendation would be to add switch for the AOT compiler that tells it which libraries are statically linked and not expected to be loaded from disk. This allows one to switch between static linking and dynamic linking per individual library without changing sources.
Is having two parallel builds or build systems an option? To provide some context: The experience we want .NET users to have is that they add NuGet package to the project and it works. We have discussed dllmap in length and it is hard to see how to fit it into this experience. On top of that, dllmap has problematic security characteristics. It is equivalent to code and allows changing the control flow of the application in arbitrary ways, but it does not come with the same security features like ability to be signed. |
It's been a while since I had to deal with it, but at at least one point the MSVC stdlib redirected some of its exports to other libraries. I know for sure that I ran into this with strdup and free because I wasted an entire day debugging it when it broke my local source builds of Firefox. Also, since we were previously discussing ICU, the number of dynamic libraries and where the entry points live depends on platform - afaik windows is two DLLs, while I see at least 4 .so files on Linux. NativeLibrary 1:1 mapping of libraries does not handle this, you need per-entry-point mapping. If the claim is that this is an unimportant use case that's fine, but it's a simple enough one that it shouldn't require an exhaustive list of scenarios where it happens... we move APIs like this around in the managed environment, it's not that implausible for it to happen on native.
If the solution is for everyone to write a native shim for every library they consume I guess that does count as a solution. It's not clear to me why dllmap is specifically not capable of addressing this.
Dllmap allows externally substituting relevant native libraries to address a missing dependency in a managed assembly you're consuming or adapt it to a new platform, as I mentioned. Doing this with NativeLibrary is far more convoluted because you can't reach in and modify the assembly to insert the appropriate setup code and/or cctors.
OK, it sounds like the solution here is that you come up with matching SOs for your platform and either rename them or generate appropriately named symlinks and .net 5 will magically fix it up? If that works, maybe that's good enough. I had to use LD debug flags to observe the .net 5 behavior here but maybe I didn't read the docs properly.
Is it realistic to expect everyone to migrate all their software to C# 9 when dllmap is an existing solution in the runtime they used to use? Is C#9 going to be well-baked enough on .NET 5 launch day such that not only is it easy to migrate but the tools developers use every day - resharper, etc - will understand things like fn pointers and module initializers?
This doesn't solve the cctor overhead problem and also doesn't solve the dependency graph problem. If one library consumes 3 other libraries they all need to be updated with their own custom resolver for all their pinvokes and the pinvokes all need to be relocated into a single class, ruining encapsulation and organization. If this is the cost people have to pay to adopt .net 5 so be it I guess, but it's hard to justify wasting the energy as long as netframework and mono are still functioning unless .net 5 is life-changing for end-users. I agree that this is much easier if you follow best practices, but most customers will not have the luxury of ensuring all their code and all their libraries are C#9 and conformant to current best practices.
So we migrate configuration that used to be in dllmap to a bunch of compiler switches in a response file? I guess that's functionally not that different, it's just only works for AOT instead of for most scenarios.
I mean, if it solves the problem we could have 8+ parallel builds and 3+ meta-build-systems. It's good enough for Chromium. I would hope .NET could do better, since in the good old days I could ship one managed .dll that worked on both mono and netframework with minimal assistance (i.e. from a dllmap), vs the current situation with .netcore which feels like "the only supported scenario is
It's not clear to me how dllmap is an obstacle to consuming nuget packages, though I can see how you would not consider it useful for that. I don't understand how being able to drop a dllmap into the folder is any less safe than the reality that you can drop a dll into the folder right now and have a more significant security impact than a dllmap file. At the point where an attacker can drop files into the application folder the application is already 100% compromised. Dllmap is less powerful than this because all it can do is redirect entry points. As you point out it can be signed (much like executables) so in both cases the solution there would be requiring that all executables and dllmaps be signed. Of course an attacker getting a signing key is game over, and I can attest that getting an authenticode certificate is not hard. |
Yes, native shim is the appropriate solution in many cases (not everyone). For the record, we have tried very hard to avoid the native shims during the early days of the .NET Core project. We have found that it is just not feasible and ended up on standardizing on shims for majority of the PInvokes done by .NET runtime libraries.
Instead of this, we want to strongly encourage people to make the proper fixes in the libraries so that it will just work for the next person. |
I have opened NuGet/Home#9631 on this |
There's been discussion on From my perspective, the biggest gaps in functionality are First, I think the main issue with Next, symbol name remapping. As Katelyn mentioned above, this functionality existed with dllmap but is lost with NativeLibrary. In addition to any scenarios she mentioned, I want to again bring up the Xamarin products, specifically iOS. As discussed previously over email with @jkotas, they have to remap |
I believe that there is also third meaning that is "static linking". It is used for example here: https://github.com/grpc/grpc/blob/04cdb1826666e0de67cfd11c1f5214e0fd53a018/templates/src/csharp/Grpc.Core/Internal/NativeMethods.Generated.cs.template#L81
The official Xamarin documentation tells people to P/Invoke Mono has historic quirks for Xamarin and it is ok to keep some of them even in .NET 5+ world to ease migration. Similarly, CoreCLR has historic quirks for Windows and COM that we are not removing either. Speaking of P/Invoke entry-point remapping, CoreCLR.dll has a similar one-off entrypoint remapping compatibility quirk for runtime/src/coreclr/src/vm/dllimport.cpp Line 2824 in ef6c035
|
I don't think that's a third meaning since the usage should be covered by the I think now that we support QCalls in mono (so there's no need for |
For CoreCLR, QCalls are internal implementation detail that only works in CoreLib. They are intentionally not recognized anywhere else. |
@CoffeeFlux I think the |
The follow ups on this discussion are tracked by #7267 NuGet/Home#9631 . I do not see anything actionable left here. |
With the transition to .NET 5, many problems people previously solved with mono+dllmap will need to be solved using pure .NET 5 (and potentially user-authored code).
We have NativeLibrary to handle some basic problems like "how do I find the appropriate .so file for this p/invoke" with effort from the end-user, but for other things dllmap was able to do it's not clear how to solve them in .NET 5. There are also some unfortunate limitations/downsides to NativeLibrary that may need to be fixed. I'll try to enumerate everything here, ideally we can eventually have solutions for all of this or have documentation for users who need to transition off dllmap so they can understand the path forward for their software. I worked with some end-users over the last day or two to puzzle out some solutions for their use case, and hit some roadblocks that motivated filing this issue.
Some functionality described in the dllmap documentation:
The
<dllentry>
directive can be used to map a specific dll/function pair to a different library and also a different function nameThis is a very common scenario, on win32 you will often find standard library functions that are redirected from foo.dll to bar.dll, and there are scenarios where managed code needs to do it too. We need a solution for this for P/Invoke because a given API will not be in the same place across platforms, and the number of unique assemblies you'd have to compile (and dynamically load somehow based on target) is unsustainable.
The DLL remap part can be solved by NativeLibrary but only for 1:1 mappings where everything in A.dll is implemented in B.so, which isn't always true.
Also note that the 'different function name' scenario is a real-world problem: For one example, the ICU unicode library by default mangles its own function names based on version in order to support scenarios where two versions of ICU have been statically linked into the same executable. As a result, if you probe at runtime to find ICU and grab ICU v67, all of its entry point names have to be mangled dynamically to include '67'. This is not something you can fix as-is with NativeLibrary.
You use the
<dllmap>
directive to map shared libraries referenced by P/Invoke in your assemblies to a different shared library.This appears to be fully solved by NativeLibrary, with the caveat that this occurs entirely at runtime, which means it is not AOT-friendly. For runtime environments like wasm the lack of AOT compatibility could be a serious issue, but I don't know if anyone other than me cares about that.
Both the
<dllmap>
and<dllentry>
elements allow the following attributes which make it easy to use a single configuration file and support multiple operating systems and architectures with different mapping requirements:os
: This is the name of the operating system for which the mapping should be applied. Allowed values are: linux, osx, solaris, freebsd, openbsd, netbsd, windows, aix, hpux.cpu
This is the name of the architecture for which the mapping should be applied. Allowed values are: x86, x86-64, sparc, ppc, s390, s390x, arm, armv8 (AArch64), mips, alpha, hppa, ia64.wordsize
you can use this to differentiate between 32 and 64 bit systems. The possible values are : 32 and 64.Selecting the appropriate .so/.dll file based on cpu and word size is something you can do with NativeLibrary, but if entry point names don't match you're out of luck. I don't think the entry point names varying based on platform is that common of a scenario, however, so this is probably fine, aside from the above-mentioned AOT issue.
Additional scenarios and issues worth considering that could be solved declaratively or programmatically:
Based on the characteristics of the current environment, you may want to dispatch a p/invoke to a different function in a DLL that uses a different instruction set. For example, an image processing library may have AVX, AVX2 and SSE versions of a given function.
You could have entire separate .dll files compiled with different /arch flags, but this quickly becomes unreasonable because you could end up with 8 different libraries that bloat your download when in practice only a few specific functions might have support for AVX2. In production software it is common to see _avx versions of specific functions like this when you profile them. Doing this branching on every single invoke via a wrapper is not only complicated, it has potential performance overhead. Being able to resolve this once automatically for each p/invoke (with a config file or user-authored resolve handler) would be ideal.
An end-user or developer consuming a third-party library may need to adjust its resolution behavior to support a new platform or compiled library.
For example, I pulled down a NuGet recently that wrapped an open-source native library. I later discovered that the NuGet only included native binaries for Windows, not Linux. Because pointing it to a .so file would normally require that the nuget's developer make the relevant changes, I had no way to fix this even if I compiled the relevant native binaries from source. If I could drop a .dllmap equivalent next to the nuget assembly perhaps I could have fixed this. Setting a custom resolve handler program/appdomain-wide would also work. (Perhaps there is a hack to do this with NativeLibrary I'm not aware of?)
Automatically applying dllmap-style remapping to an assembly at load or use time is difficult and potentially expensive due to C# limitations
Module initializers are not exposed to C#, so the closest thing you have is putting a
.cctor
on every class that contains pinvokes. This means that in theory, every method call or pinvoke on that class will have a.cctor
check-and-initialize generated for it. While the branch for that check will predict well, it's still an indirect load and branch on every call. JITs could potentially patch all those branches out but in AOT they're there whether you like it or not.If you try to avoid the
.cctor
via manual initialization, now library consumers all need to know about this problem and you can end up with bugs from forgetting to initialize it in the right places.This gets worse when consuming multiple libraries that all need manual initialization or dependency trees that all need it. Developers will have to manually test for every scenario (or run tests for it on CI) because this behavior is all implemented at runtime instead of declaratively.
How do you implement
__Internal
This isn't specifically dllmap related but it comes back around to what I mentioned about AOT above - some platforms like iOS or wasm will effectively require AOT compilation and reward static-linking. In that scenario, you can end up with an application where half of its pinvokes have been statically linked into the executable. My understanding is that previously, you would have solved this by setting the source library to
__Internal
. How do you solve this now, and how do you programmatically sense it to wire things up with NativeLibrary? The AOT'd code doesn't live inside the assembly so Assembly.Location isn't immediately going to be of use for you.ccing @TheSpydog and @flibitijibibo because they've been struggling with these issues lately and can provide more detail.
The text was updated successfully, but these errors were encountered: