-
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
Native Library Loader API #31612
Comments
A few comments:
For the callback alternative, the existing Marshal or NativeLibrary would be a good place for it and it can be a single method for both, like: static class Marshal // or NativeLibrary
{
static void RegisterNativeLibraryResolver(Assembly assembly, LoadNativeLibraryDelegate loadNativeLibrary, GetEntrypointDelegate getEntryPoint = null);
} For the events alternative, I am not sure. |
I think the title shouldn't say "Dllmap specific events." For example, the |
I also think that GetEntrypointDelegate() should take a NativeLibrary handle, since this information is unique enough to identify all other information, if necessary. |
We need two pieces of information from
|
|
|
You cannot always tell the path that got actually loaded. |
I am inclined to say 'no'. The CLR has a number of caching mechanisms for native library scenarios and I don't know how easy it would be to properly update all of them. There is the native library cache that is for load library and we would also need to look at |
The intention of providing the non-modified version of libraryname and entrypoint from the dllmap is to provide the right information to the developer to choose to either return the symbol as declared in code or find a suitable replacement. The NativeLibrary would then be completely managed by the developer and the runtime/fx would not use it as an exchange type.
We discussed the callback mechanism with the caveat that it would likely use an override semantic and only respect the last callback provided for a given assembly. In that case returning a potenial overriden callback seemed important.
The current design is ambigous on this point, but the initial thought would be that events are multi subscriber but callbacks would be single with the caller having to chain if one is overwritten.
This is the reconcilation, eg. new version. We are drafting off of the approved proposal but there are differences in how complete a solution we are proposing. |
@svick I updated the events version to follow the Framework Design Guidelines: namespace System.Reflection
{
public abstract partial class Assembly
{
public static event EventHandler<LoadNativeLibraryArgs> LoadNativeLibrary;
public static event EventHandler<GetEntrypointArgs> GetEntrypoint;
}
public class LoadNativeLibraryArgs : EventArgs
{
public LoadNativeLibraryArgs(string libraryName, DllImportSearchPath dllImportSearchPath, Assembly callingAssembly)
{
LoadedAssembly = loadedAssembly;
DllImportSearchPath = dllImportSearchPath;
CallingAssembly = callingAssembly;
}
public string LibraryName { get; set; }
public DllImportSearchPath DllImportSearchPath { get; set; }
public Assembly CallingAssembly { get; set; }
}
public class GetEntrypointArgs : EventArgs
{
public GetEntrypointArgs(string libraryName, string entrypointName)
{
LoadedAssembly = loadedAssembly;
EntrypointName = entrypointName;
}
public string LibraryName { get; set; }
public string EntrypointName { get; set; }
}
}
namespace System
{
public delegate void LoadNativeLibraryEventHandler(object sender, LoadNativeLibraryArgs args);
public delegate void GetEntrypointEventHandler(object sender, GetEntrypointArgs args);
} |
This is not providing complete information to resolve the entrypoint. It would also need to provide the signature of the method and the method attributes to do the right name mangling. The name mangling is actually a pretty hard problem as Levi pointed out. I am wondering whether we need to have the callback for entrypoint remapping at all. Limiting this to library name remapping would simplify the design a lot. Do we know how often is the entrypoint remapping done via dllmap today? It may be useful to collect some statistics about it. All real examples of dllmap that I have seen have done library name remapping only. If there are a few rare cases that need entrypoint remapping, they can do it via delegates - it does not need to be built in. |
@annaaniol You can edit the post at the top to incorporate the feedback. It makes it easy to see the current version of the proposal in its entirety. |
How can we collect such statistics? |
@jeffschwMSFT should be able to help to connect you with the right people. We have data mining experts who should be able to do this easily. |
I feel that the entrypoint portion is pretty important. Regarding the name mangling, if GetNativeEntryPoint provides the same level of name mangling as the original p/invoke, won't that be sufficient?
A quick search on github for dllmap and dllentry yields hits for both. I agree with Jan that many of them are just for a dll, but there are certainly plenty (that is a scientific measurement) that have name and target. |
/cc @masonwheeler |
@Wraith2 Yes? There doesn't seem to be much of anything here that impacts my reasoning wrt why DllMap is a bad idea that should not be incorporated into .Net Core. |
Path is probably a bad name (for assets like memory mapped loads etc) . However, we'll need some string to report the information here (mapped name / path / memory identifier, etc.) for diagnostic purpouses |
@jkotas had recommended using Func<> notation instead of delegate typenames. So, the callback-registration version should be something like:
|
@jeffschwMSFT I have manually looked at first 500 hits in your query. I found 3 distinct cases (there is a lot of duplicated forks of the same project) that have name and target:
My hypothesis of why entrypoint remapping is very rarely used: We can divide the native libraries into (1) libraries that maintain binary compatibility between versions and (2) libraries that do not care about binary compatibility between versions. The entrypoint remapping is not necessary to make (1) work. The entrypoint remapping is not enough to make (2) work. An example of library that does not maintain binary compatibility between versions is Open SSL. https://github.com/dotnet/corefx/pull/32006/files is an example of what kind of stuff you have to do to be compatible with multiple versions of Open SSL. The entrypoint remapping is only useful to what falls through the cracks in (1) or when you get extremely lucky in (2), either of which can be expected to be very rare. |
I agree that we can make the entrypoint remapping work if we need to. My question is whether it is useful enough to add it to .NET Core. So far, I am failing to find a set of good examples of what it is actually needed for. It feels like a corner case that it is not worth to have and the few libraries that use it can workaround easily. |
Perhaps I am missing the cost of getting entry point mapping working. We could expose the equivalence of https://github.com/dotnet/coreclr/blob/master/src/vm/dllimport.cpp#L5683 as GetEntryPoint. Another thought on why entry point may be important is for the DSL case that Miguel pointed out. If you wanted to put your own encoding of library and entrypoint in the dllimport you would need the pair to complete that scenario. My only concern is that dllmap with only libraries may not be complete enough. |
So we agreed on the following things:
I updated the description. |
I included your suggestions and updated the Proposed API. Please take a final look and tell me if you have additional comments or questions. |
These comments do not appear to be addressed:
|
Anna and I discussed this. I think the challenge is that to achieve the proper Assembly level isolation we need to associate the callback with an assembly. The simplest approach would be too extend Assembly, but I hear your feedback. I think we should align with your recommendation, which moves us away from events and back to callbacks. Within that design we felt that we would only allow one callback and then return any potentially overwritten callback. Similar to @swaroop-sridhar suggestion https://github.com/dotnet/corefx/issues/32015#issuecomment-417106670 (two seperate callbacks, using the Func notation and returns a potentially overwritten callback, NativeLibrary would then retain a mapping of Assembly with the callbacks).
I thought that based on our discussion that GetEntryPoint would be provided for the simple case. The runtime would then use the method and method attributes provided in the original dllimport to mangle as necessary. In the event that is not sufficient the developer could provide a pre-managed name to the API (we will ensure that pattern works with our implementation). In essese we will expose a p/invoke to GetProcAddress/GetSymbol in a way that does not introduce a cycle with the dllmap hook. |
I did some more search for Dll entry point map usage (outside of github), and I did not see widespread use of them. Most of the projects seem to use it for Mono specific mapings. In particular:
I agree with @jkotas that the reason these mappings are not widely used is that they only work in the situation where we have Dlls on different platforms that differ in name but not in signature. The place where I can imagine this being useful is: A developer implements the same native code with different naming conventions on different platforms. For example, it is common to name public functions in the style If we want to support entrypoint maps, we'll need to support remangling the name. The method-descriptor has pre-computed the mangled name wrt the original name, and we need to compute it based on the mapped name in order to execute We’ll need to do one of: Of these, I think the first option is more generic -- ex: Some other implementation of GetEntryPoint handler may make decisions based on the signature. |
Do we feel this would not be a viable option? The existing NDirectEntryPoint uses the method descriptor in the p/invoke to compute these values. If the API were to provide a different set of method information would that even work? |
Here is the API shape we agreed on: namespace System.Runtime.InteropServices
{
public static partial class Marshal
{
// Typical or for dependencies which must be there or failure should happen.
public static IntPtr LoadLibrary(string libraryPath);
public static IntPtr LoadLibrary(string libraryName,
Assembly assembly,
DllImportSearchPath? searchPath);
// For fast probing scenarios:
public static bool TryLoadLibrary(string libraryPath,
out IntPtr handle);
public static bool TryLoadLibrary(string libraryName,
Assembly assembly,
DllImportSearchPath? searchPath,
out IntPtr handle);
public static void FreeLibrary(IntPtr handle);
public static IntPtr GetLibraryExport(IntPtr handle, string name);
public static bool TryGetLibraryExport(IntPtr handle, string name, out IntPtr address);
}
} We also concluded that this API needs another meeting: public static bool RegisterDllImportResolver(
Assembly assembly,
Func<string, DllImportSearchPath, Assembly, IntPtr> callback
); |
Thanks @terrajobst for the update, it looks much better 👍 I have a few questions: For But if we pass only Is the version
I'm wondering if it would be valuable to expose a method that returns common library extensions for the current platform (or just a string might be enough, but on some platforms, like macos, I think that public static string[] GetLibraryExtensions(); |
Right.
This API exposes the runtime built-in probing algorithm that is used for DllImport (next to assembly, prepend/append OS specific suffixes, ...).
To get the probing paths for the above. |
@jkotas Thanks for all the answers.
Yeah, sorry, on Windows it does, but I'm not sure that dlopen does this actually? In the case we would like to have custom dll search path, but still we would like to get the benefit of the auto prefix/postfix and search probe path, wouldn't make sense to pass a list of additional probing paths directly to LoadLibrary instead of an Assembly? (Do we use anything else the public static IntPtr LoadLibrary(string libraryName,
string[] additionalProbingPaths,
DllImportSearchPath? searchPath); |
Right.
We know that there are scenarios that may want to supply custom dll search paths of various forms. We are leaving designing APIs for those for the next iteration.
Yes, there is more than just |
public static IntPtr GetLibraryExport(string name);
public static bool TryGetLibraryExport(string name, out IntPtr address); |
It may actually be practical to have
yep an oversight likely 😉 |
Updated. |
@terrajobst, should this have been marked |
There is one more API pending to be discussed: |
Right, but that shouldn't block the already approved subset, correct? |
Yes, correct. |
This is the shape we agreed on:
namespace System.Runtime.InteropServices
{
public delegate IntPtr DllImportResolver(string libraryName,
Assembly assembly,
DllImportSearchPath? searchPath);
public static partial class NativeLibrary
{
// Typical or for dependencies which must be there or failure should happen.
public static IntPtr Load(string libraryPath);
public static IntPtr Load(string libraryName,
Assembly assembly,
DllImportSearchPath? searchPath);
// For fast probing scenarios:
public static bool TryLoad(string libraryPath,
out IntPtr handle);
public static bool TryLoad(string libraryName,
Assembly assembly,
DllImportSearchPath? searchPath,
out IntPtr handle);
public static void Free(IntPtr handle);
public static IntPtr GetExport(IntPtr handle, string name);
public static bool TryGetExport(IntPtr handle, string name, out IntPtr address);
public static bool SetDllImportResolver(Assembly assembly, DllImportResolver resolver);
}
} |
The "The common language runtime handles the call to the
Therefore, the difference between default behavior (when no attributes are found) and legacy behavior (when the attribute is present) is that:
So, I think that we should keep the |
It's unclear to me how the values of |
Yes DefaultDllImportSearchPath is currently Windows-specific, except for the SearchAssemblyDirectory value. I filed https://github.com/dotnet/coreclr/issues/21584 to track this. |
What do we expect the callers to do when this returns false? |
Having consumers to design around a single global resource sounds to me like a recipe for frustration. How can we prevent 3rd party libraries from calling it? If only a single delegate can be registered it should be reserved to the main application, otherwise libraries are gonna try and take ownership of the resolver for themselves. If you insist on supporting only a single resolver then this should be designed in a way that makes it only work in the main application (the |
@weltkante This callback is associated only with the passed in |
@jkotas, Since the API is now called |
Right. We just throw exceptions for patterns that are bugs in the app. We do not return
Why would anybody want to do that? It would be just a factory for race conditions and other hard to diagnose problems. |
|
Expose the APIs to set the Native Library resolver callback. API Review: https://github.com/dotnet/corefx/issues/32015 Implementation: https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/src/System/Runtime/InteropServices/NativeLibrary.cs Tests: https://github.com/dotnet/coreclr/blob/master/tests/src/Interop/NativeLibraryResolveCallback/CallbackTests.cs
Expose the APIs to set the Native Library resolver callback. API Review: https://github.com/dotnet/corefx/issues/32015 Implementation: https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/src/System/Runtime/InteropServices/NativeLibrary.cs Tests: https://github.com/dotnet/coreclr/blob/master/tests/src/Interop/NativeLibraryResolveCallback/CallbackTests.cs
This adds support for .NET Standard 2.0 / .NET Core 2.2. The trickiest part of this was the handling of the library file name. The file name differs on Windows (`libgpgme-11.dll`) vs on Linux (`libgpgme.so.11`). Mono has a feature called "dllmap" which makes this remapping quite easy to perform: ``` <dllmap dll="libgpgme-11.dll" target="libgpgme.so.11" /> ``` However, this same functionality is not available in .NET Core. Work is currently underway to have some sort of native library loader functionality that would support this (see https://github.com/dotnet/corefx/issues/32015), but as of now, it's not available. This means I had to hack around it. My initial approach was going to be to have two classes (one for Windows, one for Linux) that implement a common interface: ``` interface INativeMethods { IntPtr gpgme_check_version(IntPtr req_version); } class LinuxNativeMethods : INativeMethods { [DllImport("libgpgme.so.11", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] internal extern IntPtr gpgme_check_version([In] IntPtr req_version); } class WindowsNativeMethods : INativeMethods { [DllImport("libgpgme-11.dll", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)] internal extern IntPtr gpgme_check_version([In] IntPtr req_version); } ``` However, `DllImport` methods **must** be static, and C# doesn't allow static methods on an interface. Because of this, my approach ended up being quite hacky: I have one class with the Linux methods, one class with the Windows methods, and a wrapper class that has properties for each of them (see `NativeMethodsWrapper.cs`). There was another issue: I wanted to avoid copying-and-pasting the P/Invoke code, however .NET doesn't let you have the same file twice in the same assembly, with two different sets of preprocessor values. So, the Windows and Linux classes needed to be in two separate assemblies. I updated the build to use ILRepack to merge all the assemblies together, so users shouldn't actually notice any of this hackiness. Ideally this will all be cleaned up once https://github.com/dotnet/corefx/issues/32015 is completed.
Closed with dotnet/corefx#34686 |
This issue introduces a NativeLibrary abstraction for loading native libraries from managed code in a platform independent way. It provides two key functionalities
Context
This API is introduced as part of implementing the Dllmap feature. This issue introduces a generic callback strategy, where DllMap is one specific implementation using the API.
Approved API
Related
Earlier Proposal
The text was updated successfully, but these errors were encountered: