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

Handling p/invokes for different platforms and discussions about dllmap #4216

Closed
akoeplinger opened this issue May 5, 2015 · 207 comments
Closed

Comments

@akoeplinger
Copy link
Member

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 is libeay32.dll (there are countless other examples, e.g. libz.so vs zlib1.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:

  1. It automatically probes for variations of the string passed to DllImport, e.g. if I specify DllImport("myFoo") it tries myFoo.dll, myFoo.so, libmyFoo.so and so on. In the happy case this is enough to find the library on all the platforms.
  2. DllMap: this is for cases where the library name is just too different, or if only a certain function should be redirected, or only on some specific 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 :

[assembly: DllMap("foo", "foo.dll", OSName.Windows)]
[assembly: DllMap("foo", "libfoo.dylib", OSName.OSX)]
[assembly: DllMap(Dll="foo", Target="libfoo.so", OS=OSName.Linux)]
[assembly: DllMap(Dll="dl", Name="dlclose", Target="libc.so", OS="FreeBSD")]
@jkotas
Copy link
Member

jkotas commented May 5, 2015

cc @stephentoub

Related to #4032

@janhenke
Copy link
Member

janhenke commented May 5, 2015

myFoo.so should never exist. All tools I know enforce the naming convention of having a lib prefix for every shared library. On the other hand things can be even more complicated by the ABI version suffix, i.e. libFoo.so.0.6.

@akoeplinger
Copy link
Member Author

@janhenke in theory yes, Mono just tries a few of the combinations that make sense.

@sergiy-k
Copy link
Contributor

sergiy-k commented May 5, 2015

This is an interesting problem to tackle. I’m curious what is your experience with DllMap? What works well and what doesn’t?

@davidfowl
Copy link
Member

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).

@akoeplinger
Copy link
Member Author

@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?

@davidfowl
Copy link
Member

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:

https://github.com/aspnet/KestrelHttpServer/blob/dev/src/Microsoft.AspNet.Server.Kestrel/Networking/PlatformApis.cs

And the usage:

https://github.com/aspnet/KestrelHttpServer/blob/dev/src/Microsoft.AspNet.Server.Kestrel/Networking/Libuv.cs

@davidfowl
Copy link
Member

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 :)

@OtherCrashOverride
Copy link

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.

@OtherCrashOverride
Copy link

One other comment regarding .so versioning: This is usually solved by providing a symlink to the explicit verison named library.

@Joe4evr
Copy link
Contributor

Joe4evr commented May 6, 2015

.Net code assumes a windows platform and is not case sensitive as unix is.

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.

@akoeplinger
Copy link
Member Author

@OtherCrashOverride

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.

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.

@OtherCrashOverride
Copy link

Let me expand that a bit:
[The authors of existing] .Net code assumes [operation on] a windows platform and [with a file system that] is not case sensitive as unix is.

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".

@OtherCrashOverride
Copy link

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.

@xanather
Copy link

xanather commented May 6, 2015

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.

@janhenke
Copy link
Member

janhenke commented May 6, 2015

One other comment regarding .so versioning: This is usually solved by providing a symlink to the explicit verison named library.

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 libFoo.so. We should neither enforce creating such symlink just for CoreCLR nor break in that case.

@jkotas
Copy link
Member

jkotas commented May 6, 2015

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.

@akoeplinger
Copy link
Member Author

@jkotas That's nice, but I still think there needs to be something simple built in. E.g. dlopen() (which is what LoadUnmanagedDll boils down to) is implemented in libdl on Linux and OS X, but FreeBSD implements it in libc. I don't think every application should have to deal with that.

@Joe4evr
Copy link
Contributor

Joe4evr commented May 6, 2015

@akoeplinger Couldn't that be implemented in the PAL, or am I misunderstanding?

@jkotas
Copy link
Member

jkotas commented May 6, 2015

@akoeplinger It sounds reasonable to add a helper API on AssemblyLoadContext for it. dotnet/coreclr#937

@OtherCrashOverride
Copy link

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'.

@davidfowl
Copy link
Member

+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

@GeirGrusom
Copy link

I have made a library for this, but it is not supported by AOT since it generates the implementation in runtime using Emit.

