-
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
Helper class for dealing with native shared libraries and function pointers #20635
Comments
You've eliminated the option of mimicing Mono's solution (DllMaps, as you mention)? |
Related discussion (which you're probably aware of): https://github.com/dotnet/coreclr/issues/930 |
@danmosemsft See the link that @akoeplinger shared. |
@mellinoe since my thread is pretty old by now, do you know what the current status of MCG (which was proposed as the solution by .NET runtime folks) is? |
I don't know. I'd be interested to hear an update from @yizhang82 about it. |
Nobody uses unsafe pointers in extern methods, only IntPtr or managed equivalents. Usage of generic delegates should be allowed in GetDelegateForFunctionPointer, and frankly I don't see what difficulties would implementing it have. On the other hand, I think that the "LoadFunction" doesn't allow for as much marshalling options as the DllImport system can. What if you could specify "instance extern" methods on types inheriting from NativeLibrary, which would be automatically imported from the library with the specified marshalling options? |
It is very common to use unsafe pointers in extern methods, in my experience. I certainly do it in a lot of my libraries, and I've seen it in many libraries from others, as well. Wrapping things into IntPtr's is unnecessarily verbose when you're already doing fundamentally unsafe things. Now, it's also very common to "wrap" the PInvokes into methods will fully-safe signatures, but those call the unsafe version in turn.
That sounds like it would involve a lot of external machinery (IL rewriting), or actual C# language support. Certainly an interesting idea, but probably outside of scope here. @yizhang82 Do you have any feedback for this proposal? |
Another problem with the proposed API is that calling |
That is a good point, and it may also be the case for UWP applications. |
Just my 2 cents on this:
namespace System.Runtime.InteropServices
{
// Exposes functionality for loading native shared libraries and function pointers.
public class NativeLibrary : IDisposable
{
+ // The default search paths
+ public string[] NativeLibrarySearchDirectories { get; }
// The operating system handle of the loaded library.
public IntPtr Handle { get; }
// Constructs a new NativeLibrary using the platform's default library loader.
public NativeLibrary(string name);
// Loads a native function pointer by name.
public IntPtr LoadFunction(string name);
// Loads a function whose signature matches the given delegate type's signature.
// This is roughly equivalent to calling LoadFunction + Marshal.GetDelegateForFunctionPointer
public T LoadDelegate<T>(string name);
// Frees the native library. Function pointers retrieved from this library will be void.
public void Dispose();
}
} |
This wrapper looks good, additionally, I would add the following API, that would lookup a symbol (not a function) from the specified dynamic library:
This would be useful to access global variables and other global symbols in the loaded library. |
@mellinoe can this be used to perform lookups using |
@tdms Looking at https://linux.die.net/man/3/dlvsym and https://www.gnu.org/software/gnulib/manual/html_node/dlvsym.html, I'm wondering whether it makes sense to add support for a glibc-extension in the core API? |
I'm not requesting |
@tmds Well with this API you should be able to do something like this, right:
I do hope that the |
I think in this proposal, I'm looking to override |
What happens when someone uses a delegate after disposing the library?
Are libraries used via PInvoke ever closed? When? |
simple ObjectDisposedException? |
Adding the area owners @russellhadley @luqun @shrah to this thread. One of the issues we're trying to solve is how managed code can load native libraries in a cross-platform way. For example, System.Drawing.Common depends on libgdiplus which ships in various forms in various distros. So the code probes for a couple of files and loads the correctly library using One of the limitations we've hit is that I've tried to address that in dotnet/corefx#25134 by adding Hence this issue 😄 . Can you give your feedback on this API proposal? |
We may need a constructor overload that takes |
@jkotas Something like this? namespace System.Runtime.InteropServices
{
// Exposes functionality for loading native shared libraries and function pointers.
public class NativeLibrary : IDisposable
{
+ // The default search paths
+ public static IEnumerable<string> NativeLibrarySearchDirectories { get; }
// Constructs a new NativeLibrary using the platform's default library loader.
public NativeLibrary(string name);
+ public NativeLibrary(string name, DllImportSearchPath paths);
+ // Constructs a new NativeLibrary using user-provided search libraries.
+ public static NativeLibrary Open(string name, DllImportSearchPath paths, bool throwOnError)
// The operating system handle of the loaded library.
public IntPtr Handle { get; }
// Loads a native function pointer by name.
public IntPtr LoadFunction(string name);
+ // Lookup a symbol (not a function) from the specified dynamic library
+ public IntPtr SymbolAddress (string name);
// Loads a function whose signature matches the given delegate type's signature.
// This is roughly equivalent to calling LoadFunction + Marshal.GetDelegateForFunctionPointer
public T LoadDelegate<T>(string name);
// Frees the native library. Function pointers retrieved from this library will be void.
public void Dispose();
}
} |
I meant something like this: public class NativeLibrary : IDisposable
{
public NativeLibrary(string name);
+ public NativeLibrary(string name, DllImportSearchPath paths);
...
}
|
Also, to allow implementation custom probing logic like the one in https://github.com/dotnet/corefx/blob/35d0838c20965c526e05e119028dd7226084987c/src/System.Drawing.Common/src/System/Drawing/GdiplusNative.Unix.cs#L39 we may want to have non-throwing factory like: public class NativeLibrary : IDisposable
{
+ public static NativeLibrary Open(string name, DllImportSearchPath paths, bool throwOnError);
...
} |
@jkotas Ah, that makes sense. I updated the API spec. So the libgdiplus probing logic could be rewritten to something like this: NativeLibrary libgdiplus = null;
string libraryName = null;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
libraryName = "libgdiplus.dylib";
libgdiplus = NativeLibrary.Open("libgdiplus.dylib", DllImportSearchPath.SafeDirectories, throwOnError: false);
}
else
{
// Various Unix package managers have chosen different names for the "libgdiplus" shared library.
// The mono project, where libgdiplus originated, allowed both of the names below to be used, via
// a global configuration setting. We prefer the "unversioned" shared object name, and fallback to
// the name suffixed with ".0".
libraryName = "libgdiplus.so";
libgdiplus = NativeLibrary.Open("libgdiplus.so", DllImportSearchPath.SafeDirectories, throwOnError: false);
if (libgdiplus == null)
{
libgdiplus = NativeLibrary.Open("libgdiplus.so.0", DllImportSearchPath.SafeDirectories, throwOnError: false);
}
}
// If we couldn't find libgdiplus in the system search path, try to look for libgdiplus in the
// NuGet package folders. This matches the DllImport behavior.
if (libgdiplus ==null)
{
libgdiplus = NativeLibrary.Open(libraryName, DllImportSearchPath.ApplicationDirectory,, throwOnError: false);
} Assuming |
@qmfrederik did you leave out @jkotas's constructor overload on purpose? Also, should |
@karelz the interface alone doesn't really enforce immutability. There a standard pattern for this in corefx? I should read up on that. :) |
I don't think there is a standard pattern documented. But in general I think we try to expose interfaces instead of underlying collections (at least I vaguely remember that being discussed in API reviews). |
@karelz That was an oversight on my side, although having both a constructor and a static Good point on IEnumerable vs string[]. I changed that; it does help communicate the values are read-only. |
Top post updated with latest proposal. @jkotas can you mark it api-ready-for-review when you are ok with the API shape? |
My comments about API shape:
Is it roughly equivalent, or exactly equivalent? It would be nice it |
I agree, and the good news is you can already do so today - actually .NET Core has a pretty good story there. If you control the naming of your native library, you can something like Since you control the naming, you also control the distribution, in which case you can ship your native libraries in a NuGet package and .NET Core will "do the right thing". That's how I ship imobiledevice-net, for example; and ASP.NET ships libuv IIRC. |
Yep, and that was my proposal just above (to make LoadLibrary acting similar to DllImport behavior) So you agree a bit then? 😅 |
Yes! |
It sounds like there is almost consensus here, what are the next steps? I'm itching to use whatever the solution is to fix cross platform support with dotnet that previously worked with .net and mono. |
@chmorgan the next steps are for us to finish the existing PR for this feature when the checkin window opens back up. |
What about the fact that on iOS callbacks must be static? That's where all my interop code diverges the most. For us to be able to create truly reusable interop code, these sort of differences needs to be addressed too. |
we need to restore packages on each system |
@kasper3 Good point. I really like the |
@kasper3, @dotMorten no offence, but I guess warping the native to OS packaging systems is a bit of topic, as in case you are the owner of the native library - you can maintain a library naming in a dotnet friendly way. So you need this functionality only in case you are not the owner of the original library(s) which is exposing C-API. To summarize this is needed to cover discrepancies between different OS and habits for naming these libraries. |
@Ruslan-B I fail to understand the relevance to whether you own the original library or not. The way you deploy your native libs via NuGet is quite different on each platform. Having the |
@dotMorten in this case I would suggest to reread the original proposal. |
@Ruslan-B Read the entire thing, and you'll find the discussion has expanded quite beyond that. |
@dotMorten true, however, I think the only reason we need this, is to handle discrepancies between different OS with stable habits of naming things. |
This issue is an API proposal issue. It concerns an API for loading native libraries and using the functions they expose. It was created because we need to load (lib)gdiplus for System.Drawing across platforms. How you package these native libraries with your app (and whether you should package them at all or use a different acquisition mechanism) is related but not strictly in scope for the API proposal. I don't know about iOS and Android, but the .NET Core SDK supports the |
Currently there is not much difference between Future and 2.2. I moved it to 2.2, but it depends if we get to agreement and the result passes code reviews (there has been quite a few people with strong opinions). |
@karelz How should we proceed from there? Based on my previous pseudo-proposal should I open a new issue? |
I will defer to @GrabYourPitchforks who worked on it and has PR ready (https://github.com/dotnet/corefx/issues/17135#issuecomment-373878407), he should be able to suggest best next steps (once he is back from vacation - one more week). |
@dotMorten btw, what do you mean by static? Static linking required by iOS with mono? Isn't dlopen being supported starting from iOS8+? (or you need to support older iOS?) |
Actually we should defer this to @jeffschwMSFT and the Interop team, who are interested in driving this feature area forward in .NET Core. @jeffschwMSFT could you share your team's current thoughts on direction / shape? |
@qmfrederik we are taking a step back and considering the broader scenario (exploring a dllmap like mechanism). Right now we don't have a concrete design, but once we do we will share and update this thread. |
So, based on all the feedback, perhaps we should split the issue in two. The core gist of this is that we want a way to call Making cross-platform library loading (i.e. dllmap & friends) would be orthogonal to that, no? |
Currently for 2.2, there is one inconsistency left in DllImport tracked by https://github.com/dotnet/coreclr/issues/17604. |
Yeah, I agree, maybe not completely orthogonal, but it is more an implementation detail or an enhancement to the resolution. It should not change the API shape, nor the default behavior if a dllmap file is not present (which is good). Dllmap is a super nice feature/addition and it should be considered, but maybe we should split first an implementation without, which was almost done it seems, and then proceed on the full dllmap story (which will take likely several months of study+shape+implem+tests...etc.) |
Closing this issue, in favor of dotnet/corefx#32015 |
Approved Proposal
From https://github.com/dotnet/corefx/issues/17135#issuecomment-365364216
Original proposal
Note: Proposal updated based on discussion: https://github.com/dotnet/corefx/issues/17135#issuecomment-353571556
Background
Many popular C-callable shared libraries exist which do not have a consistent name across platforms. Examples include:
CoreCLR does not support any notion of "remappable" PInvokes, a la DllImport in Mono. The name of the shared library must be encoded directly in the
DllImport
attribute, which makes it impossible to create a single wrapper assembly which works on many platforms. This imposes an unnecessary development and deployment burden on library developers who could otherwise ship a single, simple DLL. Many third-party libraries which rely on PInvoke's only work on Mono platforms because of the DllMap feature. These libraries do not work on .NET Core at all, even if they are otherwise completely compatible with its profile.The alternative to using
[DllImport]
is to write logic which manually opens the shared library, discovers function pointers, and converts them to managed delegates. In a sense, this very similar to what we are doing with our "Portable Linux" version of the runtime and native shims. However, this code can be complicated, tedious, and error-prone. This proposal is for a small helper library, with optional cooperation by the runtime, to make this scenarion easier.Proposed API
Usage
Open Design Questions
Probing Path Logic
CoreCLR applies particular probing logic when discovering shared libraries listed in a
DllImport
. Ideally, callingnew NativeLibrary("lib")
would follow the same probing logic as[DllImport("lib")], so that PInvoke's can be easily converted. This could potentially be solved with some internal or public runtime helper function exposed from System.Private.CoreLib. Alternatively, the constructor for
NativeLibrary` could include another parameter which controlled probing logic, allowing someone to plug their own logic in.Generic delegate types
The simplest way to convert from a native function pointer to a clean managed delegate is through the use of
Marshal.GetDelegateForFunctionPointer<T>(IntPtr)
. Unfortunately, this cannot be used with generic delegates, e.g.Func<string, int, int, int, int, int, IntPtr>
in the above example. Given that many native function signatures include pointer types, which cannot be used as generic type arguments, other than asIntPtr
, this is not a major problem. However, many cases would benefit from being able to useAction
andFunc
types in theLoadFunction<T>
method. We could probably allow this using Reflection.Emit, but it would most likely be ugly, complicated, and slower than if custom delegate types were used. IfMarshal.GetDelegateForFunctionPointer<T>(IntPtr)
accepted generic delegate types, this feature would greatly benefit.Disposability and lifetime tracking
The constructor of
NativeLibrary
involves opening an operating system handle to a native shared library, viaLoadLibrary
,dlopen
, etc. I have proposed thatNativeLibrary
be disposable, which would involve callingFreeLibrary
,dlclose
, etc. Is this desirable or useful?A related issue is how the runtime currently tracks and manages these handles. Should handles opened by
NativeLibrary
be coordinated and tracked in the same way that other handles (via PInvoke, etc.) are?Prototype
I have a prototype version of this library implemented here: https://github.com/mellinoe/nativelibraryloader, and I have successfully used the pattern described here in a few projects.
@yizhang82 @janvorli @danmosemsft @conniey
The text was updated successfully, but these errors were encountered: