Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,26 @@ internal static IntPtr NativeDllHandler(Assembly assembly, string libraryName)
{
s_nativeDllSubFolder ??= GetNativeDllSubFolderName(out s_nativeDllExtension);
string folder = Path.GetDirectoryName(assembly.Location);
string fullName = Path.Combine(folder, s_nativeDllSubFolder, libraryName) + s_nativeDllExtension;

// Some developers specify `.dll` in their DllImport attribute even though
// they ship native libraries for Linux and macOS, but they aren't found because the extension is now
// treated as part of the filename. If the libraryName contains a known extension for wrong OS, we
// remove it and add the OS specific extension.
var extension = Path.GetExtension(libraryName).ToLowerInvariant();
string fullName;

if (string.IsNullOrEmpty(extension))
{
fullName = Path.Combine(folder, s_nativeDllSubFolder, libraryName) + s_nativeDllExtension;
}
else if (extension != s_nativeDllExtension)
{
fullName = Path.Combine(folder, s_nativeDllSubFolder, Path.GetFileNameWithoutExtension(libraryName)) + s_nativeDllExtension;
Copy link
Collaborator

@iSazonov iSazonov Dec 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be controversial. Is it worth changing the extension that is specified by the developer in his code?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please look #20740 (comment)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the problem with the sqlite repro? It seems that the developer accidentally specified .dll but also shipped libraries for other OS.

Copy link

@rhubarb-geek-nz rhubarb-geek-nz Dec 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no requirement that a shared library does end with dylib, dll or so. merely convention. If you remember on Windows control panel extensions, drivers and OLE controls were dlls but did not have dll as the extension.

I don't think the developer accidentally specified .dll in the DllImport, the examples from Microsoft have "user32.dll" etc in them. As the module is common and only the shared libraries are different, it is consistent to have them all with the same name but just in a different directory according to architecture.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DllImport documentation says that we can use You can provide a full or relative path. or As a minimum requirement, you must supply the name of the DLL containing the entry point. and says nothing about extensions. So developers are free to use any file extension for dynamic library.
While I agree that deviating from the usual values looks amazing, there are no standards to call it a bug, especially since these non-standard extensions work in C# applications. If we forcibly change the extension, then obviously these applications will stop working.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps if it has an extension, we try loading it as-is, and if it fails, then try the os specific extension?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest not removing anything from the string that was originally given, trust that the original string was correct, because if it is in published code it must have been tested like that and worked.

The simplest would be...

  1. try the exact given name

  2. if that does not work try with operating system specific extension appended

No replacing of the extension with something else, only appending.

So if you don't specify an extension you will get the OS one.

If you do, your extension will be honoured first, then it will try with your full name plus OS extension.

And of course it has to go through (a) in the root directory (b) in the directory with just the CPU name (c) in the directory with OS-CPU

So may take 6 attempts to load the DLL.

Copy link
Collaborator

@iSazonov iSazonov Dec 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback doesn't make a lot of sense. If a developer specified DllImport("interop.msft") in the code, then obviously he is referring to the real interop.msft file on file system and there is no interop.msft.dll file or interop.dll file.
(And I don't think DllImport is smart enough to handle multi-dot names. I mean DllImport("interop.msft") will do not search interop.msft.dll on Windows and interop.msft.so on Linux. If he does, it will work for us automatically.)

}
else
{
fullName = Path.Combine(folder, s_nativeDllSubFolder, libraryName);
}

return NativeLibrary.TryLoad(fullName, out IntPtr pointer) ? pointer : IntPtr.Zero;
}
Expand Down
88 changes: 87 additions & 1 deletion test/powershell/engine/Basic/Assembly.LoadNative.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Describe "Can load a native assembly" -Tags "CI" {
Copy-Item -Path $sourceDllName -Destination $archFolder\$nativeDllName

$managedDllPath = Join-Path $root managed.dll
$managedDllPath_wrongextension = Join-Path $root managed2.dll
$managedDllPath_rightextension = Join-Path $root managed3.dll

$source = @"
using System;
Expand All @@ -72,13 +74,97 @@ Describe "Can load a native assembly" -Tags "CI" {

Add-Type -OutputAssembly $managedDllPath -TypeDefinition $source
Add-Type -Assembly $managedDllPath

# mix up the extension to make sure the right one is loaded
if ($IsWindows) {
$extension = "so"
} elseif ($IsLinux) {
$extension = "dylib"
} elseif ($IsMacOS) {
$extension = "dll"
} else {
throw "Unsupported OS"
}

$source_wrongextension = @"
using System;
using System.Runtime.InteropServices;
public class TestNativeClass3
{
public static int Add(int a, int b)
{
return (a + b);
}

public static void LoadNative()
{
TestEntry();
}

[DllImport ("nativedll.$extension", CallingConvention = CallingConvention.Cdecl)]
internal static extern void TestEntry();
}
"@

Add-Type -OutputAssembly $managedDllPath_wrongextension -TypeDefinition $source_wrongextension
Add-Type -Assembly $managedDllPath_wrongextension

# specify the right extension and ensure it's loaded
if ($IsWindows) {
$extension = "dll"
} elseif ($IsLinux) {
$extension = "so"
} elseif ($IsMacOS) {
$extension = "dylib"
} else {
throw "Unsupported OS"
}

$source_rightextension = @"
using System;
using System.Runtime.InteropServices;
public class TestNativeClass4
{
public static int Add(int a, int b)
{
return (a + b);
}

public static void LoadNative()
{
TestEntry();
}

[DllImport ("nativedll.$extension", CallingConvention = CallingConvention.Cdecl)]
internal static extern void TestEntry();
}
"@

Add-Type -OutputAssembly $managedDllPath_rightextension -TypeDefinition $source_rightextension
Add-Type -Assembly $managedDllPath_rightextension
}

It "Can load native dll" {
It "Can load native libary without extension" {
# Managed dll is loaded
[TestNativeClass2]::Add(1,2) | Should -Be 3

# Native dll is loaded from the same managed dll
{ [TestNativeClass2]::LoadNative() } | Should -Throw -ErrorId "EntryPointNotFoundException"
}

It "Can load native libary with wrong extension" {
# Managed dll is loaded
[TestNativeClass3]::Add(1,2) | Should -Be 3

# Native dll is loaded from the same managed dll
{ [TestNativeClass3]::LoadNative() } | Should -Throw -ErrorId "EntryPointNotFoundException"
}

It "Can load native libary with extension" {
# Managed dll is loaded
[TestNativeClass4]::Add(1,2) | Should -Be 3

# Native dll is loaded from the same managed dll
{ [TestNativeClass4]::LoadNative() } | Should -Throw -ErrorId "EntryPointNotFoundException"
}
}