@OtherCrashOverride
Copy link

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.

@davidfowl
Copy link
Member

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.

@OtherCrashOverride
Copy link

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:

[DllImport("msvcrt", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false)]
public static extern IntPtr Memcpy(IntPtr dest, IntPtr src, UIntPtr count);

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:

  1. [assembly name].[exe | dll].config allows per library rather than global overriding.
  2. Overrides can be specified per entry point for systems like FreeBSD where a function may exist in a different library.
  3. End users and integrators can author the mapping as needed.
  4. New target platforms do not require libraries to be re-authored.
  5. Existing code does not need to be re-authored.

@davidfowl
Copy link
Member

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 😄

@OtherCrashOverride
Copy link

So how does DNX cope with the above mentioned example of memcpy?

I should also point out that

[DllImport("libc.so", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false)]
public static extern IntPtr Memcpy(IntPtr dest, IntPtr src, UIntPtr count);

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?

@davidfowl
Copy link
Member

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.

@OtherCrashOverride
Copy link

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.

@yizhang82
Copy link
Contributor

@mjsabby Absolutely. Let's chat offline and see how we can work together to move this forward.

@OtherCrashOverride
Copy link

The attribute is here : https://www.nuget.org/packages/NativeLibraryAttribute

Is there source code for it? Nuget is forbidden in many environments due to security constraints.

except Native Long (which I still think is something the native bindings should take care of or not expose to consumers)

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?

@bojanrajkovic
Copy link
Contributor

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 IntPtr.Zero (or indicate failure in some other way), fall back to the "native" built-in loading.

@surban
Copy link

surban commented Sep 11, 2017

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:

touch empty.c
gcc -shared -o libLinuxName.so empty.c    
gcc -Wl,--no-as-needed -shared -o libWindowsName.so -fPIC -L. -l:libLinuxName.so
rm -f libLinuxName.so

The result can be checked using the readelf command:

$ readelf -d libWindowsName.so
Dynamic section at offset 0xe38 contains 22 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libLinuxName.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000c (INIT)               0x4b8
 0x000000000000000d (FINI)               0x600
...

Each stub is very small (8 KB) and thus can easily be included in cross-platform NuGet packages.
It is probably possible to generate the stub library with a simple invocation of ld since no compilation is actually involved, but I was not able to figure out the correct arguments to do that.

A real world example of this technique can be seen at https://github.com/surban/managedCuda/tree/master/StubsForLinux

@yatli
Copy link

yatli commented Sep 13, 2017

@surban +1, note that the library cannot have a dot in the name. The loader stops guessing at the first dot.
Another problem is that the names would conflict for *nix systems. You cannot place both Linux/OSX bits together because the loader guesses the same name under the platforms. A workaround is to dynamically release the binary before the first P/Invoke call.

Please take a look here for an actual example:
https://github.com/Microsoft/GraphEngine/blob/master/src/Trinity.Core/Trinity/Runtime/TrinityC.cs

@zwcloud
Copy link

zwcloud commented Jan 24, 2018

@yizhang82
Has MCG been open-sourced? If not, where is it and how can we use it?

@efimackerman
Copy link

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.
The DllImport attribute excepts not only a library name, but a map name too.
///


/// 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" } });

}

private const string LibName = "MapName";

[DllImport(LibName)]
public static extern int CFunc(string val);

}

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);
}

internal static Dictionary<OSPlatform, string> ByKey(string key)
{
  Dictionary<OSPlatform, string> map;
  masterMap.TryGetValue(key, out map);
  return 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;

  // what is an OS platform?
  OSPlatform platform;
  if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
    platform = OSPlatform.Windows;
  else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
    platform = OSPlatform.Linux;
  else
    platform = OSPlatform.OSX;

  // retrieve a library for the OS, or null if not specified
  string foundLibName;
  map.TryGetValue(platform, out foundLibName);
  return foundLibName;
}

}

@migueldeicaza
Copy link
Contributor

<dllmap> in Mono is not implemented with a dependency on System.Configuration, it is implemented independently of that stack.

migueldeicaza referenced this issue in mono/mono-curses Feb 25, 2018
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
migueldeicaza referenced this issue in gui-cs/Terminal.Gui Feb 25, 2018
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
@qmfrederik
Copy link
Contributor

@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)?

@efimackerman
Copy link

@qmfrederik I don't see how my proposal is similar to the one you have mentioned in your comment.

@Nihlus
Copy link

Nihlus commented Mar 19, 2018

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 T? and ref T?.

https://github.com/Firwood-Software/AdvanceDLSupport

@tannergooding
Copy link
Member

I would like to see DllMap (or an equivalent) supported in addition to https://github.com/dotnet/corefx/issues/17135.

I voiced the 'issues' I have with the alternative here: https://github.com/dotnet/corefx/issues/17135#issuecomment-374248831

dlech referenced this issue in DandyCode/Dandy.GPG Jun 2, 2018
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
Copy link
Member

dotnet/coreclr#19373

@masonwheeler
Copy link
Contributor

@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.

@sdmaclea
Copy link
Contributor

trying to emulate Mono's XML DllMap system and why that's a very bad idea

👍

@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.

@ststeiger
Copy link

ststeiger commented Sep 5, 2018

So, how do I do this now in .NET-Core ?
I have SharpFont which loads freetype via dllmap, and it doesn't work on .NET Core.
I have to manually alter the dllimport attribute to filename (and it only works with fullpath+filename ?

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
	<dllmap dll="freetype6" os="linux" target="libfreetype.so.6" />
	<dllmap dll="freetype6" os="osx" target="/Library/Frameworks/Mono.framework/Libraries/libfreetype.6.dylib" />
	<dllmap dll="freetype6" os="freebsd" target="libfreetype.so.6" />
</configuration>

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 ?
LoadLibrary & dlopen are written in C, and even then they aren't that unflexible.

And why having that feature anyway ?
It would have been much better to have a cross-platform wrapper around LoadLibrary/dlopen and GetProcAddress/dlsym, so people couldn't do it this way in the first place.

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 ?
Name-picking could then be done in a user-defined fashion, and this issue would be solved, and all old-code would still work.

@ststeiger
Copy link

ststeiger commented Sep 5, 2018

Since attribute doesn't exactly allow for a callback, here's an example of what I mean:

namespace NetStandardReporting
{


    // [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false, Inherited = true)]
    [System.AttributeUsage(System.AttributeTargets.Method, Inherited = false)]
    public class DynamicDllImportAttribute
        : System.Attribute
    {
        protected string m_dllName;


        public string Value
        {
            get
            {
                return this.m_dllName;
            }
        }

        public string EntryPoint;
        public System.Runtime.InteropServices.CharSet CharSet;
        public bool SetLastError;
        public bool ExactSpelling;
        public System.Runtime.InteropServices.CallingConvention CallingConvention;
        public bool BestFitMapping;
        public bool PreserveSig;
        public bool ThrowOnUnmappableChar;


        public DynamicDllImportAttribute(string dllName)
            : base()
        {
            this.m_dllName = dllName;
        }


        private static System.Type CreateDelegateType(System.Reflection.MethodInfo methodInfo)
        {
            System.Func<System.Type[], System.Type> getType;
            bool isAction = methodInfo.ReturnType.Equals((typeof(void)));

            System.Reflection.ParameterInfo[] pis = methodInfo.GetParameters();
            System.Type[] types = new System.Type[pis.Length + (isAction ? 0: 1)];

            for (int i = 0; i < pis.Length; ++i)
            {
                types[i] = pis[i].ParameterType;
            }

            if (isAction)
            {
                getType = System.Linq.Expressions.Expression.GetActionType;
            }
            else
            {
                getType = System.Linq.Expressions.Expression.GetFuncType;
                types[pis.Length] = methodInfo.ReturnType;
            }

            return getType(types);
        }


        private static System.Delegate CreateDelegate(System.Reflection.MethodInfo methodInfo, object target)
        {
            System.Type tDelegate = CreateDelegateType(methodInfo);

            if(target != null)
                return System.Delegate.CreateDelegate(tDelegate, target, methodInfo.Name);

            return System.Delegate.CreateDelegate(tDelegate, methodInfo);
        }


        protected delegate string getName_t();

        public DynamicDllImportAttribute(System.Type classType, string delegateName)
            : base()
        {
            System.Reflection.MethodInfo mi = classType.GetMethod(delegateName,
                  System.Reflection.BindingFlags.Static
                | System.Reflection.BindingFlags.Public
                | System.Reflection.BindingFlags.NonPublic
            );

            // System.Delegate getName = CreateDelegate(mi, null);
            // object name = getName.DynamicInvoke(null);
            // this.m_dllName = System.Convert.ToString(name);

            // System.Func<string> getName = (System.Func<string>)CreateDelegate(mi, null);
            // this.m_dllName = getName();

            getName_t getName = (getName_t)System.Delegate.CreateDelegate(typeof(getName_t), mi);
            this.m_dllName = getName();
        }


    } // End Class DynamicDllImportAttribute 


    public static class DynamicDllImportTest 
    {

        private static string GetFreetypeName()
        {
            if (System.Environment.OSVersion.Platform == System.PlatformID.Unix)
                return "libfreetype.so.6";

            return "freetype6.dll";
        }


        // [DynamicDllImport("freetype6")]
        // [DynamicDllImport(typeof(DynamicDllImportTest), nameof(GetFreetypeName))]
        // [DynamicDllImport("foo", CallingConvention = System.Runtime.InteropServices.CallingConvention.Cdecl)]
        [DynamicDllImport(typeof(DynamicDllImportTest), nameof(GetFreetypeName), CallingConvention = System.Runtime.InteropServices.CallingConvention.Cdecl)]
        public static string bar()
        {
            return "foobar";
        }


        // NetStandardReporting.DynamicDllImportTest.Test();
        public static void Test()
        {
            System.Reflection.MethodInfo mi = typeof(DynamicDllImportTest).GetMethod("bar",
                  System.Reflection.BindingFlags.Static
                | System.Reflection.BindingFlags.Public
                | System.Reflection.BindingFlags.NonPublic);

            object[] attrs = mi.GetCustomAttributes(true);
            foreach (object attr in attrs)
            {
                DynamicDllImportAttribute importAttr = attr as DynamicDllImportAttribute;
                if (importAttr != null)
                {
                    System.Console.WriteLine(importAttr.Value);
                }
            } // Next attr 

        } // End Sub Test 


    } // End Class 


} // End Namespace 

@Nihlus
Copy link

Nihlus commented Sep 5, 2018

@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

@ststeiger
Copy link

ststeiger commented Sep 5, 2018

@Nihlus:
Nice, and by using interfaces, one could use DependcyInjection on native libraries.
Definitely worth a look.

I already made a similar thing, long ago (loading oracle native-dlls in asp.net and mono).
However, changing the current source-code of DllImportAttribute to the one of DynamicDllImportAttribute would be far easier for porting old code.

No backwards-compatibility problems whatsoever.
With this revision, no Linq required either, so even compatible with .NET 2.0.

You could now even fetch the dll-name from a database.
Now would be funny to store the delegate into a member variable, and change value to:

    public string Value
    {
        get
        {
            return getName();
        }
    }

Then you could theoretically even use a different freetype version per tenant in the database depending on HttpContext (domain), if available ;)
(assuming the signature and all else stays the same)
Would be a bit hacky, but it would work.
But I guess that would give a mess with hmodule.
Assuming it loads the delegate every time, which it probably doesn't.

What we would need now is extension-attributes with extension properties, to retroactively add this to the full .net framework without changing its source.

@jkotas
Copy link
Member

jkotas commented Sep 5, 2018

It would have been much better to have a cross-platform wrapper around LoadLibrary/dlopen and GetProcAddress/dlsym

Yes, this is exactly what we plan to do. Tracked by https://github.com/dotnet/corefx/issues/32015.

@swaroop-sridhar
Copy link
Contributor

Closing this issue, the matter is tracked by dotnet/corefx#32015.

@msftgits msftgits transferred this issue from dotnet/coreclr Jan 30, 2020
@msftgits msftgits added this to the Future milestone Jan 30, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Jan 6, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests